diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0d20b648 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/Commands.md b/Commands.md index a961a6c9..1b3d0e7b 100644 --- a/Commands.md +++ b/Commands.md @@ -29,7 +29,7 @@ Typical sequence "!%d\x03" % speed # 1..10 "FX%d\x03" % press # 1..33 "FC%d\x03" % off # off=18: cutting, off=0: pen. Other values unknown. #FC p,q,[n] [t] -"FY%d\x03" % enh # enh=1/0: Trackenhancing on/off +"FY%d\x03" % enh # enh=0/1: Trackenhancing on/off "FN%d\x03" %ori # ori=1/0: Landscape/Portrait "FE0\x03" # ?? "TB71\x03" # ?? @@ -37,7 +37,7 @@ Typical sequence "FA\x03" # begin page definition "FU%d,%d\x03" % (w,h) # h,w page dimensions without top/left margin. Needed to start left. "FM1\x03" # ?? ---------------------------------- if registraion marks +--------------------------------- if registration marks "TB50,381\x03" "TB99\x03" "TB55,1\x03" diff --git a/README.md b/README.md index 31d93d8c..252e0793 100644 --- a/README.md +++ b/README.md @@ -1,158 +1,208 @@ -inkscape-silhouette -=================== +# inkscape-silhouette -An extension to drive a Silhoutte Cameo from within inkscape. +An extension to drive a Silhoutte Cameo and similar plotter devices from within inkscape. 100% pure python, ontop of the libusb backend. -Here is the wiki with photos and a video: https://github.com/jnweiger/inkscape-silhouette/wiki +Here is the wiki with photos and a video: https://github.com/fablabnbg/inkscape-silhouette/wiki -Installation ------------- +## Suported Devices -Ubuntu 16.04 +This extension should work with the following devices: -* apt install python-usb +* Silhouette Portrait +* Silhouette Portrait 2 (working confirmed) +* Silhouette Cameo +* Silhouette Cameo 2 +* Silhouette Cameo 3 +* Silhouette Curio (partial success confirmed in #36) +* Craft Robo CC200-20 +* Craft Robo CC300-20 +* Silhouette SD 1 +* Silhouette SD 2 + +## Installation + +### Ubuntu 16.04 + +* `apt install python-usb` * https://github.com/fablabnbg/inkscape-silhouette/releases Scroll down to Downloads and click on the *.deb file. -Ubuntu 14.04 +### Ubuntu 14.04 -* apt install python-pip python-setuptools -* sudo pip install pyusb +* `apt install python-pip python-setuptools` +* `sudo pip install pyusb` * https://github.com/fablabnbg/inkscape-silhouette/releases Scroll down to Downloads and click on the *.deb file. - -Other Debian based Linux +### Other Debian based Linux * Download https://github.com/fablabnbg/inkscape-silhouette/archive/master.zip -* Copy the the folder silhouette and the two files sendto_silhouette.inx and -sendto_silhouette.py to ~/.config/inkscape/extensions/ or (if you have permissions) /usr/share/inkscape/extensions/ +* Copy the the folder silhouette and the two files `sendto_silhouette.inx` and +`sendto_silhouette.py` to `~/.config/inkscape/extensions/` or (if you have permissions) `/usr/share/inkscape/extensions/` + +* `sudo apt-get install python-usb` +* restart inkscape, check the menu entry "Extensions -> Export -> Send to Silhouette" -* sudo apt-get install python-usb -* restart inkscape, check the menu entry Extensions -> Export -> Send to Silhouette +### openSUSE -openSUSE: +* `~/.config/inkscape/extensions/` or +* `/usr/share/inkscape/extensions/` +* and run `sudo zypper in python-usb` -* ~/.config/inkscape/extensions/ or -* /usr/share/inkscape/extensions/ -* and run '''sudo zypper in python-usb''' +### Arch Linux -Arch Linux: -* sudo pacman -S inkscape python2 python2-lxml python2-pyusb -* git clone https://github.com/fablabnbg/inkscape-silhouette.git -* cd inkscape-silhouette -* sudo python2 setup.py build && sudo python2 setup.py install -* sudo cp sendto_silhouette.* /usr/share/inkscape/extensions/ -* sudo cp -R silhouette /usr/share/inkscape/extensions/ +```shell +sudo pacman -S inkscape python2 python2-lxml python2-pyusb +git clone https://github.com/fablabnbg/inkscape-silhouette.git +cd inkscape-silhouette +sudo python2 setup.py build && sudo python2 setup.py install +sudo cp sendto_silhouette.* /usr/share/inkscape/extensions/ +sudo cp -R silhouette /usr/share/inkscape/extensions/ +``` + +### Mac OS X -Mac OS X * Install prerequisites: * install homebrew http://brew.sh/ - * brew install libusb + * `brew install libusb` * Install the extension: - * sudo ./install_osx.py - -Windows (untested): -* Download and install the free test version of **winzip** from http://www.winzip.com -* Download https://github.com/jnweiger/inkscape-silhouette/archive/master.zip -* Navigate to your Downloads folder and double-click on **inkscape-silhouette-master.zip** -* Click open the **inkscape-silhouette-master** folder. -* Select the following three items (with Ctrl-Click): **silhouette**, **sendto_silhouette.inx**, and **sendto_silhouette.py** -* Extract to My Computer **C:\Program Files\Inkscape\share\extensions** -* untested: if you have a Silhouette Studio CD, install the device driver. Then your Silhouette Cameo may show up as a printer device and the extension could now work. If not, installing pywinusb might help. -* The following tips are for a Windows-7 64-bit machine: - * Download and install http://sourceforge.net/projects/libusb-win32/files/libusb-win32-releases/ - * x86\libusb0_x86.dll: x86 32-bit library. Must be renamed to libusb0.dll
- On 64 bit, Installs to Windows\syswow64\libusb0.dll. - On 32 bit, Installs to Windows\system32\libusb0.dll. - * X86 ONLY ARCHITECTURES:
- x86\libusb0.sys: x86 32-bit driver.
- Installs to Windows\system32\drivers\libusb0.sys - * When you get a number of options, chose the printer option. -* If you don't have python installed, then install the latest stable version. -* Download and unpack http://sourceforge.net/projects/pyusb/ + * `sudo ./install_osx.py` + +### Windows + +#### Requirements + +* Download http://zadig.akeo.ie/downloads/zadig-2.3.exe +* Go to menu options `List all devices` +* Look for USB Printing Support in the dropdown list +* Use manufacturer ID: 10B4D Graftek America` +* Select driver `libusb0 (v1.2.6.0)` +* Click install driver +* If you don't have python installed, then install the latest stable version. +* Download and unpack http://sourceforge.net/projects/pyusb/ * cd ..\pyusb-1.0.0b1; python.exe setup.py install +* An error message `ImportError: No module named usb.core` means you are close, but `pyusb` was not correctly installed. Please check, if there are multiple python installations in your system, e.g. one that came with inkscape. +* Find the path to the `python.exe` in the inkscape directory, e.g. on a Windows 7 (64 bits) it may be `C:\Program Files (x86)\Inkscape\python\python.exe` +* Change directory to the `pyusb` one and run it again. + +#### Silhouette inkscape extension itself + +* Download and install `7zip` from https://www.7-zip.org/download.html +* Download https://github.com/fablabnbg/inkscape-silhouette/archive/master.zip +* Navigate to your `Downloads` folder and double-click on `inkscape-silhouette-master.zip` +* Click open the `inkscape-silhouette-master` folder. +* Select the following three items (with Ctrl-Click): `silhouette`, `sendto_silhouette.inx`, and `sendto_silhouette.py` +* Extract to `My Computer`: `C:\Program Files\Inkscape\share\extensions` or `C:\Program Files (x86)\Inkscape\share\extensions` if first does not exists. * Restart inkscape -* An error message 'ImportError: No module named usb.core' means you are close, but pyusb was not correctly installed. Please check, if there are multiple python installations in your system, e.g. one that came with inkscape. +## Usage + +1. Open your document with inkscape. +2. Ensure the unit of document width and height is mm or inch, but not px. (File - Document settings - Page - Custom - Unit mm) Otherwise you may observe differences in dimensions at inkscape 0.91/0.92, because default dpi has changed from 90 to 96. +3. Convert all objects and texts to paths (Path - Convert object to path) +4. Select the parts you want to plot. +5. Open Extension (Extensions - Export - Send to Silhouette) +6. Set your desired plot parameters: + * **X-Offset, Y-Offset** An addtitional offset of your drawing from the top left corner. Default is 0/0 + * **Tool Cut/Pen** Cut mode drews small circles for orientation of the blade, Pen mode draws exactly as given. + * **Media** Select a predfined media or set to custom settings. + * **Speed** Custom speed of the movements + * **Pressure** Custom Pressure on the blade. One unit is said to be 7g force. +7. Press Apply button to start cut. + +## Troubleshooting -Troubleshooting ---------------- +```python +>>> import usb.core +>>> usb.core.find() + +>>> +``` - python - >>> import usb.core - >>> usb.core.find() - - >>> +If this reports `no usb.core.Device` to you, please help troubleshoot. -If this reports no usb.core.Device to you, please help troubleshoot. +```python +python +>>> import usb.core +>>> usb.version_info[0] +``` + +This fails on win32/64 with 'module has no attribute 'version info' which then causes Graphtec.py to error even though usb.core is installed. + +## Using of registration marks + +The plotter will search the registration marks at the given positions. +If it founds the marks, they will serve as accurate reference and define the origin. +Therefore it is necessary to set the correct offset values of the mark. +As a result the cut will go precisely along the graphics. -Using of registration marks ---------------------------- To plot with registration marks do the following: -1. Open the document examples/registration-marks.svg +1. Open the document which fit to your setup, e.g. `examples/registration-marks-cameo-silhouette-a4-maxi.svg` for Silhouette Cameo using A4 paper format. 2. Insert your cutting paths and graphics on the apropriate layers. -3. Printout the whole document including registration marks. You probably want to hide the cutting layer. +3. Printout the whole document including registration marks. You probably want to hide the cutting layer. 4. Select your cutting paths in the document, but exclude regmarks and graphics. -5. Set or ensure the correct values (regmark position/width/height) on the regmark tab. -6. Enable 'Document has registration marks' and 'Search for registration marks' +5. On the **Regmarks** tab: + * Check **Document has registration marks** + * Check **Search for registration marks** +6. Set all following parameters according to the registration file used: + * **X mark distance** (e.g. *190*) + * **Y mark distance** (e.g. *277*) + * **Position of regmark from document left** (e.g. *10*) + * **Position of regmark from document top** (e.g. *10*) 7. Start cut. -The plotter will search the registration marks at the given positions. If it founds the marks, they will serve as accurate reference and define the origin. Therefore it is necessary to set the correct offset values of the mark. As a result the cut will go precisely along the graphics. -At my device there seems to be a little offset between the search optics and the cutting knife. For enhanced precision I have to set an offset of 0,1mm for both x and y on the first tab to compensate. +On some devices have an offset between the search optics and the cutting knife. +For enhanced precision, you may have to set an offset on **X-Offset** and/or **Y-Offset** on the **Silhouette** tab to compensate. -Features --------- +## Features -* Path sorting for monotonic cut. We limit backwards movement to only a few - millimeters, and make the knive pull only towards sharp corners +* Path sorting for monotonic cut. We limit backwards movement to only a few + millimeters, and make the knive pull only towards sharp corners so that most designs can be done without a cutting mat! * Coordinate system conforms to inkscape SVG. * Exact Margins. Can start at (0,0). * Pen mode used to avoid the precut movement of the knive. - Those movements are visible a) at the left hand side, when + Those movements are visible a) at the left hand side, when starting, b) at each sharp turn. -* Bounding Box. Can optionally plot (or calculate only) +* Bounding Box. Can optionally plot (or calculate only) the bounding box instead of plotting all strokes. - This can be used (with low pressure=1 or removed knive) to just + This can be used (with low pressure=1 or removed knive) to just check, where the plot would be. -* The standalone script arrow_test.py can be used to test drive - the SilhoutteCameo class. +* The standalone script `arrow_test.py` can be used to test drive + the `SilhoutteCameo` class. * Robust communication with the device. Small writes and timeouts are handled gracefully. Timeouts will occur, when we travel far with low speed. -* Multipass: Can repeat each stroke multiple times to enhance plot or +* Multipass: Can repeat each stroke multiple times to enhance plot or cut quality. This can also be used to attempt a cut without cutting mat, by applying very little pressure. -* reverse toggle options, to cut the opposite direction. This might also be +* reverse toggle options, to cut the opposite direction. This might also be helpful with mat-free cutting via multipass. * honors hidden layers. -Misfeatures of InkCut that we do not 'feature' ----------------------------------------------- +## Misfeatures of InkCut that we do not 'feature' * object transforms are missing most of the time. -* Stars, polygons, and boxes are plotted not closed, the final stroke +* Stars, polygons, and boxes are plotted not closed, the final stroke is missing. (Must be me, no?) * always plots all layers, even if hidden. -TODO ----- +## TODO * Implement the triangle in a square test cut. * test MatFree cutting strategy with the WC-Wunderbach-Wimpern font, which is especially well suited as a test-case. + * improve MatFree cutting by finding a better scan sort algorithm. Wide shadow casting towards negative y? -* Implement paper-zip as a seperate inkscape extension. +* Implement paper-zip as a seperate inkscape extension. -REFERENCES ----------- +## References * http://wiki.inkscape.org/wiki/index.php/Extensions * http://wiki.inkscape.org/wiki/index.php/INX_Parameters diff --git a/doc/commands.md b/doc/commands.md new file mode 100644 index 00000000..fe67c627 --- /dev/null +++ b/doc/commands.md @@ -0,0 +1,923 @@ +FROM: https://github.com/fablabnbg/inkscape-silhouette/issues/72#issue-357943739 + +Just an FYI: + +I've been talking with the Silhouette America support people, and the development team is "exploring development of an official SDK for general release". + +The idea is that we would finally have real official documentation on the protocol. All I had asked for was a 'cheat sheet' of the commands, so that we could make some progress on updating the export functions. + +In the mean time, here's what I've come up with so far by sniffing the protocol: + +# Table of Contents + +1. [Commands for Silhouette Cameo 3](#org368d528) +2. [Units](#org44c9db7) +3. [Examples](#orgbe65902) + 1. [Initialization (US Letter, Portrait)](#org910d281) + 2. [Initialize Autoblade In Tool 1](#org6925a25) + 3. [Initialize Ratchet Blade in Tool 2](#org6c1d32c) + 4. [End Sequence](#orged666d2) + 5. [Status (Ready)](#org12ecad4) + 6. [Status (Moving)](#orgff234cc) + 7. [Status (Pause)](#orgb18a6a6) + 8. [Status (Cancel)](#org6022836) + 9. [Startup Sequence From Power Up](#orgab63b55) + + + + + +# Commands for Silhouette Cameo 3 + + + + +++ ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KEYDescription
00 - ffTwo hexadecimal digits, unquoted, are sent as is.
'…'Text inside quotes is a character string sent verbatim.
xxxX coordinate.1
yyyY coordinate.1
nPen number
dAuto Blade Depth
sSpeed
oBlade Offset
zunknown
fForce (0-33)
nnnFirst unknown purpose number.1
mmmSecond unknown purpose number.1
  
