Writing support for new printable file types

Writing support for a new type of printable file requires three steps:

  1. Writing a new java class that extends this simple interface: ```org.area515.resinprinter.job.PrintFileProcessor```
  2. Add the following line to your config.properties file: ```printFileProcessor.[NameOfYourNewClass]=true```
  3. Update the ```resourcesnew\cwh\js\index.js``` with the gui icon of your choice in the javascript function: ```getPrintFileProcessorIconClass()```

It really is that simple.

Now let's talk about #1 for a bit since steps 2 and 3 are more of a finalizing formality. There are quite a few things to think about when it comes to building the ```PrintFileProcessor```; however, we've tried to give you a pretty good head start with these implementations:

One thing you will quickly see is that ```org.area515.resinprinter.job.AbstractPrintFileProcessor<G, E>``` is the common base class for all implementations for print file processors. It certainly doesn't have to be the base class for your new class, but if it isn't, you will need to deal with the following concepts:

  1. Throttle up/down CPU execution time during gcode execution hotspots
  2. Engaging and communicating with resin detectors at the appropriate times
  3. Negotiating hardware with other available printers through things like the SerialManager and DisplayManager
  4. Applying the bulb mask from the printer setup
  5. Applying image transforms specified in the customizer
  6. Executing ALL calculators setup in the printer profile at the appropriate times
  7. Implementing your own imaging cache
  8. Holding your own print progress state (eventually this will be persistant state as well)
  9. Computing your own build area volume in square millimeters
  10. Implement all rendering to the display that was pre-negotiated with the DisplayManager
  11. Most importantly you'll need to execute all gcode communications with the SerialManager

You probably don't want to do that stuff…

Step 1: Is your new file a 3d modelable file?

  1. ```boolean isThreeDimensionalGeometryAvailable()``` - First you have to ask yourself if your file can be visualized from a '3d' standpoint. If so, you are going to want the GUI to call the next two methods, so return true from this method. That simple.
  1. ```<G> getGeometry(PrintJob printJob)``` - If your new file type is a true 3d file and you'd like your file displayed as a 3d model when the print job is running you need to return a list of json marshallable objects that will be sent to the gui.
    • The json format spec is very simple: ```[{v:[x:double,y:double,z:double],n:[x:double,y:double,z:double]}]```
    • ```v``` is an array of exactly three vectors corresponding to a single triangle
    • ```n``` is the normal of that triangle.
  1. ```<E> getErrors(PrintJob printJob)``` - If you'd like your print job to be paused when you experience an error while printing/slicing your 3d file then return those errors from this method. You also have the ability to highlight portions of your 3d file that have been determined to be “in error”. For example if you are printing an STL file and errors in the geometry are found, those triangles involved in the invalid geometry are highlighted.
  • The json format spec is very simple: ```[{i:integer}]```
  • ```i``` is the index of a triangle in the array that was sent down as a part of the ```getGeometry(PrintJob printJob)``` call above. All indices will be highlighted in the gui signifying that they are the result of a geometry error.

Step 2: Does your print require some initial setup or teardown procedures?

  1. ```prepareEnvironment(File processingFile, PrintJob printJob)``` - This method will be called every time that your print starts. Do what you would like to prepare your environment for this file. For example, zip files could be unzipped during this stage.
  2. ```cleanupEnvironment(File processingFile)``` - This method will only be called when a file is deleted. Take notice, that cleanup is not the “inverse” operation as prepare. A prepare will only be called when a print starts, but cleanup only happens when the printable file is deleted. The reason for this is that the developer is already in full control of the print when it starts. This means they can always add cleanup procedures to their finally block when the print is complete. This method is a trigger point for the deletion of the file.

Step 3: The easy stuff

  1. ```String[] getFileExtensions()``` - Return an array of file extensions that you are going to support.
  2. ```String getFriendlyName()``` - Return the nice name of your printable file type for the gui to display.
  3. ```boolean acceptsFile(File processingFile)``` - This method is going to be called when a file is uploaded that matches a file extension that is returned from the above method: ```getFileExtensions()```. Inside of this method, you have to internally inspect this file to determine if you would like to take responsibility for printing it. This is necessary because there may be many print file processors that will register for the “zip” file extension. Yet only one print file processor can take responsibility for printing it.

Step 4: The most important method

```JobStatus processFile(PrintJob printJob)``` - This is the workhorse method for printing, and we'll cover how this method should be structured in the rest of this doc:

