/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package eu.mihosoft.vrl.fxscad; import eu.mihosoft.vrl.v3d.CSG; import eu.mihosoft.vrl.v3d.MeshContainer; import groovy.lang.Binding; import groovy.lang.GroovyShell; import groovy.lang.Script; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javafx.embed.swing.SwingFXUtils; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.geometry.Bounds; import javafx.scene.Group; import javafx.scene.PerspectiveCamera; import javafx.scene.SceneAntialiasing; import javafx.scene.SnapshotParameters; import javafx.scene.SubScene; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextArea; import javafx.scene.effect.BlendMode; import javafx.scene.image.WritableImage; import javafx.scene.input.MouseButton; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.paint.PhongMaterial; import javafx.scene.shape.CullFace; import javafx.scene.shape.MeshView; import javafx.scene.transform.Scale; import javafx.stage.FileChooser; import javax.imageio.ImageIO; import org.apache.commons.io.IOUtils; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.controlsfx.control.action.Action; import org.controlsfx.dialog.Dialogs; import org.fxmisc.richtext.CodeArea; import org.fxmisc.richtext.StyleSpansBuilder; import org.reactfx.Change; import org.reactfx.EventStream; import org.reactfx.EventStreams; /** * FXML Controller class * * @author Michael Hoffer <info@michaelhoffer.de> */ public class MainController implements Initializable { private static final String[] KEYWORDS = new String[]{ "def", "in", "as", "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", "volatile", "while" }; private static final Pattern KEYWORD_PATTERN = Pattern.compile("\\b(" + String.join("|", KEYWORDS) + ")\\b"); private final Group viewGroup = new Group(); private final CodeArea codeArea = new CodeArea(); private boolean autoCompile = true; private CSG csgObject; @FXML private TextArea logView; @FXML private ScrollPane editorContainer; @FXML private Pane viewContainer; private SubScene subScene; /** * Initializes the controller class. * * @param url * @param rb */ @Override public void initialize(URL url, ResourceBundle rb) { // codeArea.textProperty().addListener( (ov, oldText, newText) -> { Matcher matcher = KEYWORD_PATTERN.matcher(newText); int lastKwEnd = 0; StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>(); while (matcher.find()) { spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd); spansBuilder.add(Collections.singleton("keyword"), matcher.end() - matcher.start()); lastKwEnd = matcher.end(); } spansBuilder.add(Collections.emptyList(), newText.length() - lastKwEnd); codeArea.setStyleSpans(0, spansBuilder.create()); }); EventStream<Change<String>> textEvents = EventStreams.changesOf(codeArea.textProperty()); textEvents.reduceSuccessions((a, b) -> b, Duration.ofMillis(500)). subscribe(code -> { if (autoCompile) { compile(code.getNewValue()); } }); codeArea.replaceText( "CSG cube = new Cube(2).toCSG()\n" + "CSG sphere = new Sphere(1.25).toCSG()\n" + "\n" + "cube.difference(sphere)"); editorContainer.setContent(codeArea); subScene = new SubScene(viewGroup, 100, 100, true, SceneAntialiasing.BALANCED); subScene.widthProperty().bind(viewContainer.widthProperty()); subScene.heightProperty().bind(viewContainer.heightProperty()); PerspectiveCamera subSceneCamera = new PerspectiveCamera(false); subScene.setCamera(subSceneCamera); viewContainer.getChildren().add(subScene); } private void setCode(String code) { codeArea.replaceText(code); } private String getCode() { return codeArea.getText(); } private void clearLog() { logView.setText(""); } private void compile(String code) { csgObject = null; clearLog(); viewGroup.getChildren().clear(); try { CompilerConfiguration cc = new CompilerConfiguration(); cc.addCompilationCustomizers( new ImportCustomizer(). addStarImports("eu.mihosoft.vrl.v3d", "eu.mihosoft.vrl.v3d.samples"). addStaticStars("eu.mihosoft.vrl.v3d.Transform")); GroovyShell shell = new GroovyShell(getClass().getClassLoader(), new Binding(), cc); Script script = shell.parse(code); Object obj = script.run(); if (obj instanceof CSG) { CSG csg = (CSG) obj; csgObject = csg; MeshContainer meshContainer = csg.toJavaFXMesh(); final MeshView meshView = meshContainer.getAsMeshViews().get(0); setMeshScale(meshContainer, viewContainer.getBoundsInLocal(), meshView); PhongMaterial m = new PhongMaterial(Color.RED); meshView.setCullFace(CullFace.NONE); meshView.setMaterial(m); viewGroup.layoutXProperty().bind( viewContainer.widthProperty().divide(2)); viewGroup.layoutYProperty().bind( viewContainer.heightProperty().divide(2)); viewContainer.boundsInLocalProperty().addListener( (ov, oldV, newV) -> { setMeshScale(meshContainer, newV, meshView); }); VFX3DUtil.addMouseBehavior(meshView, viewContainer, MouseButton.PRIMARY); viewGroup.getChildren().add(meshView); } else { System.out.println(">> no CSG object returned :("); } } catch (Throwable ex) { ex.printStackTrace(System.err); } } private void setMeshScale( MeshContainer meshContainer, Bounds t1, final MeshView meshView) { double maxDim = Math.max(meshContainer.getWidth(), Math.max(meshContainer.getHeight(), meshContainer.getDepth())); double minContDim = Math.min(t1.getWidth(), t1.getHeight()); double scale = minContDim / (maxDim * 2); meshView.setScaleX(scale); meshView.setScaleY(scale); meshView.setScaleZ(scale); } /** * Returns the location of the Jar archive or .class file the specified * class has been loaded from. <b>Note:</b> this only works if the class is * loaded from a jar archive or a .class file on the locale file system. * * @param cls class to locate * @return the location of the Jar archive the specified class comes from */ public static File getClassLocation(Class<?> cls) { // VParamUtil.throwIfNull(cls); String className = cls.getName(); ClassLoader cl = cls.getClassLoader(); URL url = cl.getResource(className.replace(".", "/") + ".class"); String urlString = url.toString().replace("jar:", ""); if (!urlString.startsWith("file:")) { throw new IllegalArgumentException("The specified class\"" + cls.getName() + "\" has not been loaded from a location" + "on the local filesystem."); } urlString = urlString.replace("file:", ""); urlString = urlString.replace("%20", " "); int location = urlString.indexOf(".jar!"); if (location > 0) { urlString = urlString.substring(0, location) + ".jar"; } else { //System.err.println("No Jar File found: " + cls.getName()); } return new File(urlString); } @FXML private void onLoadFile(ActionEvent e) { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Open JFXScad File"); fileChooser.getExtensionFilters().add( new FileChooser.ExtensionFilter( "JFXScad files (*.jfxscad, *.groovy)", "*.jfxscad", "*.groovy")); File f = fileChooser.showOpenDialog(null); if (f == null) { return; } String fName = f.getAbsolutePath(); if (!fName.toLowerCase().endsWith(".groovy") && !fName.toLowerCase().endsWith(".jfxscad")) { fName += ".jfxscad"; } try { setCode(new String(Files.readAllBytes(Paths.get(fName)), "UTF-8")); } catch (IOException ex) { Logger.getLogger(MainController.class.getName()). log(Level.SEVERE, null, ex); } } @FXML private void onSaveFile(ActionEvent e) { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Save JFXScad File"); fileChooser.getExtensionFilters().add( new FileChooser.ExtensionFilter( "JFXScad files (*.jfxscad, *.groovy)", "*.jfxscad", "*.groovy")); File f = fileChooser.showSaveDialog(null); if (f == null) { return; } String fName = f.getAbsolutePath(); if (!fName.toLowerCase().endsWith(".groovy") && !fName.toLowerCase().endsWith(".jfxscad")) { fName += ".jfxscad"; } try { Files.write(Paths.get(fName), getCode().getBytes("UTF-8")); } catch (IOException ex) { Logger.getLogger(MainController.class.getName()). log(Level.SEVERE, null, ex); } } @FXML private void onExportAsSTLFile(ActionEvent e) { if (csgObject == null) { Action response = Dialogs.create() .title("Error") .message("Cannot export STL. There is no geometry :(") .lightweight() .showError(); return; } FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Export STL File"); fileChooser.getExtensionFilters().add( new FileChooser.ExtensionFilter( "STL files (*.stl)", "*.stl")); File f = fileChooser.showSaveDialog(null); if (f == null) { return; } String fName = f.getAbsolutePath(); if (!fName.toLowerCase().endsWith(".stl")) { fName += ".stl"; } try { eu.mihosoft.vrl.v3d.FileUtil.write( Paths.get(fName), csgObject.toStlString()); } catch (IOException ex) { Logger.getLogger(MainController.class.getName()). log(Level.SEVERE, null, ex); } } @FXML private void onExportAsPngFile(ActionEvent e) { if (csgObject == null) { Action response = Dialogs.create() .title("Error") .message("Cannot export PNG. There is no geometry :(") .lightweight() .showError(); return; } FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Export PNG File"); fileChooser.getExtensionFilters().add( new FileChooser.ExtensionFilter( "Image files (*.png)", "*.png")); File f = fileChooser.showSaveDialog(null); if (f == null) { return; } String fName = f.getAbsolutePath(); if (!fName.toLowerCase().endsWith(".png")) { fName += ".png"; } int snWidth = 1024; int snHeight = 1024; double realWidth = viewGroup.getBoundsInLocal().getWidth(); double realHeight = viewGroup.getBoundsInLocal().getHeight(); double scaleX = snWidth / realWidth; double scaleY = snHeight / realHeight; double scale = Math.min(scaleX, scaleY); PerspectiveCamera snCam = new PerspectiveCamera(false); snCam.setTranslateZ(-200); SnapshotParameters snapshotParameters = new SnapshotParameters(); snapshotParameters.setTransform(new Scale(scale, scale)); snapshotParameters.setCamera(snCam); snapshotParameters.setDepthBuffer(true); snapshotParameters.setFill(Color.TRANSPARENT); WritableImage snapshot = new WritableImage(snWidth, (int) (realHeight * scale)); viewGroup.snapshot(snapshotParameters, snapshot); try { ImageIO.write(SwingFXUtils.fromFXImage(snapshot, null), "png", new File(fName)); } catch (IOException ex) { Logger.getLogger(MainController.class.getName()). log(Level.SEVERE, null, ex); } } @FXML private void onCompileAndRun(ActionEvent e) { compile(getCode()); } @FXML private void onServoMountSample(ActionEvent e) { try { String code = IOUtils.toString(this.getClass(). getResourceAsStream("ServoMount.jfxscad"), "UTF-8"); setCode(code); } catch (IOException ex) { Logger.getLogger(MainController.class.getName()). log(Level.SEVERE, null, ex); } } @FXML private void onBatteryHolderSample(ActionEvent e) { try { String code = IOUtils.toString(this.getClass(). getResourceAsStream("BatteryHolder.jfxscad"), "UTF-8"); setCode(code); } catch (IOException ex) { Logger.getLogger(MainController.class.getName()). log(Level.SEVERE, null, ex); } } @FXML private void onWheelSample(ActionEvent e) { try { String code = IOUtils.toString(this.getClass(). getResourceAsStream("Wheel.jfxscad"), "UTF-8"); setCode(code); } catch (IOException ex) { Logger.getLogger(MainController.class.getName()). log(Level.SEVERE, null, ex); } } @FXML private void onBreadBoardConnectorSample(ActionEvent e) { try { String code = IOUtils.toString(this.getClass(). getResourceAsStream("BreadBoardConnector.jfxscad"), "UTF-8"); setCode(code); } catch (IOException ex) { Logger.getLogger(MainController.class.getName()). log(Level.SEVERE, null, ex); } } @FXML private void onBoardMountSample(ActionEvent e) { try { String code = IOUtils.toString(this.getClass(). getResourceAsStream("BoardMount.jfxscad"), "UTF-8"); setCode(code); } catch (IOException ex) { Logger.getLogger(MainController.class.getName()). log(Level.SEVERE, null, ex); } } @FXML private void onClose(ActionEvent e) { System.exit(0); } @FXML private void onAutoCompile(ActionEvent e) { autoCompile = !autoCompile; } public TextArea getLogView() { return logView; } }