+ + + + +++ ++ ++ ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryCommand FormatResponse FormatDescription
Action'BE1' … 03n/aNew drawing command.2
Action'BE2' … 03n/aNew drawing command.
Action'M' xxx ',' yyy 03n/aMove selected pen to coordinates.
Auto Blade'FY' n 03n/aReset Auto Blade3
Auto Blade'TF' d ',' n 03n/aSet Auto Blade4
Get Info'FA' 03nnnnn ',' mmmmm 035Unknown: 2 five digit numbers.
Get Info'FG' 03'CAMEO 3 V1.50 ' 03Get Version: 17 Chars
Get Info'FQ0' 03nnnnn 03Unknown: 1 five digit number. Maybe last speed set?
Get Info'FQ2' 03nnnnn 03Unknown: 1 five digit number. Maybe last blade offset?
Get Info'TB71' 03nnnnn ',' mmmmm 035Unknown: 2 five digit numbers.
Get Info'U' 03xxxxxx ',' yyyyyy 03Get Lower Right Coordinates: 2 six digit numbers.
Get Info'[' 03xxxxxx ',' yyyyyy 03Get Upper Left Coords: 2 six digit numbers.
Set Config'!' s ',' n 03n/aSet Speed to 5 for Pen #1
Set Config'FC' o ',' z ',' n 03n/aSet Cutting Offset to 18 for Pen #1
Set Config'FX' f ',' n 03n/aSet Force to 33 for Pen #1
Set Config'Z,4288' 03n/aSet Lower Right Boundary
Set Config'\\30,30' 03n/aSet Upper Left Boundary
Set Pen'J' n 03n/aSelect Pen #N6
Status1b 0530 03Status? Ready
Status1b 0531 03Status? Moving
Status1b 0532 03Status? ???
Status1b 0533 03Status? Paused
Unknown'FN0' 03n/aUnknown
Unknown'L0' 03n/aLine type?7
Unknown'TB50,0' 03n/aUnknown
Unknown'TG1' 03n/aUnknown
+ + + + +# Units + + + + +++ ++ ++ + + + + + + + + + + + + + + + + + + + + + +
ummicrometer 
SUSilhouette UnitAll command coordinates are in Silhouette Units
inInches 
+ + 50 um = 1 SU + 508 SU = 1 in + 8.5 in = 4318 SU + 11 in = 5588 SU + + + + +# Examples + +All of the examples are using US Letter size paper (8.5 x 11 in OR 4318 x 5588 SU). + +All command strings end with ETX (0x03) unless otherwise specified. + + + + +++ ++ + + + + + + + + + + + + + + + + + + + + + + + + + +
OccuranceMeaning
Not FirstThis command does not appear on the first such sequence, but does appear on all subsequent sequences of the same type.
AllThis command appears on all squences of this type.
FirstThis command appears only on the first such sequence, and is absent from all subsequent sequences of the same type.
+ + + + +## Initialization (US Letter, Portrait) + + + + +++ ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TG1Unknown
FN0Unknown
TB50,0Unknown
\\30,30Write Upper Left - Sets the upper left to 1.5 mm from the true upper left corner.
Z5558,4288Write Lower Right - Sets the lowr right 1.5mm from the true lower right.
+ + + + +## Initialize Autoblade In Tool 1 + +Since an autoblade can only ever be in tool slot one, all pen numbers are always one. + + + + +++ ++ ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandOccuranceMeaning/Notes
L0Not FirstUnknown8
J1AllPen Select
FX33,1AllSet pen one force to 33.
!5,1AllSet pen one speed to 5.
FC0,1,1FirstSet pen one cutter offset to 0,1.
FC18,1,1Not FirstSet pen one cutter offset to 18,1.
FE0,1AllUnknown
FF1,0,1FirstOnly on first autoblade initalization: Unknown
FF1,1,1AllOn all autoblade initialization: Unknown
FC18,1,1AllSet pen one cutter offset to 0.9mm x 50 um.
FY1AllReset Blade to depth 109.
TF1,1AllSet pen one to depth 110.
+ +The initialization sequence is followed by a 'M' (move) command, then +by a 'BE[12]' command sequence that supposedly does the cutting. + + + + +## Initialize Ratchet Blade in Tool 2 + +This initialization sequence was after 6 autoblade initialization +sequences. + +TODO Run a capture of only ratchet blade initializations. + + + + +++ ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
L0Unknown8
J2Pen Select.
!4,2Set pen two speed to 4.11
FX20,2Set pen two force to 20.
FE0,2Unknown
FF1,0,2Unknown
FF1,1,2Unknown
FC0,1,2Set pen two cutter offset to 0.0mm x 50 um.
FC18,1,2Set pen two cutter offset to 0.9mm x 50 um.
+ +As with the autoblade, the initialization sequence is followed by an +'M' (move) command, then by a 'BE[12]' command sequence that +supposedly does the cutting. + + + + +## End Sequence + + + + +++ ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
L0Unknown8
\\0,0Write Upper Left : reset the upper left to zero,zero so that you can move to it.
M0,0Move to zero zero.
J0Select no pen.
FN0Unknown
TB50,0Unknown
+ + + + +## Status (Ready) + +Normal exchange when the device is ready. + + + + +++ ++ ++ + + + + + + + + + + + + + + +
0x1b 0x05 Query Status
 0x30 0x03Ready
+ + + + +## Status (Moving) + +Normal exchange when the device is executing the last command set given. + + + + +++ ++ ++ + + + + + + + + + + + + + + +
0x1b 0x05 Query Status
 0x31 0x03Moving
+ + + + +## Status (Pause) + +The Pause button has been pushed on the device, hold all subsequent commands. + +I believe that the device completes the last set of commands sent +before it acts on the pause button. + + + + +++ ++ ++ + + + + + + + + + + + + + + +
0x1b 0x05 Query Status
 0x33 0x03Paused
+ + + + +## Status (Cancel) + +The cancel button has been pushed on the device. Since the cancel +button is only available if the device has been paused, this triggers +an immediate reset sequence followed by a sequence that resembles the +normal startup sequence; but which contains … errors … For +example, the upper left coordinates are reported as 0,0 but the lower +right coordinates are reported as less than a US Letter portrait +oriented page. + + + + +## Startup Sequence From Power Up + + + + +++ ++ ++ + + + + + + + + + + + + + + +
0x1b 0x05 Query Status
 0x34 0x03Cancel