public JobStatus processFile(PrintJob printJob) throws Exception {
	try {
                // Setup your data aid with:
                DataAid dataAid = initializeJobCacheWithDataAid(printJob);
		// Note: If you have already called initializeJobCacheWithDataAid() in your prepareEnvironment() then call this method instead:
		DataAid dataAid = getDataAid(printJob);
		
		// get ALL configuration information for your print through the dataAid that was given to you:
		//    1. dataAid.configuration
		//    2. dataAid.printer
		//    3. dataAid.printJob
		//    4. dataAid.slicingProfile
		//    5. dataAid.inkConfiguration
		//    6. dataAid.scriptEngine
		//    7. dataAid.customizer
		//    8. dataAid.xPixelsPerMM
		//    9. dataAid.yPixelsPerMM
		//   10. And much much more is available...

		// Get the total number of slices from your file by using a combination of:
		//    1. dataAid.customizer.getZScale()
		//    2. dataAid.sliceHeight
		printJob.setTotalSlices([totalslices]);

		// Determine the starting slice and final slice of your print with:
		//    1. dataAid.slicingProfile.getDirection()
		//    2. dataAid.customizer.getNextSlice()
		int startPoint = dataAid.slicingProfile.getDirection() == BuildDirection.Bottom_Up?[BottomSliceIndexOfImage]:[TopSliceIndexOfImage];
		int endPoint = dataAid.slicingProfile.getDirection() == BuildDirection.Bottom_Up?[TopSliceIndexOfImage]:[BottomSliceIndexOfImage];

		// Set the first "renderingPointer" for your image cache. This can be an object of any type
		// but it needs to correspond to the first image that you are rendering. You could use an integer
		// that represents the slice count or perhaps the filename of a slice that you have extracted on
		// on disk. Ultimately it's just a key to a hashmap that contains the image in cache.
		dataAid.cache.setCurrentRenderingPointer(startPoint);
		
		// Create your renderer. More about this later....
		Future<RenderedData> currentImage = Main.GLOBAL_EXECUTOR.submit([createYourRendererHere]);
		
		// Finally you are able to send your first gcode initialization to your printer like this:
		performHeader(dataAid);
		
		// Now you are ready to start printing. Basically you are going to count from
		// top->bottom or bottom->top, making sure that your print is still active after each
		// iteration. Your for loop could look something like this:
		for (int z = startPoint; dataAid.slicingProfile.getDirection().isSliceAvailable(z, endPoint) && dataAid.printer.isPrintActive(); z += dataAid.slicingProfile.getDirection().getVector()) {
			
			// Next we execute gcode that occurs "before" a slice is displayed
			JobStatus status = performPreSlice(dataAid, null);
			// Next we check to determine if the user cancelled the print and exit if necessary.
			if (status != null) {
				return status;
			}
			
			// Our image has been rendering since we started it on the previous iteration or if
			// this is the first iteration, when we initialized it outside of the loop.
			// When all rendering and volume computation is finished, 
			// this method will return the newly rendered image. Unless your processing is quite
			// complicated, you won't have to wait for this method to return.
			BufferedImage image = currentImage.get().getPrintableImage();
			
			// Now that the next image has rendered, let's set the renderingPointer to it.
			// This basically tells the rest of the world that we are ready to display the next image.
			dataAid.cache.setCurrentRenderingPointer(nextRenderingPointer);
			
			// Get the next rendering pointer to start rendering the image
			nextRenderingPointer = z + dataAid.slicingProfile.getDirection().getVector();
			
			// Only start the next renderer if there is actually another image to print
			if (dataAid.slicingProfile.getDirection().isSliceAvailable(z, endPoint + dataAid.slicingProfile.getDirection().getVector())) {
				currentImage = Main.GLOBAL_EXECUTOR.submit([createYourRendererHere]);
			}
			
			// Prints the image that we just rendered. Keep in mind that this is NOT the image
			// that we just submitted above.
			status = printImageAndPerformPostProcessing(dataAid, image);
			// Next we check to determine if the user cancelled the print and exit if necessary.
			if (status != null) {
				return status;
			}
		}
		
		// Perform the final "footer" gcode and return the final status to the gui
		return performFooter(dataAid);
	} finally {
		// Execute the method below and perform any cleanup unique to your file type for this printjob
		clearDataAid(printJob);
	}
}

Step 5: Write your rendering code

In Step 4(in the program above) I mentioned two places where you need to: ```[createYourRendererHere]``` To do this you simply implement the ```java.util.concurrent.Callable``` interface and return an instance of ```org.area515.resinprinter.job.render.RenderedData```. This can be a bit tricky because there are a few responsibilities you need to handle in order to optimize caching properly and compute the current slice volume. If you want these things done for you just simply create a new class that implements ```org.area515.resinprinter.job.render.CurrentImageRenderer``` and implement the ```renderImage(BufferedImage)``` method. This makes it pretty easy and all you really need to do is take care of the specific rendering for your file type.

Below are several examples of how you might want to do this: