/** Polargraph controller Copyright Sandy Noble 2015. This file is part of Polargraph Controller. Polargraph Controller is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Polargraph Controller is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Polargraph Controller. If not, see . Requires the excellent ControlP5 GUI library available from http://www.sojamo.de/libraries/controlP5/. Requires the excellent Geomerative library available from http://www.ricardmarxer.com/geomerative/. This is an application for controlling a polargraph machine, communicating using ASCII command language over a serial link. sandy.noble@gmail.com http://www.polargraph.co.uk/ https://github.com/euphy/polargraphcontroller */ /** * * * */ class Machine { protected PVector machineSize = new PVector(4000,6000); protected Rectangle page = new Rectangle(1000,1000,2000,3000); protected Rectangle imageFrame = new Rectangle(1500,1500,1000,1000); protected Rectangle pictureFrame = new Rectangle(1600,1600,800,800); protected Float stepsPerRev = 200.0; protected Float mmPerRev = 95.0; protected Float mmPerStep = null; protected Float stepsPerMM = null; protected Float maxLength = null; protected Float gridSize = 100.0; protected List gridLinePositions = null; protected PImage imageBitmap = null; protected String imageFilename = null; public Machine(Integer width, Integer height, Float stepsPerRev, Float mmPerRev) { this.setSize(width, height); this.setStepsPerRev(stepsPerRev); this.setMMPerRev(mmPerRev); } public void setSize(Integer width, Integer height) { PVector s = new PVector(width, height); this.machineSize = s; maxLength = null; } public PVector getSize() { return this.machineSize; } public Float getMaxLength() { if (maxLength == null) { maxLength = dist(0,0, getWidth(), getHeight()); } return maxLength; } public void setPage(Rectangle r) { this.page = r; } public Rectangle getPage() { return this.page; } public float getPageCentrePosition(float pageWidth) { return (getWidth()- pageWidth/2)/2; } public void setImageFrame(Rectangle r) { this.imageFrame = r; } public Rectangle getImageFrame() { return this.imageFrame; } public void setPictureFrame(Rectangle r) { this.pictureFrame = r; } public Rectangle getPictureFrame() { return this.pictureFrame; } public Integer getWidth() { return int(this.machineSize.x); } public Integer getHeight() { return int(this.machineSize.y); } public void setStepsPerRev(Float s) { this.stepsPerRev = s; } public Float getStepsPerRev() { mmPerStep = null; stepsPerMM = null; return this.stepsPerRev; } public void setMMPerRev(Float d) { mmPerStep = null; stepsPerMM = null; this.mmPerRev = d; } public Float getMMPerRev() { return this.mmPerRev; } public Float getMMPerStep() { if (mmPerStep == null) { mmPerStep = mmPerRev / stepsPerRev; } return mmPerStep; } public Float getStepsPerMM() { if (stepsPerMM == null) { stepsPerMM = stepsPerRev / mmPerRev; } return stepsPerMM; } public int inSteps(int inMM) { double steps = inMM * getStepsPerMM(); steps += 0.5; int stepsInt = (int) steps; return stepsInt; } public int inSteps(float inMM) { double steps = inMM * getStepsPerMM(); steps += 0.5; int stepsInt = (int) steps; return stepsInt; } public PVector inSteps(PVector mm) { PVector steps = new PVector(inSteps(mm.x), inSteps(mm.y)); return steps; } public int inMM(float steps) { double mm = steps / getStepsPerMM(); mm += 0.5; int mmInt = (int) mm; return mmInt; } public float inMMFloat(float steps) { double mm = steps / getStepsPerMM(); return (float) mm; } public PVector inMM (PVector steps) { PVector mm = new PVector(inMMFloat(steps.x), inMMFloat(steps.y)); return mm; } float getPixelBrightness(PVector pos, float dim, float scalingFactor) { float averageBrightness = 255.0; if (getImageFrame().surrounds(pos)) { // offset it by image position to get position over image PVector offsetPos = PVector.sub(pos, getImageFrame().getPosition()); int originX = (int) offsetPos.x; int originY = (int) offsetPos.y; PImage extractedPixels = null; extractedPixels = getImage().get(int(originX*scalingFactor), int(originY*scalingFactor), 1, 1); extractedPixels.loadPixels(); if (dim >= 2) { int halfDim = (int)dim / (int)2.0; // restrict the sample area from going off the top/left edge of the image float startX = originX - halfDim; float startY = originY - halfDim; if (startX < 0) startX = 0; if (startY < 0) startY = 0; // and do the same for the bottom / right edges float endX = originX+halfDim; float endY = originY+halfDim; if (endX > getImageFrame().getWidth()) endX = getImageFrame().getWidth(); if (endY > getImageFrame().getHeight()) endY = getImageFrame().getHeight(); // now convert end coordinates to width/height float dimWidth = (endX - startX)*scalingFactor; float dimHeight = (endY - startY)*scalingFactor; dimWidth = (dimWidth < 1.0) ? 1.0 : dimWidth; dimHeight = (dimHeight < 1.0) ? 1.0 : dimHeight; startX = int(startX*scalingFactor); startY = int(startY*scalingFactor); // get the block of pixels extractedPixels = getImage().get(int(startX), int(startY), int(dimWidth+0.5), int(dimHeight+0.5)); extractedPixels.loadPixels(); } // going to go through them and total the brightnesses int numberOfPixels = extractedPixels.pixels.length; float totalPixelBrightness = 0; for (int i = 0; i < numberOfPixels; i++) { color p = extractedPixels.pixels[i]; float r = brightness(p); totalPixelBrightness += r; } // and get an average brightness for all of these pixels. averageBrightness = totalPixelBrightness / numberOfPixels; } return averageBrightness; } color getPixelAtMachineCoords(PVector pos, float scalingFactor) { if (getImageFrame().surrounds(pos)) { // offset it by image position to get position over image PVector offsetPos = PVector.sub(pos, getImageFrame().getPosition()); int originX = (int) offsetPos.x; int originY = (int) offsetPos.y; PImage centrePixel = null; centrePixel = getImage().get(int(originX*scalingFactor), int(originY*scalingFactor), 1, 1); centrePixel.loadPixels(); color col = centrePixel.pixels[0]; return col; } else { return 0; } } boolean isMasked(PVector pos, float scalingFactor) { switch (invertMaskMode) { case MASK_IS_UNUSED: return false; case MASKED_COLOURS_ARE_HIDDEN: return isChromaKey(pos, scalingFactor); case MASKED_COLOURS_ARE_SHOWN: return !isChromaKey(pos, scalingFactor); default: return false; } } boolean isChromaKey(PVector pos, float scalingFactor) { if (getImageFrame().surrounds(pos)) { color col = getPixelAtMachineCoords(pos, scalingFactor); // get pixels from the vector coords if (col == chromaKeyColour) { // println("is chroma key " + red(col) + ", "+green(col)+","+blue(col)); return true; } else { // println("isn't chroma key " + red(col) + ", "+green(col)+","+blue(col)); return false; } } else return false; } public PVector asNativeCoords(PVector cartCoords) { return asNativeCoords(cartCoords.x, cartCoords.y); } public PVector asNativeCoords(float cartX, float cartY) { float distA = dist(0,0,cartX, cartY); float distB = dist(getWidth(),0,cartX, cartY); PVector pgCoords = new PVector(distA, distB); return pgCoords; } public PVector asCartesianCoords(PVector pgCoords) { float calcX = (pow(getWidth(), 2.0) - pow(pgCoords.y, 2.0) + pow(pgCoords.x, 2.0)) / (getWidth()*2.0); float calcY = sqrt(pow(pgCoords.x,2.0)-pow(calcX,2.0)); PVector vect = new PVector(calcX, calcY); return vect; } public Integer convertSizePreset(String preset) { Integer result = A3_SHORT; if (preset.equalsIgnoreCase(PRESET_A3_SHORT)) result = A3_SHORT; else if (preset.equalsIgnoreCase(PRESET_A3_LONG)) result = A3_LONG; else if (preset.equalsIgnoreCase(PRESET_A2_SHORT)) result = A2_SHORT; else if (preset.equalsIgnoreCase(PRESET_A2_LONG)) result = A2_LONG; else if (preset.equalsIgnoreCase(PRESET_A2_IMP_SHORT)) result = A2_IMP_SHORT; else if (preset.equalsIgnoreCase(PRESET_A2_IMP_LONG)) result = A2_IMP_LONG; else if (preset.equalsIgnoreCase(PRESET_A1_SHORT)) result = A1_SHORT; else if (preset.equalsIgnoreCase(PRESET_A1_LONG)) result = A1_LONG; else { try { result = Integer.parseInt(preset); } catch (NumberFormatException nfe) { result = A3_SHORT; } } return result; } public void loadDefinitionFromProperties(Properties props) { // get these first because they are important to convert the rest of them setStepsPerRev(getFloatProperty("machine.motors.stepsPerRev", 200.0)); setMMPerRev(getFloatProperty("machine.motors.mmPerRev", 95.0)); // now stepsPerMM and mmPerStep should have been calculated. It's safe to get the rest. // machine size setSize(inSteps(getIntProperty("machine.width", 600)), inSteps(getIntProperty("machine.height", 800))); // page size String pageWidth = getStringProperty("controller.page.width", PRESET_A3_SHORT); float pw = convertSizePreset(pageWidth); String pageHeight = getStringProperty("controller.page.height", PRESET_A3_LONG); float ph = convertSizePreset(pageHeight); PVector pageSize = new PVector(pw, ph); // page position String pos = getStringProperty("controller.page.position.x", "CENTRE"); float px = 0.0; println("machine size: " + getSize().x + ", " + inSteps(pageSize.x)); if (pos.equalsIgnoreCase("CENTRE")) { px = inMM((getSize().x - pageSize.x) / 2.0); } else { px = getFloatProperty("controller.page.position.x", (int) getDisplayMachine().getPageCentrePosition(pageSize.x)); } float py = getFloatProperty("controller.page.position.y", 120); PVector pagePos = new PVector(px, py); Rectangle page = new Rectangle(inSteps(pagePos), inSteps(pageSize)); setPage(page); // bitmap setImageFilename(getStringProperty("controller.image.filename", "")); loadImageFromFilename(imageFilename); // image position Float offsetX = getFloatProperty("controller.image.position.x", 0.0); Float offsetY = getFloatProperty("controller.image.position.y", 0.0); PVector imagePos = new PVector(offsetX, offsetY); // println("image pos: " + imagePos); // image size Float imageWidth = getFloatProperty("controller.image.width", 500); Float imageHeight = getFloatProperty("controller.image.height", 0); if (imageHeight == 0) // default was set { println("Image height not supplied - creating default."); if (getImage() != null) { float scaling = imageWidth / getImage().width; imageHeight = getImage().height * scaling; } else imageHeight = 500.0; } PVector imageSize = new PVector(imageWidth, imageHeight); Rectangle imageFrame = new Rectangle(inSteps(imagePos), inSteps(imageSize)); setImageFrame(imageFrame); // picture frame size PVector frameSize = new PVector(getIntProperty("controller.pictureframe.width", 200), getIntProperty("controller.pictureframe.height", 200)); PVector framePos = new PVector(getIntProperty("controller.pictureframe.position.x", 200), getIntProperty("controller.pictureframe.position.y", 200)); Rectangle frame = new Rectangle(inSteps(framePos), inSteps(frameSize)); setPictureFrame(frame); // penlift positions penLiftDownPosition = getIntProperty("machine.penlift.down", 90); penLiftUpPosition = getIntProperty("machine.penlift.up", 180); } public Properties loadDefinitionIntoProperties(Properties props) { // Put keys into properties file: props.setProperty("machine.motors.stepsPerRev", getStepsPerRev().toString()); props.setProperty("machine.motors.mmPerRev", getMMPerRev().toString()); // machine width props.setProperty("machine.width", Integer.toString((int) inMM(getWidth()))); // machine.height props.setProperty("machine.height", Integer.toString((int) inMM(getHeight()))); // image filename props.setProperty("controller.image.filename", (getImageFilename() == null) ? "" : getImageFilename()); // image position float imagePosX = 0.0; float imagePosY = 0.0; float imageWidth = 0.0; float imageHeight = 0.0; if (getImageFrame() != null) { imagePosX = getImageFrame().getLeft(); imagePosY = getImageFrame().getTop(); imageWidth = getImageFrame().getWidth(); imageHeight = getImageFrame().getHeight(); } props.setProperty("controller.image.position.x", Integer.toString((int) inMM(imagePosX))); props.setProperty("controller.image.position.y", Integer.toString((int) inMM(imagePosY))); // image size props.setProperty("controller.image.width", Integer.toString((int) inMM(imageWidth))); props.setProperty("controller.image.height", Integer.toString((int) inMM(imageHeight))); // page size // page position float pageSizeX = 0.0; float pageSizeY = 0.0; float pagePosX = 0.0; float pagePosY = 0.0; if (getPage() != null) { pageSizeX = getPage().getWidth(); pageSizeY = getPage().getHeight(); pagePosX = getPage().getLeft(); pagePosY = getPage().getTop(); } props.setProperty("controller.page.width", Integer.toString((int) inMM(pageSizeX))); props.setProperty("controller.page.height", Integer.toString((int) inMM(pageSizeY))); props.setProperty("controller.page.position.x", Integer.toString((int) inMM(pagePosX))); props.setProperty("controller.page.position.y", Integer.toString((int) inMM(pagePosY))); // picture frame size float frameSizeX = 0.0; float frameSizeY = 0.0; float framePosX = 0.0; float framePosY = 0.0; if (getPictureFrame() != null) { frameSizeX = getPictureFrame().getWidth(); frameSizeY = getPictureFrame().getHeight(); framePosX = getPictureFrame().getLeft(); framePosY = getPictureFrame().getTop(); } props.setProperty("controller.pictureframe.width", Integer.toString((int) inMM(frameSizeX))); props.setProperty("controller.pictureframe.height", Integer.toString((int) inMM(frameSizeY))); // picture frame position props.setProperty("controller.pictureframe.position.x", Integer.toString((int) inMM(framePosX))); props.setProperty("controller.pictureframe.position.y", Integer.toString((int) inMM(framePosY))); props.setProperty("machine.penlift.down", Integer.toString((int) penLiftDownPosition)); props.setProperty("machine.penlift.up", Integer.toString((int) penLiftUpPosition)); // println("framesize: " + inMM(frameSizeX)); return props; } protected void loadImageFromFilename(String filename) { if (filename != null && !"".equals(filename)) { // check for format etc here println("loading from filename: " + filename); try { this.imageBitmap = loadImage(filename); this.imageFilename = filename; trace_initTrace(this.imageBitmap); } catch (Exception e) { println("Image failed to load: " + e.getMessage()); this.imageBitmap = null; } } else { this.imageBitmap = null; this.imageFilename = null; } } public void sizeImageFrameToImageAspectRatio() { float scaling = getImageFrame().getWidth() / getImage().width; float frameHeight = getImage().height * scaling; getImageFrame().getSize().y = frameHeight; } public void setImage(PImage b) { this.imageBitmap = b; } public void setImageFilename(String filename) { this.loadImageFromFilename(filename); } public String getImageFilename() { return this.imageFilename; } public PImage getImage() { return this.imageBitmap; } public boolean imageIsReady() { if (imageBitmapIsLoaded()) return true; else return false; } public boolean imageBitmapIsLoaded() { if (getImage() != null) return true; else return false; } protected void setGridSize(float gridSize) { this.gridSize = gridSize; this.gridLinePositions = generateGridLinePositions(gridSize); } /** This takes in an area defined in cartesian steps, and returns a set of pixels that are included in that area. Coordinates are specified in cartesian steps. The pixels are worked out based on the gridsize parameter. d*/ Set getPixelsPositionsFromArea(PVector p, PVector s, float gridSize, float sampleSize) { // work out the grid setGridSize(gridSize); float maxLength = getMaxLength(); float numberOfGridlines = maxLength / gridSize; float gridIncrement = maxLength / numberOfGridlines; List gridLinePositions = getGridLinePositions(gridSize); Rectangle selectedArea = new Rectangle (p.x,p.y, s.x,s.y); // now work out the scaling factor that'll be needed to work out // the positions of the pixels on the bitmap. float scalingFactor = getImage().width / getImageFrame().getWidth(); // now go through all the combinations of the two values. Set nativeCoords = new HashSet(); for (Float a : gridLinePositions) { for (Float b : gridLinePositions) { PVector nativeCoord = new PVector(a, b); PVector cartesianCoord = asCartesianCoords(nativeCoord); if (selectedArea.surrounds(cartesianCoord)) { if (isMasked(cartesianCoord, scalingFactor)) { nativeCoord.z = MASKED_PIXEL_BRIGHTNESS; // magic number nativeCoords.add(nativeCoord); } else { if (sampleSize >= 1.0) { float brightness = getPixelBrightness(cartesianCoord, sampleSize, scalingFactor); nativeCoord.z = brightness; } nativeCoords.add(nativeCoord); } } } } return nativeCoords; } protected PVector snapToGrid(PVector loose, float gridSize) { List pos = getGridLinePositions(gridSize); boolean higherupperFound = false; boolean lowerFound = false; float halfGrid = gridSize / 2.0; float x = loose.x; float y = loose.y; Float snappedX = null; Float snappedY = null; int i = 0; while ((snappedX == null || snappedY == null) && i < pos.size()) { float upperBound = pos.get(i)+halfGrid; float lowerBound = pos.get(i)-halfGrid; // println("pos:" +pos.get(i) + "half: "+halfGrid+ ", upper: "+ upperBound + ", lower: " + lowerBound); if (snappedX == null && x > lowerBound && x <= upperBound) { snappedX = pos.get(i); // println("snappedX:" + snappedX); } if (snappedY == null && y > lowerBound && y <= upperBound) { snappedY = pos.get(i); // println("snappedY:" + snappedY); } i++; } PVector snapped = new PVector((snappedX == null) ? 0.0 : snappedX, (snappedY == null) ? 0.0 : snappedY); // println("loose:" + loose); // println("snapped:" + snapped); return snapped; } protected List getGridLinePositions(float gridSize) { setGridSize(gridSize); return this.gridLinePositions; } private List generateGridLinePositions(float gridSize) { List glp = new ArrayList(); float maxLength = getMaxLength(); for (float i = gridSize; i <= maxLength; i+=gridSize) { glp.add(i); } return glp; } }