+ + +# Footnotes + +1 If there are three characters, the format is no padding. If +there are more than three characters, then the format is fixed with, +padded on the left with blanks. + +2 These new commands use nothing but binary to express the +drawing/cutting desired. I have, as yet, been unable to decypher +them. The old drawing commands appear to still work, so it's not +essential that these be decyphered. + +3 The CAMEO 3 moves the selected pen to a special spot (A) in the +machine, clicks it 10 times, then moves to a second special spot (B) +and clicks once. I believe that this resets the Auto Blade to 1. + +4 After "Reset Auto Blade", the CAMEO 3 moves the selected pen to +a special spot (A) and clicks it 10 - N times, where N is the first +number in the command. + +5 The response seems to always be 0,0. + +6 Observed on Cameo 3, 0=no pen, 1=first pen, 2=second pen. + +7 Older documentation claims that this is line type. I don't +think that's what it means for Cameo 3. Unless "line type" means +something other than what you draw with a pen. Cameo 3 does dashed +lines by sending pairs of move and draw commands. + +8 This is absent on the first initialization of the autoblade, +but present on all subsequent blade initializations, including the +ratchet blade. + +9 The Cameo 3 always does ten clicks with an autoblade in one +position, followed by another click in a different position. This +resets the blade to 10. + +10 After resetting the autoblade, the Cameo 3 always does 10 - N +clicks to set the depth to N. This means the reset must guarantee +that the autoblade is set to 10, since each subsequent click reduces +the depth by one. + +11 Note that speed and force are reversed compared to autoblade. + + diff --git a/doc/more_commands.txt b/doc/more_commands.txt new file mode 100644 index 00000000..5cb8ab5f --- /dev/null +++ b/doc/more_commands.txt @@ -0,0 +1,25 @@ +``` +"FW%d\x03" % media # select media id [100..300] +"!%d\x03" % speed # without ',n' in CAMEO 1 +"FX%d\x03" % pressure # without ',n' in CAMEO 1 + # pressure 19 or higher triggers track enhancing in CAMEO 1 +"FY0\x03" # ?enable track enhancing in CAMEO 1 +"FY1\x03" # ?disable track enhancing in CAMEO 1 +"FN0\x03TB50,1\x03" # landscape mode in CAMEO 1 +"FN0\x03TB50,0\x03" # portrait mode in CAMEO 1 +"FE0,0\x03" # Don't lift plotter head between paths. +"D%d,%d" % (y,x) # draw, move with blade down. +"TB50,0\x03" # Unknown. Seen with registration marks +"TB99\x03" # Unknown +"TB52,2\x03" # set type of regmarks: 0='Original,SD', 2='Cameo,Portrait' +"TB51,400\x03" # length of regmarks +"TB53,10\x03" # width of regmarks +"TB55,1\x03" # Unknown +"TB123,%i,%i,%i,%i\x03" % ... # automatic regmark test, height, width, top, left +"TB23,%i,%i\x03" % ... # manual regmark, height, width +"FO%d\x03" % (height-top)) # ? a feed command. Sometimes it is 5588 +``` + +There is some more guesswork in the comments near https://github.com/fablabnbg/inkscape-silhouette/blob/master/silhouette/Graphtec.py#L868 + +https://github.com/fablabnbg/inkscape-silhouette/blob/master/silhouette/Graphtec.py#L868 diff --git a/examples/default_us_letter_landscape_cameo_3.svg b/examples/default_us_letter_landscape_cameo_3.svg new file mode 100644 index 00000000..7eb90a38 --- /dev/null +++ b/examples/default_us_letter_landscape_cameo_3.svg @@ -0,0 +1,123 @@ + + + + + US Letter Landscape For Silhouette Cameo 3 and InkScape Silhouette Extension + + + + + + + + + + + + image/svg+xml + + US Letter Landscape For Silhouette Cameo 3 and InkScape Silhouette Extension + This default document is designed for use with the InkScape Silhouette extension. The guides define the useable space, allowing a 5mm edge around a standard US letter size piece of paper, in landscape mode. + +The page size, as far as InkScape is concerned, is approximately 20mm larger in both directions. This allows for an issue with the silhouette extension where the bottom edge can be cut off. + +In short, keep your cuts inside the lines, and you won't get hurt. Stray outside, and your cuts are likely to get lost. + 2018-05-15 + + + Kedneck + + + + + http://creativecommons.org/licenses/by/4.0/ + + + English + + + Template + Silhouette + Cameo + Inkscape + + + + + + + + + + + + + + + diff --git a/examples/registration-marks-cameo-silhouette-a4-maxi.svg b/examples/registration-marks/inkscape-0.91/registration-marks-cameo-silhouette-a4-maxi.svg similarity index 100% rename from examples/registration-marks-cameo-silhouette-a4-maxi.svg rename to examples/registration-marks/inkscape-0.91/registration-marks-cameo-silhouette-a4-maxi.svg diff --git a/examples/registration-marks-cameo-silhouette.svg b/examples/registration-marks/inkscape-0.91/registration-marks-cameo-silhouette.svg similarity index 100% rename from examples/registration-marks-cameo-silhouette.svg rename to examples/registration-marks/inkscape-0.91/registration-marks-cameo-silhouette.svg diff --git a/examples/registration-marks-robo-landscape.svg b/examples/registration-marks/inkscape-0.91/registration-marks-robo-landscape.svg similarity index 100% rename from examples/registration-marks-robo-landscape.svg rename to examples/registration-marks/inkscape-0.91/registration-marks-robo-landscape.svg diff --git a/examples/registration-marks-robo-portrait.svg b/examples/registration-marks/inkscape-0.91/registration-marks-robo-portrait.svg similarity index 100% rename from examples/registration-marks-robo-portrait.svg rename to examples/registration-marks/inkscape-0.91/registration-marks-robo-portrait.svg diff --git a/examples/registration-marks/registration-marks-cameo-silhouette-a4.svg b/examples/registration-marks/registration-marks-cameo-silhouette-a4.svg new file mode 100644 index 00000000..818b08d6 --- /dev/null +++ b/examples/registration-marks/registration-marks-cameo-silhouette-a4.svg @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + 277mm + 190mm + 10mm + 10mm + + + + diff --git a/logs/silhouette_cameo3_12x24_cutting_mat.pcapng.gz b/logs/silhouette_cameo3_12x24_cutting_mat.pcapng.gz new file mode 100644 index 00000000..b7844c51 Binary files /dev/null and b/logs/silhouette_cameo3_12x24_cutting_mat.pcapng.gz differ diff --git a/logs/silhouette_cameo3_autoblade_depth5.pcapng.gz b/logs/silhouette_cameo3_autoblade_depth5.pcapng.gz new file mode 100644 index 00000000..d84345fc Binary files /dev/null and b/logs/silhouette_cameo3_autoblade_depth5.pcapng.gz differ diff --git a/logs/silhouette_cameo3_default_settings.pcapng.gz b/logs/silhouette_cameo3_default_settings.pcapng.gz new file mode 100644 index 00000000..ea35f088 Binary files /dev/null and b/logs/silhouette_cameo3_default_settings.pcapng.gz differ diff --git a/logs/silhouette_cameo3_line_segment_overcut.pcapng.gz b/logs/silhouette_cameo3_line_segment_overcut.pcapng.gz new file mode 100644 index 00000000..72d7a69a Binary files /dev/null and b/logs/silhouette_cameo3_line_segment_overcut.pcapng.gz differ diff --git a/logs/silhouette_cameo3_line_segment_overcut_0.9mm.pcapng.gz b/logs/silhouette_cameo3_line_segment_overcut_0.9mm.pcapng.gz new file mode 100644 index 00000000..8a82b970 Binary files /dev/null and b/logs/silhouette_cameo3_line_segment_overcut_0.9mm.pcapng.gz differ diff --git a/logs/silhouette_cameo3_max_speed_max_force.pcapng.gz b/logs/silhouette_cameo3_max_speed_max_force.pcapng.gz new file mode 100644 index 00000000..d6c359f0 Binary files /dev/null and b/logs/silhouette_cameo3_max_speed_max_force.pcapng.gz differ diff --git a/logs/silhouette_cameo3_no_cutting_mat.pcapng.gz b/logs/silhouette_cameo3_no_cutting_mat.pcapng.gz new file mode 100644 index 00000000..52fff7c7 Binary files /dev/null and b/logs/silhouette_cameo3_no_cutting_mat.pcapng.gz differ diff --git a/logs/silhouette_cameo3_pen.pcapng.gz b/logs/silhouette_cameo3_pen.pcapng.gz new file mode 100644 index 00000000..8513ce93 Binary files /dev/null and b/logs/silhouette_cameo3_pen.pcapng.gz differ diff --git a/logs/silhouette_cameo3_ratchet_blade.pcapng.gz b/logs/silhouette_cameo3_ratchet_blade.pcapng.gz new file mode 100644 index 00000000..309a0614 Binary files /dev/null and b/logs/silhouette_cameo3_ratchet_blade.pcapng.gz differ diff --git a/logs/silhouette_cameo3_tool_2.pcapng.gz b/logs/silhouette_cameo3_tool_2.pcapng.gz new file mode 100644 index 00000000..8862bd63 Binary files /dev/null and b/logs/silhouette_cameo3_tool_2.pcapng.gz differ diff --git a/logs/silhouette_cameo3_track_enhancing.pcapng.gz b/logs/silhouette_cameo3_track_enhancing.pcapng.gz new file mode 100644 index 00000000..fa8dce08 Binary files /dev/null and b/logs/silhouette_cameo3_track_enhancing.pcapng.gz differ diff --git a/sendto_silhouette.inx b/sendto_silhouette.inx index 729dc1d4..2ecbcc6c 100644 --- a/sendto_silhouette.inx +++ b/sendto_silhouette.inx @@ -7,26 +7,34 @@ sendto_silhouette.py - 0.0 - 0.0 + 0.0 + 0.0 + + Cameo 12x12 + Cameo 12x24 + None + + + Red (left tool) + Blue (right tool) + Media default Pen Cut + AutoBlade - 'pen' executes the strokes exactly as sent, 'cut' adds small serifs to help the knive find its orientation at corners. -Pressure values of 19 or more make the machine misbehave. Beware. - + 'pen' executes the strokes exactly as sent, 'cut' adds small serifs to help the knive find its orientation at corners. [P>0 S>0] Custom: use non-zero Pressure and Speed below [P=27,S=10] SCard without Craft Paper Backing [P=27,S=10] Card with Craft Paper Backing - [P=10,S=10] Vinyl Sticker + [P=10,S= 5,D=1] Vinyl Sticker [P=14,S=10] Film Labels [P=27,S=10] Thick Media [P= 2,S=10] Thin Media - [P=10,S=10] Pen + [P=18,S=10] Pen [P=30,S=10] Bond Paper 13-28 lbs (105g) [P=30,S=10] Bristol Paper 57-67 lbs (145g) [P=30,S=10] Cardstock 40-60 lbs (90g) @@ -48,13 +56,15 @@ Pressure values of 19 or more make the machine misbehave. Beware. [P=30,S=10] Writing Paper 24-70 lbs (105g) 0 - 0 - Use speed=0, pressure=0 to take the media defaults. + 0 + -1 + Use speed=0, pressure=0, depth=-1 to take the media defaults. Pressure values of 19 or more could trigger the trackenhancing feature, which means a movement along the full media height before start. Beware. + + false Convert paths with dashed strokes to separate subpaths for perforated cuts. false Shift to the top lefthand corner, then do offsets. false To see the used area, tick the checkmark above and use pressure=1 (or better remove tool) - 0.5 1 false @@ -79,6 +89,10 @@ Pressure values of 19 or more make the machine misbehave. Beware. false Keep dialog open until device becomes idle again. + false Lift head at sharp corners + 0.1 + 0.1 + 0.5 Z-Order Without mat @@ -89,12 +103,13 @@ Pressure values of 19 or more make the machine misbehave. Beware. Without mat: Subdivide, sort, and choose cut directions, so that a cutting mat is not needed in most cases. Minimal Traveling: Find the nearest startpoint to minimize travel movements Minimal Traveling (fully optimized): Additionally search startpoints in closed paths + true false Used for debugging and developent only! - + Always use the least amount of blade possible. 1) Take a sheet of the media you are trying to cut and fold it in half. @@ -109,12 +124,13 @@ Always use the least amount of blade possible. 6) Once this is done the blade can be put back in to the machine. + 0.9 + Correct value for the silhouette blade is 0.9mm - - inkscape-silhouette extension from https://github.com/jnweiger/inkscape-silhouette by Jürgen Weigert [juewei@fabmail.org] and contributors + inkscape-silhouette extension from https://github.com/jnweiger/inkscape-silhouette by Jürgen Weigert [juergen@fabmail.org] and contributors - Version 1.19 + Version 1.21 diff --git a/sendto_silhouette.py b/sendto_silhouette.py index d76b26cc..b1a5f877 100644 --- a/sendto_silhouette.py +++ b/sendto_silhouette.py @@ -72,22 +72,25 @@ # Added misc/silhouette_move.py misc/silhouette_cut.py, misc/endless_clock.py # 2016-01-15 jw, v1.15 -- ubuntu loads the wrong usb library. # 2016-05-15 jw, v1.16 -- merged regmarks code from https://github.com/fablabnbg/inkscape-silhouette/pull/23 -# 2016-05-17 jw, v1.17 -- fix avoid dev.reset in Graphtec.py, fix helps with +# 2016-05-17 jw, v1.17 -- fix avoid dev.reset in Graphtec.py, fix helps with # https://github.com/fablabnbg/inkscape-silhouette/issues/10 # 2016-05-21 jw, v1.18 -- warn about python-usb < 1.0 and give instructions. -# Limit pressure to 18. 19 or 20 make the machine +# Limit pressure to 18. 19 or 20 make the machine # scroll forward backward for several minutes. # Support document unit inches. https://github.com/fablabnbg/inkscape-silhouette/issues/19 # 2016-12-18, jw, v1.19 -- support for dashed lines added. Thanks to mehtank # https://github.com/fablabnbg/inkscape-silhouette/pull/33 # Added new cutting strategy "Minimized Traveling" -# 2019-08-03, jw, v1.22 - merged cameo3 autoblase code, added a copy of pyusb-1.0.2 to be used as a -# fallback on any platform. +# Added parameter for blade diameter +# 2018-06-01, jw, v1.20 -- Make it compile again. Hmm. +# 2019-07-25, jw, v1.21 -- merge from github.com/olegdeezus/inkscape-silhouette +# merge from fablabnbg +# 2019-08-03, jw, v1.22 - added a copy of pyusb-1.0.2 as a fallback on any platform. __version__ = '1.22' # Keep in sync with sendto_silhouette.inx ca line 79 -__author__ = 'Juergen Weigert and contributors' +__author__ = 'Juergen Weigert and contributors' -import sys, os, shutil, time, logging, tempfile +import sys, os, shutil, time, logging, tempfile, math, re # we sys.path.append() the directory where this @@ -126,17 +129,20 @@ from silhouette.Strategy import MatFree from silhouette.convert2dashes import splitPath import silhouette.StrategyMinTraveling +from silhouette.Geometry import dist_sq, XY_a N_PAGE_WIDTH = 3200 N_PAGE_HEIGHT = 800 +IDENTITY_TRANSFORM = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] + def px2mm(px): ''' Convert inkscape pixels to mm. - The default inkscape unit, called 'px' is 90dpi + The default inkscape unit, called 'px' is 96dpi ''' - return px*25.4/90 + return px*25.4/96 # Lifted with impunity from eggbot.py # Added all known inkscape units. https://github.com/fablabnbg/inkscape-silhouette/issues/19 @@ -270,6 +276,15 @@ def __init__(self): self.OptionParser.add_option('-b', '--bbox', '--bbox-only', '--bbox_only', action = 'store', dest = 'bboxonly', type = 'inkbool', default = False, help='draft the objects bounding box instead of the objects') + self.OptionParser.add_option('-c', '--bladediameter', + action = 'store', dest = 'bladediameter', type = 'float', default = 0.9, + help="[0..2.3] diameter of the used blade [mm], default = 0.9") + self.OptionParser.add_option('-C', '--cuttingmat', action = 'store', + choices=('cameo_12x12', 'cameo_12x24', 'no_mat'), dest = 'cuttingmat', default = 'cameo_12x12', + help='Use cutting mat') + self.OptionParser.add_option('-D', '--depth', + action = 'store', dest = 'depth', type = 'int', default = -1, + help="[0..10], or -1 for media default") self.OptionParser.add_option('--dummy', action = 'store', dest = 'dummy', type = 'inkbool', default = False, help="Dump raw data to "+self.dumpname+" instead of cutting.") @@ -277,6 +292,9 @@ def __init__(self): action = 'store', dest = 'strategy', default = 'mintravel', choices=('mintravel','mintravelfull','matfree','zorder' ), help="Cutting Strategy: mintravel, mintravelfull, matfree or zorder") + self.OptionParser.add_option('-l', '--sw_clipping', + action = 'store', dest = 'sw_clipping', type = 'inkbool', default = True, + help='Enable software clipping') self.OptionParser.add_option('-m', '--media', '--media-id', '--media_id', action = 'store', dest = 'media', default = '132', choices=('100','101','102','106','111','112','113', @@ -292,6 +310,15 @@ def __init__(self): self.OptionParser.add_option('-p', '--pressure', action = 'store', dest = 'pressure', type = 'int', default = 10, help="[1..18], or 0 for media default") + self.OptionParser.add_option('-P', '--sharpencorners', + action = 'store', dest = 'sharpencorners', type = 'inkbool', default = False, + help='Lift head at sharp corners') + self.OptionParser.add_option('--sharpencorners_start', + action = 'store', dest = 'sharpencorners_start', type = 'float', default = 0.1, + help="Sharpen Corners - Start Ext. [mm]") + self.OptionParser.add_option('--sharpencorners_end', + action = 'store', dest = 'sharpencorners_end', type = 'float', default = 0.1, + help="Sharpen Corners - End Ext. [mm]") self.OptionParser.add_option('-r', '--reversetoggle', action = 'store', dest = 'reversetoggle', type = 'inkbool', default = False, help="Cut each path the other direction. Affects every second pass when multipass.") @@ -301,7 +328,9 @@ def __init__(self): self.OptionParser.add_option( "-S", "--smoothness", action="store", type="float", dest="smoothness", default=.2, help="Smoothness of curves" ) self.OptionParser.add_option('-t', '--tool', action = 'store', - choices=('cut', 'pen','default'), dest = 'tool', default = None, help="Optimize for pen or knive") + choices=('autoblade', 'cut', 'pen','default'), dest = 'tool', default = None, help="Optimize for pen or knive") + self.OptionParser.add_option('-T', '--toolholder', action = 'store', + choices=('1', '2'), dest = 'toolholder', default = None, help="[1..2]") self.OptionParser.add_option('-V', '--version', action = 'store_const', const=True, dest = 'version', default = False, help='Just print version number ("'+__version__+'") and exit.') @@ -452,8 +481,8 @@ def DoWePlotLayer( self, strLayerName ): def recursivelyTraverseSvg( self, aNodeList, - matCurrent=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], - parent_visibility='visible' ): + parent_visibility='visible', + extra_transform=IDENTITY_TRANSFORM ): """ Recursively traverse the svg file to plot out all of the paths. The function keeps track of the composite transformation @@ -469,14 +498,23 @@ def recursivelyTraverseSvg( self, aNodeList, for node in aNodeList: # Ignore invisible nodes - v = node.get( 'visibility', parent_visibility ) + v = None + style = node.get('style') + if style is not None: + kvs = {k.strip():v.strip() for k,v in [x.split(':', 1) for x in style.split(';')]} + if 'display' in kvs and kvs['display'] == 'none': + v = 'hidden' + if v is None: + v = node.get( 'visibility', parent_visibility ) if v == 'inherit': v = parent_visibility if v == 'hidden' or v == 'collapse': - pass + continue - # first apply the current matrix transform to this node's tranform - matNew = composeTransform( matCurrent, parseTransform( node.get( "transform" ) ) ) + # calculate this object's transform + transform = composeParents(node, IDENTITY_TRANSFORM) + transform = composeTransform(self.docTransform, transform) + transform = composeTransform(extra_transform, transform) if node.tag == inkex.addNS( 'g', 'svg' ) or node.tag == 'g': @@ -490,7 +528,7 @@ def recursivelyTraverseSvg( self, aNodeList, if not self.allLayers: # inkex.errormsg('Plotting layer named: ' + node.get(inkex.addNS('label', 'inkscape'))) self.DoWePlotLayer( node.get( inkex.addNS( 'label', 'inkscape' ) ) ) - self.recursivelyTraverseSvg( node, matNew, parent_visibility=v ) + self.recursivelyTraverseSvg( node, parent_visibility=v ) elif node.tag == inkex.addNS( 'use', 'svg' ) or node.tag == 'use': @@ -517,11 +555,9 @@ def recursivelyTraverseSvg( self, aNodeList, y = float( node.get( 'y', '0' ) ) # Note: the transform has already been applied if ( x != 0 ) or (y != 0 ): - matNew2 = composeTransform( matNew, parseTransform( 'translate(%f,%f)' % (x,y) ) ) - else: - matNew2 = matNew + transform = composeTransform( transform, parseTransform( 'translate(%f,%f)' % (x,y) ) ) v = node.get( 'visibility', v ) - self.recursivelyTraverseSvg( refnode, matNew2, parent_visibility=v ) + self.recursivelyTraverseSvg( refnode, parent_visibility=v, extra_transform=transform ) else: pass else: @@ -544,7 +580,7 @@ def recursivelyTraverseSvg( self, aNodeList, if self.resumeMode and ( self.pathcount < self.svgLastPath ): pass else: - self.plotPath( node, matNew ) + self.plotPath( node, transform ) if ( not self.bStopped ): #an "index" for resuming plots quickly-- record last complete path self.svgLastPath += 1 self.svgLastPathNC = self.nodeCount @@ -592,7 +628,7 @@ def recursivelyTraverseSvg( self, aNodeList, a.append( [' l ', [-w, 0]] ) a.append( [' Z', []] ) newpath.set( 'd', simplepath.formatPath( a ) ) - self.plotPath( newpath, matNew ) + self.plotPath( newpath, transform ) elif node.tag == inkex.addNS( 'line', 'svg' ) or node.tag == 'line': @@ -632,7 +668,7 @@ def recursivelyTraverseSvg( self, aNodeList, a.append( ['M ', [x1, y1]] ) a.append( [' L ', [x2, y2]] ) newpath.set( 'd', simplepath.formatPath( a ) ) - self.plotPath( newpath, matNew ) + self.plotPath( newpath, transform ) if ( not self.bStopped ): #an "index" for resuming plots quickly-- record last complete path self.svgLastPath += 1 self.svgLastPathNC = self.nodeCount @@ -683,7 +719,7 @@ def recursivelyTraverseSvg( self, aNodeList, t = node.get( 'transform' ) if t: newpath.set( 'transform', t ) - self.plotPath( newpath, matNew ) + self.plotPath( newpath, transform ) if ( not self.bStopped ): #an "index" for resuming plots quickly-- record last complete path self.svgLastPath += 1 self.svgLastPathNC = self.nodeCount @@ -735,7 +771,7 @@ def recursivelyTraverseSvg( self, aNodeList, t = node.get( 'transform' ) if t: newpath.set( 'transform', t ) - self.plotPath( newpath, matNew ) + self.plotPath( newpath, transform ) if ( not self.bStopped ): #an "index" for resuming plots quickly-- record last complete path self.svgLastPath += 1 self.svgLastPathNC = self.nodeCount @@ -799,7 +835,7 @@ def recursivelyTraverseSvg( self, aNodeList, t = node.get( 'transform' ) if t: newpath.set( 'transform', t ) - self.plotPath( newpath, matNew ) + self.plotPath( newpath, transform ) if ( not self.bStopped ): #an "index" for resuming plots quickly-- record last complete path self.svgLastPath += 1 self.svgLastPathNC = self.nodeCount @@ -899,11 +935,15 @@ def getLength( self, name, default ): elif ( u == '' ) or ( u == 'px' ): return v elif u == 'mm': - return v*90./25.4 # inverse of px2mm + return v*96./25.4 # inverse of px2mm elif u == 'in': - return v*90. + return v*96. elif u == 'cm': - return v*90./2.54 # inverse of 10*px2mm + return v*96./2.54 # inverse of 10*px2mm + elif u == 'pt': + return v*96./72. + elif u == 'pc': + return v*96./16. elif u == '%': return float( default ) * v / 100.0 else: @@ -949,6 +989,9 @@ def handleViewBox( self ): self.docTransform = parseTransform( 'scale(%f,%f)' % (sx, sy) ) + def is_closed_path(self, path): + return dist_sq(XY_a(path[0]), XY_a(path[-1])) < 0.01 + def effect(self): if self.options.version: print __version__ @@ -983,17 +1026,28 @@ def write_progress(done, total, msg): if self.options.ids: # Traverse the selected objects for id in self.options.ids: - self.recursivelyTraverseSvg( [self.selected[id]], self.docTransform ) + self.recursivelyTraverseSvg( [self.selected[id]] ) else: # Traverse the entire document - self.recursivelyTraverseSvg( self.document.getroot(), self.docTransform ) + self.recursivelyTraverseSvg( self.document.getroot() ) + if self.options.toolholder is not None: + self.options.toolholder = int(self.options.toolholder) self.pen=None + self.autoblade=False if self.options.tool == 'pen': self.pen=True if self.options.tool == 'cut': self.pen=False + if self.options.tool == 'autoblade': + self.pen=False + self.autoblade=True + + # scale all points to unit mm + for path in self.paths: + for i,pt in enumerate(path): + path[i] = (px2mm(pt[0]), px2mm(pt[1])) if self.options.strategy == 'matfree': - mf = MatFree('default', scale=px2mm(1.0), pen=self.pen) + mf = MatFree('default', scale=1.0, pen=self.pen) mf.verbose = 0 # inkscape crashes whenever something appears in stdout. self.paths = mf.apply(self.paths) elif self.options.strategy == 'mintravel': @@ -1005,32 +1059,41 @@ def write_progress(done, total, msg): # print >>self.tty, self.paths cut = [] pointcount = 0 - for px_path in self.paths: - mm_path = [] - for pt in px_path: - if self.options.strategy == 'matfree': - mm_path.append((pt[0], pt[1])) # MatFree.load() did the scaling already. - else: - mm_path.append((px2mm(pt[0]), px2mm(pt[1]))) - pointcount += 1 + for mm_path in self.paths: + pointcount += len(mm_path) multipath = [] multipath.extend(mm_path) + for i in range(1,self.options.multipass): # if reverse continue path without lifting, instead turn with rotating knife if (self.options.reversetoggle): mm_path = list(reversed(mm_path)) multipath.extend(mm_path[1:]) # if closed path (end = start) continue path without lifting - elif (mm_path[0] == mm_path[-1]): + elif self.is_closed_path(mm_path): multipath.extend(mm_path[1:]) # else start a new path - else: + else: cut.append(mm_path) # on a closed path some overlapping doesn't harm, limited to a maximum of one additional round overcut = self.options.overcut - if (overcut > 0) and (mm_path[0] == mm_path[-1]): + if (overcut > 0) and self.is_closed_path(mm_path): + precut = overcut + pfrom = mm_path[-1] + for pprev in reversed(mm_path[:-1]): + dx = pprev[0] - pfrom[0] + dy = pprev[1] - pfrom[1] + dist = math.sqrt(dx*dx + dy*dy) + if (precut > dist): # Full segment needed + precut -= dist + multipath.insert(0, pprev) + pfrom = pprev + else: # only partial segement needed, create new endpoint + pprev = (pfrom[0]+dx*(precut/dist), pfrom[1]+dy*(precut/dist)) + multipath.insert(0, pprev) + break pfrom = mm_path[0] for pnext in mm_path[1:]: dx = pnext[0] - pfrom[0] @@ -1069,7 +1132,17 @@ def write_progress(done, total, msg): if self.options.pressure == 0: self.options.pressure = None if self.options.speed == 0: self.options.speed = None + if self.options.depth == -1: self.options.depth = None dev.setup(media=int(self.options.media,10), pen=self.pen, + toolholder=self.options.toolholder, + cuttingmat=self.options.cuttingmat, + sharpencorners=self.options.sharpencorners, + sharpencorners_start=self.options.sharpencorners_start, + sharpencorners_end=self.options.sharpencorners_end, + autoblade=self.autoblade, + depth=self.options.depth, + sw_clipping=self.options.sw_clipping, + bladediameter=self.options.bladediameter, pressure=self.options.pressure, speed=self.options.speed) if self.options.autocrop: @@ -1149,15 +1222,15 @@ def write_progress(done, total, msg): e = SendtoSilhouette() if len(sys.argv) < 2: - # write a tempfile that is autoremoved on exit - tmpfile=tempfile.NamedTemporaryFile(suffix='.svg', prefix='inkscape-silhouette') - sys.argv.append(tmpfile.name) - print sys.argv - print >>tmpfile, '' - tmpfile.flush() + # write a tempfile that is autoremoved on exit + tmpfile=tempfile.NamedTemporaryFile(suffix='.svg', prefix='inkscape-silhouette') + sys.argv.append(tmpfile.name) + print sys.argv + print >>tmpfile, '' + tmpfile.flush() e.affect(sys.argv[1:]) - # os.remove(tmpfile.name) - sys.exit(0) + # os.remove(tmpfile.name) + sys.exit(0) start = time.time() e.affect() diff --git a/silhouette-udev.rules b/silhouette-udev.rules index c186a714..91d2fb89 100644 --- a/silhouette-udev.rules +++ b/silhouette-udev.rules @@ -4,14 +4,16 @@ OPTIONS:="last_rule" ATTRS{idVendor}=="0b4d", ATTRS{idProduct}=="112f", MODE="666", ENV{silhouette_cameo3}="yes", OPTIONS="last_rule" ATTRS{idVendor}=="0b4d", ATTRS{idProduct}=="1121", MODE="666", ENV{silhouette_cameo}="yes" ATTRS{idVendor}=="0b4d", ATTRS{idProduct}=="1123", MODE="666", ENV{silhouette_portrait}="yes" +ATTRS{idVendor}=="0b4d", ATTRS{idProduct}=="1132", MODE="666", ENV{silhouette_portrait2}="yes" ATTRS{idVendor}=="0b4d", ATTRS{idProduct}=="111d", MODE="666", ENV{silhouette_sd_2}="yes" ATTRS{idVendor}=="0b4d", ATTRS{idProduct}=="111c", MODE="666", ENV{silhouette_sd_1}="yes" ATTRS{idVendor}=="0b4d", ATTRS{idProduct}=="110a", MODE="666", ENV{craftrobo_cc200_20}="yes" ATTRS{idVendor}=="0b4d", ATTRS{idProduct}=="111a", MODE="666", ENV{craftrobo_cc300_20}="yes" -ENV{silhouette_cameo}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh", OPTIONS:="lastrule" -ENV{silhouette_portrait}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh" -ENV{silhouette_sd_2}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh" -ENV{silhouette_sd_1}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh" -ENV{craftrobo_cc200_20}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh" -ENV{craftrobo_cc300_20}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh" +ENV{silhouette_cameo}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh", OPTIONS:="lastrule" +ENV{silhouette_portrait}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh" +ENV{silhouette_portrait2}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh" +ENV{silhouette_sd_2}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh" +ENV{silhouette_sd_1}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh" +ENV{craftrobo_cc200_20}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh" +ENV{craftrobo_cc300_20}=="yes", RUN="/lib/udev/silhouette-udev-notify.sh" diff --git a/silhouette/Graphtec.py b/silhouette/Graphtec.py index 870e7905..8267a53e 100644 --- a/silhouette/Graphtec.py +++ b/silhouette/Graphtec.py @@ -21,10 +21,15 @@ # from __future__ import print_function -import os, sys, time, re -usb_reset_needed = False # https://github.com/fablabnbg/inkscape-silhouette/issues/10 -sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/pyusb-1.0.2') # advertise a builtin copy of pyusb. +import os +import re +import sys +import time + +usb_reset_needed = False # https://github.com/fablabnbg/inkscape-silhouette/issues/10 + +sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/pyusb-1.0.2') # have a pyusb fallback sys_platform = sys.platform.lower() if sys_platform.startswith('win'): @@ -34,19 +39,19 @@ usb1ctx = usb1.USBContext() else: # if sys_platform.startswith('linux'): try: - import usb.core # where??? + import usb.core # where??? except Exception as e: try: import libusb1 as usb except Exception as e1: - try: - import usb - except Exception as e2: - print("The python usb module could not be found. Try", file=sys.stderr) - print("\t sudo zypper in python-usb \t\t# if you run SUSE", file=sys.stderr) - print("\t sudo apt-get install python-usb \t\t# if you run Ubuntu", file=sys.stderr) - print("\n\n\n", file=sys.stderr) - raise e2; + try: + import usb + except Exception as e2: + print("The python usb module could not be found. Try", file=sys.stderr) + print("\t sudo zypper in python-usb \t\t# if you run SUSE", file=sys.stderr) + print("\t sudo apt-get install python-usb \t\t# if you run Ubuntu", file=sys.stderr) + print("\n\n\n", file=sys.stderr) + raise e2; try: try: @@ -54,8 +59,12 @@ usb_vi_str = str(usb.version_info) except AttributeError: usb_vi = 0 + if sys_platform.startswith('win'): + usb_vi = 1 + pass # windows does not seem to detect the usb.version , gives attribute error. Other tests of pyusb work, pyusb is installed. usb_vi_str = 'unknown' + if usb_vi < 1: print("Your python usb module appears to be "+usb_vi_str+" -- We need version 1.x", file=sys.stderr) print("For Debian 8 try:\n echo > /etc/apt/sources.list.d/backports.list 'deb http://ftp.debian.org debian jessie-backports main\n apt-get update\n apt-get -t jessie-backports install python-usb", file=sys.stderr) @@ -73,34 +82,34 @@ MEDIA = [ # CAUTION: keep in sync with sendto_silhouette.inx -# media, pressure, speed, cap-color, name - ( 100, 27, 10, "yellow", "Card without Craft Paper Backing"), - ( 101, 27, 10, "yellow", "Card with Craft Paper Backing"), - ( 102, 10, 10, "blue", "Vinyl Sticker"), - ( 106, 14, 10, "blue", "Film Labels"), - ( 111, 27, 10, "yellow", "Thick Media"), - ( 112, 2, 10, "blue", "Thin Media"), - ( 113, 10, 10, "pen", "Pen"), - ( 120, 30, 10, "blue", "Bond Paper 13-28 lbs (105g)"), - ( 121, 30, 10, "yellow", "Bristol Paper 57-67 lbs (145g)"), - ( 122, 30, 10, "yellow", "Cardstock 40-60 lbs (90g)"), - ( 123, 30, 10, "yellow", "Cover 40-60 lbs (170g)"), - ( 124, 1, 10, "blue", "Film, Double Matte Translucent"), - ( 125, 1, 10, "blue", "Film, Vinyl With Adhesive Back"), - ( 126, 1, 10, "blue", "Film, Window With Kling Adhesive"), - ( 127, 30, 10, "red", "Index 90 lbs (165g)"), - ( 128, 20, 10, "yellow", "Inkjet Photo Paper 28-44 lbs (70g)"), - ( 129, 27, 10, "red", "Inkjet Photo Paper 45-75 lbs (110g)"), - ( 130, 30, 3, "red", "Magnetic Sheet"), - ( 131, 30, 10, "blue", "Offset 24-60 lbs (90g)"), - ( 132, 5, 10, "blue", "Print Paper Light Weight"), - ( 133, 25, 10, "yellow", "Print Paper Medium Weight"), - ( 134, 20, 10, "blue", "Sticker Sheet"), - ( 135, 20, 10, "red", "Tag 100 lbs (275g)"), - ( 136, 30, 10, "blue", "Text Paper 24-70 lbs (105g)"), - ( 137, 30, 10, "yellow", "Vellum Bristol 57-67 lbs (145g)"), - ( 138, 30, 10, "blue", "Writing Paper 24-70 lbs (105g)"), - ( 300, None, None, "custom", "Custom"), +# media, pressure, speed, depth, cap-color, name + ( 100, 27, 10, 1, "yellow", "Card without Craft Paper Backing"), + ( 101, 27, 10, 1, "yellow", "Card with Craft Paper Backing"), + ( 102, 10, 5, 1, "blue", "Vinyl Sticker"), + ( 106, 14, 10, 1, "blue", "Film Labels"), + ( 111, 27, 10, 1, "yellow", "Thick Media"), + ( 112, 2, 10, 1, "blue", "Thin Media"), + ( 113, 18, 10,None, "pen", "Pen"), + ( 120, 30, 10, 1, "blue", "Bond Paper 13-28 lbs (105g)"), + ( 121, 30, 10, 1, "yellow", "Bristol Paper 57-67 lbs (145g)"), + ( 122, 30, 10, 1, "yellow", "Cardstock 40-60 lbs (90g)"), + ( 123, 30, 10, 1, "yellow", "Cover 40-60 lbs (170g)"), + ( 124, 1, 10, 1, "blue", "Film, Double Matte Translucent"), + ( 125, 1, 10, 1, "blue", "Film, Vinyl With Adhesive Back"), + ( 126, 1, 10, 1, "blue", "Film, Window With Kling Adhesive"), + ( 127, 30, 10, 1, "red", "Index 90 lbs (165g)"), + ( 128, 20, 10, 1, "yellow", "Inkjet Photo Paper 28-44 lbs (70g)"), + ( 129, 27, 10, 1, "red", "Inkjet Photo Paper 45-75 lbs (110g)"), + ( 130, 30, 3, 1, "red", "Magnetic Sheet"), + ( 131, 30, 10, 1, "blue", "Offset 24-60 lbs (90g)"), + ( 132, 5, 10, 1, "blue", "Print Paper Light Weight"), + ( 133, 25, 10, 1, "yellow", "Print Paper Medium Weight"), + ( 134, 20, 10, 1, "blue", "Sticker Sheet"), + ( 135, 20, 10, 1, "red", "Tag 100 lbs (275g)"), + ( 136, 30, 10, 1, "blue", "Text Paper 24-70 lbs (105g)"), + ( 137, 30, 10, 1, "yellow", "Vellum Bristol 57-67 lbs (145g)"), + ( 138, 30, 10, 1, "blue", "Writing Paper 24-70 lbs (105g)"), + ( 300, None, None,None, "custom", "Custom"), ] # robocut/Plotter.h:53 ff @@ -110,28 +119,33 @@ PRODUCT_ID_SILHOUETTE_SD_1 = 0x111c PRODUCT_ID_SILHOUETTE_SD_2 = 0x111d PRODUCT_ID_SILHOUETTE_CAMEO = 0x1121 +PRODUCT_ID_SILHOUETTE_CAMEO2 = 0x112b +PRODUCT_ID_SILHOUETTE_CAMEO3 = 0x112f PRODUCT_ID_SILHOUETTE_PORTRAIT = 0x1123 +PRODUCT_ID_SILHOUETTE_PORTRAIT2 = 0x1132 DEVICE = [ - { 'vendor_id': 0x0b4d, 'product_id': 0x1123, 'name': 'Silhouette Portrait', + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_PORTRAIT, 'name': 'Silhouette Portrait', + 'width_mm': 206, 'length_mm': 3000, 'regmark': True }, + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_PORTRAIT2, 'name': 'Silhouette Portrait2', 'width_mm': 203, 'length_mm': 3000, 'regmark': True }, - { 'vendor_id': 0x0b4d, 'product_id': 0x1121, 'name': 'Silhouette Cameo', + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_CAMEO, 'name': 'Silhouette Cameo', # margin_top_mm is just for safety when moving backwards with thin media # margin_left_mm is a physical limit, but is relative to width_mm! 'width_mm': 304, 'length_mm': 3000, 'margin_left_mm':9.0, 'margin_top_mm':1.0, 'regmark': True }, - { 'vendor_id': 0x0b4d, 'product_id': 0x112b, 'name': 'Silhouette Cameo2', + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_CAMEO2, 'name': 'Silhouette Cameo2', # margin_top_mm is just for safety when moving backwards with thin media # margin_left_mm is a physical limit, but is relative to width_mm! 'width_mm': 304, 'length_mm': 3000, 'margin_left_mm':9.0, 'margin_top_mm':1.0, 'regmark': True }, - { 'vendor_id': 0x0b4d, 'product_id': 0x112f, 'name': 'Silhouette Cameo3', + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_CAMEO3, 'name': 'Silhouette Cameo3', # margin_top_mm is just for safety when moving backwards with thin media # margin_left_mm is a physical limit, but is relative to width_mm! - 'width_mm': 304, 'length_mm': 3000, 'margin_left_mm':5, 'margin_top_mm':15.5, 'regmark': True }, - { 'vendor_id': 0x0b4d, 'product_id': 0x110a, 'name': 'Craft Robo CC200-20', + 'width_mm': 304.8, 'length_mm': 3000, 'margin_left_mm':0.0, 'margin_top_mm':0.0, 'regmark': True }, + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_CC200_20, 'name': 'Craft Robo CC200-20', 'width_mm': 200, 'length_mm': 1000, 'regmark': True }, - { 'vendor_id': 0x0b4d, 'product_id': 0x111a, 'name': 'Craft Robo CC300-20' }, - { 'vendor_id': 0x0b4d, 'product_id': 0x111c, 'name': 'Silhouette SD 1' }, - { 'vendor_id': 0x0b4d, 'product_id': 0x111d, 'name': 'Silhouette SD 2' }, + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_CC300_20, 'name': 'Craft Robo CC300-20' }, + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_SD_1, 'name': 'Silhouette SD 1' }, + { 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_SD_2, 'name': 'Silhouette SD 2' }, ] def _bbox_extend(bb, x, y): @@ -254,7 +268,7 @@ def __init__(self, log=sys.stderr, no_device=False, progress_cb=None): print(msg, file=sys.stderr) sys.exit(0) - if usb_reset_needed: + if usb_reset_needed: for i in range(5): try: dev.reset(); @@ -271,10 +285,14 @@ def __init__(self, log=sys.stderr, no_device=False, progress_cb=None): pass self.dev = dev - self.need_interface = False # probably never needed, but harmful on some versions of usb.core + self.need_interface = False # probably never needed, but harmful on some versions of usb.core self.regmark = False # not yet implemented. See robocut/Plotter.cpp:446 if self.dev is None or 'width_mm' in self.hardware: self.leftaligned = True + self.enable_sw_clipping = True + + def product_id(s): + return s.hardware['product_id'] if 'product_id' in s.hardware else None def write(s, string, timeout=10000): """Send a command to the device. Long commands are sent in chunks of 4096 bytes. @@ -388,6 +406,8 @@ def read(s, size=64, timeout=5000): raise ValueError('read failed: none') if isinstance(data, str): return data + elif isinstance(data, bytearray): + return str(data) else: return data.tostring() @@ -457,60 +477,70 @@ def initialize(s): pass # Additional commands seen in init by Silhouette Studio - #s.write("[\x03") # asks for something, no idea, just repeating sniffed communication + #s.write("[\x03") # Get Upper Left Coords: 2 six digit numbers. #try: # resp = s.read(timeout=1000) # if len(resp) > 1: - # print("[: '%s'" % (resp[:-1]), file=s.log) # response '0,0' + # print("[: '%s'" % (resp[:-1]), file=s.log) # response '0,0' #except: # pass - #s.write("U\x03") # asks for something, no idea, just repeating sniffed communication + #s.write("U\x03") # Get Lower Right Coordinates: 2 six digit numbers #try: # resp = s.read(timeout=1000) # if len(resp) > 1: - # print("U: '%s'" % (resp[:-1]), file=s.log) # response '20320,4120' max. print range? + # print("U: '%s'" % (resp[:-1]), file=s.log) # response '20320,4120' max. usable print range? #except: # pass - #s.write("FQ0\x03") # asks for something, no idea, just repeating sniffed communication + #s.write("FQ0\x03") # Unknown: 1 five digit number. Maybe last speed set? #try: # resp = s.read(timeout=1000) # if len(resp) > 1: - # print("FQ0: '%s'" % (resp[:-1]), file=s.log) # response '10' + # print("FQ0: '%s'" % (resp[:-1]), file=s.log) # response '10' #except: # pass - #s.write("FQ2\x03") # asks for something, no idea, just repeating sniffed communication + #s.write("FQ2\x03") # Unknown: 1 five digit number. Maybe last blade offset? #try: # resp = s.read(timeout=1000) # if len(resp) > 1: - # print("FQ2: '%s'" % (resp[:-1]), file=s.log) # response '18' + # print("FQ2: '%s'" % (resp[:-1]), file=s.log) # response '18' #except: # pass - #s.write("TB71\x03") # asks for something, no idea, just repeating sniffed communication - #try: - # resp = s.read(timeout=1000) - # if len(resp) > 1: - # print("TB71: '%s'" % (resp[:-1]), file=s.log) - #except: - # pass + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: - #s.write("FA\x03") # asks for something, not sure, current position? - #try: - # resp = s.read(timeout=1000) - # if len(resp) > 1: - # print("FA: '%s'" % (resp[:-1]), file=s.log) # response '0,0' - #except: - # pass + s.write("TB71\x03") # Unknown: 2 five digit numbers + try: + resp = s.read(timeout=1000) + if len(resp) > 1: + print("TB71: '%s'" % (resp[:-1]), file=s.log) + except: + pass + + s.write("FA\x03") # Unknown: 2 five digit numbers + try: + resp = s.read(timeout=1000) + if len(resp) > 1: + print("FA: '%s'" % (resp[:-1]), file=s.log) # response '0,0' + except: + pass + + s.write("TC\x03") + try: + resp = s.read(timeout=1000) + if len(resp) > 1: + print("TC: '%s'" % (resp[:-1]), file=s.log) # response '0,0' + except: + pass def get_version(s): """Retrieve the firmware version string from the device.""" if s.dev is None: return None - s.write("FG\x03") + s.write("FG\x03") # Get Version: 17 Chars try: resp = s.read(timeout=10000) # Large timeout because the plotter moves. except usb.core.USBError as e: @@ -519,7 +549,7 @@ def get_version(s): return resp[0:-2] # chop of 0x03 - def setup(s, media=132, speed=None, pressure=None, pen=None, trackenhancing=False, landscape=False, leftaligned=None): + def setup(s, media=132, speed=None, pressure=None, toolholder=None, pen=None, cuttingmat=None, sharpencorners=False, sharpencorners_start=0.1, sharpencorners_end=0.1, autoblade=False, depth=None, sw_clipping=True, trackenhancing=False, bladediameter=0.9, landscape=False, leftaligned=None, mediawidth=210.0, mediaheight=297.0): """media range is [100..300], default 132, "Print Paper Light Weight" speed range is [1..10], default None, from paper (132 -> 10) pressure range is [1..33], default None, from paper (132 -> 5) @@ -538,9 +568,35 @@ def setup(s, media=132, speed=None, pressure=None, pen=None, trackenhancing=Fals s.initialize() + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + if cuttingmat == 'cameo_12x12': + s.write("TG1\x03") + elif cuttingmat == 'cameo_12x24': + s.write("TG2\x03") + else: + s.write("TG0\x03") + + #FNx, x = 0 seem to be some kind of reset, x = 1: plotter head moves to other + # side of media (boundary check?), but next cut run will stall + #TB50,x: x = 1 landscape mode, x = 0 portrait mode + s.write("FN0\x03TB50,0\x03") + + if cuttingmat == 'cameo_12x12': + s.write("\\%d,%d\x03Z%d,%d\x03" % (0, 0, 6096, 6096)) + elif cuttingmat == 'cameo_12x24': + s.write("\\%d,%d\x03Z%d,%d\x03" % (0, 0, 12192, 6096)) + else: + width = s.hardware['width_mm'] if 'width_mm' in s.hardware else mediawidth + height = s.hardware['length_mm'] if 'length_mm' in s.hardware else mediaheight + s.write("\\%d,%d\x03Z%d,%d\x03" % (0, 0, height * 20.0, width * 20.0)) + if media is not None: if media < 100 or media > 300: media = 300 - s.write("FW%d\x03" % media); + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + # Silhouette Studio does not appear to issue this command + pass + else: + s.write("FW%d\x03" % media); if pen is None: if media == 113: pen = True @@ -548,54 +604,112 @@ def setup(s, media=132, speed=None, pressure=None, pen=None, trackenhancing=Fals pen = False for i in MEDIA: if i[0] == media: - print("Media=%d, cap='%s', name='%s'" % (media, i[3], i[4]), file=s.log) + print("Media=%d, cap='%s', name='%s'" % (media, i[4], i[5]), file=s.log) if pressure is None: pressure = i[1] if speed is None: speed = i[2] + if depth is None: depth = i[3] + break + + if toolholder is None: + toolholder = 1 + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + s.write("J%d\x03" % toolholder) + print("toolholder: %d" % toolholder, file=s.log) if speed is not None: if speed < 1: speed = 1 if speed > 10: speed = 10 - s.write("!%d\x03" % speed); + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + s.write("!%d,%d\x03" % (speed, toolholder)); + else: + s.write("!%d\x03" % speed); print("speed: %d" % speed, file=s.log) if pressure is not None: if pressure < 1: pressure = 1 if pressure > 33: pressure = 33 - s.write("FX%d\x03" % pressure); + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + s.write("FX%d,%d\x03" % (pressure, toolholder)); + else: + s.write("FX%d\x03" % pressure); # s.write("FX%d,0\x03" % pressure); # oops, graphtecprint does it like this print("pressure: %d" % pressure, file=s.log) + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + if pen: + s.write("FC0,1,%d\x03" % (toolholder)) + if s.leftaligned: print("Loaded media is expected left-aligned.", file=s.log) else: print("Loaded media is expected right-aligned.", file=s.log) + # Lift plotter head at sharp corners + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + if sharpencorners: + s.write("FE1,%d\x03" % toolholder) + else: + s.write("FE0,%d\x03" % toolholder) + + if pen: + s.write("FF0,0,%d\x03" % (toolholder)) + else: + sharpencorners_start = int((sharpencorners_start + 0.05) * 10.0) + sharpencorners_end = int((sharpencorners_end + 0.05) * 10.0) + s.write("FF%d,0,%d\x03FF%d,%d,%d\x03" % (sharpencorners_start, toolholder, sharpencorners_start, sharpencorners_end, toolholder)) + # robocut/Plotter.cpp:393 says: - # // I think this sets the distance from the position of the plotter - # // head to the actual cutting point, maybe in 0.1 mms (todo: Measure blade). - # // It is 0 for the pen, 18 for cutting. - # // C possible stands for curvature. Not that any of the other letters make sense... - cutter = 18 - if pen: cutter = 0 - s.write("FC%d\x03" % cutter) + # It is 0 for the pen, 18 for cutting. Default diameter of a blade is 0.9mm + # C possible stands for curvature. Not that any of the other letters make sense... + # C possible stands for circle. + # This value is the circle diameter which is exectuted on direction changes on corners to adjust the blade. + # Seems to be limited to 46 or 47. Values above does keep the last setting on the device. + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + if not pen: + circle = 0.5 + bladediameter * 20 + s.write("FC0,1,%d\x03FC%d,1,%d\x03" % (toolholder, circle, toolholder)) + + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + if autoblade and depth is not None: + if toolholder == 1: + if depth < 0: depth = 0 + if depth > 10: depth = 10 + s.write("TF%d,%d\x03" % (depth, toolholder)); + print("depth: %d" % depth, file=s.log) + else: + if pen: + circle = 0 + else: + circle = bladediameter * 20 + s.write("FC%d\x03" % circle) + + s.enable_sw_clipping = sw_clipping + # if enabled, rollers three times forward and back. + # needs a pressure of 19 or more, else nothing will happen if trackenhancing is not None: if trackenhancing: - s.write("FY1\x03") - else: s.write("FY0\x03") + else: + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + pass + else: + s.write("FY1\x03") #FNx, x = 0 seem to be some kind of reset, x = 1: plotter head moves to other # side of media (boundary check?), but next cut run will stall #TB50,x: x = 1 landscape mode, x = 0 portrait mode - if landscape is not None: - if landscape: - s.write("FN0\x03TB50,1\x03") - else: - s.write("FN0\x03TB50,0\x03") + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + pass + else: + if landscape is not None: + if landscape: + s.write("FN0\x03TB50,1\x03") + else: + s.write("FN0\x03TB50,0\x03") - # Don't lift plotter head between paths - s.write("FE0,0\x03") + # Don't lift plotter head between paths + s.write("FE0,0\x03") def find_bbox(s, cut): """Find the bounding box of the cut, returns (xmin,ymin,xmax,ymax)""" @@ -606,7 +720,7 @@ def find_bbox(s, cut): return bb def flip_cut(s, cut): - """this returns a flipped copy of the cut about the x-axis, + """this returns a flipped copy of the cut about the y-axis, keeping min and max values as they are.""" bb = s.find_bbox(cut) new_cut = [] @@ -728,7 +842,7 @@ def plot_cmds(s, plist, bbox, x_off_mm, y_off_mm): bbox['clip']['count'] = 1 if bbox['only'] is False: - if inside and last_inside: + if not s.enable_sw_clipping or (inside and last_inside): plotcmds.append("D%d,%d" % (int(0.5+y), int(0.5+x))) else: # // if outside the range just move @@ -830,7 +944,7 @@ def plot(s, mediawidth=210.0, mediaheight=297.0, margintop=None, # s.write("\1b\05") #request status # resp = s.read(timeout=1000) # if resp != " 1\x03": - # break; + # break; resp = s.read(timeout=40000) ## Allow 20s for reply... if resp != " 0\x03": @@ -856,8 +970,17 @@ def plot(s, mediawidth=210.0, mediaheight=297.0, margintop=None, #FEx,0 , x = 0 cutting of distinct paths in one go, x = 1 head is lifted at sharp angles #\xmin, ymin Zxmax,ymax, designate cutting area - p = "\\0,0\x03Z%d,%d\x03L0\x03FE0,0\x03FF0,0,0\x03" % (height, width) #FIXME Is coordinate swap necessary here? - s.write(p) + # needed only for the trackenhancing feature, defines the usable length, rollers three times forward and back. + # needs a pressure of 19 or more, else nothing will happen + #p = "FU%d\x03" % (height) + #p = "FU%d,%d\x03" % (height,width) # optional + #s.write(p) + + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + pass + else: + p = "\\0,0\x03Z%d,%d\x03L0\x03FE0,0\x03FF0,0,0\x03" % (height, width) + s.write(p) bbox['clip'] = {'urx':width, 'ury':top, 'llx':left, 'lly':height} bbox['only'] = bboxonly @@ -877,12 +1000,15 @@ def plot(s, mediawidth=210.0, mediaheight=297.0, margintop=None, # Silhouette Cameo2 does not start new job if not properly parked on left side # Attention: This needs the media to not extend beyond the left stop - if not 'llx' in bbox: bbox['llx'] = 0 # survive empty pathlist + if not 'llx' in bbox: bbox['llx'] = 0 # survive empty pathlist if not 'lly' in bbox: bbox['lly'] = 0 if not 'urx' in bbox: bbox['urx'] = 0 if not 'ury' in bbox: bbox['ury'] = 0 if endposition == 'start': - new_home = "H\x03" + if s.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3: + new_home = "L0\x03\\0,0\x03M0,0\x03J0\x03FN0\x03TB50,0\x03" + else: + new_home = "H\x03" else: #includes 'below' new_home = "M%d,%d\x03SO0\x03" % (int(0.5+bbox['lly']+end_paper_offset*20.), 0) #! axis swapped when using Cameo-system #new_home += "FN0\x03TB50,0\x03" diff --git a/silhouette/beutil.py b/silhouette/beutil.py new file mode 100644 index 00000000..8e58a3ca --- /dev/null +++ b/silhouette/beutil.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +# Conversion utilities for printer's binary encoding commands +# (c) 2018 D. Bajar + +from __future__ import print_function +import sys + +def to_BE(x, y): + if abs(x) < 112 and abs(y) < 112: + index = 224 * (x + 112) + (y + 112) + d2 = index // 224 + index -= d2 * 224 + d1 = index + be1 = "%02X%02X" % (d1 + 0x20, d2 + 0x20) + # print("BE1:", be1) + return ("BE1", be1) + elif abs(x) < 1676 and abs(y) < 1676: + index = 3352 * (x + 1676) + (y + 1676) + d3 = index // (224 * 224) + index -= d3 * (224 * 224) + d2 = index // 224 + index -= d2 * 224 + d1 = index + be2 = "%02X%02X%02X" % (d1 + 0x20, d2 + 0x20, d3 + 0x20) + # print("BE2:", be2) + return ("BE2", be2) + elif abs(x) < 375482 and abs(y) < 375482: + index = 750964 * (x + 375482) + (y + 375482) + d5 = index // (224 * 224 * 224 * 224) + index -= d5 * (224 * 224 * 224 * 224) + d4 = index // (224 * 224 * 224) + index -= d4 * (224 * 224 * 224) + d3 = index // (224 * 224) + index -= d3 * (224 * 224) + d2 = index // 224 + index -= d2 * 224 + d1 = index + be3 = "%02X%02X%02X%02X%02X" % (d1 + 0x20, d2 + 0x20, d3 + 0x20, d4 + 0x20, d5 + 0x20) + # print("BE3:", be3) + return ("BE3", be3) + else: + raise ValueError("Invalid coordinate") + # end if +# end def to_BE + +def from_BE(be_stream): + + if len(be_stream) == 4: + d1 = int(be_stream[:2], 16) + d2 = int(be_stream[2:], 16) + if d1 < 0x20 or d2 < 0x20: + raise ValueError("Invalid BE1 stream digit") + # end if + index = (d2 - 0x20) * 224 + (d1 - 0x20) + x = index // 224 - 112 + y = index % 224 - 112 + # print("BE1: %d,%d" % (x, y)) + return ("BE1", (x, y)) + elif len(be_stream) == 6: + d1 = int(be_stream[0:2], 16) + d2 = int(be_stream[2:4], 16) + d3 = int(be_stream[4:6], 16) + if d1 < 0x20 or d2 < 0x20 or d3 < 0x20: + raise ValueError("Invalid BE2 stream digit") + # end if + index = (d3 - 0x20) * (224 * 224) + (d2 - 0x20) * 224 + (d1 - 0x20) + x = index // 3352 - 1676 + y = index % 3352 - 1676 + # print("BE2: %d,%d" % (x, y)) + return ("BE2", (x, y)) + elif len(be_stream) == 10: + d1 = int(be_stream[0:2], 16) + d2 = int(be_stream[2:4], 16) + d3 = int(be_stream[4:6], 16) + d4 = int(be_stream[6:8], 16) + d5 = int(be_stream[8:10], 16) + if d1 < 0x20 or d2 < 0x20 or d3 < 0x20 or d4 < 0x20 or d5 < 0x20: + raise ValueError("Invalid BE3 stream digit") + # end if + index = (d5 - 0x20) * (224 * 224 * 224 * 224) + (d4 - 0x20) * (224 * 224 * 224) + (d3 - 0x20) * (224 * 224) + (d2 - 0x20) * 224 + (d1 - 0x20) + x = index // 750964 - 375482 + y = index % 750964 - 375482 + # print("BE3: %d,%d" % (x, y)) + return ("BE3", (x, y)) + else: + raise ValueError("Invalid length hex stream") + # end if + +# end def from_BE + +def test_BE(x, y, be_stream, be_enc): + + enc, stream = to_BE(x, y) + passed = True if enc == be_enc and stream == be_stream else False + print("to_BE: (%d, %d) -> '%s' %s= '%s' : %s" % (x, y, stream, '=' if passed else '!', be_stream, "PASSED" if passed else "FAILED")) + if not passed: + sys.exit(-1) + # end if + + enc, xy = from_BE(be_stream) + passed = True if enc == be_enc and xy == (x, y) else False + print("from_BE: '%s -> (%d, %d) %s= (%d, %d) : %s" % (be_stream, xy[0], xy[1], '=' if passed else '!', x, y, "PASSED" if passed else "FAILED")) + if not passed: + sys.exit(-1) + # end if + +# end def test_BE + +def test(): + + print("Running tests ...") + + test_BE( 0, 0, "9090", "BE1") + test_BE( 0, 1, "9190", "BE1") + test_BE( 1, 0, "9091", "BE1") + test_BE( 0, -1, "8F90", "BE1") + test_BE( -1, 0, "908F", "BE1") + test_BE( 1, 1, "9191", "BE1") + test_BE( 1, -1, "8F91", "BE1") + test_BE( -1, -1, "8F8F", "BE1") + test_BE( -1, 1, "918F", "BE1") + test_BE( 0, 111, "FF90", "BE1") + test_BE( 111, 0, "90FF", "BE1") + test_BE( 0, -111, "2190", "BE1") + test_BE(-111, 0, "9021", "BE1") + test_BE( 111, 111, "FFFF", "BE1") + test_BE( 111, -111, "21FF", "BE1") + test_BE(-111, -111, "2121", "BE1") + test_BE(-111, 111, "FF21", "BE1") + test_BE( 56, -27, "75C8", "BE1") + test_BE( -77, 44, "BC43", "BE1") + test_BE( -39, 106, "FA69", "BE1") + test_BE( 72, -25, "77D8", "BE1") + + test_BE( 0, 112, "3C2090", "BE2") + test_BE( 112, 0, "AC8B97", "BE2") + test_BE( 0, -112, "3CFF8F", "BE2") + test_BE( -112, 0, "AC9388", "BE2") + test_BE( 112, 112, "3C8C97", "BE2") + test_BE( 112, -112, "3C8B97", "BE2") + test_BE( -112, -112, "3C9388", "BE2") + test_BE( -112, 112, "3C9488", "BE2") + test_BE( 0, 1675, "372790", "BE2") + test_BE( 1675, 0, "D4E8FF", "BE2") + test_BE( 0, -1675, "41F88F", "BE2") + test_BE(-1675, 0, "843620", "BE2") + test_BE( 1675, 1675, "5FF0FF", "BE2") + test_BE( 1675, -1675, "69E1FF", "BE2") + test_BE(-1675, -1675, "F92E20", "BE2") + test_BE(-1675, 1675, "EF3D20", "BE2") + test_BE( 1091, 674, "B6E8D8", "BE2") + test_BE( 116, 1421, "D9CD97", "BE2") + test_BE( -702, 485, "E13861", "BE2") + test_BE(-1463, -1153, "C3552E", "BE2") + + print("All test PASSED !!!") + sys.exit(0) + +# end def test + +def main(argv): + + # test() + + if len(argv) <= 1: + print("Usage: %s | " % (argv[0])) + return 1 + # end if + + if len(argv) <= 2: + be_stream = argv[1] + res = from_BE(be_stream) + print("%s %s -> %d,%d" % (res[0], be_stream, res[1][0], res[1][1])) + else: + x = int(argv[1], 0) + y = int(argv[2], 0) + res = to_BE(x, y) + print("%d,%d -> %s %s" % (x, y, res[0], res[1])) + # end if + + return 0 + +# end def main + +if __name__ == '__main__': + sys.exit(main(sys.argv)) +# end if diff --git a/silhouette/convert2dashes.py b/silhouette/convert2dashes.py index c1a93fd3..826e1b66 100644 --- a/silhouette/convert2dashes.py +++ b/silhouette/convert2dashes.py @@ -22,9 +22,8 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ''' -# local library -import cubicsuperpath import bezmisc +import cubicsuperpath import simplestyle def tpoint((x1,y1), (x2,y2), t = 0.5): diff --git a/silhouette_multi.inx b/silhouette_multi.inx new file mode 100644 index 00000000..bd780fa1 --- /dev/null +++ b/silhouette_multi.inx @@ -0,0 +1,21 @@ + + + <_name>Silhouette Multiple Actions + com.github.jnweiger.inskscape-silhouette-multi + org.inkscape.output.svg.inkscape + inkex.py + silhouette_multi.py + + false + + + all + + + + + + + diff --git a/silhouette_multi.py b/silhouette_multi.py new file mode 100644 index 00000000..ab47131d --- /dev/null +++ b/silhouette_multi.py @@ -0,0 +1,802 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +import os +import sys +import time +import cPickle +import subprocess +from threading import Thread +from tempfile import NamedTemporaryFile +from collections import defaultdict, OrderedDict +import xmltodict +import traceback +from cStringIO import StringIO +import wx +from wx.lib.scrolledpanel import ScrolledPanel +from wx.lib.agw import ultimatelistctrl as ulc +from wx.lib.embeddedimage import PyEmbeddedImage +from collections import defaultdict +import inkex +import simplestyle +from functools import partial +from itertools import groupby + + +small_up_arrow = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADxJ" + "REFUOI1jZGRiZqAEMFGke2gY8P/f3/9kGwDTjM8QnAaga8JlCG3CAJdt2MQxDCAUaOjyjKMp" + "cRAYAABS2CPsss3BWQAAAABJRU5ErkJggg==") + +small_down_arrow = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAEhJ" + "REFUOI1jZGRiZqAEMFGke9QABgYGBgYWdIH///7+J6SJkYmZEacLkCUJacZqAD5DsInTLhDR" + "bcPlKrwugGnCFy6Mo3mBAQChDgRlP4RC7wAAAABJRU5ErkJggg==") + + + +def presets_path(): + try: + import appdirs + config_path = appdirs.user_config_dir('inkscape-silhouette') + except ImportError: + config_path = os.path.expanduser('~/.inkscape-silhouette') + + if not os.path.exists(config_path): + os.makedirs(config_path) + return os.path.join(config_path, 'presets.cPickle') + +def load_presets(): + try: + with open(presets_path(), 'r') as presets: + presets = cPickle.load(presets) + return presets + except: + return {} + +def save_presets(presets): + #print "saving presets", presets + with open(presets_path(), 'w') as presets_file: + cPickle.dump(presets, presets_file) + + +def load_preset(name): + return load_presets().get(name) + + +def save_preset(name, data): + presets = load_presets() + presets[name] = data + save_presets(presets) + + +def delete_preset(name): + presets = load_presets() + presets.pop(name, None) + save_presets(presets) + + +def confirm_dialog(parent, question, caption = 'Silhouette Multiple Actions'): + dlg = wx.MessageDialog(parent, question, caption, wx.YES_NO | wx.ICON_QUESTION) + result = dlg.ShowModal() == wx.ID_YES + dlg.Destroy() + return result + + +def info_dialog(parent, message, caption = 'Silhouette Multiple Actions'): + dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION) + dlg.ShowModal() + dlg.Destroy() + + +class ParamsNotebook(wx.Notebook): + """Handle a notebook of tabs that contain params. + + Each param has a name, and all names are globally unique across all tabs. + """ + + def __init__(self, *args, **kwargs): + wx.Notebook.__init__(self, *args, **kwargs) + + self.load_inx() + self.create_tabs() + self.add_tabs() + + def load_inx(self): + with open('sendto_silhouette.inx') as inx_file: + self.inx = xmltodict.parse(inx_file, force_list=('param',)) + + def create_tabs(self): + self.notebook = self.inx['inkscape-extension']['param'][0] + + if self.notebook['@type'] != 'notebook': + print >> sys.stderr, "unexpected INX format" + return + + self.tabs = [] + for page in self.notebook['page']: + self.tabs.append(ParamsTab(self, wx.ID_ANY, name=page['@name'], title=page['@_gui-text'], params=page['param'])) + + def add_tabs(self): + for tab in self.tabs: + self.AddPage(tab, tab.title) + + def get_values(self): + values = {} + + for tab in self.tabs: + values.update(tab.get_values()) + + return values + + def get_defaults(self): + values = {} + + for tab in self.tabs: + values.update(tab.get_defaults()) + + return values + + + def set_values(self, values): + for tab in self.tabs: + tab.set_values(values) + + + def set_defaults(self): + for tab in self.tabs: + tab.set_defaults() + +class ParamsTab(ScrolledPanel): + def __init__(self, *args, **kwargs): + self.params = kwargs.pop('params', []) + self.name = kwargs.pop('name', None) + self.title = kwargs.pop('title', None) + kwargs["style"] = wx.TAB_TRAVERSAL + ScrolledPanel.__init__(self, *args, **kwargs) + self.SetupScrolling() + + self.param_inputs = {} + self.choices_by_label = {} + self.choices_by_value = {} + self.defaults = {} + + self.settings_grid = wx.GridBagSizer(hgap=0, vgap=0) + self.settings_grid.AddGrowableCol(0, 1) + self.settings_grid.SetFlexibleDirection(wx.HORIZONTAL) + + self.__set_properties() + self.__do_layout() + + def get_values(self): + values = {} + + for name, input in self.param_inputs.iteritems(): + if isinstance(input, wx.Choice): + choice = input.GetSelection() + + if choice == wx.NOT_FOUND: + values[name] = None + else: + values[name] = self.choices_by_label[name][input.GetString(choice)] + else: + values[name] = input.GetValue() + return values + + def get_defaults(self): + return self.defaults + + def set_values(self, values): + for name, value in values.iteritems(): + if name not in self.param_inputs: + # ignore params not contained in this tab + continue + + input = self.param_inputs[name] + + if isinstance(input, wx.Choice): + if value is None: + input.SetSelection(wx.NOT_FOUND) + else: + label = self.choices_by_value[name][value] + input.SetStringSelection(label) + else: + input.SetValue(value) + + def set_defaults(self): + self.set_values(self.defaults) + + def __set_properties(self): + # begin wxGlade: SatinPane.__set_properties + # end wxGlade + pass + + def __do_layout(self): + # just to add space around the settings + box = wx.BoxSizer(wx.VERTICAL) + + for row, param in enumerate(self.params): + param_type = param['@type'] + param_name = param['@name'] + if param_type == 'description': + self.settings_grid.Add(wx.StaticText(self, label=param.get('#text', '')), + pos=(row, 0), span=(1, 2), flag=wx.EXPAND|wx.LEFT|wx.ALIGN_TOP, border=10) + else: + self.settings_grid.Add(wx.StaticText(self, label=param.get('@_gui-text', '')), + pos=(row, 0), flag=wx.EXPAND|wx.TOP|wx.ALIGN_TOP, border=5) + + if param_type == 'boolean': + input = wx.CheckBox(self) + elif param_type == 'float': + input = wx.SpinCtrlDouble(self, wx.ID_ANY, min=float(param.get('@min', 0.0)), max=float(param.get('@max', 2.0**32)), inc=0.1, value=param.get('#text', '')) + elif param_type == 'int': + input = wx.SpinCtrl(self, wx.ID_ANY, min=int(param.get('@min', 0)), max=int(param.get('@max', 2**32)), value=param.get('#text', '')) + elif param_type == 'enum': + choices = OrderedDict((item['#text'], item['@value']) for item in param['item']) + self.choices_by_label[param_name] = choices + self.choices_by_value[param_name] = { v: k for k, v in choices.iteritems() } + input = wx.Choice(self, wx.ID_ANY, choices=choices.keys(), style=wx.LB_SINGLE) + input.SetStringSelection(choices.keys()[0]) + else: + # not sure what else to do here... + continue + + self.param_inputs[param_name] = input + + self.settings_grid.Add(input, pos=(row, 1), flag=wx.ALIGN_BOTTOM|wx.TOP, border=5) + + self.defaults = self.get_values() + + box.Add(self.settings_grid, proportion=1, flag=wx.ALL, border=10) + self.SetSizer(box) + + self.Layout() + + +class SilhouetteMultiFrame(wx.Frame): + def __init__(self, *args, **kwargs): + # begin wxGlade: MyFrame.__init__ + self.colors = kwargs.pop('colors', []) + self.options = kwargs.pop('options') + self.run_callback = kwargs.pop('run_callback') + wx.Frame.__init__(self, None, wx.ID_ANY, + "Silhouette Multi-Action" + ) + + self.selected = None + self.color_settings = {} + self.color_enabled = {} + self.notebook = ParamsNotebook(self, wx.ID_ANY) + self.up_button = wx.Button(self, wx.ID_UP) + self.down_button = wx.Button(self, wx.ID_DOWN) + self.run_button = wx.Button(self, wx.ID_EXECUTE) + self.cancel_button = wx.Button(self, wx.ID_CANCEL, "Cancel") + + self.presets_box = wx.StaticBox(self, wx.ID_ANY, label="Presets") + self.preset_chooser = wx.ComboBox(self, wx.ID_ANY, style=wx.CB_SORT) + self.load_preset_button = wx.Button(self, wx.ID_REVERT_TO_SAVED, "Load") + self.add_preset_button = wx.Button(self, wx.ID_SAVE, "Add") + self.overwrite_preset_button = wx.Button(self, wx.ID_SAVEAS, "Overwrite") + self.delete_preset_button = wx.Button(self, wx.ID_DELETE, "Delete") + + self.update_preset_list() + self.init_actions() + + self.Bind(wx.EVT_BUTTON, self.move_up, self.up_button) + self.Bind(wx.EVT_BUTTON, self.move_down, self.down_button) + self.Bind(wx.EVT_BUTTON, self.run, self.run_button) + self.Bind(wx.EVT_BUTTON, self.load_preset, self.load_preset_button) + self.Bind(wx.EVT_BUTTON, self.add_preset, self.add_preset_button) + self.Bind(wx.EVT_BUTTON, self.overwrite_preset, self.overwrite_preset_button) + self.Bind(wx.EVT_BUTTON, self.delete_preset, self.delete_preset_button) + self.Bind(wx.EVT_BUTTON, self.close, self.cancel_button) + + self._load_preset('__LAST__', silent=True) + + self.__set_properties() + self.__do_layout() + # end wxGlade + + def close(self, event): + self.Close() + + def load_preset(self, event): + preset_name = self.get_preset_name() + if not preset_name: + return + + self._load_preset(preset_name) + + def add_preset(self, event, overwrite=False): + preset_name = self.get_preset_name() + if not preset_name: + return + + if not overwrite and load_preset(preset_name): + info_dialog(self, 'Preset "%s" already exists. Please use another name or press "Overwrite"' % preset_name, caption='Preset') + + self.save_color_settings() + save_preset(preset_name, self.get_preset_data()) + self.update_preset_list() + + event.Skip() + + def overwrite_preset(self, event): + self.add_preset(event, overwrite=True) + + def delete_preset(self, event): + preset_name = self.get_preset_name() + if not preset_name: + return + + preset = self.check_and_load_preset(preset_name) + if not preset: + return + + delete_preset(preset_name) + self.update_preset_list() + self.preset_chooser.SetValue("") + + event.Skip() + + def check_and_load_preset(self, preset_name): + preset = load_preset(preset_name) + if not preset: + info_dialog(self, 'Preset "%s" not found.' % preset_name, caption='Preset') + + return preset + + def get_preset_name(self): + preset_name = self.preset_chooser.GetValue().strip() + if preset_name: + return preset_name + else: + info_dialog(self, "Please enter or select a preset name first.", caption='Preset') + return + + def update_preset_list(self): + preset_names = load_presets().keys() + preset_names = [preset for preset in preset_names if not preset.startswith("__")] + self.preset_chooser.SetItems(preset_names) + + def _load_preset(self, preset_name, silent=False): + preset = load_preset(preset_name) + + #print >> sys.stderr, preset + + if not preset: + return + + if self.selected: + self.actions.Select(self.selected, False) + + old_colors = self.colors + self.colors = [] + extra_colors = [] + + for color in preset['colors']: + if color in old_colors: + old_colors.remove(color) + self.colors.append(color) + self.color_enabled[color] = preset['color_enabled'].get(color, True) + self.color_settings[color] = preset['color_settings'].get(color, {}) + else: + extra_colors.append(color) + + reassigned = 0 + # If there are any leftover colors in this SVG that weren't in the + # preset, we have to add them back into the list. Let's try to + # use the settings from one of the "unclaimed" colors in the preset. + + for color in old_colors: + self.colors.append(color) + + if extra_colors: + reassigned += 1 + assigned_color = extra_colors.pop(0) + self.color_enabled[color] = preset['color_enabled'].get(assigned_color, True) + self.color_settings[color] = preset['color_settings'].get(assigned_color, {}) + else: + self.color_enabled[color] = False + + message = [] + + #print >> sys.stderr, reassigned, extra_colors + #print >> sys.stderr, self.colors + #print >> sys.stderr, self.color_settings + + if reassigned: + message.append("%d colors were reassigned." % reassigned) + + if extra_colors: + message.append("%d colors from the preset were not used." % len(extra_colors)) + + if message and not silent: + info_dialog(self, "Colors in the preset and this SVG did not match fully. " + " ".join(message)) + + self.refresh_actions() + + def _save_preset(self, preset_name): + self.save_color_settings() + + preset = self.get_preset_data() + save_preset(preset_name, preset) + + def get_preset_data(self): + return { 'colors': self.colors, + 'color_enabled': self.color_enabled, + 'color_settings': self.color_settings } + + def run(self, event): + self.save_color_settings() + + actions = [] + + for color in self.colors: + if self.color_enabled.get(color, True): + actions.append((color, self.color_settings.get(color) or self.notebook.get_defaults())) + + if actions: + if not self.options.dry_run: + if not confirm_dialog(self, "About to perform %d actions, continue?" % len(actions)): + return + else: + info_dialog(self, "No colors were enabled, so no actions can be performed.") + return + + self._save_preset('__LAST__') + self.run_callback(actions) + + def move_up(self, event): + if self.selected is None or self.selected == 0: + return + + this = self.selected + prev = this - 1 + + self.colors[this], self.colors[prev] = self.colors[prev], self.colors[this] + self.actions.Select(this, False) + self.actions.Select(prev) + + self.refresh_actions() + + def move_down(self, event): + if self.selected is None or self.selected == len(self.colors) - 1: + return + + this = self.selected + next = this + 1 + + self.colors[this], self.colors[next] = self.colors[next], self.colors[this] + self.actions.Select(this, False) + self.actions.Select(next) + + self.refresh_actions() + + def action_selected(self, event=None): + # first, save the settings for the color they were previously working on + self.save_color_settings() + + # then load the settings for the newly-selected color + self.selected = event.m_itemIndex + self.load_color_settings() + + self.up_button.Enable() + self.down_button.Enable() + self.notebook.Enable() + + def action_deselected(self, event=None): + self.save_color_settings() + + self.selected = None + + self.up_button.Disable() + self.down_button.Disable() + self.notebook.Disable() + + def load_color_settings(self): + color = self.colors[self.selected] + settings = self.color_settings.get(color) + + if settings: + self.notebook.set_values(settings) + else: + self.notebook.set_defaults() + + def save_color_settings(self): + #print "save:", self.selected + + if self.selected is None: + return + + color = self.colors[self.selected] + settings = self.notebook.get_values() + self.color_settings[color] = settings + + #print "settings:", settings + + def item_checked(self, event): + item = event.m_itemIndex + checked = self.actions.IsItemChecked(item, 2) + self.color_enabled[self.colors[item]] = checked + + def init_actions(self): + self.actions = ulc.UltimateListCtrl(self, size=(300, 150), agwStyle=wx.LC_REPORT|ulc.ULC_HRULES|ulc.ULC_SINGLE_SEL) + + self.Bind(ulc.EVT_LIST_ITEM_SELECTED, self.action_selected, self.actions) + self.Bind(ulc.EVT_LIST_ITEM_DESELECTED, self.action_deselected, self.actions) + self.Bind(ulc.EVT_LIST_ITEM_CHECKED, self.item_checked, self.actions) + self.action_deselected() + + self.actions.InsertColumn(0, "Step") + self.actions.InsertColumn(1, "Color") + self.actions.InsertColumn(2, "Perform Action?") + self.actions.SetColumnWidth(2, ulc.ULC_AUTOSIZE_FILL) + + self.action_checkboxes = [] + + for i, color in enumerate(self.colors): + self.actions.InsertStringItem(i, "%d." % (i + 1)) + + item = self.actions.GetItem(i, 2) + item.SetKind(1) # "a checkbox-like item" + item.SetMask(ulc.ULC_MASK_KIND) + self.actions.SetItem(item) + + self.refresh_actions() + + def refresh_actions(self): + for i, color in enumerate(self.colors): + item = self.actions.GetItem(i, 1) + item.SetMask(ulc.ULC_MASK_BACKCOLOUR) + item.SetBackgroundColour(wx.Colour(*color)) + self.actions.SetItem(item) + + item = self.actions.GetItem(i, 2) + item.Check(self.color_enabled.get(color, True)) + item.SetMask(ulc.ULC_MASK_CHECK) + self.actions.SetItem(item) + + + def __set_properties(self): + # begin wxGlade: MyFrame.__set_properties + self.SetTitle("Silhouette Multi-Action") + self.notebook.SetMinSize((800, 800)) + # end wxGlade + + def __do_layout(self): + # begin wxGlade: MyFrame.__do_layout + sizer_1 = wx.BoxSizer(wx.VERTICAL) + + sizer_2 = wx.BoxSizer(wx.HORIZONTAL) + sizer_2.Add(self.actions, 0, flag=wx.ALL|wx.EXPAND, border=10) + + sizer_3 = wx.BoxSizer(wx.VERTICAL) + sizer_3.Add(self.up_button, 0, border=10) + sizer_3.Add(self.down_button, 0, border=10) + + sizer_2.Add(sizer_3, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=10) + + sizer_4 = wx.StaticBoxSizer(self.presets_box, wx.VERTICAL) + sizer_4.Add(self.preset_chooser, 0, flag=wx.BOTTOM|wx.LEFT|wx.RIGHT|wx.TOP|wx.EXPAND, border=10) + + sizer_5 = wx.BoxSizer(wx.HORIZONTAL) + sizer_5.Add(self.load_preset_button, 0, flag=wx.RIGHT|wx.LEFT|wx.BOTTOM, border=10) + sizer_5.Add(self.add_preset_button, 0, flag=wx.RIGHT, border=10) + sizer_5.Add(self.overwrite_preset_button, 0, flag=wx.RIGHT, border=10) + sizer_5.Add(self.delete_preset_button, 0, flag=wx.RIGHT, border=10) + + sizer_4.Add(sizer_5, 0) + sizer_2.Add(sizer_4, 0, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=30) + + sizer_6 = wx.BoxSizer(wx.VERTICAL) + sizer_6.Add(self.run_button, 0, flag=wx.ALIGN_RIGHT|wx.BOTTOM, border=10) + sizer_6.Add(self.cancel_button, 0, flag=wx.ALIGN_RIGHT|wx.EXPAND) + sizer_2.Add(sizer_6, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=30) + + sizer_1.Add(sizer_2, 0, flag=wx.EXPAND|wx.ALL, border=10) + + sizer_1.Add(self.notebook, 1, wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, 10) + self.SetSizer(sizer_1) + sizer_1.Fit(self) + self.Layout() + # end wxGlade + +class SilhouetteMulti(inkex.Effect): + def __init__(self, *args, **kwargs): + inkex.Effect.__init__(self, *args, **kwargs) + + self.OptionParser.add_option("-d", "--dry_run", type='inkbool', default=False) + + def get_style(self, element): + if element.get('style') is not None: + return simplestyle.parseStyle(element.get('style')) + else: + return {} + + return style + + def get_color(self, element): + if element.tag == inkex.addNS( 'g', 'svg'): + # Sometimes Inkscape sets a stroke style on a group, which seems to + # have no visual effect. If we didn't ignore those, we'd cut those + # objects twice. + return None + + color = self.get_style(element).get('stroke', 'none') + + if color == 'none': + color = self.get_style(element).get('fill', 'none') + + if color != 'none': + return simplestyle.parseColor(color) + else: + return None + + def load_selected_objects(self): + self.selected_objects = [] + + def traverse_element(element, selected=False, parent_visibility="visible"): + if self.get_style(element).get('display') == 'none': + return + + visibility = element.get('visibility', parent_visibility) + + if visibility == 'inherit': + visibility = parent_visibility + + if element.get('id') in self.selected: + selected = True + + if selected and visibility not in ('hidden', 'collapse'): + self.selected_objects.append(element) + + for child in element: + traverse_element(child, selected, visibility) + + # if they haven't selected specific objects, then process all objects + if self.selected: + select_all = False + else: + select_all = True + + traverse_element(self.document.getroot(), selected=select_all) + + def split_objects_by_color(self): + self.objects_by_color = defaultdict(list) + self.load_selected_objects() + + for obj in self.selected_objects: + color = self.get_color(obj) + if color: + self.objects_by_color[color].append(obj) + + def effect(self): + app = wx.App() + self.split_objects_by_color() + self.frame = SilhouetteMultiFrame(colors=self.objects_by_color.keys(), run_callback=self.run, options=self.options) + self.frame.Show() + app.MainLoop() + + def save_copy(self): + self.svg_file = NamedTemporaryFile(suffix='.svg', prefix='silhouette-multiple-actions') + self.document.write(self.svg_file) + self.svg_file.flush() + + def format_args(self, args): + if isinstance(args, dict): + args = args.iteritems() + + return " ".join(("--%s=%s" % (k, v) for k, v in args)) + + def id_args(self, nodes): + return self.format_args(("id", node.get("id")) for node in nodes) + + def format_commands(self, actions): + commands = [] + + for color, settings in actions: + command = "python sendto_silhouette.py" + command += " " + self.format_args(settings) + command += " " + self.id_args(self.objects_by_color[color]) + command += " " + self.svg_file.name + + commands.append(command) + + return commands + + def run(self, actions): + self.frame.Close() + restore_stderr() + + self.save_copy() + + commands = self.format_commands(actions) + + if self.options.dry_run: + print >> sys.stderr, "\n\n".join(commands) + else: + self.run_commands_with_dialog(commands) + + def run_commands_with_dialog(self, commands): + for i, command in enumerate(commands): + if not self.run_command_with_dialog(command, step=i + 1, total=len(commands)): + info_dialog(None, "Action failed.") + print >> sys.stderr, "The command that failed:" + print >> sys.stderr, command + + sys.exit(1) + + def run_command_with_dialog(self, command, step, total): + # exec ensures that the shell gets replaced so that we can terminate the + # actual python script if the user cancels + process = subprocess.Popen("exec " + command, shell=True) + + dialog = wx.ProgressDialog(style=wx.PD_APP_MODAL|wx.PD_CAN_ABORT|wx.PD_ELAPSED_TIME, + message="Performing action %d of %d..." % (step, total), + title="Silhouette Multiple Actions") + + last_tick = time.time() + + while process.returncode is None: + if time.time() - last_tick > 0.5: + dialog.Pulse() + last_tick = time.time() + + process.poll() + wx.Yield() + time.sleep(0.1) + + if dialog.WasCancelled(): + def cancel(): + process.terminate() + process.wait() + + Thread(target=cancel).start() + + dialog.Destroy() + wx.Yield() + info_dialog(None, "Action aborted. It may take awhile for the machine to cancel its operation.") + sys.exit(1) + + dialog.Destroy() + wx.Yield() + return process.returncode == 0 + +def save_stderr(): + # GTK likes to spam stderr, which inkscape will show in a dialog. + null = open('/dev/null', 'w') + sys.stderr_dup = os.dup(sys.stderr.fileno()) + os.dup2(null.fileno(), 2) + sys.stderr_backup = sys.stderr + sys.stderr = StringIO() + + +def restore_stderr(): + if hasattr(sys, "stderr_backup"): + os.dup2(sys.stderr_dup, 2) + sys.stderr_backup.write(sys.stderr.getvalue()) + sys.stderr = sys.stderr_backup + + del sys.stderr_backup + + +# end of class MyFrame +if __name__ == "__main__": + + pid = os.fork() + if pid == 0: + # Forking and closing stdout and stderr allows inkscape to continue on + # while the silhouette machine is cutting. This is useful if you're + # cutting something really big and want to work on another document. + os.close(1) + os.close(2) + + try: + e = SilhouetteMulti() + e.affect() + except: + traceback.print_exc() + + + sys.exit(0)