Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • eclipse/escet/escet
  • droy2/escet
  • ameer/escet
  • frankvlaar/escet
  • bartwesselink/escet
  • mreniers/escet
  • sthuijsman/escet
  • jverbakel/escet
  • pvberkel/escet
  • riklubking/escet
  • mbaubek/escet
  • wytseoortwijn/escet
  • mgoorden7u4/escet
  • robertb/escet-cif
  • fgurr/escet
  • ribafish/eclipse-escet
  • timokopmels/escet
  • tbeijloos/escet
  • brentelia/escet
19 results
Show changes
Commits on Source (49)
Showing
with 457 additions and 106 deletions
......@@ -20,7 +20,9 @@ import org.eclipse.escet.common.app.framework.Paths;
import org.eclipse.escet.common.app.framework.exceptions.InputOutputException;
import org.eclipse.escet.common.app.framework.exceptions.InvalidInputException;
import org.eclipse.escet.common.eclipse.ui.ControlEditor;
import org.eclipse.escet.common.eclipse.ui.RenderedImage;
import org.eclipse.escet.common.java.Assert;
import org.eclipse.escet.common.svg.Dimensions;
import org.eclipse.escet.common.svg.SvgCanvas;
import org.eclipse.escet.common.svg.SvgException;
import org.eclipse.escet.common.svg.SvgVisualizer;
......@@ -177,20 +179,19 @@ public class ChiSvgOutput extends ChiFileHandle {
private void displaySvg() {
if (!firstDone) {
try {
canvas.updateImageSize();
canvas.updateImageAndCanvasSizes();
} catch (InvalidInputException ex) {
String msg = fmt("Failed to update image size for SVG image file \"%s\".", svgPath);
String msg = fmt("Failed to update image and canvas sizes for SVG image file \"%s\".", svgPath);
throw new InvalidInputException(msg, ex);
}
}
int width = canvas.getImageWidth();
int height = canvas.getImageHeight();
byte[] pixelData = canvas.paintInMemory(width, height);
Dimensions dims = canvas.getImageDimensions();
byte[] pixelData = canvas.paintInMemory(dims.width(), dims.height());
Assert.check(pixelData.length > 0);
synchronized (canvas.pixelDataLock) {
canvas.pixelData = pixelData;
synchronized (canvas.renderedImageLock) {
canvas.renderedImage = new RenderedImage(pixelData, dims.width(), dims.height());
}
if (!firstDone) {
......@@ -232,9 +233,7 @@ public class ChiSvgOutput extends ChiFileHandle {
if (canvas.isDisposed()) {
return;
}
int width = canvas.getImageWidth();
int height = canvas.getImageHeight();
canvas.redraw(0, 0, width, height, true);
canvas.redraw();
}
});
......
......@@ -16,6 +16,8 @@ package org.eclipse.escet.cif.simulator.output.plotviz;
import java.awt.Graphics2D;
import org.eclipse.escet.common.eclipse.ui.G2dSwtCanvas;
import org.eclipse.escet.common.eclipse.ui.RenderedImage;
import org.eclipse.escet.common.java.Assert;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.graphics.Point;
......@@ -27,13 +29,22 @@ public class PlotVisualizerCanvas extends G2dSwtCanvas {
/** The chart to display on the canvas. */
private final XYChart chart;
/** The lock to use to synchronize on {@link #pixels} to ensure thread safety. */
private final Object pixelsLock = new Object();
/** The lock object to use to synchronize access to the {@link #imageData}. */
private final Object imageDataLock = new Object();
/** The last rendered pixels, or {@code null} if not available. */
private byte[] pixels;
/**
* The last rendered image, or {@code null} if not available. All read and write access to this field must be
* synchronized using the {@link #imageDataLock} lock object to prevent synchronization issues.
*/
private RenderedImage imageData = null;
/** The lock object to use to synchronize access to {@link #lastSize}. */
public final Object lastSizeLock = new Object();
/** The last known size of the canvas, or {@code null} if not available. */
/**
* The last known size of the canvas, or {@code null} if not available. All read and write access to this field must
* be synchronized using the {@link #lastSizeLock} lock object to prevent synchronization issues.
*/
private Point lastSize;
/**
......@@ -55,20 +66,26 @@ public class PlotVisualizerCanvas extends G2dSwtCanvas {
* @return The last known size of the canvas, or {@code null}.
*/
public Point getLastSize() {
return lastSize;
synchronized (lastSizeLock) {
return lastSize;
}
}
/** Sets up the canvas. */
private void setupCanvas() {
// Store current size.
lastSize = getSize();
synchronized (lastSizeLock) {
lastSize = getSize();
}
// Ensure proper updating when canvas is resized.
addControlListener(new ControlListener() {
@Override
public void controlResized(ControlEvent e) {
// Set new size, create new image and force redraw.
lastSize = getSize();
synchronized (lastSizeLock) {
lastSize = getSize();
}
updatePixels();
redraw();
}
......@@ -81,11 +98,11 @@ public class PlotVisualizerCanvas extends G2dSwtCanvas {
}
@Override
public byte[] getImageToPaint(int width, int height) {
// Use pre-rendered pixel data, if available.
synchronized (pixelsLock) {
if (pixels != null) {
return pixels;
public RenderedImage getImageToPaint(int width, int height) {
// Use pre-rendered image, if available.
synchronized (imageDataLock) {
if (imageData != null) {
return imageData;
}
}
......@@ -93,11 +110,20 @@ public class PlotVisualizerCanvas extends G2dSwtCanvas {
return super.getImageToPaint(width, height);
}
/** Renders the chart and updates the pre-rendered pixel data. */
/** Renders the chart and updates the pre-rendered image. */
public void updatePixels() {
byte[] newPixels = paintInMemory(lastSize.x, lastSize.y);
synchronized (pixelsLock) {
pixels = newPixels;
int width = 0;
int height = 0;
synchronized (lastSizeLock) {
Assert.notNull(lastSize);
width = lastSize.x;
height = lastSize.y;
}
byte[] newPixels = paintInMemory(width, height);
RenderedImage newImageData = new RenderedImage(newPixels, width, height);
synchronized (imageDataLock) {
imageData = newImageData;
}
}
......
......@@ -20,6 +20,7 @@ import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.escet.cif.simulator.options.FrameRateOption;
import org.eclipse.escet.cif.simulator.options.SimulationSpeedOption;
import org.eclipse.escet.cif.simulator.options.TestModeOption;
import org.eclipse.escet.common.eclipse.ui.RenderedImage;
import org.eclipse.escet.common.java.Pair;
import org.eclipse.escet.common.svg.SvgCanvas;
import org.eclipse.escet.common.svg.SvgVisualizer;
......@@ -61,10 +62,10 @@ public class SvgPaintThread extends Thread {
private static final int QUEUE_SIZE = 1;
/**
* Queue of unprocessed items (model time and pixel data). Is modified in-place. Model time {@code -1} indicates a
* Queue of unprocessed items (model time, rendered image). Is modified in-place. Model time {@code -1} indicates a
* shutdown request.
*/
private final BlockingQueue<Pair<Double, byte[]>> queue = new LinkedBlockingQueue<>(QUEUE_SIZE);
private final BlockingQueue<Pair<Double, RenderedImage>> queue = new LinkedBlockingQueue<>(QUEUE_SIZE);
/** The SVG visualizer. */
public final SvgVisualizer visualizer;
......@@ -139,9 +140,9 @@ public class SvgPaintThread extends Thread {
/**
* Adds data to the queue. This method blocks for as long as the queue is full.
*
* @param data The data (model time and pixel data) to add. Model time {@code -1} indicates a shutdown request.
* @param data The data (model time, rendered image) to add. Model time {@code -1} indicates a shutdown request.
*/
public void addData(Pair<Double, byte[]> data) {
public void addData(Pair<Double, RenderedImage> data) {
try {
queue.put(data);
} catch (InterruptedException e) {
......@@ -149,6 +150,16 @@ public class SvgPaintThread extends Thread {
}
}
/**
* Adds data to the queue if possible. This method does not block when the queue is full. It then simply does not
* add the data at all.
*
* @param data The data (model time, rendered image) to add. Model time {@code -1} indicates a shutdown request.
*/
public void offerData(Pair<Double, RenderedImage> data) {
queue.offer(data);
}
@Override
public void run() {
try {
......@@ -179,7 +190,7 @@ public class SvgPaintThread extends Thread {
while (true) {
// Take the data of the next image (call blocks while queue is
// empty).
Pair<Double, byte[]> data;
Pair<Double, RenderedImage> data;
try {
data = queue.take();
} catch (InterruptedException e) {
......@@ -187,7 +198,9 @@ public class SvgPaintThread extends Thread {
}
double modelTime = data.left;
byte[] pixelData = data.right;
byte[] pixelData = data.right.pixelData();
int width = data.right.width();
int height = data.right.height();
// If we get a shutdown request, we shutdown ourselves. The
// visualizer remains open, to be closed by the user.
......@@ -258,29 +271,30 @@ public class SvgPaintThread extends Thread {
}
}
// Set new current pixel data (of the rendered image).
synchronized (canvas.pixelDataLock) {
canvas.pixelData = pixelData;
// Provide the newly rendered image to the canvas.
synchronized (canvas.renderedImageLock) {
canvas.renderedImage = new RenderedImage(pixelData, width, height);
}
// Force repaint, on UI thread.
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
if (canvas.isDisposed()) {
return;
}
int width = canvas.getImageWidth();
int height = canvas.getImageHeight();
canvas.redraw(0, 0, width, height, true);
Runnable redraw = () -> {
if (canvas.isDisposed()) {
return;
}
});
canvas.redraw();
};
// If this is the first image, let the visualizer know that it may
// now show the image, as it has just been rendered.
if (first) {
// First image. Synchronously request to redraw the canvas, to ensure the image is shown as soon as
// possible. Then let the visualizer know that it may now show the image, as it was just or soon will
// be visible.
Display.getDefault().syncExec(redraw);
visualizer.initDone();
first = false;
} else {
// Non-first image. Asynchronously request to redraw the canvas, to prevent mutual deadlock with the
// SVG render thread while zooming.
Display.getDefault().asyncExec(redraw);
}
}
}
......
......@@ -22,10 +22,13 @@ import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.escet.cif.simulator.input.SvgInputComponent;
import org.eclipse.escet.cif.simulator.options.FrameRateOption;
import org.eclipse.escet.cif.simulator.options.TestModeOption;
import org.eclipse.escet.cif.simulator.runtime.CifSimulatorException;
import org.eclipse.escet.cif.simulator.runtime.model.RuntimeState;
import org.eclipse.escet.common.eclipse.ui.RenderedImage;
import org.eclipse.escet.common.java.Assert;
import org.eclipse.escet.common.svg.Dimensions;
import org.eclipse.escet.common.svg.SvgCanvas;
import org.eclipse.escet.common.svg.SvgVisualizer;
import org.eclipse.escet.common.svg.selector.SvgSelector;
......@@ -90,7 +93,16 @@ public class SvgRenderThread extends Thread {
private final Semaphore testModeSemaphore;
/** The exception that occurred in this thread, or {@code null} if not applicable. */
public AtomicReference<Throwable> exception = new AtomicReference<>();
public final AtomicReference<Throwable> exception = new AtomicReference<>();
/** The lock object to use to synchronize access to {@link #lastTime}. */
private final Object lastTimeLock = new Object();
/**
* Timestamp of the last frame. All read and write access to this field must be synchronized using the
* {@link #lastTimeLock} lock object to prevent synchronization issues.
*/
private double lastTime = 0.0;
/**
* Constructor for the {@link SvgRenderThread} class.
......@@ -109,6 +121,34 @@ public class SvgRenderThread extends Thread {
this.paintThread = paintThread;
this.testMode = TestModeOption.isEnabled();
this.testModeSemaphore = this.testMode ? new Semaphore(0) : null;
// Register callback to redraw the last frame when the canvas changes size.
boolean doBlock = !FrameRateOption.isRealTimeEnabled();
this.canvas.registerCanvasSizeChangedCallback(() -> redrawFrame(doBlock));
}
/**
* Redraw the last frame.
*
* @param doBlock If {@code true} forces a redraw of the last frame by blocking until the request is queued. If
* {@code false} skips the request for redrawing the last frame when the queue is full.
*/
private void redrawFrame(boolean doBlock) {
RenderedImage r = render();
// Only create a new thread when requiring to block.
if (doBlock) {
Runnable runnable = () -> {
synchronized (lastTimeLock) {
queue(doBlock, lastTime, r);
}
};
new Thread(runnable, getClass().getName() + ".redrawFrame").run();
} else {
synchronized (lastTimeLock) {
queue(doBlock, lastTime, r);
}
}
}
/**
......@@ -194,7 +234,7 @@ public class SvgRenderThread extends Thread {
signalSyncLock();
// Shutdown.
paintThread.addData(pair(-1.0, (byte[])null));
paintThread.addData(pair(-1.0, new RenderedImage(null, 0, 0)));
return;
}
......@@ -232,7 +272,7 @@ public class SvgRenderThread extends Thread {
// Just before the first render, update the canvas size.
if (first) {
try {
canvas.updateImageSize();
canvas.updateImageAndCanvasSizes();
} catch (CifSimulatorException ex) {
String msg = fmt("Failed to update image size for SVG image file \"%s\".",
cifSvgDecls.getSvgRelPath());
......@@ -241,17 +281,42 @@ public class SvgRenderThread extends Thread {
first = false;
}
// Render the SVG image in memory, to obtain the pixel data.
int width = canvas.getImageWidth();
int height = canvas.getImageHeight();
byte[] pixelData = canvas.paintInMemory(width, height);
Assert.check(pixelData.length > 0);
// Send model time and pixel data to the paint thread.
paintThread.addData(pair(state.getTime(), pixelData));
double newLastTime = state.getTime();
synchronized (lastTimeLock) {
lastTime = newLastTime;
}
queue(true, newLastTime, render());
// Signal that the state has been processed.
signalSyncLock();
}
}
/**
* Render the SVG image in memory.
*
* @return The rendered image.
*/
private RenderedImage render() {
Dimensions dims = canvas.getImageDimensions();
byte[] pixelData = canvas.paintInMemory(dims.width(), dims.height());
Assert.check(pixelData.length > 0);
return new RenderedImage(pixelData, dims.width(), dims.height());
}
/**
* Send model time and rendered image to the paint thread.
*
* @param doBlock Block and wait for queue to accept data as long as it is full ({@code true}), or skip queuing if
* the queue is full ({@code false}).
* @param time The model time to pass to the paint thread.
* @param renderedImage The rendered image.
*/
private void queue(boolean doBlock, double time, RenderedImage renderedImage) {
if (doBlock) {
paintThread.addData(pair(time, renderedImage));
} else {
paintThread.offerData(pair(time, renderedImage));
}
}
}
......@@ -106,11 +106,13 @@ public abstract class G2dSwtCanvas extends Canvas implements PaintListener, G2dS
int h = getBounds().height;
// Paint the image in-memory, and obtain the pixel data.
byte[] imgData = getImageToPaint(w, h);
RenderedImage renderedImage = getImageToPaint(w, h);
// Construct SWT image from array, and draw it.
PaletteData pal = new PaletteData(0xFF, 0xFF00, 0xFF0000);
ImageData data = new ImageData(w, h, 24, pal, w * 3, imgData);
int scanlinePad = renderedImage.width() * 3;
ImageData data = new ImageData(renderedImage.width(), renderedImage.height(), 24, pal, scanlinePad,
renderedImage.pixelData());
Image img = new Image(getDisplay(), data);
e.gc.drawImage(img, 0, 0);
img.dispose();
......@@ -134,10 +136,10 @@ public abstract class G2dSwtCanvas extends Canvas implements PaintListener, G2dS
*
* @param width The width of the image, in pixels.
* @param height The height of the image, in pixels.
* @return The pixel data, in {@link BufferedImage#TYPE_3BYTE_BGR} format.
* @return The rendered image.
*/
public byte[] getImageToPaint(int width, int height) {
return paintInMemory(width, height);
public RenderedImage getImageToPaint(int width, int height) {
return new RenderedImage(paintInMemory(width, height), width, height);
}
/**
......
//////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2023 Contributors to the Eclipse Foundation
//
// See the NOTICE file(s) distributed with this work for additional
// information regarding copyright ownership.
//
// This program and the accompanying materials are made available
// under the terms of the MIT License which is available at
// https://opensource.org/licenses/MIT
//
// SPDX-License-Identifier: MIT
//////////////////////////////////////////////////////////////////////////////
package org.eclipse.escet.common.eclipse.ui;
import java.awt.image.BufferedImage;
/**
* Data for a rendered image.
*
* @param pixelData The pixel data, in {@link BufferedImage#TYPE_3BYTE_BGR} format.
* @param width The width of the image, in pixels.
* @param height The height of the image, in pixels.
*/
public record RenderedImage(byte[] pixelData, int width, int height) {
// Nothing to do here. Just storage.
}
//////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2023 Contributors to the Eclipse Foundation
//
// See the NOTICE file(s) distributed with this work for additional
// information regarding copyright ownership.
//
// This program and the accompanying materials are made available
// under the terms of the MIT License which is available at
// https://opensource.org/licenses/MIT
//
// SPDX-License-Identifier: MIT
//////////////////////////////////////////////////////////////////////////////
package org.eclipse.escet.common.svg;
/**
* Dimensions.
*
* @param width The width in pixels.
* @param height The height in pixels.
*/
public record Dimensions(int width, int height) {
// Nothing to do here. Just storage.
}
......@@ -32,6 +32,7 @@ import org.apache.batik.gvt.GraphicsNode;
import org.eclipse.core.runtime.Platform;
import org.eclipse.escet.common.app.framework.exceptions.InvalidInputException;
import org.eclipse.escet.common.eclipse.ui.G2dSwtCanvas;
import org.eclipse.escet.common.eclipse.ui.RenderedImage;
import org.eclipse.escet.common.java.Assert;
import org.eclipse.escet.common.java.Strings;
import org.eclipse.swt.graphics.Rectangle;
......@@ -43,6 +44,31 @@ import org.w3c.dom.Document;
/** SVG canvas, used to display an SVG image (an SVG document). */
public class SvgCanvas extends G2dSwtCanvas {
/** Default SVG scaling factor. */
private static final double NO_SCALING = 1.0;
/** The minimum image scale factor. */
private static final double SCALE_MIN = 0.05;
/** Step size of the scale factor while zooming. */
private static final double ZOOM_STEP = 0.05;
/** The maximum image size is limited to 'width * height = DIMENSIONS_MAX'. */
private static final int DIMENSIONS_MAX = 80_000_000;
/** The lock object to use to synchronize access to {@link #scale}. */
private final Object scaleLock = new Object();
/**
* Scale factor of the SVG image.
*
* <p>
* All read and write access to this field must be synchronized using the {@link #scaleLock} lock object to prevent
* synchronization issues.
* </p>
*/
private double scale = NO_SCALING;
/**
* The SVG document: the XML document of the SVG image to display on the canvas. The document may be modified
* in-place, but should always be locked (using the document itself as locking object) while modifying or reading
......@@ -62,31 +88,38 @@ public class SvgCanvas extends G2dSwtCanvas {
/** The user agent to which the bridge context reports exceptions. */
private final SvgUserAgent userAgent;
/** The width of the SVG image, in pixels. */
private int width;
/** The lock object to use to synchronize access to {@link #dimensions}. */
private final Object dimensionsLock = new Object();
/** The height of the SVG image, in pixels. */
private int height;
/**
* The dimensions of the SVG image.
*
* <p>
* All read and write access to this field must be synchronized using the {@link #dimensionsLock} lock object to
* prevent synchronization issues.
* </p>
*/
private Dimensions dimensions = new Dimensions(0, 0);
/**
* Whether to use {@link #pixelData} ({@code true}) or let the canvas render the SVG image by itself
* Whether to use {@link #renderedImage} ({@code true}) or let the canvas render the SVG image by itself
* ({@code false}).
*/
public final boolean usePixelData;
private final boolean useAlreadyRenderedImage;
/** The lock object to use to synchronize the {@link #pixelData}. */
public final Object pixelDataLock = new Object();
/** The lock object to use to synchronize access to the {@link #renderedImage}. */
public final Object renderedImageLock = new Object();
/**
* The current pixel data (image) to paint. Must be {@code null} if not used ({@code #usePixelData} is
* The current rendered image to paint. Must be {@code null} if not used ({@code #useAlreadyRenderedImage} is
* {@code false}). May be {@code null} otherwise, if the image is not yet available (not yet rendered).
*
* <p>
* All read and write access to this field must be synchronized using the {@link #pixelDataLock} lock object, to
* All read and write access to this field must be synchronized using the {@link #renderedImageLock} lock object, to
* prevent synchronization issues.
* </p>
*/
public byte[] pixelData = null;
public RenderedImage renderedImage = null;
/**
* The absolute or relative local file system path to the SVG image file from which the SVG document was loaded, or
......@@ -94,20 +127,23 @@ public class SvgCanvas extends G2dSwtCanvas {
*/
public String path = null;
/** Callback function to invoke when the canvas changes size. May be {@code null} if no callback is registered. */
private Runnable canvasSizeChangedCallback = null;
/**
* Constructor for the {@link SvgCanvas} class.
*
* @param parent The parent of the SVG canvas.
* @param document The SVG document: the XML document of the SVG image to display on the canvas.
* @param usePixelData Whether to use {@link #pixelData} ({@code true}) or let the canvas render the SVG image by
* itself ({@code false}).
* @param useAlreadyRenderedImage Whether to use the last already rendered image if available ({@code true}) or let
* the canvas render the SVG image by itself ({@code false}).
* @throws SvgException If the SVG image file can not be processed, because it is invalid. The caller should wrap
* this with a message that includes the path to the SVG image file.
*/
public SvgCanvas(Composite parent, Document document, boolean usePixelData) {
public SvgCanvas(Composite parent, Document document, boolean useAlreadyRenderedImage) {
super(parent);
this.document = document;
this.usePixelData = usePixelData;
this.useAlreadyRenderedImage = useAlreadyRenderedImage;
// Construct user agent, bridge context, and graphics node.
userAgent = new SvgUserAgent();
......@@ -127,8 +163,12 @@ public class SvgCanvas extends G2dSwtCanvas {
}
}
// Register interaction handler.
this.addKeyListener(new SvgUserInteractionHandler(this));
// Get the dimensions.
updateImageSize();
setScale(NO_SCALING);
updateImageAndCanvasSizes();
}
/**
......@@ -162,21 +202,75 @@ public class SvgCanvas extends G2dSwtCanvas {
}
/**
* Returns the width of the SVG image, in pixels.
* Returns the dimensions of the SVG image.
*
* @return The width of the SVG image, in pixels.
* @return The dimensions of the SVG image.
*/
public int getImageWidth() {
return width;
public Dimensions getImageDimensions() {
synchronized (dimensionsLock) {
return this.dimensions;
}
}
/**
* Returns the height of the SVG image, in pixels.
* Sets the dimensions of the SVG image.
*
* @return The height of the SVG image, in pixels.
* @param width The width of the SVG image, in pixels.
* @param height The height of the SVG image, in pixels.
*/
public int getImageHeight() {
return height;
private void setImageDimensions(int width, int height) {
synchronized (dimensionsLock) {
this.dimensions = new Dimensions(width, height);
}
}
/**
* Sets the scale factor, but cuts of the scale factor at {@link #SCALE_MIN} to never scale below it. The maximum
* scale factor is limited by pixel size 'width * height' to a maximum of {@link #DIMENSIONS_MAX} to prevent out of
* memory issues.
*
* @param scale The image scale factor.
*/
private void setScale(double scale) {
Dimensions dims = getImageDimensions();
synchronized (scaleLock) {
this.scale = scale < SCALE_MIN ? SCALE_MIN
: (dims.width() * dims.height()) > DIMENSIONS_MAX ? this.scale : scale;
}
}
/**
* Zoom in one step.
*
* @see #setScale
*/
public void zoomIn() {
synchronized (scaleLock) {
setScale(this.scale - ZOOM_STEP);
}
}
/**
* Zoom out one step.
*
* @see #setScale
*/
public void zoomOut() {
synchronized (scaleLock) {
setScale(this.scale + ZOOM_STEP);
}
}
/**
* Return the scale factor.
*
* @return The image scale factor.
*/
public double getScale() {
synchronized (scaleLock) {
return scale;
}
}
@Override
......@@ -195,6 +289,13 @@ public class SvgCanvas extends G2dSwtCanvas {
g.setColor(java.awt.Color.WHITE);
g.fillRect(0, 0, w, h);
// Scale the image to zoom in or out.
double currentScale;
synchronized (scaleLock) {
currentScale = this.scale;
}
g.scale(currentScale, currentScale);
// Paint the actual image. We make sure that painting the image and
// modifying the image are interleaved.
synchronized (document) {
......@@ -215,13 +316,22 @@ public class SvgCanvas extends G2dSwtCanvas {
}
}
/**
* Register a callback function to invoke when the canvas changes size.
*
* @param canvasSizeChangedCallback Callback function.
*/
public void registerCanvasSizeChangedCallback(Runnable canvasSizeChangedCallback) {
this.canvasSizeChangedCallback = canvasSizeChangedCallback;
}
@Override
public byte[] getImageToPaint(int width, int height) {
// Use pre-rendered pixel data.
if (usePixelData) {
synchronized (pixelDataLock) {
Assert.notNull(pixelData);
return pixelData;
public RenderedImage getImageToPaint(int width, int height) {
// Use pre-rendered image.
if (useAlreadyRenderedImage) {
synchronized (renderedImageLock) {
Assert.notNull(renderedImage);
return renderedImage;
}
}
......@@ -237,9 +347,11 @@ public class SvgCanvas extends G2dSwtCanvas {
* @throws IOException If saving the image file fails, or if the file extension is unknown or unsupported.
*/
public void saveImage(String fileName) throws IOException {
Dimensions dims = getImageDimensions();
// Paint SVG image to a buffered image using Graphics2D.
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
paint(createGraphics(image), width, height);
BufferedImage image = new BufferedImage(dims.width(), dims.height(), BufferedImage.TYPE_INT_RGB);
paint(createGraphics(image), dims.width(), dims.height());
// Write image to file.
File outFile = new File(fileName);
......@@ -252,7 +364,7 @@ public class SvgCanvas extends G2dSwtCanvas {
}
/**
* Updates the canvas to reflect the current size of the SVG image.
* Update the current size of the SVG image and resize the canvas to reflect the image size and scale factor.
*
* <p>
* If running standalone (outside of Eclipse), the client area of the {@link Shell} of the canvas is also resized to
......@@ -265,7 +377,7 @@ public class SvgCanvas extends G2dSwtCanvas {
*
* @throws InvalidInputException If the SVG image width or height is not positive.
*/
public void updateImageSize() {
public void updateImageAndCanvasSizes() {
// Invoke on UI thread if current thread is not a UI thread.
if (Display.getCurrent() == null) {
final InvalidInputException[] ex = {null};
......@@ -276,7 +388,7 @@ public class SvgCanvas extends G2dSwtCanvas {
return;
}
try {
SvgCanvas.this.updateImageSize();
SvgCanvas.this.updateImageAndCanvasSizes();
} catch (InvalidInputException e) {
ex[0] = e;
}
......@@ -292,8 +404,15 @@ public class SvgCanvas extends G2dSwtCanvas {
synchronized (document) {
// Update image width/height.
Dimension2D docsize = bridgeContext.getDocumentSize();
width = (int)Math.ceil(docsize.getWidth());
height = (int)Math.ceil(docsize.getHeight());
double scale;
synchronized (scaleLock) {
scale = this.scale;
}
int width = (int)Math.ceil(docsize.getWidth() * scale);
int height = (int)Math.ceil(docsize.getHeight() * scale);
setImageDimensions(width, height);
// Check width/height.
if (width <= 0) {
......@@ -305,8 +424,9 @@ public class SvgCanvas extends G2dSwtCanvas {
throw new InvalidInputException(msg);
}
// Resize canvas.
// Resize canvas, and request a redraw.
setSize(width, height);
redraw();
// Resize shell to the size of the image, if running outside of
// Eclipse workbench.
......@@ -321,5 +441,11 @@ public class SvgCanvas extends G2dSwtCanvas {
shell.setSize(shellWidth, shellHeight);
}
}
// Trigger canvas size updated callback, if registered.
// TODO: What is the intended use of userAgent.problem? Report problem only ones!
if (canvasSizeChangedCallback != null && userAgent.problem == null) {
canvasSizeChangedCallback.run();
}
}
}
//////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2023 Contributors to the Eclipse Foundation
//
// See the NOTICE file(s) distributed with this work for additional
// information regarding copyright ownership.
//
// This program and the accompanying materials are made available
// under the terms of the MIT License which is available at
// https://opensource.org/licenses/MIT
//
// SPDX-License-Identifier: MIT
//////////////////////////////////////////////////////////////////////////////
package org.eclipse.escet.common.svg;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
/**
* Handler for user interaction events on an {@link SvgCanvas SVG canvas}.
*
* <p>
* Supports the following interactions:
* <ul>
* <li>If 'Ctrl'+'=' / 'Ctrl'+'+' is pressed, the SVG image zooms in.</li>
* <li>If 'Ctrl'+'-' is pressed, the SVG image zooms out.</li>
* </ul>
* </p>
*/
class SvgUserInteractionHandler extends KeyAdapter {
/** The SVG canvas that this handler interacts with. */
private final SvgCanvas canvas;
/**
* Constructor for the {@link SvgUserInteractionHandler} class.
*
* @param canvas The SVG canvas that this handler interacts with.
*/
public SvgUserInteractionHandler(SvgCanvas canvas) {
this.canvas = canvas;
}
@Override
public void keyPressed(KeyEvent e) {
if ((e.stateMask & (SWT.CTRL | SWT.COMMAND)) > 0) { // 'Ctrl' or 'Command' key is currently being pressed down.
// Zoom in.
if (e.keyCode == '=' || e.keyCode == SWT.KEYPAD_ADD) { // Since 'shift' + '=' = '+', use '='.
canvas.zoomOut();
canvas.updateImageAndCanvasSizes();
}
// Zoom out.
if (e.keyCode == '-' || e.keyCode == SWT.KEYPAD_SUBTRACT) {
canvas.zoomIn();
canvas.updateImageAndCanvasSizes();
}
}
}
}
......@@ -124,7 +124,7 @@ public class SvgSelector implements MouseListener, MouseMoveListener, G2dSwtPain
* @param canvas The canvas to use for interaction with the user.
* @param interactiveIds The ids of the interactive SVG elements.
* @param idQueue The queue of SVG element ids of the interactive SVG elements on which the user has clicked, but
* that have not yet been processed further. For thread-safety, {@link ConcurrentLinkedQueue} only instances are
* that have not yet been processed further. For thread-safety, only {@link ConcurrentLinkedQueue} instances are
* allowed.
*/
public SvgSelector(final SvgCanvas canvas, Set<String> interactiveIds, Queue<Pair<SvgSelector, String>> idQueue) {
......@@ -204,8 +204,8 @@ public class SvgSelector implements MouseListener, MouseMoveListener, G2dSwtPain
}
// Process mouse event.
int x = event.event.x;
int y = event.event.y;
int x = (int)Math.round(event.event.x / canvas.getScale());
int y = (int)Math.round(event.event.y / canvas.getScale());
updateHoverInfo(x, y);
switch (event.type) {
case DOWN:
......@@ -290,7 +290,15 @@ public class SvgSelector implements MouseListener, MouseMoveListener, G2dSwtPain
String id = null;
// Obtain the graphics node for the given coordinates.
node = canvas.getGraphicsNode().nodeHitAt(new Point2D.Double(x, y));
try {
node = canvas.getGraphicsNode().nodeHitAt(new Point2D.Double(x, y));
} catch (NullPointerException ex) {
// Ignore assuming no node at coordinates.
} catch (IllegalArgumentException ex) {
// Ignore assuming no node at coordinates.
} catch (ArrayIndexOutOfBoundsException ex) {
// Ignore assuming no node at coordinates.
}
// Keep looking for an interactive graphics node, while traveling up
// the graphics node hierarchy.
......