/* Copyright (c) 2016 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky 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. * * Chunky 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 Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.ui; import javafx.application.Platform; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.ContextMenu; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.scene.control.RadioMenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.PixelFormat; import javafx.scene.image.WritableImage; import javafx.scene.image.WritablePixelFormat; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.shape.Line; import javafx.stage.PopupWindow; import javafx.stage.Stage; import se.llbit.chunky.renderer.RenderManager; import se.llbit.chunky.renderer.RenderMode; import se.llbit.chunky.renderer.RenderStatusListener; import se.llbit.chunky.renderer.Repaintable; import se.llbit.chunky.renderer.Renderer; import se.llbit.chunky.renderer.SceneStatusListener; import se.llbit.chunky.renderer.scene.Camera; import se.llbit.math.Vector2; import java.nio.IntBuffer; import java.util.concurrent.atomic.AtomicBoolean; /** * Shows the current render preview. */ public class RenderCanvasFx extends Stage implements Repaintable, SceneStatusListener { private static final WritablePixelFormat<IntBuffer> PIXEL_FORMAT = PixelFormat.getIntArgbInstance(); private WritableImage image; private final AtomicBoolean painting = new AtomicBoolean(false); private final Canvas canvas; private final Pane canvasPane; private final ScrollPane scrollPane; private final Renderer renderer; private int lastX; private int lastY; private Vector2 target = new Vector2(0, 0); private Tooltip tooltip = new Tooltip(); private RenderStatusListener renderListener; public RenderCanvasFx(se.llbit.chunky.renderer.scene.Scene scene, Renderer renderer) { setTitle("Render Preview"); getIcons().add(new Image(getClass().getResourceAsStream("/chunky-icon.png"))); this.renderer = renderer; renderer.addSceneStatusListener(this); tooltip.setAutoHide(true); tooltip.setConsumeAutoHidingEvents(false); tooltip.setAnchorLocation(PopupWindow.AnchorLocation.WINDOW_BOTTOM_LEFT); Pane parentPane = new Pane(); canvasPane = new Pane(); canvas = new Canvas(); synchronized (scene) { canvas.setWidth(scene.width); canvas.setHeight(scene.height); image = new WritableImage(scene.width, scene.height); } Line hGuide1 = new Line(); Line hGuide2 = new Line(); Line vGuide1 = new Line(); Line vGuide2 = new Line(); canvasPane.getChildren().addAll(canvas, hGuide1, hGuide2, vGuide1, vGuide2); parentPane.getChildren().add(canvasPane); scrollPane = new ScrollPane(); scrollPane.setContent(parentPane); hGuide1.setVisible(false); hGuide1.setStroke(Color.rgb(0, 0, 0, 0.5)); hGuide1.setStartX(0); hGuide1.endXProperty().bind(canvasPane.widthProperty()); hGuide1.startYProperty().bind(canvasPane.heightProperty().divide(3)); hGuide1.endYProperty().bind(hGuide1.startYProperty()); hGuide2.setVisible(false); hGuide2.setStroke(Color.rgb(0, 0, 0, 0.5)); hGuide2.setStartX(0); hGuide2.endXProperty().bind(canvasPane.widthProperty()); hGuide2.startYProperty().bind(canvasPane.heightProperty().multiply(2 / 3.0)); hGuide2.endYProperty().bind(hGuide2.startYProperty()); vGuide1.setVisible(false); vGuide1.setStroke(Color.rgb(0, 0, 0, 0.5)); vGuide1.setStartY(0); vGuide1.endYProperty().bind(canvasPane.heightProperty()); vGuide1.startXProperty().bind(canvasPane.widthProperty().divide(3)); vGuide1.endXProperty().bind(vGuide1.startXProperty()); vGuide2.setVisible(false); vGuide2.setStroke(Color.rgb(0, 0, 0, 0.5)); vGuide2.setStartY(0); vGuide2.endYProperty().bind(canvasPane.heightProperty()); vGuide2.startXProperty().bind(canvasPane.widthProperty().multiply(2 / 3.0)); vGuide2.endXProperty().bind(vGuide2.startXProperty()); canvasPane.translateXProperty().bind( scrollPane.widthProperty().subtract(canvasPane.widthProperty()).divide(2)); canvasPane.translateYProperty().bind( scrollPane.heightProperty().subtract(canvasPane.heightProperty()).divide(2)); setScene(new Scene(scrollPane)); canvas.setOnMousePressed(e -> { lastX = (int) e.getX(); lastY = (int) e.getY(); }); canvas.setOnMouseDragged(e -> { int dx = lastX - (int) e.getX(); int dy = lastY - (int) e.getY(); lastX = (int) e.getX(); lastY = (int) e.getY(); scene.camera().rotateView((Math.PI / 250) * dx, -(Math.PI / 250) * dy); }); canvas.setOnMouseExited(event -> { // Hide the tooltip so that it won't prevent clicking outside the render canvas. // This is to work around an OpenJDK bug on Linux. tooltip.hide(); }); ContextMenu contextMenu = new ContextMenu(); MenuItem setTarget = new MenuItem("Set target"); setTarget.setOnAction(e -> { scene.camera().setTarget(target.x, target.y); if (scene.getMode() == RenderMode.PREVIEW) { scene.forceReset(); } }); CheckMenuItem showGuides = new CheckMenuItem("Show guides"); showGuides.setSelected(false); showGuides.selectedProperty().addListener((observable, oldValue, newValue) -> { hGuide1.setVisible(newValue); hGuide2.setVisible(newValue); vGuide1.setVisible(newValue); vGuide2.setVisible(newValue); }); Menu canvasScale = new Menu("Canvas scale"); ToggleGroup scaleGroup = new ToggleGroup(); for (int percent : new int[] { 25, 50, 75, 100, 150, 200, 300, 400 }) { RadioMenuItem item = new RadioMenuItem(String.format("%d%%", percent)); item.setToggleGroup(scaleGroup); if (percent == 100) { item.setSelected(true); } item.setOnAction(e -> updateCanvasScale(percent / 100.0)); canvasScale.getItems().add(item); } contextMenu.getItems().addAll(setTarget, showGuides, canvasScale); canvas.setOnMouseClicked(event -> { if (event.getButton() == MouseButton.SECONDARY) { double invHeight = 1.0 / canvas.getHeight(); double halfWidth = canvas.getWidth() / (2.0 * canvas.getHeight()); target.set(-halfWidth + event.getX() * invHeight, -0.5 + event.getY() * invHeight); contextMenu.show(getScene().getWindow(), event.getScreenX(), event.getScreenY()); } }); canvas.setOnScroll(e -> { double diff = -e.getDeltaY() / e.getMultiplierY(); Camera camera = scene.camera(); double value = camera.getFov(); double scale = camera.getMaxFoV() - camera.getMinFoV(); double offset = value / scale; double newValue = scale * Math.exp(Math.log(offset) + 0.1 * diff); if (!Double.isNaN(newValue) && !Double.isInfinite(newValue)) { camera.setFoV(newValue); } }); addEventFilter(KeyEvent.KEY_PRESSED, e -> { double modifier = 1; if (e.isControlDown()) { modifier *= 100; } if (e.isShiftDown()) { modifier *= 0.1; } switch (e.getCode()) { case ESCAPE: hide(); e.consume(); break; case W: scene.camera().moveForward(modifier); e.consume(); break; case S: scene.camera().moveBackward(modifier); e.consume(); break; case A: scene.camera().strafeLeft(modifier); e.consume(); break; case D: scene.camera().strafeRight(modifier); e.consume(); break; case R: scene.camera().moveUp(modifier); e.consume(); break; case F: scene.camera().moveDown(modifier); e.consume(); break; case J: scene.camera().moveBackward(modifier); e.consume(); break; case K: scene.camera().moveForward(modifier); e.consume(); break; case SPACE: synchronized (scene) { if (scene.getMode() == RenderMode.RENDERING) { scene.pauseRender(); } else { scene.startRender(); } renderListener.renderStateChanged(scene.getMode()); } e.consume(); break; } }); setOnShowing(e -> { renderer.setCanvas(this); // Note: the buffer update flag must be copied in SceneProvider.withSceneProtected(). scene.setBufferFinalization(true); }); setOnHiding(e -> { // Note: the buffer update flag must be copied in SceneProvider.withSceneProtected(). scene.setBufferFinalization(false); renderer.setCanvas(RenderManager.EMPTY_CANVAS); }); } private void updateCanvasScale(double scale) { double scaledWidth = canvas.getWidth() * scale; double scaledHeight = canvas.getHeight() * scale; canvas.setLayoutX((scaledWidth - canvas.getWidth()) / 2); canvas.setLayoutY((scaledHeight - canvas.getHeight()) / 2); canvas.setScaleX(scale); canvas.setScaleY(scale); canvasPane.setPrefWidth(scaledWidth); canvasPane.setPrefHeight(scaledHeight); scrollPane.setPrefViewportWidth(scaledWidth); scrollPane.setPrefViewportHeight(scaledHeight); double xInset = getWidth() - scrollPane.getWidth() + 2; double yInset = getHeight() - scrollPane.getHeight() + 2; setWidth(scaledWidth + xInset); setHeight(scaledHeight + yInset); } @Override public void repaint() { if (painting.compareAndSet(false, true)) { forceRepaint(); } } public void forceRepaint() { painting.set(true); renderer.withBufferedImage(bitmap -> { if (bitmap.width == (int) image.getWidth() && bitmap.height == (int) image.getHeight()) { image.getPixelWriter().setPixels(0, 0, bitmap.width, bitmap.height, PIXEL_FORMAT, bitmap.data, 0, bitmap.width); } }); Platform.runLater(() -> { GraphicsContext gc = canvas.getGraphicsContext2D(); gc.drawImage(image, 0, 0); painting.set(false); }); } public void setRenderListener(RenderStatusListener renderListener) { this.renderListener = renderListener; } @Override public void sceneStatus(String status) { Platform.runLater(() -> { if (isShowing() && isFocused()) { tooltip.setText(status); tooltip.show(this, getX(), getY() + getHeight()); } }); } /** * Should only be called on the JavaFX application thread. */ public void setCanvasSize(int width, int height) { canvas.setWidth(width); canvas.setHeight(height); if (image == null || width != image.getWidth() || height != image.getHeight()) { image = new WritableImage(width, height); } updateCanvasScale(canvas.getScaleX()); } }