├── .gitignore ├── .idea ├── .gitignore ├── encodings.xml ├── codeStyles │ └── codeStyleConfig.xml ├── dictionaries │ └── wholder.xml ├── preferred-vcs.xml ├── vcs.xml ├── modules.xml ├── libraries │ ├── lib.xml │ └── commons_lang3_3_8_1.xml ├── misc.xml ├── artifacts │ └── LaserCut_jar.xml └── uiDesigner.xml ├── lib ├── jdxf.jar ├── jssc.jar ├── LibLaserCut.jar ├── usb4java-1.3.0.jar ├── commons-lang3-3.8.1.jar ├── commons-logging-1.2.jar ├── slf4j-simple-1.7.9.jar ├── libusb4java-1.3.0-linux-arm.jar ├── libusb4java-1.3.0-linux-x86.jar ├── libusb4java-1.3.0-win32-x86.jar ├── libusb4java-1.3.0-linux-x86-64.jar ├── libusb4java-1.3.0-win32-x86-64.jar ├── libusb4java-1.3.0-darwin-x86-64.jar └── libusb4java-1.3.0-linux-aarch64.jar ├── images ├── Add.png ├── Subtract.png ├── Jog Dialog.png ├── LaserCut Screenshot.png ├── Mini Laser Settings.png ├── Preferences Dialog.png └── LaserCut Logo.svg ├── src ├── META-INF │ └── MANIFEST.MF ├── CADNoDraw.java ├── test │ ├── FileDialogDemo.java │ ├── ContainsTest.java │ ├── SerializationTest.java │ ├── FileChooserDemo.java │ └── ImagePreviewJFileChooser.java ├── MicroLaser.java ├── CADOval.java ├── CADShapeGroup.java ├── SurfaceSettings.java ├── CADRectangle.java ├── CADReference.java ├── CADPolygon.java ├── CADScaledShape.java ├── CADNemaMotor.java ├── CADGear.java ├── ShapeWindow.java ├── CNCPath.java ├── USBIO.java ├── PDFTools.java ├── CADBobbin.java ├── BetterBoundingBox.java ├── EPSWriter.java ├── PathPlanner.java ├── CornerFinder.java ├── ShapeList.java ├── CADShapeSpline.java ├── FileChooserMenu.java ├── JSSCPort.java ├── CADArbitraryPolygon.java ├── CNCTools.java ├── ShapeOptimizer.java ├── GerberZip.java ├── CADText.java ├── CADMusicStrip.java ├── GearGen.java └── ZingLaser.java ├── resources ├── META-INF │ └── MANIFEST.MF ├── images │ ├── info.png │ └── LaserCut Logo.png └── fonts │ └── hershey1.txt ├── LaserCut Files ├── Circle 2.lzr ├── AAA (test).lzr └── AAA (test)-2.lzr ├── Test └── PNG Files │ └── Clipboard-icon.png ├── out └── artifacts │ └── LaserCut_jar │ └── LaserCut.jar ├── LaserCut.iml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /lib/jdxf.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/jdxf.jar -------------------------------------------------------------------------------- /lib/jssc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/jssc.jar -------------------------------------------------------------------------------- /images/Add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/images/Add.png -------------------------------------------------------------------------------- /src/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Main-Class: LaserCut 3 | 4 | -------------------------------------------------------------------------------- /resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Main-Class: LaserCut 3 | 4 | -------------------------------------------------------------------------------- /images/Subtract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/images/Subtract.png -------------------------------------------------------------------------------- /lib/LibLaserCut.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/LibLaserCut.jar -------------------------------------------------------------------------------- /images/Jog Dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/images/Jog Dialog.png -------------------------------------------------------------------------------- /lib/usb4java-1.3.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/usb4java-1.3.0.jar -------------------------------------------------------------------------------- /resources/images/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/resources/images/info.png -------------------------------------------------------------------------------- /LaserCut Files/Circle 2.lzr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/LaserCut Files/Circle 2.lzr -------------------------------------------------------------------------------- /lib/commons-lang3-3.8.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/commons-lang3-3.8.1.jar -------------------------------------------------------------------------------- /lib/commons-logging-1.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/commons-logging-1.2.jar -------------------------------------------------------------------------------- /lib/slf4j-simple-1.7.9.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/slf4j-simple-1.7.9.jar -------------------------------------------------------------------------------- /LaserCut Files/AAA (test).lzr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/LaserCut Files/AAA (test).lzr -------------------------------------------------------------------------------- /images/LaserCut Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/images/LaserCut Screenshot.png -------------------------------------------------------------------------------- /images/Mini Laser Settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/images/Mini Laser Settings.png -------------------------------------------------------------------------------- /images/Preferences Dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/images/Preferences Dialog.png -------------------------------------------------------------------------------- /LaserCut Files/AAA (test)-2.lzr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/LaserCut Files/AAA (test)-2.lzr -------------------------------------------------------------------------------- /Test/PNG Files/Clipboard-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/Test/PNG Files/Clipboard-icon.png -------------------------------------------------------------------------------- /lib/libusb4java-1.3.0-linux-arm.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/libusb4java-1.3.0-linux-arm.jar -------------------------------------------------------------------------------- /lib/libusb4java-1.3.0-linux-x86.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/libusb4java-1.3.0-linux-x86.jar -------------------------------------------------------------------------------- /lib/libusb4java-1.3.0-win32-x86.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/libusb4java-1.3.0-win32-x86.jar -------------------------------------------------------------------------------- /resources/images/LaserCut Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/resources/images/LaserCut Logo.png -------------------------------------------------------------------------------- /lib/libusb4java-1.3.0-linux-x86-64.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/libusb4java-1.3.0-linux-x86-64.jar -------------------------------------------------------------------------------- /lib/libusb4java-1.3.0-win32-x86-64.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/libusb4java-1.3.0-win32-x86-64.jar -------------------------------------------------------------------------------- /lib/libusb4java-1.3.0-darwin-x86-64.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/libusb4java-1.3.0-darwin-x86-64.jar -------------------------------------------------------------------------------- /lib/libusb4java-1.3.0-linux-aarch64.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/lib/libusb4java-1.3.0-linux-aarch64.jar -------------------------------------------------------------------------------- /out/artifacts/LaserCut_jar/LaserCut.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholder/LaserCut/HEAD/out/artifacts/LaserCut_jar/LaserCut.jar -------------------------------------------------------------------------------- /src/CADNoDraw.java: -------------------------------------------------------------------------------- 1 | interface CADNoDraw { 2 | } // Marker Interface (implented by CADRasterImage and CADReference to exclude them from cutting) 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/dictionaries/wholder.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bézier 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/preferred-vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ApexVCS 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/libraries/lib.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /src/test/FileDialogDemo.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | /** 7 | * Use JFileChooser, as it's intended for use with Swing components 8 | */ 9 | public class FileDialogDemo { 10 | public static void main (String[] args) { 11 | FileDialog fd = new FileDialog(new JFrame(), "Choose a file", FileDialog.LOAD); 12 | fd.setDirectory("C:\\"); 13 | fd.setFile("*.xml"); 14 | fd.setVisible(true); 15 | String filename = fd.getFile(); 16 | if (filename == null) 17 | System.out.println("You cancelled the choice"); 18 | else 19 | System.out.println("You chose " + filename); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/ContainsTest.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | // contains(): : x = -0.238, y = -0.129, wid = 1.248, hyt = 1.139 4 | // : x = -0.250, y = -0.250, wid = 0.500, hyt = 0.500 5 | 6 | // contains(): : x = 0.000, y = 0.020, wid = 2.020, hyt = 1.960 7 | // : x = -1.500, y = -0.750, wid = 3.000, hyt = 1.500 8 | 9 | 10 | import java.awt.geom.Rectangle2D; 11 | 12 | public class ContainsTest { 13 | public static void main (String[] args) { 14 | Rectangle2D.Double rect1 = new Rectangle2D.Double(-0.000, 0.000 , 2.000, 2.000); 15 | Rectangle2D.Double rect2 = new Rectangle2D.Double(-0.250, -0.250, 0.500, 0.500); 16 | System.out.println(rect1.contains(rect2)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LaserCut.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/MicroLaser.java: -------------------------------------------------------------------------------- 1 | import java.awt.geom.Rectangle2D; 2 | import java.util.prefs.Preferences; 3 | 4 | public class MicroLaser extends MiniLaser{ 5 | 6 | MicroLaser (LaserCut laserCut, Preferences prefs) { 7 | super(laserCut, prefs); 8 | } 9 | 10 | // Implement for GRBLBase to define Preferences prefix, such as "mini.laser." 11 | String getPrefix () { 12 | return "micro.laser."; 13 | } 14 | 15 | // Implement for LaserCut.OutputDevice 16 | public String getName () { 17 | return "Micro Laser"; 18 | } 19 | 20 | // Implement for LaserCut.OutputDevice 21 | @Override 22 | public Rectangle2D.Double getWorkspaceSize () { 23 | return new Rectangle2D.Double(0, 0, getDouble("workwidth", 1.5), getDouble("workheight", 1.5)); 24 | } 25 | 26 | // Implement for LaserCut.OutputDevice 27 | @Override 28 | public double getZoomFactor () { 29 | return getDouble("workzoom", 4.0); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Wayne Holder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/CADOval.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.Ellipse2D; 3 | import java.io.Serializable; 4 | 5 | class CADOval extends CADShape implements Serializable { 6 | private static final long serialVersionUID = 2518641166287730832L; 7 | public double width, height; 8 | 9 | /** 10 | * Default constructor used to instantiate subclasses in "Shapes" Menu 11 | */ 12 | @SuppressWarnings("unused") 13 | CADOval () { 14 | // Set typical initial values, which user can edit before saving 15 | width = .5; 16 | height = .5; 17 | } 18 | 19 | CADOval (double xLoc, double yLoc, double width, double height, double rotation) { 20 | this.width = width; 21 | this.height = height; 22 | setLocationAndOrientation(xLoc, yLoc, rotation); 23 | } 24 | 25 | @Override 26 | String getMenuName () { 27 | return "Oval"; 28 | } 29 | 30 | @Override 31 | public void resize (double dx, double dy) { 32 | width = Math.max(dx * 2, .1); 33 | height = Math.max(dy * 2, .1); 34 | } 35 | 36 | @Override 37 | String[] getParameterNames () { 38 | return new String[]{ 39 | "width|in", 40 | "height|in"}; 41 | } 42 | 43 | @Override 44 | Shape buildShape () { 45 | return new Ellipse2D.Double(-width / 2, -height / 2, width, height); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.idea/libraries/commons_lang3_3_8_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/CADShapeGroup.java: -------------------------------------------------------------------------------- 1 | import java.io.Serializable; 2 | import java.util.ArrayList; 3 | 4 | /** 5 | * Class used to organize CADShape objects into a items 6 | */ 7 | class CADShapeGroup implements Serializable { 8 | private static final long serialVersionUID = 3210128656295452345L; 9 | private final ArrayList shapesInGroup = new ArrayList<>(); 10 | private transient int id = 0; 11 | private static int counter = 0; 12 | 13 | public int getId () { 14 | if (id == 0) { 15 | id = ++counter; 16 | } 17 | return id; 18 | } 19 | 20 | void addToGroup (CADShape shape) { 21 | CADShapeGroup old = shape.getGroup(); 22 | if (old != null) { 23 | old.shapesInGroup.remove(shape); 24 | shape.setGroup(null); 25 | if (old.shapesInGroup.size() == 1) { 26 | CADShape newSelected = old.shapesInGroup.get(0); 27 | newSelected.setGroup(null); 28 | old.shapesInGroup.clear(); 29 | } 30 | } 31 | if (!shapesInGroup.contains(shape)) { 32 | shapesInGroup.add(shape); 33 | shape.setGroup(this); 34 | } 35 | } 36 | 37 | void removeAllFromGroup () { 38 | for (CADShape shape : shapesInGroup) { 39 | shape.setGroup(null); 40 | } 41 | shapesInGroup.clear(); 42 | } 43 | 44 | boolean containsShape (CADShape shape) { 45 | return shapesInGroup.contains(shape); 46 | } 47 | 48 | ArrayList getGroupList () { 49 | return shapesInGroup; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/SurfaceSettings.java: -------------------------------------------------------------------------------- 1 | import javax.swing.*; 2 | import java.awt.*; 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | /** 7 | * Serializable object used to save and restore settings from ".lzr" files 8 | */ 9 | 10 | public class SurfaceSettings implements Serializable { 11 | private static final long serialVersionUID = 1281736566222122122L; 12 | public final Point viewPoint; 13 | public final double zoomFactor; 14 | public final double gridStep; 15 | public final int gridMajor; 16 | public final String version; 17 | public List design; 18 | 19 | public SurfaceSettings () { 20 | this.viewPoint = new Point(0, 0); 21 | this.zoomFactor = 1; 22 | this.gridStep = 0.1; 23 | this.gridMajor = 10; 24 | this.version = null; 25 | } 26 | 27 | public SurfaceSettings (DrawSurface surface, JViewport viewPort) { 28 | this.viewPoint = viewPort.getViewPosition(); 29 | this.zoomFactor = surface.getZoomFactor(); 30 | this.gridStep = surface.getGridSize(); 31 | this.gridMajor = surface.getGridMajor(); 32 | this.version = LaserCut.VERSION; 33 | } 34 | 35 | public List getDesign () { 36 | return design; 37 | } 38 | 39 | public void setDesign (List design) { 40 | this.design = design; 41 | } 42 | 43 | public String toString () { 44 | return "SurfaceSettingsn:" + 45 | " viewPoint: " + viewPoint + "\n" + 46 | " zoomFactor: " + zoomFactor + "\n" + 47 | " gridStep: " + gridStep + "\n" + 48 | " gridMajor: " + gridMajor + "\n" + 49 | " version: " + version + "\n"; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/CADRectangle.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.Rectangle2D; 3 | import java.awt.geom.RoundRectangle2D; 4 | import java.io.Serializable; 5 | 6 | class CADRectangle extends CADShape implements Serializable { 7 | private static final long serialVersionUID = 5415641155292738232L; 8 | public double width, height, radius; 9 | 10 | /** 11 | * Default constructor is used to instantiate subclasses in "Shapes" Menu 12 | */ 13 | @SuppressWarnings("unused") 14 | CADRectangle () { 15 | // Set typical initial values, which user can edit before saving 16 | width = 1; 17 | height = 1; 18 | } 19 | 20 | CADRectangle (double xLoc, double yLoc, double width, double height, double radius, double rotation) { 21 | this.width = width; 22 | this.height = height; 23 | this.radius = radius; 24 | setLocationAndOrientation(xLoc, yLoc, rotation); 25 | } 26 | 27 | @Override 28 | String getMenuName () { 29 | return "Rectangle"; 30 | } 31 | 32 | @Override 33 | public void resize (double dx, double dy) { 34 | width = Math.max(dx * 2, .1); 35 | height = Math.max(dy * 2, .1); 36 | } 37 | 38 | @Override 39 | String[] getParameterNames () { 40 | return new String[]{ 41 | "width|in{width of rectangle}", 42 | "height|in{height of rectangle}", 43 | "radius|in{radius of corner}"}; 44 | } 45 | 46 | @Override 47 | Shape buildShape () { 48 | if (radius > 0) { 49 | // Note: specifiy 2 x radius for arc height & width 50 | return new RoundRectangle2D.Double(-width / 2, -height / 2, width, height, radius * 2, radius * 2); 51 | } else { 52 | return new Rectangle2D.Double(-width / 2, -height / 2, width, height); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/CADReference.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.Point2D; 3 | import java.awt.geom.Rectangle2D; 4 | import java.io.Serializable; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.prefs.Preferences; 9 | 10 | class CADReference extends CADShape implements Serializable, CADNoDraw { 11 | private static final long serialVersionUID = 8204176292743368277L; 12 | 13 | /** 14 | * Default constructor used to instantiate subclasses in "Shapes" Menu 15 | */ 16 | @SuppressWarnings("unused") 17 | CADReference () { 18 | rotation = 45; 19 | } 20 | 21 | CADReference (double xLoc, double yLoc) { 22 | setLocationAndOrientation(xLoc, yLoc, 0); 23 | } 24 | 25 | @Override 26 | void createAndPlace (DrawSurface surface, LaserCut laserCut, Preferences prefs) { 27 | surface.placeShape(this); 28 | } 29 | 30 | @Override 31 | String getMenuName () { 32 | return "Reference Point"; 33 | } 34 | 35 | @Override 36 | Shape buildShape () { 37 | return new Rectangle2D.Double(-.1, -.1, .2, .2); 38 | } 39 | 40 | @Override 41 | Color getShapeColor () { 42 | return new Color(0, 128, 0); 43 | } 44 | 45 | @Override 46 | BasicStroke getShapeStroke () { 47 | return Utils2D.getDashedStroke(getStrokeWidth(), 3.0f, 3.0f); 48 | } 49 | 50 | @Override 51 | boolean isShapeClicked (Point2D.Double point, double zoomFactor) { 52 | return super.isShapeClicked(point, zoomFactor) || isPositionClicked(point, zoomFactor); 53 | } 54 | 55 | @Override 56 | protected java.util.List getPlaceFields () { 57 | return new ArrayList<>(); 58 | } 59 | 60 | @Override 61 | protected List getEditFields () { 62 | return Arrays.asList( 63 | "xLoc|in", 64 | "yLoc|in"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.idea/artifacts/LaserCut_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/out/artifacts/LaserCut_jar 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/CADPolygon.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.Path2D; 3 | import java.io.Serializable; 4 | 5 | class CADPolygon extends CADShape implements Serializable { 6 | private static final long serialVersionUID = 973284612591842108L; 7 | public int sides; 8 | public double diameter; 9 | 10 | /** 11 | * Default constructor used to instantiate subclasses in "Shapes" Menu 12 | */ 13 | @SuppressWarnings("unused") 14 | CADPolygon () { 15 | // Set typical initial values, which user can edit before saving 16 | diameter = 1.0; 17 | sides = 6; 18 | } 19 | 20 | CADPolygon (double xLoc, double yLoc, double diameter, int sides, double rotation) { 21 | this.diameter = diameter; 22 | this.sides = sides; 23 | setLocationAndOrientation(xLoc, yLoc, rotation); 24 | } 25 | 26 | @Override 27 | String getMenuName () { 28 | return "Regular Polygon"; 29 | } 30 | 31 | @Override 32 | public void resize (double dx, double dy) { 33 | diameter = Math.max(Math.sqrt(dx * dx + dy + dy), .1); 34 | } 35 | 36 | @Override 37 | String[] getParameterNames () { 38 | return new String[]{ 39 | "sides{number of sides}", 40 | "diameter|in"}; 41 | } 42 | 43 | @Override 44 | Shape buildShape () { 45 | //return new Ellipse2D.Double(-width / 2, -height / 2, width, height); 46 | Path2D.Double poly = new Path2D.Double(); 47 | double radius = diameter / 2; 48 | double theta = 2 * Math.PI / sides; 49 | double angle = -Math.PI / 2; 50 | // Adjust angle, where needed, to draw shapes in familiar orientation 51 | if (sides % 2 == 0) { 52 | angle += Math.toRadians(180.0 / sides); 53 | } 54 | boolean first = true; 55 | for (int i = 0; i < sides; ++i) { 56 | double x = Math.cos(angle) * radius; 57 | double y = Math.sin(angle) * radius; 58 | angle += theta; 59 | if (first) { 60 | poly.moveTo(x, y); 61 | } else { 62 | poly.lineTo(x, y); 63 | } 64 | first = false; 65 | } 66 | poly.closePath(); 67 | return poly; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/CADScaledShape.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.AffineTransform; 3 | import java.awt.geom.Point2D; 4 | import java.awt.geom.Rectangle2D; 5 | import java.io.Serializable; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | /* 10 | * CADScaledShape is a container for a resizable Shape. Currently used to encapsulate designs loaded 11 | * by the "Import from SVG" and "Import from DXF" features. 12 | * Note: scale is in percent (%) 13 | */ 14 | class CADScaledShape extends CADShape implements Serializable { 15 | private static final long serialVersionUID = -8732521357598212914L; 16 | public double scale = 100.0; 17 | transient public double lastScale; 18 | 19 | CADScaledShape () { 20 | lastScale = scale; 21 | } 22 | 23 | CADScaledShape (Shape shape, double xLoc, double yLoc, double rotation) { 24 | super(shape, xLoc, yLoc, rotation); 25 | } 26 | 27 | @Override 28 | String getMenuName () { 29 | return "Scaled Shape"; 30 | } 31 | 32 | // Translate Shape to screen position 33 | @Override 34 | protected Shape getWorkspaceTranslatedShape () { 35 | AffineTransform at = new AffineTransform(); 36 | at.translate(xLoc, yLoc); 37 | at.scale(scale / 100.0, scale / 100.0); 38 | return at.createTransformedShape(getLocallyTransformedShape()); 39 | } 40 | 41 | @Override 42 | protected java.util.List getPlaceFields () { 43 | ArrayList list = new ArrayList<>(super.getPlaceFields()); 44 | list.add("scale|%"); 45 | return list; 46 | } 47 | 48 | @Override 49 | protected List getEditFields () { 50 | ArrayList list = new ArrayList<>(super.getEditFields()); 51 | list.add("scale|%"); 52 | return list; 53 | } 54 | 55 | @Override 56 | public void resize (double dx, double dy) { 57 | Rectangle2D bnds = shape.getBounds2D(); 58 | scale = (Math.min(dx / bnds.getWidth(), dy / bnds.getHeight())) * 200; 59 | if (scale != lastScale) { 60 | lastScale = scale; 61 | } 62 | } 63 | 64 | @Override 65 | protected Point2D.Double getResizeOrRotateHandle () { 66 | Rectangle2D bnds = shape.getBounds2D(); 67 | double sFactor = scale / 100; 68 | return new Point2D.Double(xLoc + bnds.getWidth() / 2 * sFactor, yLoc + bnds.getHeight() / 2 * sFactor); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/CADNemaMotor.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.Area; 3 | import java.awt.geom.Ellipse2D; 4 | import java.io.Serializable; 5 | 6 | class CADNemaMotor extends CADShape implements Serializable, LaserCut.NorResizable { 7 | private static final long serialVersionUID = 2518641166287730832L; 8 | private static final double M2 = Utils2D.mmToInches(2); 9 | private static final double M2_5 = Utils2D.mmToInches(2.5); 10 | private static final double M3 = Utils2D.mmToInches(3); 11 | private static final double M4_5 = Utils2D.mmToInches(4.5); 12 | // 8 11 14 17 24 13 | private static final double[] ringDiameter = {0.5906, 0.865, 0.865, 0.865, 1.5}; 14 | private static final double[] holeSpacing = {0.630, 0.91, 1.02, 1.22, 1.86}; 15 | private static final double[] holeDiameter = {M2, M2_5, M3, M3, M4_5}; 16 | public String type; // Note: value is index into tables 17 | 18 | /** 19 | * Default constructor used to instantiate subclasses in "Shapes" Menu 20 | */ 21 | @SuppressWarnings("unused") 22 | CADNemaMotor () { 23 | // Set typical initial values, which user can edit before saving 24 | type = "1"; 25 | } 26 | 27 | CADNemaMotor (double xLoc, double yLoc, String type, double rotation) { 28 | this.type = type; 29 | setLocationAndOrientation(xLoc, yLoc, rotation); 30 | } 31 | 32 | @Override 33 | String getMenuName () { 34 | return "NEMA Motor"; 35 | } 36 | 37 | @Override 38 | String[] getParameterNames () { 39 | return new String[] { 40 | "type:Nema 8|0:Nema 11|1:Nema 14|2:Nema 17|3:Nema 23|4{NEMA motor size}" 41 | }; 42 | } 43 | 44 | @Override 45 | Shape buildShape () { 46 | int idx = Integer.parseInt(type); 47 | double diameter = ringDiameter[idx]; 48 | double off = holeSpacing[idx] / 2; 49 | double hd = holeDiameter[idx]; 50 | double hr = hd / 2; 51 | Area a1 = new Area(new Ellipse2D.Double(-diameter / 2, -diameter / 2, diameter, diameter)); 52 | a1.add(new Area(new Ellipse2D.Double(-off - hr, -off - hr, hd, hd))); 53 | a1.add(new Area(new Ellipse2D.Double( off - hr, -off - hr, hd, hd))); 54 | a1.add(new Area(new Ellipse2D.Double(-off - hr, off - hr, hd, hd))); 55 | a1.add(new Area(new Ellipse2D.Double( off - hr, off - hr, hd, hd))); 56 | return a1; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/CADGear.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.Ellipse2D; 3 | import java.io.Serializable; 4 | 5 | class CADGear extends CADShape implements Serializable, LaserCut.NorResizable { 6 | private static final long serialVersionUID = 2334548672295293845L; 7 | public double module; 8 | public double pressAngle; 9 | public double profileShift; 10 | public double holeSize; 11 | public double diameter; 12 | public int numTeeth; 13 | public int numPoints; 14 | 15 | /** 16 | * Default constructor is used to instantiate subclasses in "Shapes" Menu 17 | */ 18 | @SuppressWarnings("unused") 19 | CADGear () { 20 | // Set typical initial values, which user can edit before saving 21 | module = .1; 22 | numTeeth = 15; 23 | numPoints = 10; 24 | pressAngle = 20; 25 | profileShift = .25; 26 | holeSize = .125; 27 | diameter = numTeeth * module; 28 | } 29 | 30 | CADGear (double xLoc, double yLoc, double module, int numTeeth, int numPoints, double pressAngle, double profileShift, 31 | double rotation, double holeSize) { 32 | setLocationAndOrientation(xLoc, yLoc, rotation); 33 | this.module = module; 34 | this.numTeeth = numTeeth; 35 | this.numPoints = numPoints; 36 | this.pressAngle = pressAngle; 37 | this.profileShift = profileShift; 38 | this.holeSize = holeSize; 39 | diameter = numTeeth * module; 40 | } 41 | 42 | @Override 43 | String getMenuName () { 44 | return "Gear"; 45 | } 46 | 47 | @Override 48 | String[] getParameterNames () { 49 | return new String[]{ 50 | "module{module = diameter / numTeeth}", 51 | "numTeeth", 52 | "numPoints", 53 | "pressAngle|deg", 54 | "profileShift", 55 | "*diameter|in{diameter = numTeeth * module}", 56 | "holeSize|in"}; 57 | } 58 | 59 | @Override 60 | Shape buildShape () { 61 | return GearGen.generateGear(module, numTeeth, numPoints, pressAngle, profileShift, holeSize); 62 | } 63 | 64 | @Override 65 | void draw (Graphics g, double zoom, boolean keyRotate, boolean keyResize, boolean keyOption) { 66 | super.draw(g, zoom, keyRotate, keyResize, keyOption); 67 | Graphics2D g2 = (Graphics2D) g; 68 | // Draw dashed line in magenta to show effective gear diameter 69 | g2.setColor(Color.MAGENTA); 70 | g2.setStroke(Utils2D.getDashedStroke(getStrokeWidth(), 108.0f, 10.0f)); 71 | double diam = module * numTeeth; 72 | double screen = zoom * LaserCut.SCREEN_PPI; 73 | g2.draw(new Ellipse2D.Double((xLoc - diam / 2) * screen, (yLoc - diam / 2) * screen, diam * screen, diam * screen)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/ShapeWindow.java: -------------------------------------------------------------------------------- 1 | import javax.swing.*; 2 | import java.awt.*; 3 | import java.awt.geom.AffineTransform; 4 | import java.awt.geom.Rectangle2D; 5 | import java.util.List; 6 | import java.io.*; 7 | 8 | /** 9 | * ShapeWindow: Used by test code in SVGParser, GearGen and CornerFinder 10 | */ 11 | 12 | class ShapeWindow extends JFrame { 13 | static class ShapeCanvas extends Canvas { 14 | private transient Image offScr; 15 | private Dimension lastDim; 16 | private final List shapes; 17 | private final double border; 18 | 19 | ShapeCanvas (List shapes, double border) { 20 | this.shapes = shapes; 21 | this.border = border; 22 | Rectangle2D bounds = null; 23 | // Create a bounding box that's the union of all shapes in the shapes array 24 | for (Shape shape : shapes) { 25 | bounds = bounds == null ? BetterBoundingBox.getBounds(shape) : bounds.createUnion(BetterBoundingBox.getBounds(shape)); 26 | } 27 | if (bounds != null) { 28 | int wid = (int) Math.round((bounds.getWidth() + border * 2) * LaserCut.SCREEN_PPI); 29 | int hyt = (int) Math.round((bounds.getHeight() + border * 2) * LaserCut.SCREEN_PPI); 30 | setSize(new Dimension(wid, hyt)); 31 | } 32 | } 33 | 34 | public void paint (Graphics g) { 35 | Dimension d = getSize(); 36 | if (offScr == null || (lastDim != null && (d.width != lastDim.width || d.height != lastDim.height))) 37 | offScr = createImage(d.width, d.height); 38 | lastDim = d; 39 | Graphics2D g2 = (Graphics2D) offScr.getGraphics(); 40 | g2.setBackground(getBackground()); 41 | g2.clearRect(0, 0, d.width, d.height); 42 | g2.setColor(Color.black); 43 | AffineTransform atScale = new AffineTransform(); 44 | atScale.translate(border * LaserCut.SCREEN_PPI, border * LaserCut.SCREEN_PPI); 45 | atScale.scale(LaserCut.SCREEN_PPI, LaserCut.SCREEN_PPI); 46 | for (Shape shape : shapes) { 47 | g2.draw(atScale.createTransformedShape(shape)); 48 | } 49 | g.drawImage(offScr, 0, 0, this); 50 | } 51 | } 52 | 53 | ShapeWindow (List shapes, double border) { 54 | shapes = Utils2D.removeOffset(shapes); 55 | add(new ShapeCanvas(shapes, border), BorderLayout.CENTER); 56 | setLocationRelativeTo(null); 57 | pack(); 58 | setVisible(true); 59 | } 60 | 61 | public static void main (String[] args) throws Exception { 62 | SVGParser parser = new SVGParser(); 63 | List shapes = parser.parseSVG(new File("Test/SVG Files/heart.svg")); 64 | shapes = Utils2D.removeOffset(shapes); 65 | new ShapeWindow(Utils2D.removeOffset(shapes), 0.125); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/SerializationTest.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import java.awt.*; 4 | import java.io.*; 5 | import java.util.ArrayList; 6 | 7 | public class SerializationTest { 8 | 9 | public static class SurfaceSettings implements Serializable { 10 | private static final long serialVersionUID = 1281736566222122122L; 11 | public final Point viewPoint; 12 | public final double zoomFactor; 13 | public final double gridStep; 14 | public final int gridMajor; 15 | public final String version; 16 | 17 | public SurfaceSettings () { 18 | this(new Point(0, 1), 1, 0.1, 1); 19 | } 20 | 21 | public SurfaceSettings (Point viewPoint, double zoomFactor, double gridStep, int gridMajor) { 22 | this.viewPoint = viewPoint; 23 | this.zoomFactor = zoomFactor; 24 | this.gridStep = gridStep; 25 | this.gridMajor = gridMajor; 26 | this.version = "dummy2"; 27 | } 28 | 29 | public String toString () { 30 | return "viewPoint.x: " + viewPoint.x + ", viewPoint.y: " + viewPoint.y + ", zoomFactor: " + zoomFactor + 31 | ", gridStep: " + gridStep +", gridStep: " + gridMajor +", gridMajor: " + gridMajor + ", version: " + version; 32 | } 33 | } 34 | 35 | public static void main (String[] args) throws Exception { 36 | ArrayList s1 = new ArrayList<>(); 37 | s1.add("s1a"); 38 | s1.add("s2a"); 39 | s1.add("s3a"); 40 | ArrayList s2 = new ArrayList<>(); 41 | s2.add("s1b"); 42 | s2.add("s2b"); 43 | s2.add("s3b"); 44 | SurfaceSettings settings1 = new SurfaceSettings(); 45 | // 46 | // Write serialized data 47 | // 48 | File wFile = new File("Test/SerializationTest.obj"); 49 | FileOutputStream fileOut = new FileOutputStream(wFile); 50 | ObjectOutputStream out = new ObjectOutputStream(fileOut); 51 | out.writeObject(s1); 52 | out.writeObject(s2); 53 | out.writeObject(settings1); 54 | out.close(); 55 | fileOut.close(); 56 | // 57 | // Readback serialized data 58 | // 59 | File rFile = new File("Test/SerializationTest.obj"); 60 | FileInputStream fileIn = new FileInputStream(rFile); 61 | ObjectInputStream in = new ObjectInputStream(fileIn); 62 | ArrayList r1 = (ArrayList) in.readObject(); 63 | ArrayList r2 = (ArrayList) in.readObject(); 64 | SurfaceSettings settings2 = (SurfaceSettings) in.readObject(); 65 | in.close(); 66 | fileIn.close(); 67 | System.out.println("List r1"); 68 | for (String string : r1) { 69 | System.out.println(" " + string); 70 | } 71 | System.out.println("List r2"); 72 | for (String string : r2) { 73 | System.out.println(" " + string); 74 | } 75 | System.out.println(" settings2: " + settings2); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/FileChooserDemo.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import java.beans.PropertyChangeEvent; 4 | import java.beans.PropertyChangeListener; 5 | import java.io.*; 6 | import java.awt.*; 7 | import javax.swing.*; 8 | 9 | public class FileChooserDemo extends JPanel { 10 | JButton openButton; 11 | JFileChooser fileChooser; 12 | 13 | static class MyPanel extends JPanel { 14 | ImageIcon icon; 15 | 16 | MyPanel () { 17 | //setLayout(new BorderLayout()); 18 | setPreferredSize(new Dimension(200, 200)); 19 | setBorder(BorderFactory.createLineBorder(Color.BLACK)); 20 | } 21 | 22 | void setIcon (String fName) { 23 | icon = new ImageIcon(fName); 24 | repaint(); 25 | } 26 | 27 | @Override 28 | public void paint (Graphics gg) { 29 | Graphics2D g2 = (Graphics2D) gg; 30 | Dimension dim = getSize(); 31 | g2.setColor(Color.white); 32 | g2.fillRect(0, 0, dim.width, dim.height); 33 | icon.paintIcon(this, g2, (dim.width - icon.getIconWidth()) / 2, (dim.height - icon.getIconHeight()) / 2); 34 | 35 | } 36 | } 37 | 38 | public FileChooserDemo () { 39 | super(new BorderLayout()); 40 | fileChooser = new JFileChooser(); 41 | openButton = new JButton("Open a File..."); 42 | openButton.addActionListener(ev -> { 43 | if (ev.getSource() == openButton) { 44 | int returnVal = fileChooser.showOpenDialog(FileChooserDemo.this); 45 | if (returnVal == JFileChooser.APPROVE_OPTION) { 46 | File file = fileChooser.getSelectedFile(); 47 | System.out.println(file.getAbsolutePath()); 48 | } 49 | } 50 | }); 51 | addPropertyChangeListener(new PropertyChangeListener() { 52 | @Override 53 | public void propertyChange (PropertyChangeEvent evt) { 54 | System.out.println(evt.getPropertyName()); 55 | } 56 | }); 57 | JPanel buttonPanel = new JPanel(); //use FlowLayout 58 | buttonPanel.add(openButton); 59 | add(buttonPanel, BorderLayout.PAGE_START); 60 | // Widen JChooser by 50% 61 | Dimension dim = fileChooser.getPreferredSize(); 62 | fileChooser.setPreferredSize(new Dimension((int) (dim.width * 1.5), dim.height)); 63 | MyPanel panel = new MyPanel(); 64 | panel.setIcon("/Users/wholder/IdeaProjects/LaserCut2/Test/PNG Files/Clipboard-icon.png"); 65 | fileChooser.setAccessory(panel); 66 | } 67 | 68 | public static void main (String[] args) { 69 | SwingUtilities.invokeLater(new Runnable() { 70 | public void run () { 71 | JFrame frame = new JFrame("FileChooserDemo"); 72 | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 73 | frame.add(new FileChooserDemo()); 74 | frame.pack(); 75 | frame.setLocationRelativeTo(null); 76 | frame.setVisible(true); 77 | } 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/CNCPath.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.*; 3 | import java.io.IOException; 4 | import java.io.Serializable; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | class CNCPath extends CADShape implements Serializable, LaserCut.ChangeListener { 9 | private static final long serialVersionUID = 940023721688314265L; 10 | private final CADShape baseShape; 11 | public double radius; 12 | public boolean inset; 13 | 14 | CNCPath (CADShape base, double radius, boolean inset) { 15 | this.baseShape = base; 16 | this.radius = Math.abs(radius); 17 | this.inset = inset; 18 | baseShape.addChangeListener(this); 19 | } 20 | 21 | @Override 22 | String getMenuName () { 23 | return "CNC Path"; 24 | } 25 | 26 | /* 27 | * Reattach ChangeListener after deserialization 28 | */ 29 | private void readObject (java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { 30 | in.defaultReadObject(); 31 | baseShape.addChangeListener(this); 32 | } 33 | 34 | public void shapeChanged (CADShape base) { 35 | updateShape(); 36 | } 37 | 38 | @Override 39 | protected List getEditFields () { 40 | return Arrays.asList( 41 | "radius|in{radius of tool}", 42 | "inset{If checked, toolpath is inside cadShape, else outside}"); 43 | } 44 | 45 | @Override 46 | Color getShapeColor () { 47 | return new Color(0, 153, 0); 48 | } 49 | 50 | @Override 51 | protected Shape getLocallyTransformedShape () { 52 | Shape dShape = getShape(); 53 | AffineTransform at = new AffineTransform(); 54 | // Position Shape centered on xLoc/yLoc in inches (x from left, y from top) 55 | at.rotate(Math.toRadians(rotation)); 56 | if (!centered) { // REWORK 57 | // Translate relative to the baseShape's coordinates so the generated cnc path aligns with it 58 | Rectangle2D bounds = baseShape.getShape().getBounds2D(); 59 | at.translate(bounds.getWidth() / 2, bounds.getHeight() / 2); 60 | } 61 | return at.createTransformedShape(dShape); 62 | } 63 | 64 | @Override 65 | Shape buildShape () { 66 | xLoc = baseShape.xLoc; 67 | yLoc = baseShape.yLoc; 68 | centered = baseShape.centered; 69 | rotation = baseShape.rotation; 70 | Path2D.Double path = new Path2D.Double(); 71 | boolean first = true; 72 | for (Line2D.Double[] lines : Utils2D.transformShapeToLines(baseShape.getShape(), 1.0, .01)) { 73 | Point2D.Double[] points = CNCTools.pruneOverlap(CNCTools.getParallelPath(lines, radius, !inset)); 74 | for (Point2D.Double point : points) { 75 | if (first) { 76 | path.moveTo(point.x, point.y); 77 | first = false; 78 | } else { 79 | path.lineTo(point.x, point.y); 80 | } 81 | } 82 | // Connect back to beginning 83 | path.lineTo(points[0].x, points[0].y); 84 | first = true; 85 | } 86 | return path; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/USBIO.java: -------------------------------------------------------------------------------- 1 | import org.usb4java.*; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.ByteOrder; 5 | import java.nio.IntBuffer; 6 | 7 | /** 8 | * Implements a bulk transfer I/O driver that uses usb4java to communicate with a USB Device 9 | * such as a Silhouette Curio, Cameo or Portrait using the Usb4Java Library. 10 | * 11 | * See: http://usb4java.org, and http://usb4java.org/apidocs/index.html for more info 12 | */ 13 | 14 | class USBIO { 15 | private static final int TIMEOUT = 500; 16 | private DeviceHandle handle; 17 | private final Context context = new Context(); 18 | private final byte iFace; 19 | private final byte outEnd; 20 | private final byte inEnd; 21 | 22 | USBIO (short vendorId, short productId, byte iFace, byte outEnd, byte inEnd) { 23 | this.iFace = iFace; 24 | this.outEnd = outEnd; 25 | this.inEnd = inEnd; 26 | int error = LibUsb.init(context); 27 | if (error != LibUsb.SUCCESS) { 28 | throw new LibUsbException("Unable to initialize libusb", error); 29 | } 30 | DeviceList list = new DeviceList(); 31 | if ((error = LibUsb.getDeviceList(context, list)) < 0) { 32 | throw new LibUsbException("Unable to get device list", error); 33 | } 34 | for (Device device : list) { 35 | DeviceDescriptor desc = new DeviceDescriptor(); 36 | LibUsb.getDeviceDescriptor(device, desc); 37 | if (desc.idVendor() == vendorId && desc.idProduct() == productId) { 38 | handle = new DeviceHandle(); 39 | if ((error = LibUsb.open(device, handle)) >= 0) { 40 | if ((error = LibUsb.claimInterface(handle, iFace)) == LibUsb.SUCCESS) { 41 | return; 42 | } else { 43 | if (LibUsb.detachKernelDriver(handle, iFace) == LibUsb.SUCCESS) { 44 | if ((error = LibUsb.claimInterface(handle, iFace)) == LibUsb.SUCCESS) { 45 | return; 46 | } 47 | throw new LibUsbException("Unable to claim interface", error); 48 | } 49 | } 50 | } 51 | } 52 | } 53 | throw new LibUsbException("Unable to open device", error); 54 | } 55 | 56 | void send (byte[] data) { 57 | ByteBuffer outBuf = BufferUtils.allocateByteBuffer(data.length); 58 | outBuf.put(data); 59 | IntBuffer outNum = IntBuffer.allocate(1); 60 | int error; 61 | if ((error = LibUsb.bulkTransfer(handle, outEnd, outBuf, outNum, TIMEOUT)) < 0) { 62 | throw new LibUsbException("Unable to send data", error); 63 | } 64 | } 65 | 66 | byte[] receive () { 67 | return receive(TIMEOUT); 68 | } 69 | 70 | byte[] receive (int timeout) { 71 | ByteBuffer inBuf = ByteBuffer.allocateDirect(64).order(ByteOrder.LITTLE_ENDIAN); 72 | IntBuffer inNum = IntBuffer.allocate(1); // Used to get bytes read count 73 | if (LibUsb.bulkTransfer(handle, inEnd, inBuf, inNum, timeout) >= 0) { 74 | if (inBuf.hasArray()) { 75 | return inBuf.array(); 76 | } else { 77 | int cnt = inNum.get(0); 78 | byte[] data = new byte[cnt]; 79 | for (int ii = 0; ii < cnt; ii++) { 80 | data[ii] = inBuf.get(); 81 | } 82 | inBuf.clear(); 83 | return data; 84 | } 85 | } 86 | return new byte[0]; 87 | } 88 | 89 | void close () { 90 | try { 91 | int error = LibUsb.releaseInterface(handle, iFace); 92 | if (error != LibUsb.SUCCESS) { 93 | throw new LibUsbException("Unable to release interface", error); 94 | } 95 | } finally { 96 | LibUsb.close(handle); 97 | LibUsb.exit(context); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/PDFTools.java: -------------------------------------------------------------------------------- 1 | import org.apache.pdfbox.pdmodel.PDDocument; 2 | import org.apache.pdfbox.pdmodel.PDDocumentInformation; 3 | import org.apache.pdfbox.pdmodel.PDPage; 4 | import org.apache.pdfbox.pdmodel.PDPageContentStream; 5 | import org.apache.pdfbox.pdmodel.common.PDRectangle; 6 | import org.apache.pdfbox.util.Matrix; 7 | 8 | import java.awt.*; 9 | import java.awt.geom.AffineTransform; 10 | import java.awt.geom.PathIterator; 11 | import java.io.File; 12 | import java.io.FileOutputStream; 13 | import java.util.Calendar; 14 | import java.util.List; 15 | 16 | class PDFTools { 17 | static void writePDF (List design, Dimension workSize, File file) throws Exception { 18 | FileOutputStream output = new FileOutputStream(file); 19 | double scale = 72; 20 | PDDocument doc = new PDDocument(); 21 | PDDocumentInformation docInfo = doc.getDocumentInformation(); 22 | docInfo.setCreator("Wayne Holder's LaserCut"); 23 | docInfo.setProducer("Apache PDFBox " + org.apache.pdfbox.util.Version.getVersion()); 24 | docInfo.setCreationDate(Calendar.getInstance()); 25 | double wid = workSize.width / LaserCut.SCREEN_PPI * 72; 26 | double hyt = workSize.height / LaserCut.SCREEN_PPI * 72; 27 | PDPage pdpage = new PDPage(new PDRectangle((float) wid, (float) hyt)); 28 | doc.addPage(pdpage); 29 | PDPageContentStream stream = new PDPageContentStream(doc, pdpage, PDPageContentStream.AppendMode.APPEND, false); 30 | // Flip Y axis so origin is at upper left 31 | Matrix flipY = new Matrix(); 32 | flipY.translate(0, pdpage.getBBox().getHeight()); 33 | flipY.scale(1, -1); 34 | stream.transform(flipY); 35 | AffineTransform at = AffineTransform.getScaleInstance(scale, scale); 36 | for (CADShape item : design) { 37 | if (item instanceof CADReference) 38 | continue; 39 | if (item.engrave) { 40 | stream.setStrokingColor(Color.lightGray); 41 | stream.setLineWidth(1.0f); 42 | } else { 43 | stream.setStrokingColor(Color.black); 44 | stream.setLineWidth(0.001f); 45 | } 46 | Shape shape = item.getWorkspaceTranslatedShape(); 47 | // Use PathIterator to generate sequence of line or curve segments 48 | PathIterator pi = shape.getPathIterator(at); 49 | while (!pi.isDone()) { 50 | float[] coords = new float[6]; // p1.x, p1.y, p2.x, p2.y, p3.x, p3.y 51 | int type = pi.currentSegment(coords); 52 | switch (type) { 53 | case PathIterator.SEG_MOVETO: // 0 54 | // Move to start of a line, or bezier curve segment 55 | stream.moveTo(coords[0], coords[1]); 56 | break; 57 | case PathIterator.SEG_LINETO: // 1 58 | // Draw line from previous point to new point 59 | stream.lineTo(coords[0], coords[1]); 60 | break; 61 | case PathIterator.SEG_QUADTO: // 2 62 | // Write 3 point, quadratic bezier curve from previous point to new point using one control point 63 | stream.curveTo2(coords[0], coords[1], coords[2], coords[3]); 64 | break; 65 | case PathIterator.SEG_CUBICTO: // 3 66 | // Write 4 point, cubic bezier curve from previous point to new point using two control points 67 | stream.curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]); 68 | break; 69 | case PathIterator.SEG_CLOSE: // 4 70 | // Close and write out the current curve 71 | stream.closeAndStroke(); 72 | break; 73 | default: 74 | System.out.println("Error, Unknown PathIterator Type: " + type); 75 | break; 76 | } 77 | pi.next(); 78 | } 79 | } 80 | stream.close(); 81 | doc.save(output); 82 | doc.close(); 83 | output.close(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/ImagePreviewJFileChooser.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.beans.*; 6 | import javax.swing.filechooser.*; 7 | import java.awt.image.*; 8 | import javax.imageio.*; 9 | import java.io.*; 10 | import java.nio.file.Files; 11 | import java.util.concurrent.*; 12 | 13 | // /Users/wholder/IdeaProjects/LaserCut2/Test/PNG Files/Clipboard-icon.png 14 | 15 | class ImagePreviewJFileChooser extends JFrame { 16 | private static final String FILE_DIR = "/Users/wholder/IdeaProjects/LaserCut2/Test/PNG Files/"; 17 | private static final int IMG_WID = 200; 18 | private static final int IMG_HYT = 200; 19 | private static final int IMG_BORDER = 30; 20 | 21 | public ImagePreviewJFileChooser (boolean preview) { 22 | setDefaultCloseOperation(EXIT_ON_CLOSE); 23 | JFileChooser fileChooser = new JFileChooser(); 24 | fileChooser.setAcceptAllFileFilterUsed(false); 25 | fileChooser.setFileFilter(new FileNameExtensionFilter("Image Files", "jpg", "jpeg", "png", "gif")); 26 | fileChooser.setCurrentDirectory(new File(FILE_DIR)); 27 | fileChooser.setDialogTitle("Read file"); 28 | if (preview) { 29 | JPanel panel = new JPanel(new BorderLayout()); 30 | panel.setPreferredSize(new Dimension(IMG_WID + IMG_BORDER, IMG_HYT + IMG_BORDER)); 31 | panel.setBorder(BorderFactory.createLineBorder(Color.black)); 32 | setLayout(new FlowLayout()); 33 | JLabel imgLabel = new JLabel(); 34 | imgLabel.setHorizontalAlignment(JLabel.CENTER); 35 | imgLabel.setVerticalAlignment(JLabel.CENTER); 36 | panel.add(imgLabel, BorderLayout.CENTER); 37 | fileChooser.setAccessory(panel); 38 | Dimension dim = fileChooser.getPreferredSize(); 39 | fileChooser.setPreferredSize(new Dimension((int) (dim.width * 1.25), dim.height)); 40 | fileChooser.addPropertyChangeListener(new PropertyChangeListener() { 41 | 42 | public void propertyChange (PropertyChangeEvent pe) { 43 | if (pe.getPropertyName().equals("SelectedFileChangedProperty")) { 44 | SwingWorker worker = new SwingWorker() { 45 | 46 | protected Image doInBackground () { 47 | if (pe.getPropertyName().equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)) { 48 | File file = fileChooser.getSelectedFile(); 49 | try { 50 | BufferedImage buf = ImageIO.read(Files.newInputStream(file.toPath())); 51 | return buf.getScaledInstance(IMG_WID, IMG_WID, BufferedImage.SCALE_FAST); 52 | } catch (Exception e) { 53 | imgLabel.setText(" Not valid image/Unable to read"); 54 | } 55 | } 56 | return null; 57 | } 58 | 59 | protected void done () { 60 | try { 61 | Image img = get(1L, TimeUnit.NANOSECONDS); 62 | if (img != null) { 63 | imgLabel.setIcon(new ImageIcon(img)); 64 | } 65 | } catch (Exception e) { 66 | imgLabel.setText(" Error"); 67 | } 68 | } 69 | }; 70 | worker.execute(); 71 | } 72 | } 73 | }); 74 | } 75 | JButton open = new JButton("Open File Chooser"); 76 | JPanel outer = new JPanel(new BorderLayout()); 77 | outer.setBorder(BorderFactory.createEmptyBorder(50, 20, 50, 20)); 78 | outer.add(open, BorderLayout.CENTER); 79 | add(outer); 80 | open.addActionListener(ae -> { 81 | fileChooser.setDialogType(JFileChooser.OPEN_DIALOG); 82 | fileChooser.showDialog(null, null); 83 | }); 84 | pack(); 85 | setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 86 | setLocationRelativeTo(null); 87 | setVisible(true); 88 | } 89 | 90 | 91 | public static void main (String[] args) { 92 | new ImagePreviewJFileChooser(true); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/CADBobbin.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.Path2D; 3 | import java.io.Serializable; 4 | 5 | class CADBobbin extends CADShape implements Serializable { 6 | private static final long serialVersionUID = 8835012456785552127L; 7 | public double width; 8 | public double height; 9 | public double slotDepth; 10 | public double radius; 11 | 12 | /** 13 | * Default constructor is used to instantiate subclasses in "Shapes" Menu 14 | */ 15 | @SuppressWarnings("unused") 16 | CADBobbin () { 17 | // Set typical initial values, which user can edit before saving 18 | width = 3.75; 19 | height = 5.8; 20 | slotDepth = 1.8; 21 | radius = 0.125; 22 | } 23 | 24 | CADBobbin (double xLoc, double yLoc, double width, double height, double slotDepth, double radius, double rotation) { 25 | this.width = width; 26 | this.height = height; 27 | this.slotDepth = slotDepth; 28 | this.radius = radius; 29 | setLocationAndOrientation(xLoc, yLoc, rotation); 30 | } 31 | 32 | @Override 33 | String getMenuName () { 34 | return "Bobbin"; 35 | } 36 | 37 | @Override 38 | String[] getParameterNames () { 39 | return new String[]{ 40 | "width|in", 41 | "height|in", 42 | "slotDepth|in", 43 | "radius|in"}; 44 | } 45 | 46 | @Override 47 | Shape buildShape () { 48 | // Note: Draw cadShape as if centered on origin 49 | double xx = -width / 2; 50 | double yy = -height / 2; 51 | double tab = .75; 52 | Path2D.Double polygon = new Path2D.Double(); 53 | if (radius > 0) { 54 | polygon.moveTo(xx, yy + radius); 55 | polygon.quadTo(xx, yy, xx + radius, yy); 56 | polygon.lineTo(xx + tab - radius, yy); 57 | polygon.quadTo(xx + tab, yy, xx + tab, yy + radius); 58 | polygon.lineTo(xx + tab, yy + slotDepth - radius); 59 | polygon.quadTo(xx + tab, yy + slotDepth, xx + tab + radius, yy + slotDepth); 60 | polygon.lineTo(xx + width - tab - radius, yy + slotDepth); 61 | polygon.quadTo(xx + width - tab, yy + slotDepth, xx + width - tab, yy + slotDepth - radius); 62 | polygon.lineTo(xx + width - tab, yy + radius); 63 | polygon.quadTo(xx + width - tab, yy, xx + width - tab + radius, yy); 64 | polygon.lineTo(xx + width - radius, yy); 65 | polygon.quadTo(xx + width, yy, xx + width, yy + radius); 66 | polygon.lineTo(xx + width, yy + height - radius); 67 | polygon.quadTo(xx + width, yy + height, xx + width - radius, yy + height); 68 | polygon.lineTo(xx + width - tab + radius, yy + height); 69 | polygon.quadTo(xx + width - tab, yy + height, xx + width - tab, yy + height - radius); 70 | polygon.lineTo(xx + width - tab, yy + height - slotDepth + radius); 71 | polygon.quadTo(xx + width - tab, yy + height - slotDepth, xx + width - tab - radius, yy + height - slotDepth); 72 | polygon.lineTo(xx + tab + radius, yy + height - slotDepth); 73 | polygon.quadTo(xx + tab, yy + height - slotDepth, xx + tab, yy + height - slotDepth + radius); 74 | polygon.lineTo(xx + tab, yy + height - radius); 75 | polygon.quadTo(xx + tab, yy + height, xx + tab - radius, yy + height); 76 | polygon.lineTo(xx + radius, yy + height); 77 | polygon.quadTo(xx, yy + height, xx, yy + height - radius); 78 | } else { 79 | polygon.moveTo(xx, yy); 80 | polygon.lineTo(xx + tab, yy); 81 | polygon.lineTo(xx + tab, yy + slotDepth); 82 | polygon.lineTo(xx + width - tab, yy + slotDepth); 83 | polygon.lineTo(xx + width - tab, yy); 84 | polygon.lineTo(xx + width, yy); 85 | polygon.lineTo(xx + width, yy + height); 86 | polygon.lineTo(xx + width - tab, yy + height); 87 | polygon.lineTo(xx + width - tab, yy + height - slotDepth); 88 | polygon.lineTo(xx + tab, yy + height - slotDepth); 89 | polygon.lineTo(xx + tab, yy + height); 90 | polygon.lineTo(xx, yy + height); 91 | } 92 | polygon.closePath(); 93 | return polygon; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/BetterBoundingBox.java: -------------------------------------------------------------------------------- 1 | import javax.swing.*; 2 | import java.awt.*; 3 | import java.awt.geom.*; 4 | 5 | class BetterBoundingBox { 6 | 7 | static Rectangle2D getBounds (Shape shape) { 8 | Rectangle2D bounds = null; // Note: must be null 9 | PathIterator pi = shape.getPathIterator(null); 10 | double xLoc = 0, yLoc = 0; 11 | double mX = 0, mY = 0; 12 | while (!pi.isDone()) { 13 | double[] crds = new double[6]; 14 | int type = pi.currentSegment(crds); 15 | switch (type) { 16 | case PathIterator.SEG_CLOSE: 17 | assert bounds != null; 18 | bounds = bounds.createUnion((new Line2D.Double(xLoc, yLoc, mX, mY)).getBounds2D()); 19 | break; 20 | case PathIterator.SEG_MOVETO: 21 | mX = xLoc = crds[0]; 22 | mY = yLoc = crds[1]; 23 | if (bounds == null) { 24 | bounds = new Rectangle2D.Double(xLoc, yLoc, 0, 0); 25 | } else { 26 | bounds = bounds.createUnion(new Rectangle2D.Double(xLoc, yLoc, 0, 0)); 27 | } 28 | break; 29 | case PathIterator.SEG_LINETO: 30 | assert bounds != null; 31 | bounds = bounds.createUnion((new Line2D.Double(xLoc, yLoc, xLoc = crds[0], yLoc = crds[1])).getBounds2D()); 32 | break; 33 | case PathIterator.SEG_CUBICTO: 34 | // Decompose 4 point, cubic bezier curve into line segments 35 | Point2D.Double[] tmp = new Point2D.Double[4]; 36 | Point2D.Double[] cControl = { 37 | new Point2D.Double(xLoc, yLoc), 38 | new Point2D.Double(crds[0], crds[1]), 39 | new Point2D.Double(crds[2], crds[3]), 40 | new Point2D.Double(crds[4], crds[5])}; 41 | int segmentsCubic = 6; 42 | for (int ii = 0; ii < segmentsCubic; ii++) { 43 | double t = ((double) ii) / (segmentsCubic - 1); 44 | for (int jj = 0; jj < cControl.length; jj++) 45 | tmp[jj] = new Point2D.Double(cControl[jj].x, cControl[jj].y); 46 | for (int qq = 0; qq < cControl.length - 1; qq++) { 47 | for (int jj = 0; jj < cControl.length - 1; jj++) { 48 | // Subdivide points 49 | tmp[jj].x -= (tmp[jj].x - tmp[jj + 1].x) * t; 50 | tmp[jj].y -= (tmp[jj].y - tmp[jj + 1].y) * t; 51 | } 52 | } 53 | assert bounds != null; 54 | bounds = bounds.createUnion((new Line2D.Double(xLoc, yLoc, xLoc = tmp[0].x, yLoc = tmp[0].y)).getBounds2D()); 55 | } 56 | break; 57 | case PathIterator.SEG_QUADTO: 58 | // Decompose 3 point, quadratic bezier curve into line segments 59 | int segmentsQuad = 5; 60 | Point2D.Double start = new Point2D.Double(xLoc, yLoc); 61 | Point2D.Double control = new Point2D.Double(crds[0], crds[1]); 62 | Point2D.Double end = new Point2D.Double(crds[2], crds[3]); 63 | double xLast = start.x; 64 | double yLast = start.y; 65 | for (int ii = 1; ii < segmentsQuad; ii++) { 66 | // Use step as a ratio to subdivide lines 67 | double step = (double) ii / segmentsQuad; 68 | double x = (1 - step) * (1 - step) * start.x + 2 * (1 - step) * step * control.x + step * step * end.x; 69 | double y = (1 - step) * (1 - step) * start.y + 2 * (1 - step) * step * control.y + step * step * end.y; 70 | assert bounds != null; 71 | bounds = bounds.createUnion((new Line2D.Double(xLast, yLast, x, y)).getBounds2D()); 72 | xLast = x; 73 | yLast = y; 74 | } 75 | bounds = bounds.createUnion((new Line2D.Double(xLast, yLast, xLoc = end.x, yLoc = end.y)).getBounds2D()); 76 | break; 77 | } 78 | pi.next(); 79 | } 80 | return bounds; 81 | } 82 | 83 | static class BetterBoundingTest extends JFrame { 84 | private transient Image offScr; 85 | private Dimension lastDim; 86 | 87 | private BetterBoundingTest () { 88 | setSize(200, 260); 89 | setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 90 | setVisible(true); 91 | } 92 | 93 | public void paint (Graphics g) { 94 | Dimension d = getSize(); 95 | if (offScr == null || (lastDim != null && (d.width != lastDim.width || d.height != lastDim.height))) 96 | offScr = createImage(d.width, d.height); 97 | lastDim = d; 98 | Graphics2D g2 = (Graphics2D) offScr.getGraphics(); 99 | g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 100 | g2.setBackground(getBackground()); 101 | g2.clearRect(0, 0, d.width, d.height); 102 | g2.setColor(Color.black); 103 | // Draw a cubic Bezier cadShape using Path2D.Double 104 | Path2D.Double path = new Path2D.Double(); 105 | path.moveTo(40, 140); 106 | path.curveTo(40, 60, 160, 60, 160, 140); 107 | path.curveTo(160, 220, 40, 220, 40, 140); 108 | path.closePath(); 109 | g2.draw(path); 110 | // Draw the bounding box computed for the path by getBounds2D() 111 | //Rectangle2D bounds = path.getBounds2D(); 112 | // Work around 113 | Rectangle2D bounds = BetterBoundingBox.getBounds(path); 114 | int x = (int) bounds.getMinX(); 115 | int y = (int) bounds.getMinY(); 116 | int wid = (int) bounds.getWidth(); 117 | int hyt = (int) bounds.getHeight(); 118 | g2.setColor(Color.blue); 119 | g2.drawRect(x, y, wid, hyt); 120 | // Draw the location of Bezier control points as small circles 121 | // Control points are: 40,60 - 160,60 - 160,220 - 40,220 122 | g2.setColor(Color.red); 123 | g2.drawOval(40-2, 60-3, 5, 5); 124 | g2.drawOval(160-2, 60-3, 5, 5); 125 | g2.drawOval(160-2, 220-3, 5, 5); 126 | g2.drawOval(40-2, 220-3, 5, 5); 127 | g.drawImage(offScr, 0, 0, this); 128 | } 129 | } 130 | 131 | public static void main (String[] args) { 132 | new BetterBoundingTest(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/EPSWriter.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.AffineTransform; 3 | import java.awt.geom.Path2D; 4 | import java.awt.geom.PathIterator; 5 | import java.awt.geom.Rectangle2D; 6 | import java.io.*; 7 | import java.util.Date; 8 | import java.util.List; 9 | 10 | /** 11 | * This code implements hust enough of the EPS format to write java.awt.Shape pbjects using PathIterator 12 | * 13 | * Note: Space in a PDF file, also known as user space, is measured in PDF units. The PDF specification 14 | * defines PDF units as 72 PDF units to 1 inch. Coordinate origin is the lower left corner (X values 15 | * increase to the right and Y values increase upwards. 16 | */ 17 | 18 | public class EPSWriter { 19 | private final StringBuilder doc = new StringBuilder(); 20 | private boolean closed; 21 | private Rectangle2D bounds; 22 | private final String title; 23 | 24 | EPSWriter (String title) { 25 | this.title = title; 26 | } 27 | 28 | String getEPS () { 29 | StringBuilder buf = new StringBuilder(); 30 | append(buf, "%!PS-Adobe-3.0 EPSF-3.0"); 31 | append(buf, "%%Creator: LaserCut"); 32 | append(buf, "%%Title: " + title); 33 | append(buf, "%%CreationDate: " + new Date()); 34 | if (bounds != null) { 35 | double pad = 10; 36 | double xOff = bounds.getMinX(); 37 | double yOff = bounds.getMinY(); 38 | double width = bounds.getWidth() + pad * 2; 39 | double height = bounds.getHeight() + pad * 2; 40 | append(buf, "%%BoundingBox: 0 0 " + (int) width + " " + (int) height); 41 | append(buf, "%%HiResBoundingBox: 0.0 0.0 " + width + " " + height); 42 | append(buf, "%%DocumentData: Clean7Bit"); 43 | append(buf, "%%DocumentProcessColors: Black"); 44 | append(buf, "%%ColorUsage: Color"); 45 | append(buf, "%%Origin: 0 0"); 46 | append(buf, "%%Pages: 1"); 47 | append(buf, "%%Page: 1 1"); 48 | append(buf, "%%EndComments\n"); 49 | append(buf, "gsave"); 50 | //append(buf, xOff + " " + (yOff - height) + " translate"); 51 | append(buf, (-xOff + pad) + " " + (height + yOff - pad) + " translate"); 52 | append(buf, "1 -1 scale"); 53 | append(buf, "0.0 0.0 0.0 setrgbcolor"); 54 | append(buf, "1 setlinewidth"); 55 | append(buf, "1 setmiterlimit"); 56 | append(buf, "0 setlinejoin"); // 0 = Miter join, 1 = Round join, 2 = Bevel join 57 | append(buf, "2 setlinecap"); 58 | append(buf, "[ ] 0 setdash"); 59 | buf.append(doc); 60 | } 61 | if (!closed) { 62 | append(buf, "grestore"); 63 | append(buf, "showpage"); 64 | append(buf, "\n%%EOF"); 65 | closed = true; 66 | } 67 | return buf.toString(); 68 | } 69 | 70 | static void writeEpsFile (File sFile, List list) throws Exception { 71 | AffineTransform scale = AffineTransform.getScaleInstance(72.0, 72.0); 72 | EPSWriter eps = new EPSWriter("LaserCut: " + sFile.getName()); 73 | for (CADShape item : list) { 74 | if (item instanceof CADReference) 75 | continue; 76 | Shape shape = item.getWorkspaceTranslatedShape(); 77 | shape = scale.createTransformedShape(shape); 78 | eps.draw(shape); 79 | } 80 | eps.writeEPS(sFile); 81 | } 82 | 83 | void writeEPS (File file) throws FileNotFoundException { 84 | PrintWriter out = new PrintWriter(file); 85 | out.println(getEPS()); 86 | out.close(); 87 | } 88 | 89 | public void draw (Shape shape) { 90 | if (closed) { 91 | throw new IllegalStateException("EPSWriter closed"); 92 | } 93 | bounds = bounds != null ? bounds.createUnion(shape.getBounds2D()) : shape.getBounds2D(); 94 | append("newpath"); 95 | double[] coords = new double[6]; 96 | PathIterator it = shape.getPathIterator(null); 97 | double x0 = 0; 98 | double y0 = 0; 99 | while (!it.isDone()) { 100 | int type = it.currentSegment(coords); 101 | switch (type) { 102 | case PathIterator.SEG_MOVETO: // 0 103 | append(coords[0] + " " + coords[1] + " moveto"); 104 | x0 = coords[0]; 105 | y0 = coords[1]; 106 | break; 107 | case PathIterator.SEG_LINETO: // 1 108 | append(coords[0] + " " + coords[1] + " lineto"); 109 | x0 = coords[0]; 110 | y0 = coords[1]; 111 | break; 112 | case PathIterator.SEG_QUADTO: // 2 113 | // Convert quad curve into a cubic. 114 | double x1 = x0 + 2.0 / 3.0 * (coords[0] - x0); 115 | double y1 = y0 + 2.0 / 3.0 * (coords[1] - y0); 116 | double x2 = coords[0] + 1.0 / 3.0 * (coords[2] - coords[0]); 117 | double y2 = coords[1] + 1.0 / 3.0 * (coords[3] - coords[1]); 118 | append(x1 + " " + y1 + " " + x2 + " " + y2 + " " + coords[2] + " " + coords[3] + " curveto"); 119 | x0 = coords[2]; 120 | y0 = coords[3]; 121 | break; 122 | case PathIterator.SEG_CUBICTO: // 3 123 | append(coords[0] + " " + coords[1] + " " + coords[2] + " " + coords[3] + " " + coords[4] + " " + coords[5] + " curveto"); 124 | x0 = coords[4]; 125 | y0 = coords[5]; 126 | break; 127 | case PathIterator.SEG_CLOSE: // 4 128 | append("closepath"); 129 | break; 130 | } 131 | it.next(); 132 | } 133 | append("stroke"); 134 | } 135 | 136 | private void append (String line) { 137 | doc.append(line).append("\n"); 138 | } 139 | 140 | private void append (StringBuilder buf, String line) { 141 | buf.append(line).append("\n"); 142 | } 143 | 144 | public static void main (String[] args) throws Exception { 145 | EPSWriter eps = new EPSWriter("EPSWriter"); 146 | Path2D.Double path = new Path2D.Double(); 147 | path.moveTo(100, 100); 148 | path.lineTo(400, 100); 149 | path.lineTo(400, 400); 150 | path.lineTo(100, 400); 151 | path.lineTo(100, 100); 152 | path.closePath(); 153 | eps.draw(path); 154 | eps.draw(new Rectangle2D.Double(200, 200, 100, 100)); 155 | path.moveTo(225, 225); 156 | path.lineTo(225 + 50, 225); 157 | path.lineTo(225 + 25, 225 + 50); 158 | path.lineTo(225, 225); 159 | path.closePath(); 160 | eps.draw(path); 161 | eps.writeEPS(new File("Test/EPS Files/EPSWriter.eps")); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/PathPlanner.java: -------------------------------------------------------------------------------- 1 | import java.awt.geom.Point2D; 2 | import java.awt.geom.Rectangle2D; 3 | import java.util.*; 4 | 5 | /** 6 | * PathPlanner: This class tries to organize the cutting order of CADShape objects so that interior 7 | * details of a CADShape object, such as other, nested CADShape objects are cut before the path of 8 | * the outer CADShape. The algorithm workd by first sorting shape into descending order by the area 9 | * of the shape's bounding box. Then, it it recursively tries to organize shapes into nested groups 10 | * by checking if a the bounding box of a potentially nested shape fits inside the outline of the 11 | * enclosing shape. As it works it builds a list of Tries where each Trie represents a unique area 12 | * of the workspace that is not nested in any other shape and the contents of the Trie contain the 13 | * shapes nested inside the outermost shape. 14 | * 15 | * todo: add code to minimize travel time when cutting shapes in the same level in a Trie 16 | * See: 17 | * 18 | * Ref: https://en.wikipedia.org/wiki/Trie 19 | */ 20 | 21 | public class PathPlanner { 22 | 23 | static class PathTrie { 24 | CADShape cadShape; 25 | List items = new ArrayList<>(); 26 | 27 | PathTrie (CADShape shape) { 28 | this.cadShape = shape; 29 | } 30 | 31 | private boolean contains (CADShape shape) { 32 | return cadShape.getWorkspaceTranslatedShape().contains(shape.getShapeBounds()); 33 | } 34 | 35 | boolean addShape (CADShape shape) { 36 | if (contains(shape)) { 37 | for (PathTrie item : items) { 38 | if (item.contains(shape)) { 39 | if (item.addShape(shape)) { 40 | return true; 41 | } 42 | } 43 | } 44 | items.add(new PathTrie(shape)); 45 | return true; 46 | } 47 | return false; 48 | } 49 | 50 | Point2D.Double unravel (List list, Point2D.Double startPos) { 51 | startPos = reorderGroups(startPos, items); 52 | for (PathTrie item : items) { 53 | item.unravel(list, startPos); 54 | } 55 | list.add(cadShape); 56 | return startPos; 57 | } 58 | } 59 | 60 | static class ShapeArea implements Comparable { 61 | CADShape cadShape; 62 | double area; 63 | 64 | ShapeArea (CADShape cadShape) { 65 | this.cadShape = cadShape; 66 | Rectangle2D bnds = cadShape.getShape().getBounds2D(); 67 | area = bnds.getWidth() * bnds.getHeight(); 68 | } 69 | 70 | public int compareTo (ShapeArea obj) { 71 | return Double.compare(this.area, ((ShapeArea) obj).area); 72 | } 73 | } 74 | 75 | private static Point2D.Double reorderGroups (Point2D.Double startPos, List inList) { 76 | List oldItems = new ArrayList<>(inList); 77 | List newItems = new ArrayList<>(); 78 | while (oldItems.size() > 0) { 79 | PathTrie closest = null; 80 | double minDist = Double.MAX_VALUE; 81 | for (PathTrie item : oldItems) { 82 | CADShape cadShape = item.cadShape; 83 | Point2D.Double pnt = cadShape.getStartCoords(); 84 | double dist = startPos.distance(pnt); 85 | if (dist < minDist) { 86 | minDist = dist; 87 | closest = item; 88 | } 89 | } 90 | newItems.add(closest); 91 | oldItems.remove(closest); 92 | assert closest != null; 93 | startPos = closest.cadShape.getStartCoords(); 94 | } 95 | inList.clear(); 96 | inList.addAll(newItems); 97 | return startPos; 98 | } 99 | 100 | static List optimize (List shapes) { 101 | // Sort CADShape objects into descending order by the area of each's bounding box 102 | List byArea = new ArrayList<>(); 103 | for (CADShape shape : shapes) { 104 | byArea.add(new ShapeArea(shape)); 105 | } 106 | Collections.sort(byArea); 107 | Collections.reverse((byArea)); 108 | // Organize CADShape objects into hierarchical groups 109 | List groups = new ArrayList<>(); 110 | for (ShapeArea item : byArea) { 111 | boolean added = false; 112 | for (PathTrie group : groups) { 113 | if (added = group.addShape(item.cadShape)) { 114 | break; 115 | } 116 | } 117 | if (!added) { 118 | groups.add(new PathTrie(item.cadShape)); 119 | } 120 | } 121 | List output = new ArrayList<>(); 122 | Point2D.Double startPos = new Point2D.Double(0, 0); 123 | startPos = reorderGroups(startPos, groups); 124 | for (PathTrie group : groups) { 125 | startPos = group.unravel(output, startPos); 126 | } 127 | return output; 128 | } 129 | 130 | private static final Map map = new HashMap<>(); 131 | 132 | private static CADShape add (String name, CADShape val) { 133 | map.put(val, name); 134 | return val; 135 | } 136 | 137 | public static void main (String[] args) { 138 | // A nests in E, B nests in F and D, D, E & F nest in G 139 | List shapes = new ArrayList<>(); 140 | if (false) { 141 | shapes.add(add("A", new CADRectangle(9, 5, 1, 1, 0, 0))); 142 | shapes.add(add("B", new CADRectangle(7, 3, 1, 1, 0, 0))); 143 | shapes.add(add("C", new CADRectangle(5, 2, 1, 1, 0, 0))); 144 | shapes.add(add("D", new CADRectangle(3, 2, 1, 1, 0, 0))); 145 | shapes.add(add("E", new CADRectangle(1, 1, 1, 1, 0, 0))); 146 | shapes.add(add("F", new CADRectangle(2, 4, 1, 1, 0, 0))); 147 | shapes.add(add("G", new CADRectangle(4, 5, 1, 1, 0, 0))); 148 | } else { 149 | shapes.add(add("A", new CADRectangle(15, 15, 5, 5, 0, 0))); 150 | shapes.add(add("B", new CADRectangle(35, 15, 5, 5, 0, 0))); 151 | shapes.add(add("C", new CADRectangle(50, 10, 5, 5, 0, 0))); 152 | shapes.add(add("D", new CADRectangle(50, 20, 5, 5, 0, 0))); 153 | shapes.add(add("E", new CADRectangle(10, 10, 15, 15, 0, 0))); 154 | shapes.add(add("F", new CADRectangle(30, 10, 15, 15, 0, 0))); 155 | shapes.add(add("G", new CADRectangle(5, 5, 55, 25, 0, 0))); 156 | } 157 | List oShapes = optimize(shapes); 158 | for (CADShape shape : oShapes) { 159 | Rectangle2D bnds = shape.getShapeBounds(); 160 | System.out.println(map.get(shape) + ": " + bnds.getX() + ", " + bnds.getY() + " (" + bnds.getWidth() + ", " + bnds.getHeight() + ")"); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/CornerFinder.java: -------------------------------------------------------------------------------- 1 | import java.awt.Shape; 2 | import java.awt.geom.*; 3 | import java.text.DecimalFormat; 4 | import java.util.ArrayList; 5 | 6 | // Bezier circle: http://spencermortensen.com/articles/bezier-circle/ 7 | 8 | public class CornerFinder { 9 | private static final DecimalFormat df = new DecimalFormat("#.####"); 10 | 11 | /* 12 | public static void main (String[] args) { 13 | Shape shape; 14 | // Create + cadShape via additive and subtractive geometric operations 15 | Area sq = new Area(new Rectangle2D.Double(-2, -1, 4, 2)); 16 | // Note: arcHeight and arcWith of .25 gives a radius of .125 17 | sq.add(new Area(new RoundRectangle2D.Double(-1, -2, 2, 4, .25, .25))); 18 | shape = sq; 19 | AffineTransform rot = AffineTransform.getRotateInstance(Math.toRadians(45)); 20 | shape = rot.createTransformedShape(shape); 21 | shape = Utils2D.removeOffset(new Shape[] {shape})[0]; 22 | List shapes = new ArrayList<>(); 23 | shapes.add(roundCorners(shape, .125)); 24 | new ShapeWindow(shapes.toArray(new Shape[0]), .25); 25 | } 26 | */ 27 | 28 | static Shape roundCorners (Shape shape, double radius) { 29 | ArrayList points = getPoints(shape); 30 | Point2D.Double[] pnts = points.toArray(new Point2D.Double[0]); 31 | Area a1 = new Area(shape); 32 | for (int ii = 0; ii < pnts.length; ii++) { 33 | Point2D.Double p1 = pnts[ii % pnts.length]; 34 | Point2D.Double p2 = pnts[(ii + 1) % pnts.length]; 35 | Point2D.Double p3 = pnts[(ii + 2) % pnts.length]; 36 | if (checkForCorner(p1, p2, p3)) { 37 | //shapes.add(getCircle(p2, .1)); 38 | // Compute new points for radius start/end and draw '+' to indicate 39 | Point2D.Double p1b = moveDistance(radius, p2, p1); 40 | Point2D.Double p3b = moveDistance(radius, p2, p3); 41 | //shapes.add(getPlus(p1b, .1)); 42 | //shapes.add(getPlus(p3b, .1)); 43 | // Compute point p2m between p1b and p3b 44 | Point2D.Double p2m = new Point2D.Double(p1b.x + (p3b.x - p1b.x) / 2, p1b.y + (p3b.y - p1b.y) / 2); 45 | //shapes.add(getPlus(p2m, .1)); 46 | // Compute reflection of p2 needed to complete square 47 | Point2D.Double p2r = new Point2D.Double(p2.x + (p2m.x - p2.x) * 2, p2.y + (p2m.y - p2.y) * 2); 48 | //shapes.add(getPlus(p2r, .1)); 49 | // Compute point p1p and p3p that extend slightly in direction from p2r 50 | Point2D.Double p1p = moveDistance(-.01, p2, p1b); 51 | Point2D.Double p3p = moveDistance(-.01, p2, p3b); 52 | //shapes.add(getPlus(p1p, .1)); 53 | //shapes.add(getPlus(p3p, .1)); 54 | // Compute p1x and p3x that extends slightly past p2 in direction from p1 and p3 55 | Point2D.Double p1x = moveDistance(-.01, p1b, p2r); 56 | Point2D.Double p3x = moveDistance(-.01, p3b, p2r); 57 | //shapes.add(getPlus(p1x, .1)); 58 | //shapes.add(getPlus(p3x, .1)); 59 | // Compute corner path to add, or subtract depending on whether inner corner, or outer corner 60 | Path2D.Double corner = new Path2D.Double(); 61 | // p2r -> p1x -> p1p -> p3p -> p3x -> p2r 62 | corner.moveTo(p2r.x, p2r.y); // p2r 63 | corner.lineTo(p1x.x, p1x.y); // p1x 64 | corner.lineTo(p3p.x, p3p.y); // p3p 65 | corner.lineTo(p1p.x, p1p.y); // p1p 66 | corner.lineTo(p3x.x, p3x.y); // p3x 67 | corner.lineTo(p2r.x, p2r.y); // p2r 68 | corner.closePath(); 69 | //shapes.add(corner); 70 | if (!a1.contains(p2m.x, p2m.y)) { 71 | a1.add(new Area(corner)); 72 | a1.subtract(new Area(new Ellipse2D.Double(p2r.x - radius, p2r.y - radius, radius * 2, radius * 2))); 73 | } else { 74 | a1.subtract(new Area(corner)); 75 | a1.add(new Area(new Ellipse2D.Double(p2r.x - radius, p2r.y - radius, radius * 2, radius * 2))); 76 | } 77 | } 78 | } 79 | return a1; 80 | } 81 | 82 | private static ArrayList getPoints (Shape shape) { 83 | PathIterator pi = shape.getPathIterator(null); 84 | ArrayList points = new ArrayList<>(); 85 | Point2D.Double start = null; 86 | while (!pi.isDone()) { 87 | double[] coords = new double[6]; 88 | int type = pi.currentSegment(coords); 89 | switch (type) { 90 | case PathIterator.SEG_MOVETO: 91 | points.add(new Point2D.Double(coords[0], coords[1])); 92 | if (start == null) { 93 | start = new Point2D.Double(coords[0], coords[1]); 94 | } 95 | break; 96 | case PathIterator.SEG_LINETO: 97 | Point2D.Double newLine = new Point2D.Double(coords[0], coords[1]); 98 | if (!newLine.equals(start)) { 99 | points.add(newLine); 100 | } 101 | break; 102 | case PathIterator.SEG_CUBICTO: 103 | points.add(new Point2D.Double(coords[2], coords[3])); 104 | break; 105 | case PathIterator.SEG_QUADTO: 106 | points.add(new Point2D.Double(coords[4], coords[5])); 107 | break; 108 | case PathIterator.SEG_CLOSE: 109 | break; 110 | } 111 | pi.next(); 112 | } 113 | return points; 114 | } 115 | 116 | private static boolean checkForCorner (Point2D.Double p1, Point2D.Double p2, Point2D.Double p3) { 117 | double dx12 = Math.abs(p1.x - p2.x); 118 | double dy12 = Math.abs(p1.y - p2.y); 119 | double dx23 = Math.abs(p2.x - p3.x); 120 | double dy23 = Math.abs(p2.y - p3.y); 121 | double s12 = dx12 < dy12 ? dx12 / dy12 : dy12 / dx12; 122 | double s23 = dy23 < dx23 ? dy23 / dx23 : dx23 / dy23; 123 | double dif = Math.abs(s12 - s23); 124 | //System.out.println("corner at: " + df.format(p2.x) + ", " + df.format(p2.y)); 125 | return dif < .0001; 126 | } 127 | 128 | /** 129 | * Returns a new Point2D.Double that's a distance of 'dist' away from point 'from' when 130 | * moving toward point 'to'. 131 | * @param dist distance to move along line 132 | * @param from from point 133 | * @param to to point 134 | * @return new point that is 'dist' away from 'from' point 135 | */ 136 | private static Point2D.Double moveDistance (double dist, Point2D.Double from, Point2D.Double to) { 137 | double v = from.distance(to); 138 | double t = dist / v; 139 | return new Point2D.Double(from.x + (from.x * -t) + (to.x * t), from.y + (from.y * -t) + (to.y * t)); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/ShapeList.java: -------------------------------------------------------------------------------- 1 | import javax.swing.*; 2 | import javax.swing.event.HyperlinkEvent; 3 | import java.awt.*; 4 | import java.awt.event.KeyAdapter; 5 | import java.awt.event.KeyEvent; 6 | import java.util.*; 7 | import java.util.List; 8 | 9 | /** 10 | * Displays a pop up window that lists all the CADShapeGroup and CADShape objects in the lists of 11 | * CADSgape objects in the DrawSurface object's "shapes" List. 12 | * 13 | * TODO: 14 | * [ ] Remember window position (need prefs as a parameter) 15 | * [-] Handle selecting and unselecting groups and shapes better 16 | * [X] Allow Menu/keyCode toggle window on and off 17 | * [X] Update list when list in DrawSurface changes (List listener) 18 | */ 19 | 20 | public class ShapeList implements DrawSurface.DrawSurfaceListener{ 21 | Map shapeLookup = new HashMap<>(); 22 | Map groupLookup = new HashMap<>(); 23 | JEditorPane text = new JEditorPane(); 24 | JDialog results = new JDialog(); 25 | JScrollPane scroll; 26 | DrawSurface surface; 27 | private static final int WIDTH = 150; 28 | 29 | public ShapeList (DrawSurface surface) { 30 | this.surface = surface; 31 | surface.setShaoeListListener(this); 32 | text.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 33 | text.setContentType("text/html"); 34 | text.setEditable(false); 35 | text.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 18)); 36 | // Add scrollbar 37 | scroll = new JScrollPane(text); 38 | scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); 39 | scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); 40 | results.add(scroll); 41 | results.setResizable(false); 42 | results.addKeyListener(new KeyAdapter() { 43 | @Override 44 | public void keyTyped (KeyEvent ev) { 45 | if (windowIsOpen() && ev.getKeyChar() == KeyEvent.VK_ENTER) { 46 | closeWindow(); 47 | } 48 | } 49 | }); 50 | results.addWindowListener(new java.awt.event.WindowAdapter() { 51 | @Override 52 | public void windowClosing(java.awt.event.WindowEvent windowEvent) { 53 | closeWindow(); 54 | } 55 | }); 56 | // Setup Hyperlink listener 57 | text.addHyperlinkListener(ev -> { 58 | if (ev.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { 59 | String link = ev.getURL().toString(); 60 | String[] parts = link.split(":"); 61 | if (parts.length == 3) { 62 | String type = parts[1]; 63 | String id = parts[2]; 64 | switch (type) { 65 | case "shape": 66 | CADShape shape = shapeLookup.get(Integer.parseInt(id)); 67 | if (shape != null) { 68 | //System.out.println(shape.getMenuName() + ": " + id); 69 | if (shape.isSelected) { 70 | surface.setUnselected(shape); 71 | } else { 72 | surface.setSelected(shape); 73 | } 74 | } 75 | break; 76 | case "group": 77 | CADShapeGroup group = groupLookup.get(Integer.parseInt(id)); 78 | //System.out.println("Group: " + id); 79 | break; 80 | } 81 | } 82 | } 83 | }); 84 | } 85 | 86 | public void listUpdated () { 87 | if (text != null) { 88 | text.setText(getList()); 89 | } 90 | } 91 | 92 | public boolean windowIsOpen () { 93 | return results.isVisible(); 94 | } 95 | 96 | public void closeWindow () { 97 | if (results.isVisible()) { 98 | results.setVisible(false); 99 | } 100 | } 101 | 102 | public void openWindow (Rectangle bnds) { 103 | // Pop up results window 104 | text.setText(getList()); 105 | int wHyt = bnds.height - 200; 106 | results.setSize(new Dimension(WIDTH, wHyt)); 107 | results.setPreferredSize(results.getSize()); 108 | // Position text frame on right side of main window 109 | results.setLocation(new Point(bnds.x + bnds.width + WIDTH / 2 - WIDTH, bnds.y + bnds.height / 2 - wHyt / 2)); 110 | results.setVisible(true); 111 | results.toFront(); 112 | results.requestFocus(); 113 | // Position scroll pane at the top 114 | SwingUtilities.invokeLater(() -> scroll.getVerticalScrollBar().setValue(0)); 115 | } 116 | 117 | private String getList () { 118 | List shapes = surface.getDesign(); 119 | List others = new ArrayList<>(); 120 | // Build list of shape groups and ungrouped shapes 121 | HashMap> groups = new LinkedHashMap<>(); 122 | for (CADShape shape : shapes) { 123 | CADShapeGroup group = shape.getGroup(); 124 | if (group != null) { 125 | if (groups.containsKey(group)) { 126 | List list = groups.get(group); 127 | list.add(shape); 128 | } else { 129 | List list = new ArrayList<>(); 130 | list.add(shape); 131 | groups.put(group, list); 132 | } 133 | } else { 134 | others.add(shape); 135 | } 136 | } 137 | // List grouped shapes 138 | StringBuilder buf = new StringBuilder("\n\n\n
\n");
139 |     shapeLookup.clear();
140 |     groupLookup.clear();
141 |     int groupId = 1;
142 |     int shapeId = 1;
143 |     buf.append("Groups:\n");
144 |     if (groups.size() > 1) {
145 |       for (CADShapeGroup group : groups.keySet()) {
146 |         buf.append(" ").append(getGroupLink(groupId++));
147 |         List list = groups.get(group);
148 |         for (CADShape shape : list) {
149 |           buf.append("  ").append(getShapeLink(shape.getMenuName(), shapeId));
150 |           shapeLookup.put(shapeId++, shape);
151 |         }
152 |       }
153 |     } else {
154 |       buf.append(" none\n");
155 |     }
156 |     // List ungrouped items, if any
157 |     buf.append("Other:\n");
158 |     if (others.size() > 0) {
159 |       for (CADShape shape : others) {
160 |         buf.append(" ").append(getShapeLink(shape.getMenuName(), shapeId));
161 |         shapeLookup.put(shapeId++, shape);
162 |       }
163 |     } else {
164 |       buf.append("  none\n");
165 |     }
166 |     buf.append("
\n\n"); 167 | return buf.toString(); 168 | } 169 | 170 | private static String getGroupLink (int code) { 171 | return "Group: " + code + "\n"; 172 | } 173 | 174 | private static String getShapeLink (String test, int code) { 175 | return "" + test + "\n"; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/CADShapeSpline.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.*; 3 | import java.awt.geom.Point2D.Double; 4 | import java.io.Serializable; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.prefs.Preferences; 9 | 10 | class CADShapeSpline extends CADShape implements Serializable, LaserCut.StateMessages { 11 | private static final long serialVersionUID = 1175193935200692376L; 12 | private List points = new ArrayList<>(); 13 | private Point2D.Double movePoint; 14 | private boolean pathClosed; 15 | private Path2D.Double path = new Path2D.Double(); 16 | public double scale = 100.0; 17 | transient public double lastScale; 18 | 19 | CADShapeSpline () { 20 | lastScale = scale; 21 | } 22 | 23 | @Override 24 | void createAndPlace (DrawSurface surface, LaserCut laserCut, Preferences prefs) { 25 | surface.placeShape(this); 26 | } 27 | 28 | @Override 29 | String getMenuName () { 30 | return "Spline Curve"; 31 | } 32 | 33 | // Implement StateMessages interface 34 | public String getStateMsg () { 35 | if (pathClosed) { 36 | return "Click and drag am existing point to move it\n - - \nclick on line to add new point" + 37 | "\n - - \nor SHIFT click on lower right bound point to rotate"; 38 | } else { 39 | String[] nextPnt = {"first", "second", "third", "additional"}; 40 | return "Click to add " + (nextPnt[Math.min(nextPnt.length - 1, points.size())]) + " control point" + 41 | (points.size() >= (nextPnt.length - 1) ? "\n - - \nor click 1st control point to complete spline" : ""); 42 | } 43 | } 44 | 45 | @Override 46 | protected List getEditFields () { 47 | return Arrays.asList( 48 | "xLoc|in", 49 | "yLoc|in", 50 | "rotation|deg{degrees to rotate}", 51 | "scale|%" 52 | ); 53 | } 54 | 55 | List getScaledPoints () { 56 | List scaledPts = new ArrayList<>(); 57 | for (Point2D.Double cp : points) { 58 | scaledPts.add(new Point2D.Double(cp.x * scale / 100, cp.y * scale / 100)); 59 | } 60 | return scaledPts; 61 | }; 62 | 63 | @Override 64 | boolean isShapeClicked (Point2D.Double point, double zoomFactor) { 65 | return super.isShapeClicked(point, zoomFactor) || isPositionClicked(point, zoomFactor); 66 | } 67 | 68 | boolean isPathClosed () { 69 | return pathClosed; 70 | } 71 | 72 | /** 73 | * See if we clicked on an existing Catmull-Rom Control Point other than origin 74 | * 75 | * @param surface Reference to DrawSurface 76 | * @param point Point clicked in Workspace coordinates (inches) 77 | * @param gPoint Closest grid point clicked in Workspace coordinates (inches) 78 | * @return true if clicked 79 | */ 80 | @Override 81 | boolean isControlPoint (DrawSurface surface, Point2D.Double point, Point2D.Double gPoint) { 82 | // Note: mse is in unrotated coords relative to the points, such as mse: x = 0.327, y = -0.208 83 | Point2D.Double mse = Utils2D.rotatePoint(new Point2D.Double(point.x - xLoc, point.y - yLoc), -rotation); 84 | for (int ii = 0; ii < points.size(); ii++) { 85 | Point2D.Double cp = points.get(ii); 86 | double dist = mse.distance(cp) * LaserCut.SCREEN_PPI; 87 | if (dist < 5) { 88 | if (ii == 0 && !pathClosed) { 89 | surface.pushToUndoStack(); 90 | pathClosed = true; 91 | updatePath(); 92 | } 93 | // Note: movePoint is relative to coords of points 94 | movePoint = cp; 95 | return true; 96 | } 97 | } 98 | int idx; 99 | if (pathClosed && (idx = getInsertionPoint(point)) >= 0) { 100 | surface.pushToUndoStack(); 101 | points.add(idx + 1, movePoint = Utils2D.rotatePoint(new Point2D.Double(gPoint.x - xLoc, gPoint.y - yLoc), -rotation)); 102 | updatePath(); 103 | return true; 104 | } 105 | if (!pathClosed) { 106 | surface.pushToUndoStack(); 107 | points.add(Utils2D.rotatePoint(movePoint = new Point2D.Double(gPoint.x - xLoc, gPoint.y - yLoc), -rotation)); 108 | updatePath(); 109 | return true; 110 | } 111 | return false; 112 | } 113 | 114 | /** 115 | * See if we clicked on spline cadShape to add new control point 116 | * 117 | * @param point Point clicked in Workspace coordinates (inches) 118 | * @return index into points List where we need to add new point 119 | */ 120 | int getInsertionPoint (Point2D.Double point) { 121 | Point2D.Double mse = Utils2D.rotatePoint(new Point2D.Double(point.x - xLoc, point.y - yLoc), -rotation); 122 | int idx = 1; 123 | Point2D.Double chk = points.get(idx); 124 | for (Line2D.Double[] lines : Utils2D.transformShapeToLines(getShape(), 1, .01)) { 125 | for (Line2D.Double line : lines) { 126 | double dist = line.ptSegDist(mse) * LaserCut.SCREEN_PPI; 127 | if (dist < 5) { 128 | return idx - 1; 129 | } 130 | // Advance idx as we pass control points 131 | if (idx < points.size() && chk.distance(line.getP2()) < .000001) { 132 | chk = points.get(Math.min(points.size() - 1, ++idx)); 133 | } 134 | } 135 | } 136 | return -1; 137 | } 138 | 139 | boolean isMovingControlPoint () { 140 | return movePoint != null; 141 | } 142 | 143 | @Override 144 | void doMovePoints (Double point) { 145 | if (movePoint != null) { 146 | Point2D.Double mse = Utils2D.rotatePoint(new Point2D.Double(point.x - xLoc, point.y - yLoc), -rotation); 147 | double dx = mse.x - movePoint.x; 148 | double dy = mse.y - movePoint.y; 149 | movePoint.x += dx; 150 | movePoint.y += dy; 151 | updatePath(); 152 | } 153 | } 154 | 155 | @Override 156 | void cancelMove () { 157 | movePoint = null; 158 | } 159 | 160 | @Override 161 | Shape getShape () { 162 | return path; 163 | } 164 | 165 | @Override 166 | void updateShape () { 167 | super.updateShape(); 168 | if (scale != lastScale) { 169 | // transform all the points to new scale; 170 | points = getScaledPoints(); 171 | lastScale = scale; 172 | updatePath(); 173 | } 174 | } 175 | 176 | private void updatePath () { 177 | path = Utils2D.convertPointsToBezier(points, pathClosed); 178 | updateShape(); 179 | } 180 | 181 | @Override 182 | public void resize (double dx, double dy) { 183 | Rectangle2D.Double bnds = Utils2D.boundsOf(points); 184 | scale = (Math.min(dx / bnds.getWidth(), dy / bnds.getHeight())) * 200; 185 | if (scale != lastScale) { 186 | // transform all the points to new scale; 187 | points = getScaledPoints(); 188 | lastScale = scale; 189 | updatePath(); 190 | } 191 | } 192 | 193 | @Override 194 | void draw (Graphics g, double zoom, boolean keyRotate, boolean keyResize, boolean keyOption) { 195 | super.draw(g, zoom, keyRotate, keyResize, keyOption); 196 | Graphics2D g2 = (Graphics2D) g; 197 | // Draw all Catmull-Rom Control Points 198 | g2.setColor(isSelected ? Color.red : pathClosed ? Color.lightGray : Color.darkGray); 199 | if (isSelected){ 200 | for (Point2D.Double cp : points) { 201 | Point2D.Double np = Utils2D.rotatePoint(cp, rotation); 202 | double mx = (xLoc + np.x) * zoom * LaserCut.SCREEN_PPI; 203 | double my = (yLoc + np.y) * zoom * LaserCut.SCREEN_PPI; 204 | double mWid = 3; 205 | g2.fill(new Rectangle.Double(mx - mWid, my - mWid, mWid * 2, mWid * 2)); 206 | } 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/FileChooserMenu.java: -------------------------------------------------------------------------------- 1 | import javax.imageio.ImageIO; 2 | import javax.swing.*; 3 | import javax.swing.filechooser.FileNameExtensionFilter; 4 | import java.awt.*; 5 | import java.awt.image.BufferedImage; 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.prefs.Preferences; 11 | import static javax.swing.JOptionPane.*; 12 | 13 | public class FileChooserMenu extends LaserCut.MyMenuItem { 14 | private static final int IMG_WID = 200; 15 | private static final int IMG_HYT = 200; 16 | private static final int IMG_BORDER = 30; 17 | String currentPath; 18 | String lastImgFile; 19 | JLabel imgLabel; 20 | JFileChooser fileChooser; 21 | 22 | /** 23 | * Create all the file dialogs for LaserCut 24 | * @param lcut reference to LaserCut (used to invoke message dialogs) 25 | * @param msg message at top of file dialog 26 | * @param ext file extention 27 | * @param key code for key shortcut, else zero (0) 28 | * @param save true if save file dialog, else false 29 | * @param preview true if file dialog displays a preview image (save must also be false) 30 | */ 31 | FileChooserMenu (LaserCut lcut, String msg, String ext, int key, boolean save, boolean preview) { 32 | super(msg, key); 33 | Preferences prefs = lcut.getPreferences(); 34 | fileChooser = new JFileChooser(); 35 | imgLabel = new JLabel(); 36 | addActionListener(ev -> { 37 | fileChooser = new JFileChooser(); 38 | String lastFile = prefs.get("lastFile", ""); 39 | fileChooser.setSelectedFile(new File(lastFile)); 40 | imgLabel = new JLabel(); 41 | fileChooser.setDialogTitle(msg); 42 | fileChooser.setDialogType(save ? JFileChooser.SAVE_DIALOG : JFileChooser.OPEN_DIALOG); 43 | FileNameExtensionFilter nameFilter = new FileNameExtensionFilter(ext.toUpperCase() + " files (*." + ext + ")", ext); 44 | fileChooser.addChoosableFileFilter(nameFilter); 45 | fileChooser.setFileFilter(nameFilter); 46 | fileChooser.setCurrentDirectory(new File(currentPath = prefs.get("default." + ext + ".dir", "/"))); 47 | currentPath = fileChooser.getCurrentDirectory().getPath(); 48 | JPanel accessories = new JPanel(new BorderLayout()); 49 | boolean hasAccesory = false; 50 | if (preview) { 51 | JPanel panel = new JPanel(new BorderLayout()); 52 | panel.setPreferredSize(new Dimension(IMG_WID + IMG_BORDER, IMG_HYT + IMG_BORDER)); 53 | panel.setBorder(BorderFactory.createLineBorder(Color.black)); 54 | imgLabel.setHorizontalAlignment(JLabel.CENTER); 55 | imgLabel.setVerticalAlignment(JLabel.CENTER); 56 | panel.add(imgLabel, BorderLayout.CENTER); 57 | Dimension dim1 = fileChooser.getPreferredSize(); 58 | fileChooser.setPreferredSize(new Dimension((int) (dim1.width * 1.25), dim1.height)); 59 | accessories.add(panel, BorderLayout.WEST); 60 | hasAccesory = true; 61 | } 62 | JComponent temp = getAccessory(prefs, save); 63 | if (temp != null) { 64 | accessories.add(temp, BorderLayout.EAST); 65 | hasAccesory = true; 66 | } 67 | if (hasAccesory) { 68 | fileChooser.setAccessory(accessories); 69 | } 70 | fileChooser.addPropertyChangeListener(evt -> { 71 | // Update currentPath as the user navigates directories 72 | String path = fileChooser.getCurrentDirectory().getPath(); 73 | if (!path.equals(currentPath)) { 74 | prefs.put("default." + ext + ".dir", currentPath = path); 75 | } 76 | if (preview) { 77 | // Display a file preview image 78 | String propName = evt.getPropertyName(); 79 | if ("SelectedFileChangedProperty".equals(propName)) { 80 | SwingWorker worker = new SwingWorker() { 81 | 82 | protected Image doInBackground () { 83 | // LOad the preview image 84 | if (evt.getPropertyName().equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)) { 85 | File file = fileChooser.getSelectedFile(); 86 | String newFile = file.getAbsolutePath(); 87 | if (!newFile.equals(lastImgFile)) { 88 | lastImgFile = newFile; 89 | try { 90 | BufferedImage buf = getPreview(file); 91 | if (buf != null) { 92 | int wid = buf.getWidth(); 93 | int hyt = buf.getHeight(); 94 | if (wid < hyt) { 95 | double ratio = (double) wid / hyt; 96 | return buf.getScaledInstance((int) (IMG_WID * ratio), IMG_HYT, BufferedImage.SCALE_FAST); 97 | } else { 98 | double ratio = (double) hyt / wid; 99 | return buf.getScaledInstance(IMG_WID, (int) (IMG_HYT * ratio), BufferedImage.SCALE_FAST); 100 | } 101 | } 102 | } catch (Exception ex) { 103 | imgLabel.setText("Unable to preview"); 104 | imgLabel.setIcon(null); 105 | ex.printStackTrace(); 106 | } 107 | } 108 | } 109 | return null; 110 | } 111 | 112 | protected void done () { 113 | // display the preview image 114 | try { 115 | Image img = get(1L, TimeUnit.NANOSECONDS); 116 | if (img != null) { 117 | imgLabel.setIcon(new ImageIcon(img)); 118 | imgLabel.setText(null); 119 | } 120 | } catch (Exception ex) { 121 | imgLabel.setText(" Error"); 122 | imgLabel.setIcon(null); 123 | ex.printStackTrace(); 124 | } 125 | } 126 | }; 127 | worker.execute(); 128 | } 129 | } 130 | }); 131 | if (openDialog(lcut, save)) { 132 | File sFile = fileChooser.getSelectedFile(); 133 | if (save && !sFile.exists()) { 134 | String fPath = sFile.getPath(); 135 | if (!fPath.contains(".")) { 136 | sFile = new File(fPath + "." + ext); 137 | } 138 | } 139 | try { 140 | if (!save || (!sFile.exists() || lcut.showWarningDialog("Overwrite Existing file?"))) { 141 | processFile(sFile); 142 | } 143 | } catch (Exception ex) { 144 | lcut.showErrorDialog(save ? "Unable to save file" : "Unable to open file"); 145 | //ex.printStackTrace(); 146 | } 147 | prefs.put("default." + ext, sFile.getAbsolutePath()); 148 | } 149 | }); 150 | } 151 | 152 | // Override in subclass to get an accessory component 153 | JComponent getAccessory (Preferences prefs, boolean save) { 154 | return null; 155 | } 156 | 157 | private boolean openDialog (Component comp, boolean save) { 158 | // Open an open or save dialog 159 | if (save) { 160 | return fileChooser.showSaveDialog(comp) == JFileChooser.APPROVE_OPTION; 161 | } else { 162 | return fileChooser.showOpenDialog(comp) == JFileChooser.APPROVE_OPTION; 163 | } 164 | } 165 | 166 | // Override to perform open, or save operation 167 | void processFile (File sFile) throws Exception { 168 | } 169 | 170 | // Override, as needed to load a preview image 171 | BufferedImage getPreview (File file) throws Exception { 172 | return ImageIO.read(Files.newInputStream(file.toPath())); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/JSSCPort.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | import java.util.concurrent.ArrayBlockingQueue; 3 | import java.util.prefs.Preferences; 4 | import java.util.regex.Pattern; 5 | 6 | import jssc.*; 7 | 8 | import javax.swing.*; 9 | import javax.swing.event.MenuEvent; 10 | import javax.swing.event.MenuListener; 11 | 12 | /* 13 | * Encapsulates JSSC functionality into an easy to use class 14 | * See: https://code.google.com/p/java-simple-serial-connector/ 15 | * And: https://github.com/scream3r/java-simple-serial-connector/releases 16 | * 17 | * Author: Wayne Holder, 2015-2019 (first version 10/30/2015) 18 | */ 19 | 20 | public class JSSCPort implements SerialPortEventListener { 21 | private static final Map baudRates = new LinkedHashMap<>(); 22 | private final ArrayBlockingQueue queue = new ArrayBlockingQueue<>(1000); 23 | private static Pattern macPat = Pattern.compile("cu."); 24 | private static final int dataBits = 8, stopBits = SerialPort.STOPBITS_1, parity = SerialPort.PARITY_NONE; 25 | private static final int flowCtrl = SerialPort.FLOWCONTROL_NONE; 26 | private static final int eventMasks = 0; // See: SerialPort.MASK_RXCHAR, MASK_TXEMPTY, MASK_CTS, MASK_DSR 27 | private final Preferences prefs; 28 | private String portName; 29 | private int baudRate; 30 | private SerialPort serialPort; 31 | private final String prefix; 32 | private final List rxHandlers = new ArrayList<>(); 33 | 34 | interface RXEvent { 35 | void rxChar (byte cc); 36 | } 37 | 38 | static { 39 | baudRates.put("110", SerialPort.BAUDRATE_110); 40 | baudRates.put("300", SerialPort.BAUDRATE_300); 41 | baudRates.put("600", SerialPort.BAUDRATE_600); 42 | baudRates.put("1200", SerialPort.BAUDRATE_1200); 43 | baudRates.put("2400", 2400); // Note: constant missing in JSSC 44 | baudRates.put("4800", SerialPort.BAUDRATE_4800); 45 | baudRates.put("7200", 7200); // For misconfigured Arduino 46 | baudRates.put("9600", SerialPort.BAUDRATE_9600); 47 | baudRates.put("14400", SerialPort.BAUDRATE_14400); 48 | baudRates.put("19200", SerialPort.BAUDRATE_19200); 49 | baudRates.put("38400", SerialPort.BAUDRATE_38400); 50 | baudRates.put("57600", SerialPort.BAUDRATE_57600); 51 | baudRates.put("115200", SerialPort.BAUDRATE_115200); 52 | baudRates.put("128000", SerialPort.BAUDRATE_128000); 53 | baudRates.put("256000", SerialPort.BAUDRATE_256000); 54 | } 55 | 56 | JSSCPort (String prefix, Preferences prefs) { 57 | this.prefix = prefix; 58 | this.prefs = prefs; 59 | // Determine OS Type 60 | switch (SerialNativeInterface.getOsType()) { 61 | case SerialNativeInterface.OS_LINUX: 62 | macPat = Pattern.compile("(ttyS|ttyUSB|ttyACM|ttyAMA|rfcomm)[0-9]{1,3}"); 63 | break; 64 | case SerialNativeInterface.OS_MAC_OS_X: 65 | break; 66 | case SerialNativeInterface.OS_WINDOWS: 67 | macPat = Pattern.compile(""); 68 | break; 69 | default: 70 | macPat = Pattern.compile("tty.*"); 71 | break; 72 | } 73 | portName = prefs.get(prefix + "serial.port", null); 74 | baudRate = prefs.getInt(prefix + "serial.baud", SerialPort.BAUDRATE_115200); 75 | } 76 | 77 | boolean hasSerial () { 78 | return portName != null; 79 | } 80 | 81 | boolean open (RXEvent handler) throws SerialPortException { 82 | if (serialPort != null) { 83 | if (serialPort.isOpened()) { 84 | close(); 85 | } 86 | } 87 | if (portName != null) { 88 | try { 89 | setRXHandler(handler); 90 | serialPort = new SerialPort(portName); 91 | serialPort.openPort(); 92 | serialPort.purgePort(SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_TXCLEAR); 93 | serialPort.setParams(baudRate, dataBits, stopBits, parity, false, false); // baud, 8 bits, 1 stop bit, no parity 94 | serialPort.setEventsMask(eventMasks); 95 | serialPort.setFlowControlMode(flowCtrl); 96 | serialPort.addEventListener(this); 97 | return true; 98 | } catch (SerialPortException ex) { 99 | prefs.remove(prefix + "serial.port"); 100 | throw ex; 101 | } 102 | } 103 | return false; 104 | } 105 | 106 | public void close () { 107 | if (serialPort != null && serialPort.isOpened()) { 108 | try { 109 | synchronized (this) { 110 | rxHandlers.clear(); 111 | } 112 | serialPort.removeEventListener(); 113 | serialPort.purgePort(SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_TXCLEAR); 114 | serialPort.closePort(); 115 | serialPort = null; 116 | } catch (SerialPortException ex) { 117 | ex.printStackTrace(); 118 | } 119 | } 120 | } 121 | 122 | public void serialEvent (SerialPortEvent se) { 123 | try { 124 | if (se.getEventType() == SerialPortEvent.RXCHAR) { 125 | int rxCount = se.getEventValue(); 126 | byte[] inChars = serialPort.readBytes(rxCount); 127 | if (rxHandlers.size() > 0) { 128 | for (byte cc : inChars) { 129 | for (RXEvent handler : rxHandlers) { 130 | handler.rxChar(cc); 131 | } 132 | } 133 | } else { 134 | for (byte cc : inChars) { 135 | if (queue.remainingCapacity() > 0) { 136 | queue.add((int) cc); 137 | } 138 | } 139 | } 140 | } 141 | } catch (Exception ex) { 142 | ex.printStackTrace(); 143 | } 144 | } 145 | 146 | void setRXHandler (RXEvent handler) { 147 | synchronized (this) { 148 | rxHandlers.add(handler); 149 | } 150 | } 151 | 152 | void removeRXHandler (RXEvent handler) { 153 | synchronized (this) { 154 | rxHandlers.remove(handler); 155 | } 156 | } 157 | 158 | void sendByte (byte data) throws SerialPortException { 159 | serialPort.writeByte(data); 160 | } 161 | 162 | void sendString (String data) throws SerialPortException { 163 | serialPort.writeString(data); 164 | } 165 | 166 | JMenu getPortMenu () { 167 | JMenu menu = new JMenu("Port"); 168 | menu.addMenuListener(new MenuListener() { 169 | @Override 170 | public void menuSelected (MenuEvent e) { 171 | // Populate menu on demand 172 | menu.removeAll(); 173 | ButtonGroup group = new ButtonGroup(); 174 | for (String pName : SerialPortList.getPortNames(macPat)) { 175 | JRadioButtonMenuItem item = new JRadioButtonMenuItem(pName, pName.equals(portName)); 176 | menu.setVisible(true); 177 | menu.add(item); 178 | group.add(item); 179 | item.addActionListener((ev) -> { 180 | portName = ev.getActionCommand(); 181 | prefs.put(prefix + "serial.port", portName); 182 | }); 183 | } 184 | } 185 | 186 | @Override 187 | public void menuDeselected (MenuEvent e) { } 188 | 189 | @Override 190 | public void menuCanceled (MenuEvent e) { } 191 | }); 192 | return menu; 193 | } 194 | 195 | JMenu getBaudMenu () { 196 | JMenu menu = new JMenu("Baud Rate"); 197 | ButtonGroup group = new ButtonGroup(); 198 | for (String bRate : baudRates.keySet()) { 199 | int rate = baudRates.get(bRate); 200 | JRadioButtonMenuItem item = new JRadioButtonMenuItem(bRate, baudRate == rate); 201 | menu.add(item); 202 | menu.setVisible(true); 203 | group.add(item); 204 | item.addActionListener((ev) -> { 205 | String cmd = ev.getActionCommand(); 206 | prefs.putInt(prefix + "serial.baud", baudRate = Integer.parseInt(cmd)); 207 | }); 208 | } 209 | return menu; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/CADArbitraryPolygon.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.*; 3 | import java.awt.geom.Point2D.Double; 4 | import java.io.Serializable; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.prefs.Preferences; 9 | 10 | class CADArbitraryPolygon extends CADShape implements Serializable, LaserCut.StateMessages { 11 | private static final long serialVersionUID = 1175193935200692376L; 12 | private List points = new ArrayList<>(); 13 | private Point2D.Double movePoint; 14 | private boolean pathClosed; 15 | private Path2D.Double path = new Path2D.Double(); 16 | public double scale = 100.0; 17 | transient public double lastScale; 18 | 19 | 20 | CADArbitraryPolygon () { 21 | lastScale = scale; 22 | } 23 | 24 | @Override 25 | void createAndPlace (DrawSurface surface, LaserCut laserCut, Preferences prefs) { 26 | surface.placeShape(this); 27 | } 28 | 29 | @Override 30 | String getMenuName () { 31 | return "Arbitrary Polygon"; 32 | } 33 | 34 | // Implement StateMessages interface 35 | public String getStateMsg () { 36 | if (pathClosed) { 37 | return "Click and drag am existing point to move it\n - - \nclick on line to add new point" + 38 | "\n - - \nor SHIFT click on lower right bound point to rotate"; 39 | } else { 40 | String[] nextPnt = {"first", "second", "third", "additional"}; 41 | return "Click to add " + (nextPnt[Math.min(nextPnt.length - 1, points.size())]) + " point" + 42 | (points.size() >= (nextPnt.length - 1) ? "\n - -\nor click 1st point to complete polygon)" : ""); 43 | } 44 | } 45 | 46 | @Override 47 | protected List getEditFields () { 48 | return Arrays.asList( 49 | "xLoc|in", 50 | "yLoc|in", 51 | "rotation|deg{degrees to rotate}", 52 | "scale|%" 53 | ); 54 | } 55 | 56 | List getScaledPoints () { 57 | List scaledPts = new ArrayList<>(); 58 | for (Point2D.Double cp : points) { 59 | scaledPts.add(new Point2D.Double(cp.x * scale / 100, cp.y * scale / 100)); 60 | } 61 | return scaledPts; 62 | }; 63 | 64 | @Override 65 | boolean isShapeClicked (Point2D.Double point, double zoomFactor) { 66 | return super.isShapeClicked(point, zoomFactor) || isPositionClicked(point, zoomFactor); 67 | } 68 | 69 | boolean isPathClosed () { 70 | return pathClosed; 71 | } 72 | 73 | /** 74 | * See if we clicked on an existing Control Point other than origin 75 | * 76 | * @param surface Reference to DrawSurface 77 | * @param point Point clicked in Workspace coordinates (inches) 78 | * @param gPoint Closest grid point clicked in Workspace coordinates (inches) 79 | * @return true if clicked 80 | */ 81 | @Override 82 | boolean isControlPoint (DrawSurface surface, Point2D.Double point, Point2D.Double gPoint) { 83 | // Note: mse is in unrotated coords relative to the points, such as mse: x = 0.327, y = -0.208 84 | Point2D.Double mse = Utils2D.rotatePoint(new Point2D.Double(point.x - xLoc, point.y - yLoc), -rotation); 85 | for (int ii = 0; ii < points.size(); ii++) { 86 | Point2D.Double cp = points.get(ii); 87 | double dist = mse.distance(cp) * LaserCut.SCREEN_PPI; 88 | if (dist < 5) { 89 | if (ii == 0 && !pathClosed) { 90 | surface.pushToUndoStack(); 91 | pathClosed = true; 92 | updatePath(); 93 | } 94 | // Note: movePoint is relative to coords of points 95 | movePoint = cp; 96 | return true; 97 | } 98 | } 99 | int idx; 100 | if (pathClosed && (idx = getInsertionPoint(point)) >= 0) { 101 | surface.pushToUndoStack(); 102 | points.add(idx + 1, movePoint = Utils2D.rotatePoint(new Point2D.Double(gPoint.x - xLoc, gPoint.y - yLoc), -rotation)); 103 | updatePath(); 104 | return true; 105 | } 106 | if (!pathClosed) { 107 | surface.pushToUndoStack(); 108 | points.add(Utils2D.rotatePoint(movePoint = new Point2D.Double(gPoint.x - xLoc, gPoint.y - yLoc), -rotation)); 109 | updatePath(); 110 | return true; 111 | } 112 | return false; 113 | } 114 | 115 | /** 116 | * See if we clicked on spline cadShape to add new control point 117 | * 118 | * @param point Point clicked in Workspace coordinates (inches) 119 | * @return index into points List where we need to add new point 120 | */ 121 | int getInsertionPoint (Point2D.Double point) { 122 | Point2D.Double mse = Utils2D.rotatePoint(new Point2D.Double(point.x - xLoc, point.y - yLoc), -rotation); 123 | int idx = 1; 124 | Point2D.Double chk = points.get(idx); 125 | for (Line2D.Double[] lines : Utils2D.transformShapeToLines(getShape(), 1, .01)) { 126 | for (Line2D.Double line : lines) { 127 | double dist = line.ptSegDist(mse) * LaserCut.SCREEN_PPI; 128 | if (dist < 5) { 129 | return idx - 1; 130 | } 131 | // Advance idx as we pass control points 132 | if (idx < points.size() && chk.distance(line.getP2()) < .000001) { 133 | chk = points.get(Math.min(points.size() - 1, ++idx)); 134 | } 135 | } 136 | } 137 | return -1; 138 | } 139 | 140 | boolean isMovingControlPoint () { 141 | return movePoint != null; 142 | } 143 | 144 | @Override 145 | void doMovePoints (Double point) { 146 | if (movePoint != null) { 147 | Point2D.Double mse = Utils2D.rotatePoint(new Point2D.Double(point.x - xLoc, point.y - yLoc), -rotation); 148 | double dx = mse.x - movePoint.x; 149 | double dy = mse.y - movePoint.y; 150 | movePoint.x += dx; 151 | movePoint.y += dy; 152 | updatePath(); 153 | } 154 | } 155 | 156 | @Override 157 | void cancelMove () { 158 | movePoint = null; 159 | } 160 | 161 | @Override 162 | Shape getShape () { 163 | return path; 164 | } 165 | 166 | @Override 167 | void updateShape () { 168 | super.updateShape(); 169 | if (scale != lastScale) { 170 | // transform all the points to new scale; 171 | points = getScaledPoints(); 172 | lastScale = scale; 173 | updatePath(); 174 | } 175 | } 176 | 177 | private void updatePath () { 178 | if (pathClosed) { 179 | path = Utils2D.convertPointsToPath(points.toArray(new Point2D.Double[0]), true); 180 | } else { 181 | Point2D.Double[] pnts = points.toArray(new Point2D.Double[points.size() + 1]); 182 | // Duplicate last point so we can draw a curve through all points in the path 183 | pnts[pnts.length - 1] = pnts[pnts.length - 2]; 184 | path = Utils2D.convertPointsToPath(pnts, false); 185 | } 186 | updateShape(); 187 | } 188 | 189 | @Override 190 | public void resize (double dx, double dy) { 191 | Rectangle2D.Double bnds = Utils2D.boundsOf(points); 192 | scale = (Math.min(dx / bnds.getWidth(), dy / bnds.getHeight())) * 200; 193 | if (scale != lastScale) { 194 | // transform all the points to new scale; 195 | points = getScaledPoints(); 196 | lastScale = scale; 197 | updatePath(); 198 | } 199 | } 200 | 201 | @Override 202 | void draw (Graphics g, double zoom, boolean keyRotate, boolean keyResize, boolean keyOption) { 203 | super.draw(g, zoom, keyRotate, keyResize, keyOption); 204 | Graphics2D g2 = (Graphics2D) g; 205 | // Draw all the Control Points 206 | g2.setColor(isSelected ? Color.red : pathClosed ? Color.lightGray : Color.darkGray); 207 | if (isSelected){ 208 | for (Point2D.Double cp : points) { 209 | Point2D.Double np = Utils2D.rotatePoint(cp, rotation); 210 | double mx = (xLoc + np.x) * zoom * LaserCut.SCREEN_PPI; 211 | double my = (yLoc + np.y) * zoom * LaserCut.SCREEN_PPI; 212 | double mWid = 3; 213 | g2.fill(new Rectangle.Double(mx - mWid, my - mWid, mWid * 2, mWid * 2)); 214 | } 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/CNCTools.java: -------------------------------------------------------------------------------- 1 | import java.awt.geom.Line2D; 2 | import java.awt.geom.Point2D; 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | class CNCTools { 7 | 8 | static class PLine { 9 | private static final double LARGE = 1.0e12; // Avoid divide by zero... 10 | private double slope; 11 | private final double intercept; 12 | 13 | /** 14 | * PLine class represents a line of infinite length. This constructor creates a line that passes 15 | * through 16 | * @param line parallel reference line 17 | * @param point point that PLine passes through 18 | */ 19 | private PLine (Line2D.Double line, Point2D.Double point) { 20 | double dx = line.x2 - line.x1; 21 | slope = dx == 0 ? LARGE : (line.y2 - line.y1) / dx; 22 | intercept = point.y - slope * point.x; 23 | } 24 | 25 | /** 26 | * PLine class represents a line of infinite length. This constructor creates a line that passes through 27 | * point dst and, if perpendicular is true, rotates the line 90 degrees so that is perpendicular to the line 28 | * that passes from point src to point dst, else like passes through both points 29 | * @param src point from here to dst establishes the perpendicular 30 | * @param dst point that PLine passes through 31 | */ 32 | private PLine (Point2D.Double src, Point2D.Double dst, boolean perpendicular) { 33 | double dx = dst.x - src.x; 34 | slope = dx == 0 ? LARGE : (dst.y - src.y) / dx; 35 | if (perpendicular) { 36 | slope = -1 / (slope == 0 ? LARGE : slope); 37 | } 38 | intercept = dst.y - slope * dst.x; 39 | } 40 | 41 | /** 42 | * PLine class represents a line of infinite length. This constructor creates a line that passes through line 43 | * @param line line specifing ininite line 44 | */ 45 | private PLine (Line2D.Double line) { 46 | this(new Point2D.Double(line.x1, line.y1), new Point2D.Double(line.x2, line.y2), false); 47 | } 48 | 49 | Point2D.Double intersects (PLine l2) { 50 | if (slope == l2.slope) { 51 | return null; 52 | } 53 | double div = slope - l2.slope; 54 | double x = div == 0 ? LARGE : (l2.intercept - intercept) / div; 55 | return new Point2D.Double(x, slope * x + intercept); 56 | } 57 | } 58 | 59 | /** 60 | * Takes an array of lines that forms a closed cadShape and computes a parallel path around either the 61 | * interior, or exterior of the cadShape depending on the setting of the parameter outside. 62 | * @param lines array of lines for a cadShape for which this code will compute a parallel path 63 | * @param radius offset distance for parallel path (radius of CNC tool) 64 | * @param outside true if parallel path should be around the outside of the cadShape, else inside 65 | * @return array of points for the parallel path 66 | */ 67 | static Point2D.Double[] getParallelPath (Line2D.Double[] lines, double radius, boolean outside) { 68 | // Prune any between points that are parallel (on the line from prior point to next point) 69 | List tmp = new ArrayList<>(); 70 | for (int ii = 0; ii < lines.length; ii++) { 71 | Point2D.Double p1 = (Point2D.Double) lines[ii].getP1(); 72 | Point2D.Double p2 = (Point2D.Double) lines[(ii + 1) % lines.length].getP1(); 73 | Point2D.Double p3 = (Point2D.Double) lines[(ii + 2) % lines.length].getP1(); 74 | PLine l1 = new PLine(p1, p2, false); 75 | PLine l2 = new PLine(p2, p3, false); 76 | if (l1.slope != l2.slope) { 77 | tmp.add(p2); 78 | } 79 | } 80 | Point2D.Double[] points = tmp.toArray(new Point2D.Double[0]); 81 | PLine[] pLines = new PLine[points.length]; 82 | boolean clockwise = isClockwise(points); 83 | for (int ii = 0; ii < points.length; ii++) { 84 | int jj = (ii + 1) % points.length; 85 | double d = points[ii].distance(points[jj]); 86 | double t = radius / d; 87 | // Compute point "ext" that extends "radius" distance from point cadShape[n] toward point cadShape[n+1] 88 | Point2D.Double ext = new Point2D.Double((1 - t) * points[ii].x + t * points[jj].x, (1 - t) * points[ii].y + t * points[jj].y); 89 | // Compute the normal to point cadShape[n] by rotating point "ext" +/- 90 degrees around point cadShape[n] 90 | Point2D.Double normal; 91 | if (clockwise ^ outside) { 92 | // Rotate point ext -90 degrees (clockwise) around point cadShape[ii] 93 | normal = new Point2D.Double(-(ext.y - points[ii].y) + points[ii].x, (ext.x - points[ii].x) + points[ii].y); 94 | } else { 95 | // Rotate point ext 90 degrees (counter clockwise) around point cadShape[ii] 96 | normal = new Point2D.Double((ext.y - points[ii].y) + points[ii].x, -(ext.x - points[ii].x) + points[ii].y); 97 | } 98 | pLines[ii] = new PLine(new Line2D.Double(points[ii], points[jj]), normal); 99 | } 100 | // Check for shortcut paths and compute path intersections 101 | List oPoints = new ArrayList<>(); 102 | for (int ii = 0; ii < points.length; ii++) { 103 | int jj = (ii + 1) % points.length; 104 | Point2D.Double np1 = pLines[ii].intersects(pLines[(ii + 1) % points.length]); 105 | Point2D.Double np2 = pLines[(ii + 1) % points.length].intersects(pLines[(ii + 2) % points.length]); 106 | Point2D.Double ref = points[jj]; 107 | // Compute point "tip" that extends "radius" distance from cadShape's point to the intersection 108 | double d = ref.distance(np1); 109 | double t = radius / d; 110 | Point2D.Double tip = new Point2D.Double((1 - t) * ref.x + t * np1.x, (1 - t) * ref.y + t * np1.y); 111 | // Compute line through tip that's perpendicular to the line from cadShape's point to the intersection 112 | PLine perp = new PLine(ref, tip, true); 113 | // Compute intersection with perpendicular and pLines[ii] and pLines[jj] 114 | Point2D.Double p1 = pLines[ii].intersects(perp); 115 | Point2D.Double p2 = pLines[jj].intersects(perp); 116 | double dist = np1.distance(p2) + p2.distance(np2) - np1.distance(np2); 117 | if (dist < .0001) { 118 | oPoints.add(p1); 119 | oPoints.add(p2); 120 | } else { 121 | oPoints.add(np1); 122 | } 123 | } 124 | return oPoints.toArray(new Point2D.Double[0]); 125 | } 126 | 127 | /** 128 | * Prunes sections of a closed path that cross back over themselves by scanning for overlapping line segments 129 | * Note: extremely inefficient algorithm (approx n^2 / 2 where n is numebr of input points), but it works 130 | * Todo: https://www.geeksforgeeks.org/given-a-set-of-line-segments-find-if-any-two-segments-intersect/ 131 | * @param points set of input points to prune 132 | * @return pruned array of points 133 | */ 134 | static Point2D.Double[] pruneOverlap (Point2D.Double[] points) { 135 | List oPoints = new ArrayList<>(); 136 | int exclude = 0; 137 | loop1: 138 | for (int ii = 0; ii < points.length - 1; ii++) { 139 | Line2D.Double l1 = new Line2D.Double(points[ii], points[(ii + 1) % points.length]); 140 | for (int jj = ii + 1; jj < points.length; jj++) { 141 | Line2D.Double l2 = new Line2D.Double(points[jj], points[(jj + 1) % points.length]); 142 | if (l1.intersectsLine(l2) && !l1.getP2().equals(l2.getP1()) && !l1.getP1().equals(l2.getP2())) { 143 | Point2D.Double xx = getIntersection(l1, l2); 144 | oPoints.add(xx); 145 | ii = jj - 1; 146 | exclude = jj + 1; 147 | continue loop1; 148 | } 149 | } 150 | if (ii >= exclude) { 151 | oPoints.add(points[ii]); 152 | } 153 | } 154 | oPoints.add(points[points.length - 1]); 155 | return oPoints.toArray(new Point2D.Double[0]); 156 | } 157 | 158 | /* 159 | * Computes intersection point for two lines 160 | */ 161 | public static Point2D.Double getIntersection (Line2D.Double l1, Line2D.Double l2) { 162 | return (new PLine(l1)).intersects(new PLine(l2)); 163 | } 164 | 165 | /** 166 | * Scans a set of points forming a closed cadShape to detect if points are in clockwise, or counterclockwise order 167 | * @param points a set of lines forming a closed cadShape 168 | * @return true if cadShape is drawn in clockwise order, else false 169 | */ 170 | private static boolean isClockwise (Point2D.Double[] points) { 171 | double sum = 0; 172 | for (int ii = 0; ii < points.length; ii++) { 173 | double x1 = points[ii].x; 174 | double y1 = points[ii].y; 175 | double x2 = points[(ii + 1) % points.length].x; 176 | double y2 = points[(ii + 1) % points.length].y; 177 | sum += (x2 - x1) * (y2 + y1); 178 | } 179 | return sum < 0; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/ShapeOptimizer.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.AffineTransform; 3 | import java.awt.geom.Path2D; 4 | import java.awt.geom.PathIterator; 5 | import java.awt.geom.Point2D; 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | /** 12 | * ShapeOptimizer: This code tries to reconnected disconnected line and spline segments into a 13 | * continuous path. This code is invoked by manually selecting a set of shapes and then using 14 | * the Edit menu item "Combine Selected Paths". After processing, all selected shapes are put 15 | * into a group with both newly-reconnected paths and paths that could not be connected. 16 | * 17 | * Limitations: the code assumes that the starting and ending points of the segments to reconnect 18 | * match identically (to float level precision) in order to figure out when the starting point of 19 | * a given segment can be connected to the ending point of another segment. However, it does try 20 | * to reconnect segements that match end to end, or start to start by flipping the start and end 21 | * points, as needed. 22 | */ 23 | 24 | public class ShapeOptimizer { 25 | static class ShapeSeg { 26 | int type; 27 | double sx, sy, ex, ey; 28 | double[] coords; 29 | boolean used; 30 | 31 | ShapeSeg (int type, double sx, double sy, double[] coords) { 32 | this.type = type; 33 | this.sx = sx; 34 | this.sy = sy; 35 | this.coords = coords; 36 | switch (type) { 37 | case PathIterator.SEG_LINETO: // 1 (sx c01) 38 | ex = coords[0]; 39 | ey = coords[1]; 40 | break; 41 | case PathIterator.SEG_QUADTO: // 2 (sx c01 c02) 42 | ex = coords[2]; 43 | ey = coords[3]; 44 | break; 45 | case PathIterator.SEG_CUBICTO: // 3 (sx c01 c02 c03) 46 | ex = coords[4]; 47 | ey = coords[5]; 48 | break; 49 | } 50 | } 51 | 52 | // Reverse path order of a segment 53 | void flip () { 54 | switch (type) { 55 | case PathIterator.SEG_LINETO: // 1 56 | coords[0] = sx; 57 | coords[1] = sy; 58 | sx = ex; 59 | sy = ey; 60 | ex = coords[0]; 61 | ey = coords[1]; 62 | break; 63 | case PathIterator.SEG_QUADTO: // 2 64 | coords[2] = sx; 65 | coords[3] = sy; 66 | sx = ex; 67 | sy = ey; 68 | ex = coords[2]; 69 | ey = coords[3]; 70 | break; 71 | case PathIterator.SEG_CUBICTO: // 3 72 | double tx = coords[0]; 73 | double ty = coords[1]; 74 | coords[0] = coords[2]; 75 | coords[1] = coords[3]; 76 | coords[2] = tx; 77 | coords[3] = ty; 78 | // 79 | coords[4] = sx; 80 | coords[5] = sy; 81 | sx = ex; 82 | sy = ey; 83 | ex = coords[4]; 84 | ey = coords[5]; 85 | break; 86 | } 87 | } 88 | 89 | Point2D.Float getStart () { 90 | return new Point2D.Float((float) sx, (float) sy); 91 | } 92 | 93 | Point2D.Float getEnd () { 94 | return new Point2D.Float((float) ex, (float) ey); 95 | } 96 | } 97 | 98 | static class Vert { 99 | List segs = new ArrayList<>(); 100 | 101 | ShapeSeg getSeg (ShapeSeg from, Point2D.Float vert) { 102 | for (ShapeSeg seg : segs) { 103 | if (seg == from) { 104 | // Don't connect to self 105 | continue; 106 | } 107 | Point2D.Float start = seg.getStart(); 108 | if (!seg.used && start.equals(vert)) { 109 | segs.remove(seg); 110 | return seg; 111 | } 112 | Point2D.Float end = seg.getEnd(); 113 | if (!seg.used && end.equals(vert)) { 114 | seg.flip(); 115 | segs.remove(seg); 116 | return seg; 117 | } 118 | } 119 | return null; 120 | } 121 | } 122 | 123 | /** 124 | * Experimental code designed to connect a set line segments into a continous path 125 | * Note: this code can only connect line segments when the end points have identical 'float' precision 126 | * x and y coordinates. 127 | * @param shape Shape onject containing the line segments to analyze 128 | * @return List of Shape objects (some may be rebuilt into continuous paths) 129 | */ 130 | static List optimizeShape (Shape shape) { 131 | List segs = new ArrayList<>(); 132 | // Break Shape into List of ShapeSeg objects 133 | PathIterator pi = shape.getPathIterator(new AffineTransform()); 134 | double ex = 0, ey = 0; 135 | while (!pi.isDone()) { 136 | double[] coords = new double[6]; // p1.x, p1.y, p2.x, p2.y, p3.x, p3.y 137 | int type = pi.currentSegment(coords); 138 | switch (type) { 139 | case PathIterator.SEG_MOVETO: 140 | ex = coords[0]; 141 | ey = coords[1]; 142 | break; 143 | case PathIterator.SEG_LINETO: 144 | // lineTo(coords[0], coords[1]); 145 | segs.add(new ShapeSeg(type, ex, ey, coords)); 146 | ex = coords[0]; 147 | ey = coords[1]; 148 | break; 149 | case PathIterator.SEG_QUADTO: 150 | // quadTo(coords[0], coords[1], coords[2], coords[3]); 151 | segs.add(new ShapeSeg(type, ex, ey, coords)); 152 | ex = coords[2]; 153 | ey = coords[3]; 154 | break; 155 | case PathIterator.SEG_CUBICTO: 156 | // curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]); 157 | segs.add(new ShapeSeg(type, ex, ey, coords)); 158 | ex = coords[4]; 159 | ey = coords[5]; 160 | break; 161 | case PathIterator.SEG_CLOSE: 162 | // closePath(); 163 | break; 164 | } 165 | pi.next(); 166 | } 167 | // Build Map of vertices with a list of segment (key is float precision) 168 | Map verts = new HashMap<>(); 169 | for (ShapeSeg seg : segs) { 170 | // Insert starting point 171 | Point2D.Float start = seg.getStart(); 172 | Vert vert = verts.get(start); 173 | if (vert == null) { 174 | verts.put(start, vert = new Vert()); 175 | } 176 | vert.segs.add(seg); 177 | // Insert ending point 178 | Point2D.Float end = seg.getEnd(); 179 | vert = verts.get(end); 180 | if (vert == null) { 181 | verts.put(end, vert = new Vert()); 182 | } 183 | vert.segs.add(seg); 184 | } 185 | // Scan for connected segments and organize into sequences 186 | List opts = new ArrayList<>(); 187 | for (ShapeSeg seg : segs) { 188 | if (!seg.used) { 189 | Point2D.Float end = seg.getEnd(); 190 | Vert vert = verts.get(end); 191 | ShapeSeg con = vert.getSeg(seg, end); 192 | if (con != null) { 193 | opts.add(seg); 194 | seg.used = true; 195 | while (con != null && !con.used) { 196 | con.used = true; 197 | opts.add(con); 198 | end = con.getEnd(); 199 | vert = verts.get(end); 200 | con = vert.getSeg(con, end); 201 | } 202 | } else { 203 | opts.add(seg); 204 | seg.used = true; 205 | } 206 | } 207 | } 208 | segs = opts; 209 | List out = new ArrayList<>(); 210 | // Combine list of reorganized ShapeSeg objects into a List of Shape objects 211 | Path2D.Double path = new Path2D.Double(); 212 | ex = Double.MAX_VALUE; ey = Double.MAX_VALUE; 213 | for (ShapeSeg seg : segs) { 214 | if ((float) seg.sx != (float) ex || (float) seg.sy != (float) ey) { 215 | path = new Path2D.Double(); 216 | out.add(path); 217 | path.moveTo(ex = seg.sx, ey = seg.sy); 218 | } 219 | switch (seg.type) { 220 | case PathIterator.SEG_LINETO: // 1 221 | path.lineTo(ex = seg.coords[0], ey = seg.coords[1]); 222 | break; 223 | case PathIterator.SEG_QUADTO: // 2 224 | path.quadTo(seg.coords[0], seg.coords[1], ex = seg.coords[2], ey = seg.coords[3]); 225 | break; 226 | case PathIterator.SEG_CUBICTO: // 3 227 | path.curveTo(seg.coords[0], seg.coords[1], seg.coords[2], seg.coords[3], ex = seg.coords[4], ey = seg.coords[5]); 228 | break; 229 | case PathIterator.SEG_CLOSE: // 4 230 | // Close and write out the current curve 231 | path.closePath(); 232 | break; 233 | } 234 | } 235 | return out; 236 | } 237 | 238 | public static void main (String[] args) { 239 | // Create square with disconnected and misordered line segments 240 | Path2D.Double path = new Path2D.Double(); 241 | path.moveTo(1, 1); // 1,1 -> 2,1 top 242 | path.lineTo(2, 1); 243 | path.moveTo(1, 2); // 1,2 -> 2,2 bot 244 | path.lineTo(2, 2); 245 | path.moveTo(2, 2); // 2,2 -> 2,1 right 246 | path.lineTo(2, 1); 247 | path.moveTo(1, 1); // 1,1 -> 1,2 left 248 | path.lineTo(1, 2); 249 | // Combine segments into continuous path 250 | List list = optimizeShape(path); 251 | System.out.println(list.size()); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/GerberZip.java: -------------------------------------------------------------------------------- 1 | import java.awt.geom.Path2D; 2 | import java.awt.geom.Point2D; 3 | import java.awt.geom.Rectangle2D; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.*; 8 | import java.util.zip.ZipEntry; 9 | import java.util.zip.ZipFile; 10 | 11 | // Crude Excellon Drill File Parser (just enough read the DRILL.TXT from Osmond PCB Gerber files) 12 | // See: https://web.archive.org/web/20071030075236/http://www.excellon.com/manuals/program.htm 13 | 14 | public class GerberZip { 15 | private String excellon, outline; 16 | 17 | GerberZip (File zipFile) throws IOException { 18 | ZipFile zip = new ZipFile(zipFile); 19 | Enumeration entries = zip.entries(); 20 | while (entries.hasMoreElements()) { 21 | ZipEntry entry = entries.nextElement(); 22 | InputStream stream = zip.getInputStream(entry); 23 | byte[] data = new byte[stream.available()]; 24 | stream.read(data); 25 | String tmp = new String(data); 26 | stream.close(); 27 | if (entry.getName().equals("DRILL.TXT")) { 28 | excellon = tmp; 29 | } else if (entry.getName().equals("OUTLINE.GER")) { 30 | outline = tmp; 31 | } 32 | } 33 | zip.close(); 34 | } 35 | 36 | static class ExcellonHole { 37 | double xLoc; 38 | double yLoc; 39 | double diameter; 40 | 41 | ExcellonHole (double x, double y, double dia) { 42 | xLoc = x; 43 | yLoc = y; 44 | diameter = dia; 45 | } 46 | } 47 | 48 | List getShapes () { 49 | List holes = parseExcellon(); 50 | List> outlines = parseOutlines(); 51 | Rectangle2D.Double bounds = GerberZip.getBounds(outlines); 52 | // System.out.println("PCB Size: " + bounds.getWidth() + " inches, " + bounds.getHeight() + " inches"); 53 | double yBase = bounds.getHeight(); 54 | List gShapes = new ArrayList<>(); 55 | for (GerberZip.ExcellonHole hole : holes) { 56 | gShapes.add(new CADOval(hole.xLoc, yBase - hole.yLoc, hole.diameter, hole.diameter, 0)); 57 | } 58 | // Build shapes for all outlines 59 | for (List points : outlines) { 60 | Path2D.Double path = new Path2D.Double(); 61 | boolean first = true; 62 | for (Point2D.Double point : points) { 63 | if (first) { 64 | path.moveTo(point.getX() - bounds.width / 2, yBase - point.getY() - bounds.height / 2); 65 | first = false; 66 | } else { 67 | path.lineTo(point.getX() - bounds.width / 2, yBase - point.getY() - bounds.height / 2); 68 | } 69 | } 70 | CADShape outline = new CADShape(path, 0, 0, 0); 71 | gShapes.add(outline); 72 | } 73 | CADShapeGroup group = new CADShapeGroup(); 74 | for (CADShape cShape : gShapes) { 75 | group.addToGroup(cShape); 76 | } 77 | return gShapes; 78 | } 79 | 80 | public static void main (String[] args) throws Exception { 81 | GerberZip gerber = new GerberZip(new File("Test/Gerber Files/archive.zip")); 82 | List holes = gerber.parseExcellon(); 83 | for (ExcellonHole hole : holes) { 84 | System.out.println(hole.xLoc + "," + hole.yLoc + "," + hole.diameter); 85 | } 86 | Rectangle2D.Double bounds = getBounds(gerber.parseOutlines()); 87 | System.out.println("PCB Size: " + bounds.getWidth() + " inches, " + bounds.getHeight() + " inches"); 88 | } 89 | 90 | List parseExcellon () { 91 | int holeType = 0; 92 | Map tools = new TreeMap<>(); 93 | List holes = new ArrayList<>(); 94 | StringTokenizer tok = new StringTokenizer(excellon); 95 | while (tok.hasMoreElements()) { 96 | String line = tok.nextToken(); 97 | if (line.startsWith("T")) { 98 | if (line.contains("C")) { 99 | // Read hold definition 100 | int idx = line.indexOf("C"); 101 | String type = line.substring(1, idx); 102 | String val = line.substring(idx + 1); 103 | tools.put(Integer.parseInt(type), Double.parseDouble(val)); 104 | } else { 105 | // Read beginning of hole list marker 106 | String type = line.substring(1); 107 | holeType = Integer.parseInt(type); 108 | } 109 | } else if (line.startsWith("X") && line.contains("Y")) { 110 | // Read hole position 111 | int idx = line.indexOf("Y"); 112 | String xVal = line.substring(1, idx); 113 | String yVal = line.substring(idx + 1); 114 | double xx = parseExcellonValue(xVal); 115 | double yy = parseExcellonValue(yVal); 116 | double diameter = tools.get(holeType); 117 | holes.add(new ExcellonHole(xx, yy, diameter)); 118 | } 119 | } 120 | return holes; 121 | } 122 | 123 | List> parseOutlines () { 124 | double lineWid = 0; 125 | Map apertures = new HashMap<>(); 126 | StringTokenizer tok = new StringTokenizer(outline, "\n\r"); 127 | List> outlines = new ArrayList<>(); 128 | List points = new ArrayList<>(); 129 | double lastX = 0, lastY = 0; 130 | while (tok.hasMoreElements()) { 131 | String line = tok.nextToken(); 132 | if (line.startsWith("%")) { 133 | line = line.substring(1); 134 | if (line.startsWith("ADD")) { 135 | line = line.substring(3); 136 | int idx = line.indexOf("C"); 137 | int aNum = Integer.parseInt(line.substring(0, idx)); 138 | int idx2 = line.indexOf(","); 139 | int idx3 = line.indexOf("*"); 140 | if (idx2 > 0 && idx3 > idx2) { 141 | double aSize = Double.parseDouble(line.substring(idx2 + 1, idx3)); 142 | apertures.put(aNum, aSize); 143 | } 144 | } 145 | } else { 146 | String[] items = line.split("\\*"); 147 | for (String item : items) { 148 | if (item.startsWith("G01")) { 149 | item = item.substring(3); 150 | } 151 | if (item.startsWith("D")) { 152 | int aNum = Integer.parseInt(item.substring(1)); 153 | lineWid = apertures.get(aNum); 154 | // Hmmm... Omsond has a fixed x/y offset of 1 mil and lineWid has no effect on the outline 155 | } 156 | if (item.startsWith("X") && item.contains("Y") && item.contains("D")) { 157 | int yIdx = item.indexOf("Y"); 158 | int dIdx = item.indexOf("D"); 159 | double xVal = parseExcellonValue(item.substring(1, yIdx)) - lineWid; 160 | double yVal = parseExcellonValue(item.substring(yIdx + 1, dIdx)); 161 | int dNum = Integer.parseInt(item.substring(dIdx + 1)); 162 | if (dNum == 2) { 163 | points = new ArrayList<>(); 164 | outlines.add(points); 165 | } 166 | points.add(new Point2D.Double(lastX = xVal, lastY = yVal)); 167 | } else if (item.startsWith("X") && item.contains("Y")) { 168 | int yIdx = item.indexOf("Y"); 169 | double xVal = parseExcellonValue(item.substring(1, yIdx)); 170 | double yVal = parseExcellonValue(item.substring(yIdx + 1)); 171 | points.add(new Point2D.Double(lastX = xVal, lastY = yVal)); 172 | } else if (item.startsWith("X") && item.contains("D")) { 173 | int dIdx = item.indexOf("D"); 174 | double xVal = parseExcellonValue(item.substring(1, dIdx)); 175 | int dNum = Integer.parseInt(item.substring(dIdx + 1)); 176 | if (dNum == 2) { 177 | points = new ArrayList<>(); 178 | outlines.add(points); 179 | } 180 | points.add(new Point2D.Double(lastX = xVal, lastY)); 181 | } else if (item.startsWith("X")) { 182 | double xVal = parseExcellonValue(item.substring(1)); 183 | points.add(new Point2D.Double(lastX = xVal, lastY)); 184 | } else if (item.startsWith("Y") && item.contains("D")) { 185 | int dIdx = item.indexOf("D"); 186 | double yVal = parseExcellonValue(item.substring(1, dIdx)); 187 | int dNum = Integer.parseInt(item.substring(dIdx + 1)); 188 | if (dNum == 2) { 189 | points = new ArrayList<>(); 190 | outlines.add(points); 191 | } 192 | points.add(new Point2D.Double(lastX, lastY = yVal)); 193 | } else if (item.startsWith("Y")) { 194 | double yVal = parseExcellonValue(item.substring(1)); 195 | points.add(new Point2D.Double(lastX, lastY = yVal)); 196 | } 197 | } 198 | } 199 | } 200 | return outlines; 201 | } 202 | 203 | public static Rectangle2D.Double getBounds (List> outlines) { 204 | double xMax = 0, yMax = 0; 205 | double xMin = Double.MAX_VALUE, yMin = Double.MAX_VALUE; 206 | for (List points : outlines) { 207 | for (Point2D.Double point : points) { 208 | xMax = Math.max(xMax, point.getX()); 209 | xMin = Math.min(xMin, point.getX()); 210 | yMax = Math.max(yMax, point.getY()); 211 | yMin = Math.min(yMin, point.getY()); 212 | } 213 | } 214 | return new Rectangle2D.Double(xMin, yMin, xMax, yMax); 215 | } 216 | 217 | private static double parseExcellonValue(String val) { 218 | StringBuilder valBuilder = new StringBuilder(val); 219 | while (valBuilder.length() < 2) { 220 | valBuilder.append("0"); 221 | } 222 | val = valBuilder.toString(); 223 | return Double.parseDouble(val.substring(0, 2) + "." + val.substring(2)); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /images/LaserCut Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/CADText.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.font.GlyphVector; 3 | import java.awt.font.TextAttribute; 4 | import java.awt.geom.AffineTransform; 5 | import java.awt.geom.Path2D; 6 | import java.awt.geom.Rectangle2D; 7 | import java.awt.image.BufferedImage; 8 | import java.io.Serializable; 9 | import java.util.*; 10 | import java.util.List; 11 | 12 | class CADText extends CADShape implements Serializable { 13 | private static final long serialVersionUID = 4314642313295298841L; 14 | public String text, fontName, fontStyle; 15 | public int fontSize; 16 | public double tracking; 17 | private static final Map styles = new HashMap<>(); 18 | private static final List fonts = new ArrayList<>(); 19 | 20 | static { 21 | // Define available font styles 22 | styles.put("plain", Font.PLAIN); 23 | styles.put("bold", Font.BOLD); 24 | styles.put("italic", Font.ITALIC); 25 | styles.put("bold-italic", Font.BOLD + Font.ITALIC); 26 | // Define available fonts 27 | String[] availFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); 28 | Map aMap = new HashMap<>(); 29 | for (String tmp : availFonts) { 30 | aMap.put(tmp, tmp); 31 | } 32 | addIfAvailable(aMap, "American Typewriter"); 33 | addIfAvailable(aMap, "Arial"); 34 | addIfAvailable(aMap, "Arial Black"); 35 | addIfAvailable(aMap, "Bauhaus 93"); 36 | addIfAvailable(aMap, "Bradley Hand"); 37 | addIfAvailable(aMap, "Brush Script"); 38 | addIfAvailable(aMap, "Casual"); 39 | addIfAvailable(aMap, "Chalkboard"); 40 | addIfAvailable(aMap, "Comic Sans MS"); 41 | addIfAvailable(aMap, "Edwardian Script ITC"); 42 | addIfAvailable(aMap, "Freehand"); 43 | addIfAvailable(aMap, "Giddyup Std"); 44 | addIfAvailable(aMap, "Helvetica"); 45 | addIfAvailable(aMap, "Hobo Std"); 46 | addIfAvailable(aMap, "Impact"); 47 | addIfAvailable(aMap, "Marker Felt"); 48 | addIfAvailable(aMap, "OCR A Std"); 49 | addIfAvailable(aMap, "Times New Roman"); 50 | addIfAvailable(aMap, "Stencil"); 51 | fonts.add("Vector 1"); 52 | fonts.add("Vector 2"); 53 | fonts.add("Vector 3"); 54 | } 55 | 56 | @Override 57 | String getMenuName () { 58 | return "Text"; 59 | } 60 | 61 | private static void addIfAvailable (Map avail, String font) { 62 | if (avail.containsKey(font)) { 63 | fonts.add(font); 64 | } 65 | } 66 | 67 | /** 68 | * Default constructor is used to instantiate subclasses in "Shapes" Menu 69 | */ 70 | @SuppressWarnings("unused") 71 | CADText () { 72 | // Set typical initial values, which user can edit before saving 73 | text = "Test"; 74 | fontName = "Helvetica"; 75 | fontStyle = "plain"; 76 | fontSize = 24; 77 | tracking = 0; 78 | engrave = true; 79 | } 80 | 81 | CADText (double xLoc, double yLoc, String text, String fontName, String fontStyle, int fontSize, double tracking, double rotation) { 82 | this.text = text; 83 | this.fontName = fontName; 84 | this.fontStyle = fontStyle; 85 | this.fontSize = fontSize; 86 | this.tracking = tracking; 87 | setLocationAndOrientation(xLoc, yLoc, rotation); 88 | } 89 | 90 | @Override 91 | void resize (double dx, double dy) { 92 | double width = dx * 2; 93 | int newPnts = fontSize; 94 | boolean changed = false; 95 | double wid; 96 | double sWid = getSWid(fontSize); 97 | if (sWid < width) { 98 | while ((wid = getSWid(++newPnts)) < width) { 99 | fontSize = newPnts; 100 | changed = true; 101 | } 102 | } else { 103 | while (newPnts > 8 && (wid = getSWid(--newPnts)) > width) { 104 | fontSize = newPnts; 105 | changed = true; 106 | } 107 | } 108 | if (changed) { 109 | buildShape(); 110 | } 111 | } 112 | 113 | private double getSWid (int points) { 114 | BufferedImage bi = new BufferedImage(5, 5, BufferedImage.TYPE_INT_RGB); 115 | Graphics gg = bi.getGraphics(); 116 | Font fnt = new Font(fontName, styles.get(fontStyle), points); 117 | FontMetrics fm = gg.getFontMetrics(fnt); 118 | return (double) fm.stringWidth(text) / 72.0; 119 | } 120 | 121 | @Override 122 | String[] getParameterNames () { 123 | StringBuilder fontNames = new StringBuilder("fontName"); 124 | for (String font : fonts) { 125 | fontNames.append(":"); 126 | fontNames.append(font); 127 | } 128 | return new String[]{ 129 | "text{text to display}", 130 | fontNames + "{font name}", 131 | "fontStyle:plain:bold:italic:bold-italic{font style}", 132 | "fontSize|pts{font size in points}", 133 | "tracking{controls spacing of glyphs}"}; 134 | } 135 | 136 | @Override 137 | Shape buildShape () { 138 | if (fontName.startsWith("Vector")) { 139 | Path2D.Double path = new Path2D.Double(); 140 | VectorFont font = VectorFont.getFont(fontName); 141 | int[][][] stroke = font.font; 142 | int lastX = 1000, lastY = 1000; 143 | int xOff = 0; 144 | for (int ii = 0; ii < text.length(); ii++) { 145 | char cc = text.charAt(ii); 146 | cc = cc >= 32 & cc <= 127 ? cc : '_'; // Substitute '_' for codes outside printable ASCII range 147 | int[][] glyph = stroke[cc - 32]; 148 | int left = glyph[0][0]; 149 | int right = glyph[0][1]; 150 | for (int jj = 1; jj < glyph.length; jj++) { 151 | int x1 = glyph[jj][0] - left; 152 | int y1 = glyph[jj][1]; 153 | int x2 = glyph[jj][2] - left; 154 | int y2 = glyph[jj][3]; 155 | 156 | if (x1 != lastX || y1 != lastY) { 157 | path.moveTo(x1 + xOff, y1); 158 | } 159 | path.lineTo(x2 + xOff, lastY = y2); 160 | lastX = x2; 161 | } 162 | int step = right - left; 163 | xOff += step; 164 | } 165 | AffineTransform at = new AffineTransform(); 166 | double scale = fontSize / (72.0 * font.height); 167 | at.scale(scale, scale); 168 | Shape text = at.createTransformedShape(path); 169 | Rectangle2D bounds = text.getBounds2D(); 170 | at = new AffineTransform(); 171 | at.translate(-bounds.getX(), -bounds.getY()); 172 | text = at.createTransformedShape(text); 173 | bounds = text.getBounds2D(); 174 | at = new AffineTransform(); 175 | at.translate(-bounds.getWidth() / 2, -bounds.getHeight() / 2); 176 | return at.createTransformedShape(text); 177 | } else { 178 | // Code from: http://www.java2s.com/Tutorial/Java/0261__2D-Graphics/GenerateShapeFromText.htm 179 | BufferedImage img = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB); 180 | Graphics2D g2 = img.createGraphics(); 181 | Font font = new Font(fontName, styles.get(fontStyle), fontSize); 182 | HashMap attrs = new HashMap<>(); 183 | attrs.put(TextAttribute.KERNING, TextAttribute.KERNING_ON); 184 | attrs.put(TextAttribute.LIGATURES, TextAttribute.LIGATURES_ON); 185 | attrs.put(TextAttribute.TRACKING, tracking); 186 | font = font.deriveFont(attrs); 187 | g2.setFont(font); 188 | try { 189 | GlyphVector vect = font.createGlyphVector(g2.getFontRenderContext(), text); 190 | AffineTransform at = new AffineTransform(); 191 | at.scale(1 / 72.0, 1 / 72.0); 192 | Shape text = at.createTransformedShape(vect.getOutline()); 193 | Rectangle2D bounds = text.getBounds2D(); 194 | at = new AffineTransform(); 195 | at.translate(-bounds.getX(), -bounds.getY()); 196 | text = at.createTransformedShape(text); 197 | bounds = text.getBounds2D(); 198 | at = new AffineTransform(); 199 | at.translate(-bounds.getWidth() / 2, -bounds.getHeight() / 2); 200 | return at.createTransformedShape(text); 201 | } finally { 202 | g2.dispose(); 203 | } 204 | } 205 | } 206 | 207 | static class VectorFont { 208 | private static final Map vFonts = new HashMap<>(); 209 | int[][][] font; 210 | final int height; 211 | 212 | VectorFont (String name) { 213 | height = 32; 214 | switch (name) { 215 | case "Vector 1": 216 | font = getFontData("hershey1.txt"); 217 | break; 218 | case "Vector 2": 219 | font = getFontData("hershey2.txt"); 220 | break; 221 | case "Vector 3": 222 | font = getFontData("hershey3.txt"); 223 | break; 224 | } 225 | } 226 | 227 | static VectorFont getFont (String name) { 228 | VectorFont font = vFonts.get(name); 229 | if (font == null) { 230 | vFonts.put(name, font = new VectorFont(name)); 231 | } 232 | return font; 233 | } 234 | 235 | private int[][][] getFontData (String name) { 236 | String data = Utils2D.getResourceFile("fonts/" + name); 237 | StringTokenizer tok = new StringTokenizer(data, "\n"); 238 | int[][][] font = new int[128][][]; 239 | while (tok.hasMoreElements()) { 240 | String line = tok.nextToken(); 241 | if (line.charAt(3) == ':') { 242 | char cc = line.charAt(1); 243 | line = line.substring(4); 244 | String[] vecs = line.split("\\|"); 245 | int[][] vec = new int[vecs.length][]; 246 | font[cc - 32] = vec; 247 | for (int ii = 0; ii < vecs.length; ii++) { 248 | String[] coords = vecs[ii].split(","); 249 | int[] tmp = new int[coords.length]; 250 | for (int jj = 0; jj < tmp.length; jj++) { 251 | tmp[jj] = Integer.parseInt(coords[jj]); 252 | vec[ii] = tmp; 253 | } 254 | } 255 | } 256 | } 257 | return font; 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/CADMusicStrip.java: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.geom.Ellipse2D; 3 | import java.awt.geom.Line2D; 4 | import java.awt.geom.Path2D; 5 | import java.awt.geom.Point2D; 6 | import java.io.File; 7 | import java.io.Serializable; 8 | import java.nio.file.Files; 9 | import java.util.*; 10 | import java.util.List; 11 | 12 | /** 13 | * Example music file 14 | * 6E 15 | * 6D, 4D, 3C 16 | * 4F# 17 | * etc. 18 | * 19 | * Resources: 20 | * musicboxmaniacs.com/ 21 | * www.youtube.com/watch?v=apcsggbbBFw 22 | */ 23 | 24 | class CADMusicStrip extends CADShape implements Serializable, LaserCut.Updatable, LaserCut.NorResizable { 25 | private static final long serialVersionUID = 7398125917619364676L; 26 | private static final Map noteIndex = new HashMap<>(); 27 | private static final double xStep = 4.0; 28 | private static final double yStep = 2.017; 29 | private static final double xOff = Utils2D.mmToInches(12.0); 30 | private static final double yOff = Utils2D.mmToInches(6.0); 31 | private static final double holeDiam = Utils2D.mmToInches(2.0); 32 | private boolean checkClicked; 33 | public int columns = 60; 34 | public double width, height; 35 | public boolean[][] notes; 36 | private transient Shape rect; 37 | private transient int lastCol = 0; 38 | 39 | static { 40 | // Only notes marked 'true' are available on 30 note music box player 41 | Note[] noteList = { 42 | // Oct Note Used 43 | new Note(3, "C", true), // 3C 44 | new Note(3, "C#", false), 45 | new Note(3, "D#", false), 46 | new Note(3, "E", false), 47 | new Note(3, "F", false), 48 | new Note(3, "G", true), // 3G 49 | new Note(3, "G#", false), 50 | new Note(3, "A", true), // 3A 51 | new Note(3, "A#", false), 52 | new Note(3, "B", true), // 3B 53 | new Note(4, "C", true), // 4C 54 | new Note(4, "C#", true), // 4C# 55 | new Note(4, "D", true), // 4D 56 | new Note(4, "D#", false), 57 | new Note(4, "E", true), // 3E 58 | new Note(4, "F", true), // 4F 59 | new Note(4, "F#", true), // 4F# 60 | new Note(4, "G", true), // 4G 61 | new Note(4, "G#", true), // 4G# 62 | new Note(4, "A", true), // 4A 63 | new Note(4, "A#", true), // 4A# 64 | new Note(4, "B", true), // 4B 65 | new Note(5, "C", true), // 5C 66 | new Note(5, "C#", true), // 5C# 67 | new Note(5, "D", true), // 5D 68 | new Note(5, "D#", true), // 5D# 69 | new Note(5, "E", true), // 5E 70 | new Note(5, "F", true), // %F 71 | new Note(5, "F#", true), // 5F# 72 | new Note(5, "G", true), // 5 73 | new Note(5, "G#", true), // 5G# 74 | new Note(5, "A", true), // 5A 75 | new Note(5, "A#", true), // 5A# 76 | new Note(5, "B", true), // 5B 77 | new Note(6, "C", true), // 6C 78 | new Note(6, "C#", false), 79 | new Note(6, "D", true), // 6D 80 | new Note(6, "D#", false), 81 | new Note(6, "E", true), // 6E 82 | }; 83 | int idx = 0; 84 | for (int ii = noteList.length - 1; ii >= 0; ii--) { 85 | Note note = noteList[ii]; 86 | if (note.used) { 87 | noteIndex.put(note.note, idx++); 88 | } 89 | } 90 | int dum = 0; 91 | } 92 | 93 | static class Note { 94 | String note; 95 | boolean used; 96 | 97 | Note (int octave, String note, boolean used) { 98 | this.note = octave + note; 99 | this.used = used; 100 | } 101 | } 102 | 103 | void readMusicBoxFile (File sFile) throws Exception { 104 | Scanner lines = new Scanner(Files.newInputStream(sFile.toPath())); 105 | List cols = new ArrayList<>(); 106 | while (lines.hasNextLine()) { 107 | Scanner line = new Scanner(lines.nextLine().trim()); 108 | List notes = new ArrayList<>(); 109 | while (line.hasNext()) { 110 | String item = line.next(); 111 | System.out.println(item); 112 | item = item.endsWith(",") ? item.substring(0, item.length() - 1) : item; 113 | notes.add(item); 114 | } 115 | cols.add(notes.toArray(new String[0])); 116 | notes = new ArrayList<>(); 117 | } 118 | String[][] song = cols.toArray(new String[cols.size()][0]); 119 | notes = new boolean[song.length][30]; 120 | for (int ii = 0; ii < song.length; ii++) { 121 | for (String note : song[ii]) { 122 | if (noteIndex.containsKey(note)) { 123 | notes[ii][noteIndex.get(note)] = true; 124 | } 125 | } 126 | } 127 | width = Utils2D.mmToInches(song.length * 4 + 16); 128 | height = Utils2D.mmToInches(70); 129 | } 130 | 131 | @Override 132 | String getMenuName () { 133 | return "Music Strip"; 134 | } 135 | 136 | @Override 137 | void updateStateAfterParameterEdit () { 138 | if (notes == null) { 139 | notes = new boolean[columns][30]; 140 | } else { 141 | // Resize array and copy notes from old array 142 | boolean[][] nNotes = new boolean[columns][30]; 143 | for (int ii = 0; ii < Math.min(notes.length, nNotes.length); ii++) { 144 | System.arraycopy(notes[ii], 0, nNotes[ii], 0, notes[ii].length); 145 | } 146 | notes = nNotes; 147 | } 148 | width = Utils2D.mmToInches(columns * 4 + 16); 149 | height = Utils2D.mmToInches(70); 150 | } 151 | 152 | @Override 153 | void draw (Graphics g, double zoom, boolean keyRotate, boolean keyResize, boolean keyOption) { 154 | Graphics2D g2 = (Graphics2D) g.create(); 155 | Stroke thick = new BasicStroke(1.0f); 156 | Stroke thin = new BasicStroke(0.8f); 157 | double mx = (xLoc + xOff) * zoom * LaserCut.SCREEN_PPI; 158 | double my = (yLoc + yOff) * zoom * LaserCut.SCREEN_PPI; 159 | double zf = zoom / LaserCut.SCREEN_PPI; 160 | g2.setFont(new Font("Arial", Font.PLAIN, (int) (7 * zf))); 161 | for (int ii = 0; ii <= notes.length; ii++) { 162 | double sx = mx + Utils2D.mmToInches(ii * xStep * zoom * LaserCut.SCREEN_PPI); 163 | g2.setColor((ii & 1) == 0 ? Color.black : isSelected ? Color.black : Color.lightGray); 164 | g2.setStroke((ii & 1) == 0 ? thick : thin); 165 | g2.draw(new Line2D.Double(sx, my, sx, my + Utils2D.mmToInches(29 * yStep * zoom * LaserCut.SCREEN_PPI))); 166 | for (int jj = 0; jj < 30; jj++) { 167 | double sy = my + Utils2D.mmToInches(jj * yStep * zoom * LaserCut.SCREEN_PPI); 168 | g2.setColor(jj == 0 || jj == 29 ? Color.black : isSelected ? Color.black : Color.lightGray); 169 | g2.setStroke(jj == 0 || jj == 29 ? thick : thin); 170 | g2.draw(new Line2D.Double(mx, sy, mx + Utils2D.mmToInches(columns * xStep * zoom * LaserCut.SCREEN_PPI), sy)); 171 | if (ii == lastCol) { 172 | g2.setColor(Color.red); 173 | //g2.drawString(symb[jj], (int) (sx - 14 * zf), (int) (sy + 2.5 * zf)); 174 | } 175 | } 176 | } 177 | g2.dispose(); 178 | super.draw(g, zoom, false, keyResize, keyOption); 179 | } 180 | 181 | // Implement Updatable interface 182 | public boolean updateInternalState (Point2D.Double point) { 183 | // See if user clicked on one of the note spots (Note: point in screen inch coords) 184 | double xx = Utils2D.inchesToMM(point.x - xLoc - xOff); 185 | double yy = Utils2D.inchesToMM(point.y - yLoc - yOff); 186 | double gridX = Math.floor((xx / xStep) + 0.5); 187 | double gridY = Math.floor((yy / yStep) + 0.5); 188 | double dX = xx - gridX * xStep; 189 | double dY = yy - gridY * yStep; 190 | double dist = Math.sqrt(dX * dX + dY * dY); 191 | //System.out.println(df.format(gridX) + ", " + df.format(gridY) + " - " + df.format(dist)); 192 | if (dist <= 1.5 && gridX >= 0 && gridX < notes.length && gridY >= 0 && gridY < 30) { 193 | // Used has clicked in a note circle 194 | notes[(int) gridX][(int) gridY] ^= true; 195 | lastCol = (int) gridX; 196 | updateShape(); 197 | return true; 198 | } 199 | return gridX >= 0 && gridX < notes.length && gridY >= 0 && gridY < 30; 200 | } 201 | 202 | @Override 203 | Shape getShape () { 204 | if (checkClicked && rect != null) { 205 | return rect; 206 | } 207 | return super.getShape(); 208 | } 209 | 210 | @Override 211 | boolean isShapeClicked (Point2D.Double point, double zoomFactor) { 212 | checkClicked = true; 213 | boolean clicked = super.isShapeClicked(point, zoomFactor); 214 | checkClicked = false; 215 | return clicked; 216 | } 217 | 218 | @Override 219 | Shape buildShape () { 220 | Path2D.Double path = new Path2D.Double(); 221 | double xx = -width / 2; 222 | double yy = -height / 2; 223 | // Draw enclosing box with notched corner to indicate orientation of strip 224 | path.moveTo(xx, yy); 225 | path.lineTo(xx + width, yy); 226 | path.lineTo(xx + width, yy + height); 227 | path.lineTo(xx + .2, yy + height); 228 | path.lineTo(xx, yy - .4 + height); 229 | path.lineTo(xx, yy); 230 | // Draw the holes that need to be cut for active notes 231 | double rad = holeDiam / 2; 232 | for (int ii = 0; ii < notes.length; ii++) { 233 | double sx = xx + xOff + Utils2D.mmToInches(ii * xStep); 234 | for (int jj = 0; jj < 30; jj++) { 235 | double sy = yy + yOff + Utils2D.mmToInches(jj * yStep); 236 | if (notes[ii][jj]) { 237 | path.append(new Ellipse2D.Double(sx - rad, sy - rad, holeDiam, holeDiam), false); 238 | } 239 | } 240 | } 241 | return path; 242 | } 243 | 244 | @Override 245 | protected java.util.List getEditFields () { 246 | return Arrays.asList( 247 | "columns", 248 | "xLoc|in", 249 | "yLoc|in"); 250 | } 251 | 252 | @Override 253 | protected List getPlaceFields () { 254 | return Arrays.asList("columns", "xLoc|in", "yLoc|in"); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/GearGen.java: -------------------------------------------------------------------------------- 1 | 2 | import javax.swing.*; 3 | import java.awt.*; 4 | import java.awt.geom.*; 5 | import java.util.ArrayList; 6 | import java.util.Scanner; 7 | 8 | // See: http://printobjects.me/catalogue/ujava-gear-generator-involute-and-fillet_520801/ 9 | // Also: http://khkgears.net/gear-knowledge/gear-technical-reference/calculation-gear-dimensions/ 10 | // And: http://fab.cba.mit.edu/classes/863.09/people/cranor/How_to_Make_(Almost)_Anything/David_Cranor/Entries/2009/10/12_Entry_1_files/module.pdf 11 | 12 | public class GearGen { 13 | private static final double PPI = java.awt.Toolkit.getDefaultToolkit().getScreenResolution(); 14 | private static boolean DEBUG; 15 | 16 | // Derived from GearGen.java in LibLasercut 17 | 18 | /** 19 | * Test code for GearGen 20 | */ 21 | @SuppressWarnings("ConstantIfStatement") 22 | public static void main (String[] args) { 23 | DEBUG = true; 24 | ShapeWindow sWin = new ShapeWindow(); 25 | if (true) { 26 | Shape gear = generateGear(.45, 30, 10, 20, .25, Utils2D.mmToInches(3)); 27 | sWin.addShape(gear); 28 | } else { 29 | Scanner sc = new Scanner(System.in); 30 | System.out.print("Module (mm diameter/teeth, commonly 0.5, 0.8, 1.00, 1.25, 1.50, 2.50, or 3 ): "); 31 | double module = sc.nextDouble(); 32 | System.out.print("Pressure Angle in degrees (commonly 14.5, 20, or 25): "); 33 | double pressAngle = sc.nextDouble(); 34 | System.out.print("Number of Teeth: "); 35 | int numTeeth = sc.nextInt(); 36 | double minShift = Math.max(-0.5, (30.0 - numTeeth) / 40.0); 37 | double maxShift = 0.6; 38 | System.out.print("Profile Shift (recommended [" + minShift + ", " + maxShift + "]): "); 39 | // Note: profileShift needed for gears with small number of teeth 40 | // See: http://khkgears.net/gear-knowledge/abcs-gears-b/gear-profile-shift/ 41 | double profileShift = sc.nextDouble(); 42 | System.out.print("Number of Output Points for Involute Curve and Fillet Segments: "); 43 | int numPoints = sc.nextInt(); 44 | Shape gear = generateGear(module, numTeeth, numPoints, pressAngle, profileShift, 3); 45 | sWin.addShape(gear); 46 | } 47 | } 48 | 49 | /** 50 | * Generate Involute Spur Gear based on provided parameters 51 | * @param module module = reference diameter / number of teeth (gears must have same module to mesh) 52 | * @param numTeeth total number of teeth in gear 53 | * @param numPoints number of points used when drawing tooth profile 54 | * @param pressAngle pressure angle (typically 20 degrees) 55 | * @param profileShift adjusts center distance 56 | * @param holeSize size of hole in center of gear 57 | * @return generated gear Shape 58 | */ 59 | public static Shape generateGear (double module, int numTeeth, int numPoints, double pressAngle, double profileShift, double holeSize) { 60 | pressAngle = Math.PI / 180.0 * pressAngle; 61 | double pitchDiameter = module * numTeeth; // Pitch Diameter = Module * Teeth 62 | double baseDiameter = pitchDiameter * Math.cos(pressAngle); // Base Circle Diameter = Pitch Diameter × Cosine(Pressure Angle) 63 | double dedendum = 1.157 * module; // Dedendum = 1.157 × Module 64 | double workDepth = 2 * module; // Working Depth = 2 × Module 65 | double wholeDepth = 2.157 * module; // Whole Depth = 2.157 × Module 66 | double outerDiameter = module * (numTeeth + 2); // Outside Diameter = Module × (Teeth + 2) 67 | double tipRadius = 0.25 * module; // Tip Radius = 0.25 x Module 68 | double U = -(Math.PI / 4.0 + (1.0 - 0.25) * Math.tan(pressAngle) + 0.25 / Math.cos(pressAngle)); 69 | double V = 0.25 - 1.0; 70 | ArrayList points = new ArrayList<>(); 71 | // Generate Involute Points 72 | double thetaMin = 2.0 / numTeeth * (U + (V + profileShift) / Math.tan(pressAngle)); 73 | double thetaMax = 1.0 / (numTeeth * Math.cos(pressAngle)) * Math.sqrt(Math.pow((2 + numTeeth + 2 * profileShift), 2) - 74 | Math.pow(numTeeth * Math.cos(pressAngle), 2)) - (1 + 2 * profileShift / numTeeth) * Math.tan(pressAngle) - 75 | Math.PI / (2.0 * numTeeth); 76 | double thetaInc = (thetaMax - thetaMin) / numPoints; 77 | double lastY = 0; 78 | for (int ii = numPoints - 1; ii > 0; ii--) { 79 | double theta = thetaMin + thetaInc * ii; 80 | double xx = numTeeth * module / 2.0 * (Math.sin(theta) - ((theta + Math.PI / (2.0 * numTeeth)) * Math.cos(pressAngle) + 81 | 2 * profileShift / numTeeth * Math.sin(pressAngle)) * Math.cos(theta + pressAngle)); 82 | double yy = numTeeth * module / 2.0 * (Math.cos(theta) + ((theta + Math.PI / (2.0 * numTeeth)) * Math.cos(pressAngle) + 83 | 2 * profileShift / numTeeth * Math.sin(pressAngle)) * Math.sin(theta + pressAngle)); 84 | points.add(new Point2D.Double(xx, yy)); 85 | lastY = yy; 86 | } 87 | // Generate Fillet Points 88 | thetaMin = 2.0 / numTeeth * (U + (V + profileShift) / Math.tan(pressAngle)); 89 | thetaMax = 2.0 * U / numTeeth; 90 | thetaInc = (thetaMax - thetaMin) / numPoints; 91 | for (int ii = numPoints; ii < 2 * numPoints; ii++) { 92 | double theta = thetaMin + thetaInc * (ii - numPoints); 93 | double L = Math.sqrt(1 + 4 * Math.pow((V + profileShift) / (2 * U - numTeeth * theta), 2)); 94 | double Q = 2 * 0.25 / L * (V + profileShift) / (2 * U - numTeeth * theta) + V + numTeeth / 2.0 + profileShift; 95 | double P = 0.25 / L + (U - numTeeth * theta / 2.0); 96 | double xx = module * (P * Math.cos(theta) + Q * Math.sin(theta)); 97 | double yy = module * (-P * Math.sin(theta) + Q * Math.cos(theta)); 98 | if (yy < lastY) { 99 | // Prevents backtracking line when numTeeth < 8 100 | points.add(new Point2D.Double(xx, yy)); 101 | } 102 | } 103 | Point2D.Double[] pointArray = points.toArray(new Point2D.Double[0]); 104 | // Generate tooth Shape from left/right outlines 105 | Path2D.Double tooth = new Path2D.Double(); 106 | tooth.moveTo(pointArray[pointArray.length - 1].x, pointArray[pointArray.length - 1].y); 107 | // Draw left section 108 | for (int ii = pointArray.length - 2; ii >= 0; ii--) { 109 | tooth.lineTo(pointArray[ii].x, pointArray[ii].y); 110 | } 111 | // Mirror left section around Y axis to draw right section and fill gap at tip of tooth 112 | for (Point2D.Double point : pointArray) { 113 | tooth.lineTo(-point.x, point.y); 114 | } 115 | // Rotate and place teeth around pitch diameter to generate gear 116 | Path2D.Double gear = new Path2D.Double(); 117 | AffineTransform rot = new AffineTransform(); 118 | boolean connect = false; 119 | for (int angle = 0; angle < numTeeth; angle++) { 120 | // Use 'connect' to control adding line from prior tooth to newly-placed tooth 121 | gear.append(tooth.getPathIterator(rot), connect); 122 | connect = true; 123 | rot.rotate(Math.toRadians(360.0 - 360.0 / numTeeth)); 124 | } 125 | gear.closePath(); 126 | Area area = new Area(gear); 127 | if (holeSize > 0) { 128 | Ellipse2D.Double hole = new Ellipse2D.Double(-holeSize / 2, -holeSize / 2, holeSize, holeSize); 129 | gear.append(hole.getPathIterator(new AffineTransform()), false); 130 | } 131 | if (DEBUG) { 132 | System.out.println("module: " + module); 133 | System.out.println("pitch diameter: " + pitchDiameter); 134 | System.out.println("circular pitch: " + pitchDiameter * Math.PI / numTeeth); 135 | System.out.println("extended pitch diameter: " + (pitchDiameter + 2 * profileShift)); 136 | System.out.println("profile shift: " + profileShift); 137 | System.out.println("base diameter: " + baseDiameter); 138 | System.out.println("addendum: " + module); 139 | System.out.println("dedendum: " + dedendum); 140 | System.out.println("working depth: " + workDepth); 141 | System.out.println("whole depth: " + wholeDepth); 142 | System.out.println("outer diameter: " + outerDiameter); 143 | System.out.println("tip radius: " + tipRadius); 144 | } 145 | return gear; 146 | } 147 | 148 | static class ShapeWindow extends JFrame { 149 | private transient Image offScr; 150 | private Dimension lastDim; 151 | private final ArrayList shapes = new ArrayList<>(); 152 | 153 | ShapeWindow () { 154 | setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 155 | setSize(2000, 2000); 156 | setVisible(true); 157 | } 158 | 159 | void addShape (Shape shape) { 160 | shapes.add(shape); 161 | repaint(); 162 | } 163 | 164 | public void paint (Graphics g) { 165 | Dimension d = getSize(); 166 | if (offScr == null || (lastDim != null && (d.width != lastDim.width || d.height != lastDim.height))) 167 | offScr = createImage(d.width, d.height); 168 | lastDim = d; 169 | double cx = d.getWidth() / 2; 170 | double cy = d.getHeight() / 2; 171 | Graphics2D g2 = (Graphics2D) offScr.getGraphics(); 172 | g2.setBackground(getBackground()); 173 | g2.clearRect(0, 0, d.width, d.height); 174 | g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 175 | g2.setStroke(new BasicStroke(0.5f)); 176 | g2.setColor(Color.black); 177 | // Translate Shape to position and scale up to true size on screen using mm units 178 | AffineTransform at = AffineTransform.getTranslateInstance(cx, cy); 179 | at.scale(PPI, PPI); 180 | for (Shape shape :shapes) { 181 | shape = at.createTransformedShape(shape); 182 | g2.draw(shape); 183 | } 184 | g.drawImage(offScr, 0, 0, this); 185 | } 186 | } 187 | } 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /resources/fonts/hershey1.txt: -------------------------------------------------------------------------------- 1 | // Font: Roman Simplex - Bounds {-11, -16, 11, 16} 22 x 32 - Note: {left,right},{x1,y1,x2,y2},.. 2 | ' ':-8,8 3 | '!':-5,5|0,-12,0,2|0,7,-1,8|-1,8,0,9|0,9,1,8|1,8,0,7 4 | '"':-8,8|-4,-12,-4,-5|4,-12,4,-5 5 | '#':-10,11|1,-16,-6,16|7,-16,0,16|-6,-3,8,-3|-7,3,7,3 6 | '$':-10,10|-2,-16,-2,13|2,-16,2,13|7,-9,5,-11|5,-11,2,-12|2,-12,-2,-12|-2,-12,-5,-11|-5,-11,-7,-9|-7,-9,-7,-7|-7,-7,-6,-5|-6,-5,-5,-4|-5,-4,-3,-3|-3,-3,3,-1|3,-1,5,0|5,0,6,1|6,1,7,3|7,3,7,6|7,6,5,8|5,8,2,9|2,9,-2,9|-2,9,-5,8|-5,8,-7,6 7 | '%':-12,12|9,-12,-9,9|-4,-12,-2,-10|-2,-10,-2,-8|-2,-8,-3,-6|-3,-6,-5,-5|-5,-5,-7,-5|-7,-5,-9,-7|-9,-7,-9,-9|-9,-9,-8,-11|-8,-11,-6,-12|-6,-12,-4,-12|-4,-12,-2,-11|-2,-11,1,-10|1,-10,4,-10|4,-10,7,-11|7,-11,9,-12|5,2,3,3|3,3,2,5|2,5,2,7|2,7,4,9|4,9,6,9|6,9,8,8|8,8,9,6|9,6,9,4|9,4,7,2|7,2,5,2 8 | '&':-13,13|10,-3,10,-4|10,-4,9,-5|9,-5,8,-5|8,-5,7,-4|7,-4,6,-2|6,-2,4,3|4,3,2,6|2,6,0,8|0,8,-2,9|-2,9,-6,9|-6,9,-8,8|-8,8,-9,7|-9,7,-10,5|-10,5,-10,3|-10,3,-9,1|-9,1,-8,0|-8,0,-1,-4|-1,-4,0,-5|0,-5,1,-7|1,-7,1,-9|1,-9,0,-11|0,-11,-2,-12|-2,-12,-4,-11|-4,-11,-5,-9|-5,-9,-5,-7|-5,-7,-4,-4|-4,-4,-2,-1|-2,-1,3,6|3,6,5,8|5,8,7,9|7,9,9,9|9,9,10,8|10,8,10,7 9 | ''':-5,5|0,-10,-1,-11|-1,-11,0,-12|0,-12,1,-11|1,-11,1,-9|1,-9,0,-7|0,-7,-1,-6 10 | '(':-7,7|4,-16,2,-14|2,-14,0,-11|0,-11,-2,-7|-2,-7,-3,-2|-3,-2,-3,2|-3,2,-2,7|-2,7,0,11|0,11,2,14|2,14,4,16 11 | ')':-7,7|-4,-16,-2,-14|-2,-14,0,-11|0,-11,2,-7|2,-7,3,-2|3,-2,3,2|3,2,2,7|2,7,0,11|0,11,-2,14|-2,14,-4,16 12 | '*':-8,8|0,-12,0,0|-5,-9,5,-3|5,-9,-5,-3 13 | '+':-13,13|0,-9,0,9|-9,0,9,0 14 | ',':-5,5|1,8,0,9|0,9,-1,8|-1,8,0,7|0,7,1,8|1,8,1,10|1,10,0,12|0,12,-1,13 15 | '-':-13,13|-9,0,9,0 16 | '.':-5,5|0,7,-1,8|-1,8,0,9|0,9,1,8|1,8,0,7 17 | '/':-11,11|9,-16,-9,16 18 | '0':-10,10|-1,-12,-4,-11|-4,-11,-6,-8|-6,-8,-7,-3|-7,-3,-7,0|-7,0,-6,5|-6,5,-4,8|-4,8,-1,9|-1,9,1,9|1,9,4,8|4,8,6,5|6,5,7,0|7,0,7,-3|7,-3,6,-8|6,-8,4,-11|4,-11,1,-12|1,-12,-1,-12 19 | '1':-10,10|-4,-8,-2,-9|-2,-9,1,-12|1,-12,1,9 20 | '2':-10,10|-6,-7,-6,-8|-6,-8,-5,-10|-5,-10,-4,-11|-4,-11,-2,-12|-2,-12,2,-12|2,-12,4,-11|4,-11,5,-10|5,-10,6,-8|6,-8,6,-6|6,-6,5,-4|5,-4,3,-1|3,-1,-7,9|-7,9,7,9 21 | '3':-10,10|-5,-12,6,-12|6,-12,0,-4|0,-4,3,-4|3,-4,5,-3|5,-3,6,-2|6,-2,7,1|7,1,7,3|7,3,6,6|6,6,4,8|4,8,1,9|1,9,-2,9|-2,9,-5,8|-5,8,-6,7|-6,7,-7,5 22 | '4':-10,10|3,-12,-7,2|-7,2,8,2|3,-12,3,9 23 | '5':-10,10|5,-12,-5,-12|-5,-12,-6,-3|-6,-3,-5,-4|-5,-4,-2,-5|-2,-5,1,-5|1,-5,4,-4|4,-4,6,-2|6,-2,7,1|7,1,7,3|7,3,6,6|6,6,4,8|4,8,1,9|1,9,-2,9|-2,9,-5,8|-5,8,-6,7|-6,7,-7,5 24 | '6':-10,10|6,-9,5,-11|5,-11,2,-12|2,-12,0,-12|0,-12,-3,-11|-3,-11,-5,-8|-5,-8,-6,-3|-6,-3,-6,2|-6,2,-5,6|-5,6,-3,8|-3,8,0,9|0,9,1,9|1,9,4,8|4,8,6,6|6,6,7,3|7,3,7,2|7,2,6,-1|6,-1,4,-3|4,-3,1,-4|1,-4,0,-4|0,-4,-3,-3|-3,-3,-5,-1|-5,-1,-6,2 25 | '7':-10,10|7,-12,-3,9|-7,-12,7,-12 26 | '8':-10,10|-2,-12,-5,-11|-5,-11,-6,-9|-6,-9,-6,-7|-6,-7,-5,-5|-5,-5,-3,-4|-3,-4,1,-3|1,-3,4,-2|4,-2,6,0|6,0,7,2|7,2,7,5|7,5,6,7|6,7,5,8|5,8,2,9|2,9,-2,9|-2,9,-5,8|-5,8,-6,7|-6,7,-7,5|-7,5,-7,2|-7,2,-6,0|-6,0,-4,-2|-4,-2,-1,-3|-1,-3,3,-4|3,-4,5,-5|5,-5,6,-7|6,-7,6,-9|6,-9,5,-11|5,-11,2,-12|2,-12,-2,-12 27 | '9':-10,10|6,-5,5,-2|5,-2,3,0|3,0,0,1|0,1,-1,1|-1,1,-4,0|-4,0,-6,-2|-6,-2,-7,-5|-7,-5,-7,-6|-7,-6,-6,-9|-6,-9,-4,-11|-4,-11,-1,-12|-1,-12,0,-12|0,-12,3,-11|3,-11,5,-9|5,-9,6,-5|6,-5,6,0|6,0,5,5|5,5,3,8|3,8,0,9|0,9,-2,9|-2,9,-5,8|-5,8,-6,6 28 | ':':-5,5|0,-5,-1,-4|-1,-4,0,-3|0,-3,1,-4|1,-4,0,-5|0,7,-1,8|-1,8,0,9|0,9,1,8|1,8,0,7 29 | ';':-5,5|0,-5,-1,-4|-1,-4,0,-3|0,-3,1,-4|1,-4,0,-5|1,8,0,9|0,9,-1,8|-1,8,0,7|0,7,1,8|1,8,1,10|1,10,0,12|0,12,-1,13 30 | '<':-12,12|8,-9,-8,0|-8,0,8,9 31 | '=':-13,13|-9,-3,9,-3|-9,3,9,3 32 | '>':-12,12|-8,-9,8,0|8,0,-8,9 33 | '?':-9,9|-6,-7,-6,-8|-6,-8,-5,-10|-5,-10,-4,-11|-4,-11,-2,-12|-2,-12,2,-12|2,-12,4,-11|4,-11,5,-10|5,-10,6,-8|6,-8,6,-6|6,-6,5,-4|5,-4,4,-3|4,-3,0,-1|0,-1,0,2|0,7,-1,8|-1,8,0,9|0,9,1,8|1,8,0,7 34 | '@':-13,14|5,-4,4,-6|4,-6,2,-7|2,-7,-1,-7|-1,-7,-3,-6|-3,-6,-4,-5|-4,-5,-5,-2|-5,-2,-5,1|-5,1,-4,3|-4,3,-2,4|-2,4,1,4|1,4,3,3|3,3,4,1|-1,-7,-3,-5|-3,-5,-4,-2|-4,-2,-4,1|-4,1,-3,3|-3,3,-2,4|5,-7,4,1|4,1,4,3|4,3,6,4|6,4,8,4|8,4,10,2|10,2,11,-1|11,-1,11,-3|11,-3,10,-6|10,-6,9,-8|9,-8,7,-10|7,-10,5,-11|5,-11,2,-12|2,-12,-1,-12|-1,-12,-4,-11|-4,-11,-6,-10|-6,-10,-8,-8|-8,-8,-9,-6|-9,-6,-10,-3|-10,-3,-10,0|-10,0,-9,3|-9,3,-8,5|-8,5,-6,7|-6,7,-4,8|-4,8,-1,9|-1,9,2,9|2,9,5,8|5,8,7,7|7,7,8,6|6,-7,5,1|5,1,5,3|5,3,6,4 35 | 'A':-9,9|0,-12,-8,9|0,-12,8,9|-5,2,5,2 36 | 'B':-11,10|-7,-12,-7,9|-7,-12,2,-12|2,-12,5,-11|5,-11,6,-10|6,-10,7,-8|7,-8,7,-6|7,-6,6,-4|6,-4,5,-3|5,-3,2,-2|-7,-2,2,-2|2,-2,5,-1|5,-1,6,0|6,0,7,2|7,2,7,5|7,5,6,7|6,7,5,8|5,8,2,9|2,9,-7,9 37 | 'C':-10,11|8,-7,7,-9|7,-9,5,-11|5,-11,3,-12|3,-12,-1,-12|-1,-12,-3,-11|-3,-11,-5,-9|-5,-9,-6,-7|-6,-7,-7,-4|-7,-4,-7,1|-7,1,-6,4|-6,4,-5,6|-5,6,-3,8|-3,8,-1,9|-1,9,3,9|3,9,5,8|5,8,7,6|7,6,8,4 38 | 'D':-11,10|-7,-12,-7,9|-7,-12,0,-12|0,-12,3,-11|3,-11,5,-9|5,-9,6,-7|6,-7,7,-4|7,-4,7,1|7,1,6,4|6,4,5,6|5,6,3,8|3,8,0,9|0,9,-7,9 39 | 'E':-10,9|-6,-12,-6,9|-6,-12,7,-12|-6,-2,2,-2|-6,9,7,9 40 | 'F':-10,8|-6,-12,-6,9|-6,-12,7,-12|-6,-2,2,-2 41 | 'G':-10,11|8,-7,7,-9|7,-9,5,-11|5,-11,3,-12|3,-12,-1,-12|-1,-12,-3,-11|-3,-11,-5,-9|-5,-9,-6,-7|-6,-7,-7,-4|-7,-4,-7,1|-7,1,-6,4|-6,4,-5,6|-5,6,-3,8|-3,8,-1,9|-1,9,3,9|3,9,5,8|5,8,7,6|7,6,8,4|8,4,8,1|3,1,8,1 42 | 'H':-11,11|-7,-12,-7,9|7,-12,7,9|-7,-2,7,-2 43 | 'I':-4,4|0,-12,0,9 44 | 'J':-8,8|4,-12,4,4|4,4,3,7|3,7,2,8|2,8,0,9|0,9,-2,9|-2,9,-4,8|-4,8,-5,7|-5,7,-6,4|-6,4,-6,2 45 | 'K':-11,10|-7,-12,-7,9|7,-12,-7,2|-2,-3,7,9 46 | 'L':-10,7|-6,-12,-6,9|-6,9,6,9 47 | 'M':-12,12|-8,-12,-8,9|-8,-12,0,9|8,-12,0,9|8,-12,8,9 48 | 'N':-11,11|-7,-12,-7,9|-7,-12,7,9|7,-12,7,9 49 | 'O':-11,11|-2,-12,-4,-11|-4,-11,-6,-9|-6,-9,-7,-7|-7,-7,-8,-4|-8,-4,-8,1|-8,1,-7,4|-7,4,-6,6|-6,6,-4,8|-4,8,-2,9|-2,9,2,9|2,9,4,8|4,8,6,6|6,6,7,4|7,4,8,1|8,1,8,-4|8,-4,7,-7|7,-7,6,-9|6,-9,4,-11|4,-11,2,-12|2,-12,-2,-12 50 | 'P':-11,10|-7,-12,-7,9|-7,-12,2,-12|2,-12,5,-11|5,-11,6,-10|6,-10,7,-8|7,-8,7,-5|7,-5,6,-3|6,-3,5,-2|5,-2,2,-1|2,-1,-7,-1 51 | 'Q':-11,11|-2,-12,-4,-11|-4,-11,-6,-9|-6,-9,-7,-7|-7,-7,-8,-4|-8,-4,-8,1|-8,1,-7,4|-7,4,-6,6|-6,6,-4,8|-4,8,-2,9|-2,9,2,9|2,9,4,8|4,8,6,6|6,6,7,4|7,4,8,1|8,1,8,-4|8,-4,7,-7|7,-7,6,-9|6,-9,4,-11|4,-11,2,-12|2,-12,-2,-12|1,5,7,11 52 | 'R':-11,10|-7,-12,-7,9|-7,-12,2,-12|2,-12,5,-11|5,-11,6,-10|6,-10,7,-8|7,-8,7,-6|7,-6,6,-4|6,-4,5,-3|5,-3,2,-2|2,-2,-7,-2|0,-2,7,9 53 | 'S':-10,10|7,-9,5,-11|5,-11,2,-12|2,-12,-2,-12|-2,-12,-5,-11|-5,-11,-7,-9|-7,-9,-7,-7|-7,-7,-6,-5|-6,-5,-5,-4|-5,-4,-3,-3|-3,-3,3,-1|3,-1,5,0|5,0,6,1|6,1,7,3|7,3,7,6|7,6,5,8|5,8,2,9|2,9,-2,9|-2,9,-5,8|-5,8,-7,6 54 | 'T':-8,8|0,-12,0,9|-7,-12,7,-12 55 | 'U':-11,11|-7,-12,-7,3|-7,3,-6,6|-6,6,-4,8|-4,8,-1,9|-1,9,1,9|1,9,4,8|4,8,6,6|6,6,7,3|7,3,7,-12 56 | 'V':-9,9|-8,-12,0,9|8,-12,0,9 57 | 'W':-12,12|-10,-12,-5,9|0,-12,-5,9|0,-12,5,9|10,-12,5,9 58 | 'X':-10,10|-7,-12,7,9|7,-12,-7,9 59 | 'Y':-9,9|-8,-12,0,-2|0,-2,0,9|8,-12,0,-2 60 | 'Z':-10,10|7,-12,-7,9|-7,-12,7,-12|-7,9,7,9 61 | '[':-7,7|-3,-16,-3,16|-2,-16,-2,16|-3,-16,4,-16|-3,16,4,16 62 | '\':-7,7|-7,-12,7,12 63 | ']':-7,7|2,-16,2,16|3,-16,3,16|-4,-16,3,-16|-4,16,3,16 64 | '^':-8,8|-2,-6,0,-9|0,-9,2,-6|-5,-3,0,-8|0,-8,5,-3|0,-8,0,9 65 | '_':-8,8|-8,11,8,11 66 | '`':-5,5|1,-12,0,-11|0,-11,-1,-9|-1,-9,-1,-7|-1,-7,0,-6|0,-6,1,-7|1,-7,0,-8 67 | 'a':-9,10|6,-5,6,9|6,-2,4,-4|4,-4,2,-5|2,-5,-1,-5|-1,-5,-3,-4|-3,-4,-5,-2|-5,-2,-6,1|-6,1,-6,3|-6,3,-5,6|-5,6,-3,8|-3,8,-1,9|-1,9,2,9|2,9,4,8|4,8,6,6 68 | 'b':-10,9|-6,-12,-6,9|-6,-2,-4,-4|-4,-4,-2,-5|-2,-5,1,-5|1,-5,3,-4|3,-4,5,-2|5,-2,6,1|6,1,6,3|6,3,5,6|5,6,3,8|3,8,1,9|1,9,-2,9|-2,9,-4,8|-4,8,-6,6 69 | 'c':-9,9|6,-2,4,-4|4,-4,2,-5|2,-5,-1,-5|-1,-5,-3,-4|-3,-4,-5,-2|-5,-2,-6,1|-6,1,-6,3|-6,3,-5,6|-5,6,-3,8|-3,8,-1,9|-1,9,2,9|2,9,4,8|4,8,6,6 70 | 'd':-9,10|6,-12,6,9|6,-2,4,-4|4,-4,2,-5|2,-5,-1,-5|-1,-5,-3,-4|-3,-4,-5,-2|-5,-2,-6,1|-6,1,-6,3|-6,3,-5,6|-5,6,-3,8|-3,8,-1,9|-1,9,2,9|2,9,4,8|4,8,6,6 71 | 'e':-9,9|-6,1,6,1|6,1,6,-1|6,-1,5,-3|5,-3,4,-4|4,-4,2,-5|2,-5,-1,-5|-1,-5,-3,-4|-3,-4,-5,-2|-5,-2,-6,1|-6,1,-6,3|-6,3,-5,6|-5,6,-3,8|-3,8,-1,9|-1,9,2,9|2,9,4,8|4,8,6,6 72 | 'f':-5,7|5,-12,3,-12|3,-12,1,-11|1,-11,0,-8|0,-8,0,9|-3,-5,4,-5 73 | 'g':-9,10|6,-5,6,11|6,11,5,14|5,14,4,15|4,15,2,16|2,16,-1,16|-1,16,-3,15|6,-2,4,-4|4,-4,2,-5|2,-5,-1,-5|-1,-5,-3,-4|-3,-4,-5,-2|-5,-2,-6,1|-6,1,-6,3|-6,3,-5,6|-5,6,-3,8|-3,8,-1,9|-1,9,2,9|2,9,4,8|4,8,6,6 74 | 'h':-9,10|-5,-12,-5,9|-5,-1,-2,-4|-2,-4,0,-5|0,-5,3,-5|3,-5,5,-4|5,-4,6,-1|6,-1,6,9 75 | 'i':-4,4|-1,-12,0,-11|0,-11,1,-12|1,-12,0,-13|0,-13,-1,-12|0,-5,0,9 76 | 'j':-5,5|0,-12,1,-11|1,-11,2,-12|2,-12,1,-13|1,-13,0,-12|1,-5,1,12|1,12,0,15|0,15,-2,16|-2,16,-4,16 77 | 'k':-9,8|-5,-12,-5,9|5,-5,-5,5|-1,1,6,9 78 | 'l':-4,4|0,-12,0,9 79 | 'm':-15,15|-11,-5,-11,9|-11,-1,-8,-4|-8,-4,-6,-5|-6,-5,-3,-5|-3,-5,-1,-4|-1,-4,0,-1|0,-1,0,9|0,-1,3,-4|3,-4,5,-5|5,-5,8,-5|8,-5,10,-4|10,-4,11,-1|11,-1,11,9 80 | 'n':-9,10|-5,-5,-5,9|-5,-1,-2,-4|-2,-4,0,-5|0,-5,3,-5|3,-5,5,-4|5,-4,6,-1|6,-1,6,9 81 | 'o':-9,10|-1,-5,-3,-4|-3,-4,-5,-2|-5,-2,-6,1|-6,1,-6,3|-6,3,-5,6|-5,6,-3,8|-3,8,-1,9|-1,9,2,9|2,9,4,8|4,8,6,6|6,6,7,3|7,3,7,1|7,1,6,-2|6,-2,4,-4|4,-4,2,-5|2,-5,-1,-5 82 | 'p':-10,9|-6,-5,-6,16|-6,-2,-4,-4|-4,-4,-2,-5|-2,-5,1,-5|1,-5,3,-4|3,-4,5,-2|5,-2,6,1|6,1,6,3|6,3,5,6|5,6,3,8|3,8,1,9|1,9,-2,9|-2,9,-4,8|-4,8,-6,6 83 | 'q':-9,10|6,-5,6,16|6,-2,4,-4|4,-4,2,-5|2,-5,-1,-5|-1,-5,-3,-4|-3,-4,-5,-2|-5,-2,-6,1|-6,1,-6,3|-6,3,-5,6|-5,6,-3,8|-3,8,-1,9|-1,9,2,9|2,9,4,8|4,8,6,6 84 | 'r':-7,6|-3,-5,-3,9|-3,1,-2,-2|-2,-2,0,-4|0,-4,2,-5|2,-5,5,-5 85 | 's':-8,9|6,-2,5,-4|5,-4,2,-5|2,-5,-1,-5|-1,-5,-4,-4|-4,-4,-5,-2|-5,-2,-4,0|-4,0,-2,1|-2,1,3,2|3,2,5,3|5,3,6,5|6,5,6,6|6,6,5,8|5,8,2,9|2,9,-1,9|-1,9,-4,8|-4,8,-5,6 86 | 't':-5,7|0,-12,0,5|0,5,1,8|1,8,3,9|3,9,5,9|-3,-5,4,-5 87 | 'u':-9,10|-5,-5,-5,5|-5,5,-4,8|-4,8,-2,9|-2,9,1,9|1,9,3,8|3,8,6,5|6,-5,6,9 88 | 'v':-8,8|-6,-5,0,9|6,-5,0,9 89 | 'w':-11,11|-8,-5,-4,9|0,-5,-4,9|0,-5,4,9|8,-5,4,9 90 | 'x':-8,9|-5,-5,6,9|6,-5,-5,9 91 | 'y':-8,8|-6,-5,0,9|6,-5,0,9|0,9,-2,13|-2,13,-4,15|-4,15,-6,16|-6,16,-7,16 92 | 'z':-8,9|6,-5,-5,9|-5,-5,6,-5|-5,9,6,9 93 | '{':-7,7|2,-16,0,-15|0,-15,-1,-14|-1,-14,-2,-12|-2,-12,-2,-10|-2,-10,-1,-8|-1,-8,0,-7|0,-7,1,-5|1,-5,1,-3|1,-3,-1,-1|0,-15,-1,-13|-1,-13,-1,-11|-1,-11,0,-9|0,-9,1,-8|1,-8,2,-6|2,-6,2,-4|2,-4,1,-2|1,-2,-3,0|-3,0,1,2|1,2,2,4|2,4,2,6|2,6,1,8|1,8,0,9|0,9,-1,11|-1,11,-1,13|-1,13,0,15|-1,1,1,3|1,3,1,5|1,5,0,7|0,7,-1,8|-1,8,-2,10|-2,10,-2,12|-2,12,-1,14|-1,14,0,15|0,15,2,16 94 | '|':-4,4|0,-16,0,16 95 | '}':-7,7|-2,-16,0,-15|0,-15,1,-14|1,-14,2,-12|2,-12,2,-10|2,-10,1,-8|1,-8,0,-7|0,-7,-1,-5|-1,-5,-1,-3|-1,-3,1,-1|0,-15,1,-13|1,-13,1,-11|1,-11,0,-9|0,-9,-1,-8|-1,-8,-2,-6|-2,-6,-2,-4|-2,-4,-1,-2|-1,-2,3,0|3,0,-1,2|-1,2,-2,4|-2,4,-2,6|-2,6,-1,8|-1,8,0,9|0,9,1,11|1,11,1,13|1,13,0,15|1,1,-1,3|-1,3,-1,5|-1,5,0,7|0,7,1,8|1,8,2,10|2,10,2,12|2,12,1,14|1,14,0,15|0,15,-2,16 96 | '~':-12,12|-9,3,-9,1|-9,1,-8,-2|-8,-2,-6,-3|-6,-3,-4,-3|-4,-3,-2,-2|-2,-2,2,1|2,1,4,2|4,2,6,2|6,2,8,1|8,1,9,-1|-9,1,-8,-1|-8,-1,-6,-2|-6,-2,-4,-2|-4,-2,-2,-1|-2,-1,2,2|2,2,4,3|4,3,6,3|6,3,8,2|8,2,9,-1|9,-1,9,-3 97 | '':-7,7|-1,-12,-3,-11|-3,-11,-4,-9|-4,-9,-4,-7|-4,-7,-3,-5|-3,-5,-1,-4|-1,-4,1,-4|1,-4,3,-5|3,-5,4,-7|4,-7,4,-9|4,-9,3,-11|3,-11,1,-12|1,-12,-1,-12 98 | 99 | -------------------------------------------------------------------------------- /src/ZingLaser.java: -------------------------------------------------------------------------------- 1 | import com.t_oster.liblasercut.*; 2 | import com.t_oster.liblasercut.drivers.EpilogZing; 3 | import com.t_oster.liblasercut.utils.BufferedImageAdapter; 4 | 5 | import javax.swing.*; 6 | import javax.swing.text.DefaultCaret; 7 | import java.awt.*; 8 | import java.awt.event.ItemEvent; 9 | import java.awt.geom.AffineTransform; 10 | import java.awt.geom.Line2D; 11 | import java.awt.geom.Point2D; 12 | import java.awt.geom.Rectangle2D; 13 | import java.awt.image.BufferedImage; 14 | import java.util.*; 15 | import java.util.List; 16 | import java.util.prefs.Preferences; 17 | 18 | class ZingLaser implements LaserCut.OutputDevice { 19 | private static final double ZING_PPI = 500; 20 | private static final int ZING_SPEED_DEFAUlT = 55; 21 | private static final int ZING_FREQ_DEFAUlT = 500; 22 | private static final int ZING_CUT_POWER_DEFAUlT = 85; 23 | private static final int ZING_ENGRAVE_POWER_DEFAUlT = 5; 24 | private static final int ZING_RASTER_POWER_DEFAUlT = 50; 25 | private static final Rectangle2D.Double zingFullSize = new Rectangle2D.Double(0, 0, 16, 12); 26 | private static final Rectangle2D.Double zing12x12Size = new Rectangle2D.Double(0, 0, 12, 12); 27 | private final LaserCut laserCut; 28 | private final Preferences prefs; 29 | 30 | ZingLaser (LaserCut laserCut, Preferences prefs) { 31 | this.laserCut = laserCut; 32 | this.prefs = prefs; 33 | } 34 | 35 | // Implemented for LaserCut.OutputDevice 36 | public String getName () { 37 | return "Epilog Zing"; 38 | } 39 | 40 | // Implemented for LaserCut.OutputDevice 41 | public Rectangle2D.Double getWorkspaceSize () { 42 | return zingFullSize; 43 | } 44 | 45 | // Implemented for LaserCut.OutputDevice 46 | public void closeDevice () { 47 | // Nothing to do 48 | } 49 | 50 | // Implemented for LaserCut.OutputDevice 51 | public double getZoomFactor () { 52 | return 1.0; 53 | } 54 | 55 | public JMenu getDeviceMenu () { 56 | JMenu zingMenu = new JMenu(getName()); 57 | // Add "Send to Zing" Submenu Item 58 | JMenuItem sendToZing = new JMenuItem("Send Job to " + getName()); 59 | sendToZing.addActionListener(ev -> { 60 | String zingIpAddress = prefs.get("zing.ip", "10.0.1.201"); 61 | if (zingIpAddress == null || zingIpAddress.length() == 0) { 62 | laserCut.showErrorDialog("Please set the " + getName() + "'s IP Address in " + getName() + "->Zing Settings"); 63 | return; 64 | } 65 | if (laserCut.showWarningDialog("Press OK to Send Job to " + getName())) { 66 | EpilogZing lasercutter = new EpilogZing(zingIpAddress); 67 | // Set Properties for Materials, such as for 3 mm birch plywood, Set: 60% speed, 80% power, 0 focus, 500 Hz. 68 | PowerSpeedFocusFrequencyProperty cutProperties = new PowerSpeedFocusFrequencyProperty(); 69 | cutProperties.setProperty("speed", prefs.getInt("zing.speed", ZING_SPEED_DEFAUlT)); 70 | cutProperties.setProperty("power", prefs.getInt("zing.power", ZING_CUT_POWER_DEFAUlT)); 71 | cutProperties.setProperty("frequency", prefs.getInt("zing.freq", ZING_FREQ_DEFAUlT)); 72 | cutProperties.setProperty("focus", 0.0f); 73 | PowerSpeedFocusFrequencyProperty engraveProperties = new PowerSpeedFocusFrequencyProperty(); 74 | engraveProperties.setProperty("speed", prefs.getInt("zing.espeed", ZING_SPEED_DEFAUlT)); 75 | engraveProperties.setProperty("power", prefs.getInt("zing.epower", ZING_ENGRAVE_POWER_DEFAUlT)); 76 | engraveProperties.setProperty("frequency", prefs.getInt("zing.efreq", ZING_FREQ_DEFAUlT)); 77 | engraveProperties.setProperty("focus", 0.0f); 78 | PowerSpeedFocusFrequencyProperty rasterProperties = new PowerSpeedFocusFrequencyProperty(); 79 | rasterProperties.setProperty("speed", prefs.getInt("zing.rspeed", ZING_SPEED_DEFAUlT)); 80 | rasterProperties.setProperty("power", prefs.getInt("zing.rpower", ZING_RASTER_POWER_DEFAUlT)); 81 | rasterProperties.setProperty("frequency", ZING_FREQ_DEFAUlT); 82 | rasterProperties.setProperty("focus", 0.0f); 83 | boolean planPath = prefs.getBoolean("zing.pathplan", true); 84 | LaserJob job = new LaserJob("laserCut", "laserCut", "laserCut"); // title, name, user 85 | // Process raster engrave passes, if any 86 | for (CADShape shape : laserCut.surface.getDesign()) { 87 | if (shape instanceof CADRasterImage && shape.engrave) { 88 | CADRasterImage raster = (CADRasterImage) shape; 89 | double[] scale = raster.getScale(ZING_PPI); 90 | Rectangle2D bb = raster.getScaledRotatedBounds(scale); 91 | AffineTransform at = raster.getScaledRotatedTransform(bb, scale); 92 | BufferedImage scaledImg = raster.getScaledRotatedImage(bb, scale); 93 | Point2D.Double offset = raster.getScaledRotatedOrigin(at, bb); 94 | int xLoc = (int) Math.round(shape.xLoc * ZING_PPI - offset.x); 95 | int yLoc = (int) Math.round(shape.yLoc * ZING_PPI - offset.y); 96 | com.t_oster.liblasercut.platform.Point loc = new com.t_oster.liblasercut.platform.Point(xLoc, yLoc); 97 | if (raster.engrave3D) { 98 | Raster3dPart rp = new Raster3dPart(new BufferedImageAdapter(scaledImg), 99 | rasterProperties, new com.t_oster.liblasercut.platform.Point(xLoc, yLoc), ZING_PPI); 100 | job.addPart(rp); 101 | } else { 102 | RasterPart rp = new RasterPart(new BlackWhiteRaster(new BufferedImageAdapter(scaledImg), 103 | BlackWhiteRaster.DitherAlgorithm.AVERAGE), new PowerSpeedFocusProperty(), loc, ZING_PPI); 104 | job.addPart(rp); 105 | } 106 | } 107 | } 108 | // Process cut and vector engrave passes 109 | for (int ii = 0; ii < 2; ii++) { 110 | boolean doCut = ii == 1; 111 | // Transform all the shapesInGroup into a series of line segments 112 | int lastX = 0, lastY = 0; 113 | VectorPart vp = new VectorPart(doCut ? cutProperties : engraveProperties, ZING_PPI); 114 | // Loop detects pen up/pen down based on start and end points of line segments 115 | boolean hasVector = false; 116 | List shapes = laserCut.surface.selectLaserItems(doCut, planPath); 117 | for (CADShape shape : shapes) { 118 | for (Line2D.Double[] lines : shape.getListOfScaledLines(ZING_PPI, .001)) { 119 | if (lines.length > 0) { 120 | hasVector = true; 121 | boolean first = true; 122 | for (Line2D.Double line : lines) { 123 | Point p1 = new Point((int) Math.round(line.x1), (int) Math.round(line.y1)); 124 | Point p2 = new Point((int) Math.round(line.x2), (int) Math.round(line.y2)); 125 | if (first) { 126 | vp.moveto(p1.x, p1.y); 127 | vp.lineto(lastX = p2.x, lastY = p2.y); 128 | } else { 129 | if (lastX != p1.x || lastY != p1.y) { 130 | vp.moveto(p1.x, p1.y); 131 | } 132 | vp.lineto(lastX = p2.x, lastY = p2.y); 133 | } 134 | first = false; 135 | } 136 | } 137 | } 138 | if (hasVector) { 139 | job.addPart(vp); 140 | } 141 | } 142 | } 143 | new ZingSender(laserCut, lasercutter, job); 144 | } 145 | }); 146 | zingMenu.add(sendToZing); 147 | 148 | // Build JComboBox List of Materials for "Zing Settings" parameters dialog 149 | String[] materials = Utils2D.getResourceFile("/materials/zing.materials").split("==="); 150 | JComboBox matMenu = new JComboBox<>(); 151 | matMenu.addItem(""); 152 | for (String material : materials) { 153 | Properties props = Utils2D.getProperties(material); 154 | if (props.containsKey("name")) { 155 | String name = props.getProperty("name"); 156 | matMenu.addItem(name); 157 | } 158 | } 159 | matMenu.setSelectedIndex(0); 160 | JMenuItem zingSettings = new JMenuItem(getName() + " Settings"); 161 | zingSettings.addActionListener(ev -> { 162 | ParameterDialog.ParmItem[] parmSet = { 163 | new ParameterDialog.ParmItem(getName() + " IP Add", prefs.get("zing.ip", "10.0.1.201")), 164 | new ParameterDialog.ParmItem(new JSeparator()), 165 | new ParameterDialog.ParmItem(matMenu), 166 | new ParameterDialog.ParmItem(new JSeparator()), 167 | new ParameterDialog.ParmItem("Cut Power|%(0-100)", prefs.getInt("zing.power", ZING_CUT_POWER_DEFAUlT)), 168 | new ParameterDialog.ParmItem("Cut Speed", prefs.getInt("zing.speed", ZING_SPEED_DEFAUlT)), 169 | new ParameterDialog.ParmItem("Cut Freq|Hz", prefs.getInt("zing.freq", ZING_FREQ_DEFAUlT)), 170 | new ParameterDialog.ParmItem(new JSeparator()), 171 | new ParameterDialog.ParmItem("Engrave Power|%(0-100)", prefs.getInt("zing.epower", ZING_ENGRAVE_POWER_DEFAUlT)), 172 | new ParameterDialog.ParmItem("Engrave Speed", prefs.getInt("zing.espeed", ZING_SPEED_DEFAUlT)), 173 | new ParameterDialog.ParmItem("Engrave Freq|Hz", prefs.getInt("zing.efreq", ZING_FREQ_DEFAUlT)), 174 | new ParameterDialog.ParmItem(new JSeparator()), 175 | new ParameterDialog.ParmItem("Raster Power|%(0-100)", prefs.getInt("zing.rpower", ZING_RASTER_POWER_DEFAUlT)), 176 | new ParameterDialog.ParmItem("Raster Speed", prefs.getInt("zing.rspeed", ZING_SPEED_DEFAUlT)), 177 | new ParameterDialog.ParmItem(new JSeparator()), 178 | new ParameterDialog.ParmItem("Use Path Planner", prefs.getBoolean("zing.pathplan", true)), 179 | 180 | }; 181 | matMenu.addItemListener(ev2 -> { 182 | if (ev2.getStateChange() == ItemEvent.SELECTED) { 183 | String name = (String) matMenu.getSelectedItem(); 184 | for (int ii = 0; ii <= materials.length; ii++) { 185 | boolean none = ii == materials.length; 186 | Properties props = none ? new Properties() : Utils2D.getProperties( materials[ii]); 187 | if ((name != null && name.equals(props.getProperty("name"))) || none) { 188 | parmSet[4].setField(props.getProperty("power")); 189 | parmSet[5].setField(props.getProperty("speed")); 190 | parmSet[6].setField(props.getProperty("freq")); 191 | parmSet[8].setField(props.getProperty("epower")); 192 | parmSet[9].setField(props.getProperty("espeed")); 193 | parmSet[10].setField(props.getProperty("efreq")); 194 | parmSet[12].setField(props.getProperty("rpower")); 195 | parmSet[13].setField(props.getProperty("rspeed")); 196 | break; 197 | } 198 | } 199 | } 200 | }); 201 | if (ParameterDialog.showSaveCancelParameterDialog(parmSet, prefs.get("displayUnits", "in"), laserCut)) { 202 | prefs.put("zing.ip", (String) parmSet[0].value); 203 | prefs.putInt("zing.power", (Integer) parmSet[4].value); 204 | prefs.putInt("zing.speed", (Integer) parmSet[5].value); 205 | prefs.putInt("zing.freq", (Integer) parmSet[6].value); 206 | prefs.putInt("zing.epower", (Integer) parmSet[8].value); 207 | prefs.putInt("zing.espeed", (Integer) parmSet[9].value); 208 | prefs.putInt("zing.efreq", (Integer) parmSet[10].value); 209 | prefs.putInt("zing.rpower", (Integer) parmSet[12].value); 210 | prefs.putInt("zing.rspeed", (Integer) parmSet[13].value); 211 | prefs.putBoolean("zing.pathplan", (Boolean) parmSet[15].value); 212 | } 213 | }); 214 | zingMenu.add(zingSettings); 215 | // Add "Resize for Zing" Full Size Submenu Items 216 | JMenuItem zingResize = new JMenuItem("Resize for " + getName() + " (" + zingFullSize.width + " x " + zingFullSize.height + ")"); 217 | zingResize.addActionListener(ev -> { 218 | laserCut.surface.setZoomFactor(1); 219 | laserCut.surface.setSurfaceSize(zingFullSize); 220 | }); 221 | zingMenu.add(zingResize); 222 | JMenuItem zing12x12 = new JMenuItem("Resize for " + getName() + " (" + zing12x12Size.width + " x " + zing12x12Size.height + ")"); 223 | zing12x12.addActionListener(ev -> laserCut.surface.setSurfaceSize(zing12x12Size)); 224 | zingMenu.add(zing12x12); 225 | return zingMenu; 226 | } 227 | 228 | class ZingSender extends JDialog implements Runnable { 229 | final EpilogZing lasercutter; 230 | final LaserJob job; 231 | private final JProgressBar progress; 232 | private final JTextArea status; 233 | 234 | ZingSender (LaserCut laserCut, EpilogZing lasercutter, LaserJob job) { 235 | super(laserCut); 236 | setTitle(ZingLaser.this.getName() + " Monitor"); 237 | this.lasercutter = lasercutter; 238 | this.job = job; 239 | add(progress = new JProgressBar(), BorderLayout.NORTH); 240 | progress.setMaximum(100); 241 | JScrollPane sPane = new JScrollPane(status = new JTextArea()); 242 | status.setMargin(new Insets(3, 3, 3, 3)); 243 | DefaultCaret caret = (DefaultCaret) status.getCaret(); 244 | caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); 245 | add(sPane, BorderLayout.CENTER); 246 | Rectangle loc = laserCut.getBounds(); 247 | setSize(300, 150); 248 | setLocation(loc.x + loc.width / 2 - 150, loc.y + loc.height / 2 - 75); 249 | setVisible(true); 250 | new Thread(this).start(); 251 | status.append("Starting Job...\n"); 252 | paint(getGraphics()); // Kludge to get JTextArea to update 253 | } 254 | 255 | public void run () { 256 | List warnings = new LinkedList<>(); 257 | boolean hadError = false; 258 | String errMsg = "Unknown error"; 259 | try { 260 | lasercutter.sendJob(job, new ProgressListener() { 261 | @Override 262 | public void progressChanged (Object obj, int ii) { 263 | progress.setValue(ii); 264 | status.append("Completed " + ii + "%\n"); 265 | } 266 | 267 | @Override 268 | public void taskChanged (Object obj, String str) { 269 | // setProgress(i); 270 | } 271 | }, warnings); 272 | } catch (Exception ex) { 273 | hadError = true; 274 | errMsg = ex.getMessage(); 275 | } finally { 276 | setVisible(false); 277 | dispose(); 278 | if (hadError) { 279 | laserCut.showErrorDialog("Unable to send job to " + ZingLaser.this.getName() + ".\n" + errMsg); 280 | } else if (warnings.size() > 0) { 281 | StringBuilder buf = new StringBuilder(); 282 | boolean addLf = false; 283 | for (String warning : warnings) { 284 | if (addLf) { 285 | buf.append('\n'); 286 | } 287 | addLf = true; 288 | buf.append(warning); 289 | } 290 | laserCut.showInfoDialog(buf.toString()); 291 | } 292 | } 293 | } 294 | } 295 | } 296 | --------------------------------------------------------------------------------