/** * This file is part of VisiCut. * Copyright (C) 2011 - 2013 Thomas Oster <thomas.oster@rwth-aachen.de> * RWTH Aachen University - 52062 Aachen, Germany * * VisiCut is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * VisiCut is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with VisiCut. If not, see <http://www.gnu.org/licenses/>. **/ package com.t_oster.visicut.gui.beans; import com.t_oster.uicomponents.ZoomablePanel; import com.t_oster.liblasercut.LaserCutter; import com.t_oster.liblasercut.ProgressListener; import com.t_oster.liblasercut.platform.Util; import com.t_oster.visicut.VisicutModel; import com.t_oster.visicut.misc.Helper; import com.t_oster.visicut.model.LaserDevice; import com.t_oster.visicut.model.LaserProfile; import com.t_oster.visicut.model.MaterialProfile; import com.t_oster.visicut.model.PlfPart; import com.t_oster.visicut.model.Raster3dProfile; import com.t_oster.visicut.model.RasterProfile; import com.t_oster.visicut.model.VectorProfile; import com.t_oster.visicut.model.graphicelements.GraphicObject; import com.t_oster.visicut.model.graphicelements.GraphicSet; import com.t_oster.visicut.model.mapping.FilterSet; import com.t_oster.visicut.model.mapping.Mapping; import com.t_oster.visicut.model.mapping.MappingSet; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Composite; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JOptionPane; /** * This class implements the Panel which provides the Preview * of the current LaserJob * * @author Thomas Oster <thomas.oster@rwth-aachen.de> */ public class PreviewPanel extends ZoomablePanel implements PropertyChangeListener { private double bedWidth = 600; private double bedHeight = 300; public PreviewPanel() { VisicutModel.getInstance().addPropertyChangeListener(this); updateBedSize(VisicutModel.getInstance().getSelectedLaserDevice()); } private void updateBedSize(LaserDevice d) { if (d != null) { LaserCutter lc = d.getLaserCutter(); bedWidth = lc.getBedWidth(); bedHeight = lc.getBedHeight(); setAreaSize(new Point2D.Double(lc.getBedWidth(), lc.getBedHeight())); repaint(); } } public void propertyChange(PropertyChangeEvent pce) { if (VisicutModel.PROP_SELECTEDLASERDEVICE.equals(pce.getPropertyName())) { updateBedSize((LaserDevice) pce.getNewValue()); repaint(); } else if (VisicutModel.PROP_SELECTEDPART.equals(pce.getPropertyName())) { updateEditRectangle(); } else if (VisicutModel.PROP_PLF_PART_UPDATED.equals(pce.getPropertyName())) { if (updatesToIgnore > 0) { updatesToIgnore--; } else { PlfPart p = (PlfPart) pce.getNewValue(); this.clearCache(p); if (p.equals(VisicutModel.getInstance().getSelectedPart())) { updateEditRectangle(); } } } else if (VisicutModel.PROP_PLF_PART_REMOVED.equals(pce.getPropertyName())) { PlfPart p = (PlfPart) pce.getOldValue(); this.clearCache(p); } else if (VisicutModel.PROP_MATERIAL.equals(pce.getPropertyName()) || VisicutModel.PROP_PLF_FILE_CHANGED.equals(pce.getPropertyName())) { updateEditRectangle(); this.clearCache(); repaint(); } else if (VisicutModel.PROP_BACKGROUNDIMAGE.equals(pce.getPropertyName()) || VisicutModel.PROP_STARTPOINT.equals(pce.getPropertyName()) || VisicutModel.PROP_PLF_PART_ADDED.equals(pce.getPropertyName())) { repaint(); } } private static final Logger logger = Logger.getLogger(PreviewPanel.class.getName()); private int updatesToIgnore = 0; public void ignoreNextUpdate() { updatesToIgnore++; } private class ImageProcessingThread extends Thread implements ProgressListener { private Logger logger = Logger.getLogger(ImageProcessingThread.class.getName()); private BufferedImage buffer = null; private GraphicSet set; private AffineTransform mm2laserPx; private LaserProfile p; private Rectangle bb; private Rectangle2D bbInMm; private int progress = 0; private boolean isFinished = false; /** * Returns the bounding box of this preview image IN pixels * in LASERCUTTER-space * @return */ public Rectangle getBoundingBox() { return bb; } public Rectangle2D getBoundingBoxInMm() { return bbInMm; } public BufferedImage getImage() { return buffer; } public ImageProcessingThread(GraphicSet objects, LaserProfile p) { this.set = objects; this.p = p; double factor = Util.dpi2dpmm(p.getDPI()); this.mm2laserPx = AffineTransform.getScaleInstance(factor, factor); bbInMm = set.getBoundingBox(); bb = Helper.toRect(Helper.transform(bbInMm, mm2laserPx)); if (bb == null || bb.width == 0 || bb.height == 0) { logger.log(Level.SEVERE, "invalid BoundingBox"); throw new IllegalArgumentException("Boundingbox zero"); } } public synchronized boolean isFinished() { return isFinished; } private synchronized void setFinished(boolean finished) { this.isFinished = finished; } private void render() { if (p instanceof RasterProfile) { RasterProfile rp = (RasterProfile) p; buffer = rp.getRenderedPreview(set, VisicutModel.getInstance().getMaterial(), mm2laserPx, this); } else if (p instanceof Raster3dProfile) { Raster3dProfile rp = (Raster3dProfile) p; buffer = rp.getRenderedPreview(set, VisicutModel.getInstance().getMaterial(), mm2laserPx, this); } } @Override public void run() { try { logger.log(Level.FINE, "Rendering started"); long start = System.currentTimeMillis(); render(); long stop = System.currentTimeMillis(); logger.log(Level.FINE, "Rendering finished. Took: {0} ms", (stop-start)); } catch (OutOfMemoryError e) { logger.log(Level.FINE, "Out of memory during rendering. Staring garbage collection"); System.gc(); try { logger.log(Level.FINE, "Re started Rendering"); long start = System.currentTimeMillis(); render(); long stop = System.currentTimeMillis(); logger.log(Level.FINE, "2nd Rendering took {0} ms", (stop-start)); } catch (OutOfMemoryError ee) { JOptionPane.showMessageDialog(PreviewPanel.this, java.util.ResourceBundle.getBundle("com/t_oster/visicut/gui/beans/resources/PreviewPanel").getString("ERROR: NOT ENOUGH MEMORY PLEASE START THE PROGRAM FROM THE PROVIDED SHELL SCRIPTS INSTEAD OF RUNNING THE .JAR FILE"), java.util.ResourceBundle.getBundle("com/t_oster/visicut/gui/beans/resources/PreviewPanel").getString("ERROR: OUT OF MEMORY"), JOptionPane.ERROR_MESSAGE); } } this.setFinished(true); PreviewPanel.this.repaint(); logger.log(Level.FINE, "Thread finished"); } private void cancel() { logger.log(Level.FINE, "Canceling Thread"); this.stop(); this.buffer = null; this.set = null; logger.log(Level.FINE, "Thread canceled"); } public int getProgress() { return this.progress; } public void progressChanged(Object source, int percent) { this.progress = percent; PreviewPanel.this.repaint(); } public void taskChanged(Object source, String taskName) { } } private boolean fastPreview = false; /** * Get the value of fastPreview * * @return the value of fastPreview */ public boolean isFastPreview() { return fastPreview; } /** * Set the value of fastPreview * if true, profiles won't be rendered as they look on * the laser-cutter, but just as they look in the image * * @param fastPreview new value of fastPreview */ public void setFastPreview(boolean fastPreview) { this.fastPreview = fastPreview; } protected boolean showGrid = false; /** * Get the value of showGrid * * @return the value of showGrid */ public boolean isShowGrid() { return showGrid; } /** * Set the value of showGrid * * @param showGrid new value of showGrid */ public void setShowGrid(boolean showGrid) { boolean oldShowGrid = this.showGrid; this.showGrid = showGrid; this.firePropertyChange("showGrid", oldShowGrid, showGrid); this.repaint(); } public void clearCache(PlfPart p) { synchronized(this.renderBuffers) { if (this.renderBuffers.get(p) != null) { for (ImageProcessingThread thr : this.renderBuffers.get(p).values()) { if (!thr.isFinished()) { thr.cancel(); } } this.renderBuffers.get(p).clear(); } } } public void clearCache() { synchronized(this.renderBuffers) { for(PlfPart p : this.renderBuffers.keySet()) { clearCache(p); } this.renderBuffers.clear(); } } public static final String PROP_SHOW_BACKGROUNDIMAGE = "showBackgroundImage"; protected boolean showBackgroundImage = true; /** * Get the value of showBackgroundImage * * @return the value of showBackgroundImage */ public boolean isShowBackgroundImage() { return showBackgroundImage; } /** * Set the value of showBackgroundImage * * @param showBackgroundImage new value of showBackgroundImage */ public void setShowBackgroundImage(boolean showBackgroundImage) { boolean oldValue = this.showBackgroundImage; this.showBackgroundImage = showBackgroundImage; this.firePropertyChange(PROP_SHOW_BACKGROUNDIMAGE, oldValue, this.showBackgroundImage); this.repaint(); } private boolean highlightSelection = false; public boolean isHighlightSelection() { return highlightSelection; } public void setHighlightSelection(boolean highlightSelection) { this.highlightSelection = highlightSelection; } protected EditRectangle editRectangle = null; /** * Get the value of editRectangle * * @return the value of editRectangle */ public EditRectangle getEditRectangle() { return editRectangle; } /** * set editRectangle to the bounding-box of the selected part (or null if nothing is selected) * side-effects: highlights the selection (thick border with resize controls) and calls repaint */ public void updateEditRectangle() { // TODO: instead of calling this from many different places, always update the edit-rectangle on repaint // TODO: for this, the side effects of setEditRectangle need to be removed and, where necessary, explicit calls to them be added PlfPart selectedPart = VisicutModel.getInstance().getSelectedPart(); if (selectedPart == null) { setEditRectangle(null); } else { setEditRectangle(new EditRectangle(selectedPart.getBoundingBox())); } } /** * Set the value of editRectangle * The EditRectangele is drawn if * The Values of the Rectangle are exspected to be * in LaserCutter Coordinate Space. * * @param editRectangle new value of editRectangle * @see updateEditRectangle */ public void setEditRectangle(EditRectangle editRectangle) { this.editRectangle = editRectangle; this.setHighlightSelection(true); this.repaint(); } /** * The renderBuffer contains a BufferedImage for each Mapping of each PlfPart. * On refreshRenderBuffer, the Images are created by rendering * the GraphicElements matching the mapping onto an Image, * with the size of the BoundingBox. * When drawn the offset of the BoundingBox has to be used as Upper Left * Corner */ private final HashMap<PlfPart,HashMap<Mapping, ImageProcessingThread>> renderBuffers = new LinkedHashMap<PlfPart,HashMap<Mapping, ImageProcessingThread>>(); private boolean renderOriginalImage(Graphics2D gg, Mapping m, PlfPart p, boolean transparent) { boolean somethingMatched = false; AffineTransform bak = gg.getTransform(); AffineTransform tr = gg.getTransform(); tr.concatenate(this.getMmToPxTransform()); if (p.getGraphicObjects().getTransform() != null) { tr.concatenate(p.getGraphicObjects().getTransform()); } Composite bc = gg.getComposite(); if (transparent) { gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f)); } gg.setTransform(tr); if (m == null || m.getFilterSet() == null)//matches everything else { for (GraphicObject o : p.getUnmatchedObjects()) { somethingMatched = true; o.render(gg); } } else { for (GraphicObject o : m.getFilterSet().getMatchingObjects(p.getGraphicObjects())) { somethingMatched = true; o.render(gg); } } gg.setTransform(bak); if (transparent) { gg.setComposite(bc); } return somethingMatched; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (g instanceof Graphics2D) { Graphics2D gg = (Graphics2D) g; Point2D dim = this.getMmToPxTransform().transform(this.getAreaSize(), null); Rectangle r = this.getVisibleRect(); r=r.intersection(new Rectangle(0, 0, (int) dim.getX(), (int) dim.getY())); gg.setClip(r.x, r.y, r.width, r.height); gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); gg.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); RenderedImage backgroundImage = VisicutModel.getInstance().getBackgroundImage(); if (backgroundImage != null && showBackgroundImage && VisicutModel.getInstance().getSelectedLaserDevice() != null) { AffineTransform img2px = new AffineTransform(this.getMmToPxTransform()); if (VisicutModel.getInstance().getSelectedLaserDevice().getCameraCalibration() != null) { img2px.concatenate(VisicutModel.getInstance().getSelectedLaserDevice().getCameraCalibration()); } gg.drawRenderedImage(backgroundImage, img2px); } Rectangle box = Helper.toRect(Helper.transform( new Rectangle2D.Double(0, 0, this.bedWidth, this.bedHeight), this.getMmToPxTransform() )); if (backgroundImage != null && showBackgroundImage) { gg.setColor(Color.BLACK); gg.drawRect(box.x, box.y, box.width, box.height); } else { MaterialProfile m = VisicutModel.getInstance().getMaterial(); gg.setColor(m != null && m.getColor() != null ? m.getColor() : Color.WHITE); gg.fillRect(box.x, box.y, box.width, box.height); } if (showGrid) { gg.setColor(Color.DARK_GRAY); drawGrid(gg); } for (PlfPart part : VisicutModel.getInstance().getPlfFile()) { boolean selected = (part.equals(VisicutModel.getInstance().getSelectedPart())); HashMap<Mapping,ImageProcessingThread> renderBuffer = renderBuffers.get(part); if (renderBuffer == null) { renderBuffer = new LinkedHashMap<Mapping,ImageProcessingThread>(); renderBuffers.put(part,renderBuffer); } if (part.getGraphicObjects() != null) { MappingSet mappingsToDraw = part.getMapping(); if (VisicutModel.getInstance().getMaterial() == null || mappingsToDraw == null || mappingsToDraw.isEmpty()) {//Just draw the original Image this.renderOriginalImage(gg, null, part, this.fastPreview && selected); } else { boolean somethingMatched = false; for (Mapping m : mappingsToDraw) {//Render Original Image if (m.getProfile() != null && (this.fastPreview && selected)) { somethingMatched = this.renderOriginalImage(gg, m, part, this.fastPreview); } else if (m.getProfile() != null) {//Render only parts the material supports, or where Profile = null LaserProfile p = m.getProfile(); GraphicSet current = m.getFilterSet() != null ? m.getFilterSet().getMatchingObjects(part.getGraphicObjects()) : part.getUnmatchedObjects(); Rectangle2D bbInMm = current.getBoundingBox(); Rectangle bbInPx = Helper.toRect(Helper.transform(bbInMm, this.getMmToPxTransform())); if (bbInPx != null && bbInPx.getWidth() > 0 && bbInPx.getHeight() > 0) { somethingMatched = true; if (!(p instanceof VectorProfile)) { synchronized (renderBuffer) { ImageProcessingThread procThread = renderBuffer.get(m); if (procThread == null || !procThread.isFinished() || Math.abs(bbInMm.getWidth()-procThread.getBoundingBoxInMm().getWidth()) > 0.01 || Math.abs(bbInMm.getHeight()-procThread.getBoundingBoxInMm().getHeight()) > 0.01) {//Image not rendered or Size differs if (!renderBuffer.containsKey(m)) {//image not yet scheduled for rendering logger.log(Level.FINE, "Starting ImageProcessing Thread for {0}", m); procThread = new ImageProcessingThread(current, p); renderBuffer.put(m, procThread); procThread.start();//start processing thread } else if (bbInMm.getWidth() != procThread.getBoundingBoxInMm().getWidth() || bbInMm.getHeight() != procThread.getBoundingBoxInMm().getHeight()) {//Image is (being) rendered with wrong size logger.log(Level.FINE, "Image has wrong size"); if (!procThread.isFinished()) {//stop the old thread if still running procThread.cancel(); } logger.log(Level.FINE, "Starting ImageProcessingThread for{0}", m); procThread = new ImageProcessingThread(current, p); renderBuffer.put(m, procThread); procThread.start();//start processing thread } this.renderOriginalImage(gg, m, part, false); Composite o = gg.getComposite(); gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f)); Point po = new Point(bbInPx.x + bbInPx.width / 2, bbInPx.y + bbInPx.height / 2); String txt = java.util.ResourceBundle.getBundle("com/t_oster/visicut/gui/beans/resources/PreviewPanel").getString("PLEASE WAIT (")+procThread.getProgress()+java.util.ResourceBundle.getBundle("com/t_oster/visicut/gui/beans/resources/PreviewPanel").getString("%)"); int w = gg.getFontMetrics().stringWidth(txt); int h = gg.getFontMetrics().getHeight(); gg.setColor(Color.GRAY); gg.fillRoundRect(po.x -w /2 -5, po.y-h, w+10, (int) (1.5d*h), 5, 5); gg.setComposite(o); gg.setColor(Color.BLACK); gg.drawString(txt, po.x - w / 2, po.y); } else { AffineTransform laserPxToPreviewPx = Helper.getTransform(procThread.getBoundingBox(), bbInPx); laserPxToPreviewPx.translate(procThread.getBoundingBox().x, procThread.getBoundingBox().y); gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); gg.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); gg.drawRenderedImage(procThread.getImage(), laserPxToPreviewPx); } } } else if (p instanceof VectorProfile) { p.renderPreview(gg, current, VisicutModel.getInstance().getMaterial(), this.getMmToPxTransform()); } } else {//Mapping is Empty or BoundingBox zero synchronized (renderBuffer) { renderBuffer.remove(m); } } } } if (!somethingMatched) {//Nothing drawn because of no Matching mapping gg.drawString(java.util.ResourceBundle.getBundle("com/t_oster/visicut/gui/beans/resources/PreviewPanel").getString("NO MATCHING PARTS FOR THE CURRENT MAPPING FOUND."), 10, this.getHeight() / 2); } } } } Point2D.Double sp = VisicutModel.getInstance().getStartPoint(); if (sp != null && (sp.x != 0 || sp.y != 0)) { drawStartPoint(sp, gg); } if (this.editRectangle != null) { this.editRectangle.render(gg, this.getMmToPxTransform(), this.highlightSelection); } } } private void drawStartPoint(Point2D.Double p, Graphics2D gg) { gg.setColor(Color.RED); Point2D sp = this.getMmToPxTransform().transform(p, null); int x = (int) sp.getX(); int y = (int) sp.getY(); int s = 15; gg.drawOval(x-s/2, y-s/2, s, s); gg.drawLine(x-s/2, y, x+s/2, y); gg.drawLine(x, y-s/2, x, y+s/2); } private void drawGrid(Graphics2D gg) { /** * The minimal distance of 2 grid lines in Pixel */ int minPixelDst = 40; /** * The minimal distance of 2 grid lines in mm */ double minDrawDst = minPixelDst / this.getMmToPxTransform().getScaleX(); /** * The grid distance in mm */ double gridDst = 0.1; while (gridDst < minDrawDst) { gridDst *= 2; if (gridDst < minDrawDst) { gridDst *= 5; } } for (double x = 0; x < this.bedWidth; x += gridDst) { Point a = Helper.toPoint(this.getMmToPxTransform().transform(new Point2D.Double(x, 0), null)); Point b = Helper.toPoint(this.getMmToPxTransform().transform(new Point2D.Double(x, this.bedHeight), null)); if (a.getX() > 0)//only draw if in viewing range { if (a.getX() > this.getWidth()) { break; } gg.drawLine(a.x, a.y, b.x, b.y); } } for (double y = 0; y < this.bedHeight; y += gridDst) { Point a = Helper.toPoint(this.getMmToPxTransform().transform(new Point2D.Double(0, y), null)); Point b = Helper.toPoint(this.getMmToPxTransform().transform(new Point2D.Double(bedWidth, y), null)); if (a.y > 0) { if (a.y > this.getHeight()) { break; } gg.drawLine(a.x, a.y, b.x, b.y); } } } }