mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-13 02:32:09 +10:00
Render optimization (#1087)
* Caching and culling improvements * doc * Update canvas-rendering-cache-and-culling.md * caching for wires * cache invalidation improvements * caching improvements * twisted wire performance update * Improved grid rendering on screen and when exporting; PDF export of the grid is now vector-based and does not get blurry on zooming in * area append improvement * bug fix for subtracting trace cuts * Twisted wire stroke render improvement * various performance improvements * lazy area merging * regression update * Update update.xml * code cleanup * Twisted wire border thickness fix
This commit is contained in:
@@ -30,6 +30,33 @@ import java.util.Set;
|
||||
|
||||
public class AreaUtils {
|
||||
|
||||
/**
|
||||
* Unions the areas pairwise, rather than adding each one to a single growing area, which would
|
||||
* make every addition cost as much as everything added before it. The areas in the list are
|
||||
* changed in the process.
|
||||
*
|
||||
* @param areas
|
||||
* @return the union, or an empty area for an empty list
|
||||
*/
|
||||
public static Area union(List<Area> areas) {
|
||||
if (areas.isEmpty()) {
|
||||
return new Area();
|
||||
}
|
||||
List<Area> current = areas;
|
||||
while (current.size() > 1) {
|
||||
List<Area> next = new ArrayList<Area>((current.size() + 1) / 2);
|
||||
for (int i = 0; i < current.size(); i += 2) {
|
||||
Area area = current.get(i);
|
||||
if (i + 1 < current.size()) {
|
||||
area.add(current.get(i + 1));
|
||||
}
|
||||
next.add(area);
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
return current.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges all areas that either overlap or are joined by connections.
|
||||
*
|
||||
|
||||
@@ -21,43 +21,120 @@
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.Area;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* What a component drew while its areas were being recorded: the shapes making up its outline and
|
||||
* its continuity areas, and the bounds of everything it drew. The shapes are merged into areas only
|
||||
* the first time an area is asked for, because recording happens on every drag step for the
|
||||
* components being moved, while the merged areas are needed far less often.
|
||||
*/
|
||||
public class ComponentArea implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int zOrder;
|
||||
private Area outlineArea;
|
||||
private Collection<Area> continuityPositiveAreas;
|
||||
private Collection<Area> continuityNegativeAreas;
|
||||
private List<Shape> outlineParts;
|
||||
// one list per continuity area; its shapes are merged into that area
|
||||
private Collection<List<Shape>> continuityPositiveParts;
|
||||
private Collection<List<Shape>> continuityNegativeParts;
|
||||
// everything the component drew, including parts left out of the outline area such as labels;
|
||||
// null when it was not recorded
|
||||
private Rectangle2D drawnBounds;
|
||||
|
||||
private transient Rectangle2D outlineBounds;
|
||||
private transient Area outlineArea;
|
||||
private transient Collection<Area> continuityPositiveAreas;
|
||||
private transient Collection<Area> continuityNegativeAreas;
|
||||
|
||||
public ComponentArea() {
|
||||
}
|
||||
|
||||
public ComponentArea(int zOrder, Area outlineArea, Collection<Area> continuityPositiveAreas, Collection<Area> continuityNegativeAreas) {
|
||||
public ComponentArea(int zOrder, List<Shape> outlineParts,
|
||||
Collection<List<Shape>> continuityPositiveParts,
|
||||
Collection<List<Shape>> continuityNegativeParts, Rectangle2D drawnBounds) {
|
||||
super();
|
||||
this.zOrder = zOrder;
|
||||
this.outlineArea = outlineArea;
|
||||
this.continuityPositiveAreas = continuityPositiveAreas;
|
||||
this.continuityNegativeAreas = continuityNegativeAreas;
|
||||
this.outlineParts = outlineParts;
|
||||
this.continuityPositiveParts = continuityPositiveParts;
|
||||
this.continuityNegativeParts = continuityNegativeParts;
|
||||
this.drawnBounds = drawnBounds;
|
||||
}
|
||||
|
||||
public int getZOrder() {
|
||||
return zOrder;
|
||||
}
|
||||
|
||||
public Area getOutlineArea() {
|
||||
public boolean hasOutline() {
|
||||
return !outlineParts.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bounds of the outline area, worked out from the recorded shapes without merging them.
|
||||
* A new rectangle on every call.
|
||||
*/
|
||||
public synchronized Rectangle2D getOutlineBounds() {
|
||||
// kept, because the cursor check asks every component on every mouse move
|
||||
if (outlineBounds == null) {
|
||||
Rectangle2D bounds = null;
|
||||
for (Shape part : outlineParts) {
|
||||
if (bounds == null) {
|
||||
bounds = part.getBounds2D();
|
||||
} else {
|
||||
bounds.add(part.getBounds2D());
|
||||
}
|
||||
}
|
||||
outlineBounds = bounds == null ? new Rectangle2D.Double() : bounds;
|
||||
}
|
||||
return (Rectangle2D) outlineBounds.clone();
|
||||
}
|
||||
|
||||
// synchronized because the netlist may be built away from the EDT
|
||||
public synchronized Area getOutlineArea() {
|
||||
if (outlineArea == null) {
|
||||
outlineArea = merge(outlineParts);
|
||||
}
|
||||
return outlineArea;
|
||||
}
|
||||
|
||||
public Collection<Area> getContinuityPositiveAreas() {
|
||||
public synchronized Collection<Area> getContinuityPositiveAreas() {
|
||||
if (continuityPositiveAreas == null) {
|
||||
continuityPositiveAreas = mergeEach(continuityPositiveParts);
|
||||
}
|
||||
return continuityPositiveAreas;
|
||||
}
|
||||
|
||||
public Collection<Area> getContinuityNegativeAreas() {
|
||||
public synchronized Collection<Area> getContinuityNegativeAreas() {
|
||||
if (continuityNegativeAreas == null) {
|
||||
continuityNegativeAreas = mergeEach(continuityNegativeParts);
|
||||
}
|
||||
return continuityNegativeAreas;
|
||||
}
|
||||
}
|
||||
|
||||
public Rectangle2D getDrawnBounds() {
|
||||
return drawnBounds;
|
||||
}
|
||||
|
||||
private static Area merge(List<Shape> parts) {
|
||||
List<Area> areas = new ArrayList<Area>(parts.size());
|
||||
for (Shape part : parts) {
|
||||
// a new Area each time, because merging changes the areas it is given
|
||||
areas.add(new Area(part));
|
||||
}
|
||||
return AreaUtils.union(areas);
|
||||
}
|
||||
|
||||
private static Collection<Area> mergeEach(Collection<List<Shape>> parts) {
|
||||
List<Area> areas = new ArrayList<Area>(parts.size());
|
||||
for (List<Shape> area : parts) {
|
||||
areas.add(merge(area));
|
||||
}
|
||||
return areas;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class Counter {
|
||||
return nanotime;
|
||||
}
|
||||
|
||||
public void add(long nanotime) {
|
||||
public synchronized void add(long nanotime) {
|
||||
count++;
|
||||
this.nanotime += nanotime;
|
||||
}
|
||||
|
||||
@@ -18,15 +18,18 @@
|
||||
package org.diylc.presenter;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Area;
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
@@ -43,19 +46,48 @@ import org.diylc.utils.ScaledBufferedImage;
|
||||
/**
|
||||
* A layer that injects itself between the provided {@link G2DWrapper} instance and the underlying
|
||||
* {@link Graphics2D} instance. For all components that are enabled for caching, it stores a
|
||||
* {@link BufferedImage} representing the component. The state of the component is recorded and
|
||||
* compared next time it needs to be rendered to make sure that the cache is up to date.
|
||||
* {@link BufferedImage} representing the component. The cache does not detect changes to the
|
||||
* component itself: whoever changes a component must {@link #invalidate(IDIYComponent)} it. Zoom,
|
||||
* component state and draw options are recorded and compared each time.
|
||||
*
|
||||
* @author bancika
|
||||
*/
|
||||
public class DrawingCache {
|
||||
|
||||
private Map<IDIYComponent<?>, SoftReference<CacheValue>> imageCache = new HashMap<IDIYComponent<?>, SoftReference<CacheValue>>();
|
||||
|
||||
private Map<String, Pair<Counter, Counter>> renderStatsByType = new HashMap<String, Pair<Counter, Counter>>();
|
||||
// access-ordered, so iteration starts from the least recently used entry; guarded by itself
|
||||
// because bulkPrepare renders from multiple threads
|
||||
private final Map<IDIYComponent<?>, CacheValue> imageCache =
|
||||
new LinkedHashMap<IDIYComponent<?>, CacheValue>(16, 0.75f, true);
|
||||
private long imageCacheBytes = 0;
|
||||
|
||||
// lookups made by bulkPrepare, reported by logStats
|
||||
private final AtomicLong cacheHits = new AtomicLong();
|
||||
private final AtomicLong cacheMissesNotCached = new AtomicLong();
|
||||
private final AtomicLong cacheMissesOutdated = new AtomicLong();
|
||||
// entries that bulkPrepare rendered but were evicted again before the same repaint drew them,
|
||||
// a sign that the visible components alone exceed the budget
|
||||
private final AtomicLong rerenderedWhilePainting = new AtomicLong();
|
||||
|
||||
private final Map<String, Pair<Counter, Counter>> renderStatsByType =
|
||||
new ConcurrentHashMap<String, Pair<Counter, Counter>>();
|
||||
|
||||
// when set, each entry keeps a copy of its component and every cache hit is checked against it,
|
||||
// to find code paths that change a component without invalidating it
|
||||
private final boolean verifyCache = "true".equalsIgnoreCase(System.getProperty(VERIFY_CACHE));
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(DrawingCache.class);
|
||||
|
||||
// evicted images are released straight away rather than lingering behind soft references until
|
||||
// the heap is nearly full, which lets G1 reclaim the large pixel arrays without a full GC
|
||||
private static final long MAX_CACHE_BYTES =
|
||||
Math.min(Runtime.getRuntime().maxMemory() / 4, 768L * 1024 * 1024);
|
||||
|
||||
// 32 MB per image; a large board at high zoom on a HiDPI screen would otherwise take a big share
|
||||
// of the budget on its own
|
||||
private static final long MAX_CACHED_IMAGE_PIXELS = 8L * 1024 * 1024;
|
||||
|
||||
public static String VERIFY_CACHE = "org.diylc.verifyDrawingCache";
|
||||
|
||||
public static DrawingCache Instance = new DrawingCache();
|
||||
|
||||
/**
|
||||
@@ -75,8 +107,10 @@ public class DrawingCache {
|
||||
ComponentType type = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) x.getComponent().getClass());
|
||||
|
||||
if (type != null && type.getEnableCache()) {
|
||||
renderAndCache(x.getComponent(), g2d, x.getState(), outlineMode, project, zoom, scaleFactor, x.getZOrder(), drawOptions);
|
||||
if (type != null && type.getEnableCache()
|
||||
&& shouldCache(x.getComponent(), x.getState(), zoom, scaleFactor)) {
|
||||
renderAndCache(x.getComponent(), g2d, x.getState(), outlineMode, project, zoom, scaleFactor,
|
||||
x.getZOrder(), drawOptions, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -98,16 +132,16 @@ public class DrawingCache {
|
||||
ComponentType type = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) component.getClass());
|
||||
|
||||
if (type != null && type.getEnableCache()) {
|
||||
// if we need to apply caching
|
||||
CacheValue value = null;
|
||||
if (type != null && type.getEnableCache()
|
||||
&& shouldCache(component, componentState, zoom, scaleFactor)) {
|
||||
value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom,
|
||||
scaleFactor, zOrder, drawOptions, false);
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
Point2D firstPoint = component.getControlPoint(0);
|
||||
|
||||
CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom, scaleFactor, zOrder, drawOptions);
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
long componentStart = System.nanoTime();
|
||||
|
||||
// only render the cached image if it's within the clipping region
|
||||
@@ -129,21 +163,17 @@ public class DrawingCache {
|
||||
}
|
||||
|
||||
long componentEnd = System.nanoTime();
|
||||
|
||||
Pair<Counter, Counter> stats;
|
||||
String key = component.getClass().getCanonicalName().replace("org.diylc.components.", "");
|
||||
if (renderStatsByType.containsKey(key)) {
|
||||
stats = renderStatsByType.get(key);
|
||||
} else {
|
||||
stats = new Pair<Counter, Counter>(new Counter(), new Counter());
|
||||
renderStatsByType.put(key, stats);
|
||||
}
|
||||
stats.getSecond().add(componentEnd - componentStart);
|
||||
|
||||
getRenderStats(component).getSecond().add(componentEnd - componentStart);
|
||||
|
||||
// copy over tracking area from the cache, regardless if we rendered or not
|
||||
g2d.merge(value.getCurrentArea(), value.getContinuityPositiveAreas(), value.getContinuityNegativeAreas());
|
||||
g2d.merge(value.getOutlineParts(), value.getContinuityPositiveParts(),
|
||||
value.getContinuityNegativeParts());
|
||||
g2d.mergeDrawnBounds(value.getCacheBounds());
|
||||
} else {
|
||||
// no caching, just draw as usual
|
||||
// either not cached, or nothing was rendered because the component is outside the clip;
|
||||
// drawing it directly still records its areas, which the netlist and continuity checks read,
|
||||
// while Java2D discards the pixels
|
||||
component.draw(g2d, componentState, outlineMode, project, g2d);
|
||||
}
|
||||
g2d.stopTracking();
|
||||
@@ -151,18 +181,20 @@ public class DrawingCache {
|
||||
}
|
||||
|
||||
public void logStats() {
|
||||
int totalSizeMB = imageCache.values()
|
||||
.stream()
|
||||
.map(x -> x.get() == null ? 0 : x.get().image.getData().getDataBuffer().getSize())
|
||||
.reduce(0, Integer::sum) * 4 / 1024 / 1024; // 4 bytes per pixel
|
||||
LOG.debug(String.format("Render cache contains %d elements, approx size is %d MB", imageCache.size(), totalSizeMB));
|
||||
|
||||
long evictedCount = imageCache.values()
|
||||
.stream()
|
||||
.filter(x -> x.get() == null)
|
||||
.count();
|
||||
LOG.debug(String.format("Render cache contains %d evicted entries", evictedCount));
|
||||
|
||||
synchronized (imageCache) {
|
||||
LOG.debug(String.format("Render cache contains %d elements, approx size is %d MB of %d MB",
|
||||
imageCache.size(), imageCacheBytes / 1024 / 1024, MAX_CACHE_BYTES / 1024 / 1024));
|
||||
}
|
||||
|
||||
long hits = cacheHits.get();
|
||||
long missesNotCached = cacheMissesNotCached.get();
|
||||
long missesOutdated = cacheMissesOutdated.get();
|
||||
long lookups = hits + missesNotCached + missesOutdated;
|
||||
LOG.debug(String.format("Render cache hits: %d, misses: %d not cached + %d outdated, "
|
||||
+ "hit rate: %.1f%%, re-rendered while painting: %d",
|
||||
hits, missesNotCached, missesOutdated, lookups == 0 ? 0d : 100d * hits / lookups,
|
||||
rerenderedWhilePainting.get()));
|
||||
|
||||
String mapAsString = renderStatsByType.entrySet().stream().sorted((e1, e2) -> -Long.compare(e1.getValue().getFirst().getNanoTime() + e1.getValue().getSecond().getNanoTime(),
|
||||
e2.getValue().getFirst().getNanoTime() + e2.getValue().getSecond().getNanoTime()))
|
||||
.map(e -> e.getKey() + ": render: " + e.getValue().getFirst() + ", paste: " + e.getValue().getSecond())
|
||||
@@ -172,17 +204,82 @@ public class DrawingCache {
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
imageCache.clear();
|
||||
}
|
||||
|
||||
public void invalidate(Collection<IDIYComponent<?>> components) {
|
||||
for (IDIYComponent<?> component : components) {
|
||||
imageCache.remove(component);
|
||||
synchronized (imageCache) {
|
||||
for (CacheValue value : imageCache.values()) {
|
||||
value.getImage().flush();
|
||||
}
|
||||
imageCache.clear();
|
||||
imageCacheBytes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void invalidate(Collection<IDIYComponent<?>> components) {
|
||||
synchronized (imageCache) {
|
||||
for (IDIYComponent<?> component : components) {
|
||||
release(imageCache.remove(component));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void invalidate(IDIYComponent<?> component) {
|
||||
synchronized (imageCache) {
|
||||
release(imageCache.remove(component));
|
||||
}
|
||||
}
|
||||
|
||||
private CacheValue getCachedValue(IDIYComponent<?> component) {
|
||||
synchronized (imageCache) {
|
||||
return imageCache.get(component);
|
||||
}
|
||||
}
|
||||
|
||||
private void putCachedValue(IDIYComponent<?> component, CacheValue value) {
|
||||
synchronized (imageCache) {
|
||||
release(imageCache.put(component, value));
|
||||
imageCacheBytes += value.getSizeInBytes();
|
||||
// the entry just added is the most recently used one, so it is never evicted here, even if
|
||||
// it alone exceeds the budget
|
||||
Iterator<CacheValue> iterator = imageCache.values().iterator();
|
||||
while (imageCacheBytes > MAX_CACHE_BYTES && imageCache.size() > 1) {
|
||||
CacheValue eldest = iterator.next();
|
||||
iterator.remove();
|
||||
release(eldest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// must be called while holding the imageCache lock
|
||||
private void release(CacheValue value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
imageCacheBytes -= value.getSizeInBytes();
|
||||
// drops the accelerated (VRAM / native) copy Java2D keeps for images drawn repeatedly, which
|
||||
// would otherwise live until the image is finalized
|
||||
value.getImage().flush();
|
||||
}
|
||||
|
||||
private boolean shouldCache(IDIYComponent<?> component, ComponentState componentState,
|
||||
double zoom, double scaleFactor) {
|
||||
// a dragged component changes on every frame, so its image would be discarded right after
|
||||
// rendering while still pushing useful entries out of the cache
|
||||
if (componentState == ComponentState.DRAGGING) {
|
||||
return false;
|
||||
}
|
||||
Rectangle2D rect = component.getCachingBounds();
|
||||
double pixelScale = zoom * scaleFactor;
|
||||
return rect.getWidth() * pixelScale * rect.getHeight() * pixelScale <= MAX_CACHED_IMAGE_PIXELS;
|
||||
}
|
||||
|
||||
private Pair<Counter, Counter> getRenderStats(IDIYComponent<?> component) {
|
||||
String key = component.getClass().getCanonicalName().replace("org.diylc.components.", "");
|
||||
return renderStatsByType.computeIfAbsent(key,
|
||||
k -> new Pair<Counter, Counter>(new Counter(), new Counter()));
|
||||
}
|
||||
|
||||
private CacheValue renderAndCache(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
|
||||
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Set<DrawOption> drawOptions) {
|
||||
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder,
|
||||
Set<DrawOption> drawOptions, boolean preparing) {
|
||||
|
||||
if (!g2d.getClipBounds().intersects(component.getCachingBounds())) {
|
||||
return null;
|
||||
@@ -192,20 +289,18 @@ public class DrawingCache {
|
||||
Point2D firstPoint = component.getControlPoint(0);
|
||||
|
||||
// cache hit!
|
||||
CacheValue value = null;
|
||||
if (imageCache.containsKey(component)) {
|
||||
SoftReference<CacheValue> weakReference = imageCache.get(component);
|
||||
if (weakReference != null) {
|
||||
value = weakReference.get();
|
||||
}
|
||||
}
|
||||
CacheValue value = getCachedValue(component);
|
||||
|
||||
// only honor the cache if the component hasn't changed in the meantime
|
||||
// Also check if drawOptions changed (e.g., MONOCHROMATIC affects rendering)
|
||||
boolean drawOptionsChanged = value == null || !value.getDrawOptions().equals(drawOptions);
|
||||
if (value == null || !value.getComponent().equalsTo(component)
|
||||
|| value.getState() != componentState || value.getZoom() != zoom || value.getOutlineMode() != outlineMode
|
||||
|| value.getScaleFactor() != scaleFactor || drawOptionsChanged) {
|
||||
boolean stale = value == null || value.getState() != componentState || value.getZoom() != zoom
|
||||
|| value.getOutlineMode() != outlineMode || value.getScaleFactor() != scaleFactor
|
||||
|| !value.getDrawOptions().equals(drawOptions);
|
||||
if (!stale && verifyCache && !value.getComponent().equalsTo(component)) {
|
||||
LOG.warn("Cached drawing of " + component.getName() + " of type "
|
||||
+ component.getClass().getName() + " was not invalidated after the component changed");
|
||||
stale = true;
|
||||
}
|
||||
if (stale) {
|
||||
// figure out size and placement of buffer image
|
||||
Rectangle2D rect = component.getCachingBounds();
|
||||
int width = (int)Math.round(rect.getWidth() * zoom);
|
||||
@@ -217,6 +312,14 @@ public class DrawingCache {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!preparing) {
|
||||
rerenderedWhilePainting.incrementAndGet();
|
||||
} else if (value == null) {
|
||||
cacheMissesNotCached.incrementAndGet();
|
||||
} else {
|
||||
cacheMissesOutdated.incrementAndGet();
|
||||
}
|
||||
|
||||
// create image
|
||||
BufferedImage image = new ScaledBufferedImage(width, height, BufferedImage.TYPE_INT_ARGB, scaleFactor);
|
||||
@@ -248,16 +351,8 @@ public class DrawingCache {
|
||||
component.draw(wrapper, componentState, outlineMode, project, wrapper);
|
||||
|
||||
long componentEnd = System.nanoTime();
|
||||
|
||||
Pair<Counter, Counter> stats;
|
||||
String key = component.getClass().getCanonicalName().replace("org.diylc.components.", "");
|
||||
if (renderStatsByType.containsKey(key)) {
|
||||
stats = renderStatsByType.get(key);
|
||||
} else {
|
||||
stats = new Pair<Counter, Counter>(new Counter(), new Counter());
|
||||
renderStatsByType.put(key, stats);
|
||||
}
|
||||
stats.getFirst().add(componentEnd - componentStart);
|
||||
|
||||
getRenderStats(component).getFirst().add(componentEnd - componentStart);
|
||||
|
||||
// finalize wrapper
|
||||
wrapper.finishedDrawingComponent();
|
||||
@@ -273,25 +368,30 @@ public class DrawingCache {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
|
||||
if (wrapper.getCurrentArea() == null || wrapper.getCurrentArea().isEmpty()) {
|
||||
if (wrapper.getOutlineParts().isEmpty()) {
|
||||
LOG.warn("Empty outline area detected for " + component.getName());
|
||||
}
|
||||
|
||||
// add to the cache
|
||||
value = new CacheValue(component, image, wrapper.getCurrentArea(), wrapper.getContinuityPositiveAreas(),
|
||||
wrapper.getContinuityNegativeAreas(), componentState, outlineMode, zoom, scaleFactor, dx, dy, rect, drawOptions);
|
||||
imageCache.put(component, new SoftReference<DrawingCache.CacheValue>(value));
|
||||
value = new CacheValue(component, image, wrapper.getOutlineParts(),
|
||||
wrapper.getContinuityPositiveParts(), wrapper.getContinuityNegativeParts(),
|
||||
componentState, outlineMode, zoom, scaleFactor, dx, dy, rect, drawOptions);
|
||||
putCachedValue(component, value);
|
||||
} else if (preparing) {
|
||||
cacheHits.incrementAndGet();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private class CacheValue {
|
||||
// only kept when verifyCache is set
|
||||
IDIYComponent<?> component;
|
||||
BufferedImage image;
|
||||
Area currentArea;
|
||||
Map<String, Area> continuityPositiveAreas;
|
||||
Map<String, Area> continuityNegativeAreas;
|
||||
// the shapes recorded while rendering, handed to the canvas wrapper on every paste
|
||||
List<Shape> outlineParts;
|
||||
Map<String, List<Shape>> continuityPositiveParts;
|
||||
Map<String, List<Shape>> continuityNegativeParts;
|
||||
ComponentState state;
|
||||
boolean outlineMode;
|
||||
double zoom;
|
||||
@@ -301,21 +401,23 @@ public class DrawingCache {
|
||||
Rectangle2D cacheBounds;
|
||||
Set<DrawOption> drawOptions;
|
||||
|
||||
public CacheValue(IDIYComponent<?> component, BufferedImage image, Area currentArea,
|
||||
Map<String, Area> continuityPositiveAreas, Map<String, Area> continuityNegativeAreas,
|
||||
public CacheValue(IDIYComponent<?> component, BufferedImage image, List<Shape> outlineParts,
|
||||
Map<String, List<Shape>> continuityPositiveParts,
|
||||
Map<String, List<Shape>> continuityNegativeParts,
|
||||
ComponentState state, boolean outlineMode, double zoom, double scaleFactor, int dx, int dy,
|
||||
Rectangle2D cacheBounds, Set<DrawOption> drawOptions) {
|
||||
super();
|
||||
this.cacheBounds = cacheBounds;
|
||||
// take a copy of the component, so we can check if it changed in the meantime
|
||||
try {
|
||||
this.component = component.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
if (verifyCache) {
|
||||
try {
|
||||
this.component = component.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
}
|
||||
}
|
||||
this.image = image;
|
||||
this.currentArea = currentArea;
|
||||
this.continuityPositiveAreas = continuityPositiveAreas;
|
||||
this.continuityNegativeAreas = continuityNegativeAreas;
|
||||
this.outlineParts = outlineParts;
|
||||
this.continuityPositiveParts = continuityPositiveParts;
|
||||
this.continuityNegativeParts = continuityNegativeParts;
|
||||
this.state = state;
|
||||
this.outlineMode = outlineMode;
|
||||
this.zoom = zoom;
|
||||
@@ -333,16 +435,16 @@ public class DrawingCache {
|
||||
return image;
|
||||
}
|
||||
|
||||
public Area getCurrentArea() {
|
||||
return currentArea;
|
||||
public List<Shape> getOutlineParts() {
|
||||
return outlineParts;
|
||||
}
|
||||
|
||||
public Map<String, Area> getContinuityPositiveAreas() {
|
||||
return continuityPositiveAreas;
|
||||
public Map<String, List<Shape>> getContinuityPositiveParts() {
|
||||
return continuityPositiveParts;
|
||||
}
|
||||
|
||||
public Map<String, Area> getContinuityNegativeAreas() {
|
||||
return continuityNegativeAreas;
|
||||
public Map<String, List<Shape>> getContinuityNegativeParts() {
|
||||
return continuityNegativeParts;
|
||||
}
|
||||
|
||||
public ComponentState getState() {
|
||||
@@ -377,9 +479,14 @@ public class DrawingCache {
|
||||
return drawOptions;
|
||||
}
|
||||
|
||||
// TYPE_INT_ARGB, four bytes per pixel
|
||||
public long getSizeInBytes() {
|
||||
return (long) image.getWidth() * image.getHeight() * 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return component.getName() + ":" + state + ":" + zoom;
|
||||
return (component == null ? "" : component.getName() + ":") + state + ":" + zoom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,8 @@ import java.awt.Stroke;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Area;
|
||||
import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.Line2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
@@ -76,6 +74,9 @@ public class DrawingManager {
|
||||
|
||||
private static boolean SHADE_EXTRA_SPACE = true;
|
||||
|
||||
// absorbs antialiasing and the whole-pixel placement of cached images around the drawn bounds
|
||||
private static final double OFF_SCREEN_MARGIN = 4;
|
||||
|
||||
public static String DEBUG_COMPONENT_AREAS = "org.diylc.debugComponentAreas";
|
||||
public static String DEBUG_CONTINUITY_AREAS = "org.diylc.debugContinuityAreas";
|
||||
|
||||
@@ -156,6 +157,9 @@ public class DrawingManager {
|
||||
* @param controlPointSlot
|
||||
* @param componentSlot
|
||||
* @param dragInProgress
|
||||
* @param movingComponents components whose control points the current drag moves, including
|
||||
* those stuck to the dragged ones; they are drawn directly instead of through the cache,
|
||||
* because they change on every frame
|
||||
* @param externalZoom
|
||||
* @param scaleFactor
|
||||
* @param visibleRect
|
||||
@@ -165,8 +169,9 @@ public class DrawingManager {
|
||||
Set<DrawOption> drawOptions, IComponentFilter filter, Rectangle selectionRect,
|
||||
Collection<IDIYComponent<?>> selectedComponents, Set<IDIYComponent<?>> lockedComponents,
|
||||
Set<IDIYComponent<?>> groupedComponents, List<Point2D> controlPointSlot,
|
||||
List<IDIYComponent<?>> componentSlot, boolean dragInProgress, Double externalZoom,
|
||||
Double scaleFactor, Rectangle2D visibleRect) {
|
||||
List<IDIYComponent<?>> componentSlot, boolean dragInProgress,
|
||||
Collection<IDIYComponent<?>> movingComponents, Double externalZoom, Double scaleFactor,
|
||||
Rectangle2D visibleRect) {
|
||||
long totalStartTime = System.nanoTime();
|
||||
failedComponents.clear();
|
||||
if (project == null) {
|
||||
@@ -237,8 +242,8 @@ public class DrawingManager {
|
||||
// calculate size to be rendered
|
||||
double extraSpace = getExtraSpace(project) * zoom;
|
||||
|
||||
drawGrid(project, g2d, zoom, extraSpace, canvasDimension, visibleRect, drawOptions,
|
||||
extraSpaceRect, extraSpaceTx);
|
||||
drawGridAndExtraSpace(project, g2d, zoom, extraSpace, canvasDimension, visibleRect,
|
||||
drawOptions, extraSpaceRect, extraSpaceTx);
|
||||
|
||||
// apply zoom
|
||||
if (Math.abs(1.0 - zoom) > 1e-4) {
|
||||
@@ -268,7 +273,9 @@ public class DrawingManager {
|
||||
}
|
||||
}
|
||||
componentStateMap.put(component, state);
|
||||
componentForRenderList.add(new ComponentForRender(component, state, i));
|
||||
if (!movingComponents.contains(component)) {
|
||||
componentForRenderList.add(new ComponentForRender(component, state, i));
|
||||
}
|
||||
}
|
||||
|
||||
boolean outlineMode = drawOptions.contains(DrawOption.OUTLINE_MODE);
|
||||
@@ -291,11 +298,16 @@ public class DrawingManager {
|
||||
// Do not track the area if component is not invalidated and was
|
||||
// drawn in the same state.
|
||||
boolean trackArea = lastDrawnStateMap.get(component) != state;
|
||||
// areas are only recorded while drawing, so a component is drawn regardless of visibility
|
||||
// until they are
|
||||
boolean offScreen = !trackArea && isOffScreen(component, visibleRect);
|
||||
|
||||
synchronized (this) {
|
||||
g2dWrapper.startedDrawingComponent(i);
|
||||
if (!trackArea) {
|
||||
g2dWrapper.stopTracking();
|
||||
} else {
|
||||
g2dWrapper.startRecordingDrawnBounds();
|
||||
}
|
||||
Composite oldComposite = g2d.getComposite();
|
||||
Font oldFont = g2d.getFont();
|
||||
@@ -306,12 +318,14 @@ public class DrawingManager {
|
||||
// Draw the component through the g2dWrapper.
|
||||
long componentStart = System.nanoTime();
|
||||
try {
|
||||
if (!offScreen) {
|
||||
drawComponent(project, drawOptions, scaleFactor, visibleRect, component, g2dWrapper,
|
||||
state, zoom, i, outlineMode, !movingComponents.contains(component));
|
||||
|
||||
drawComponent(project, drawOptions, scaleFactor, visibleRect, component, g2dWrapper, state, zoom, i, outlineMode);
|
||||
|
||||
if (g2dWrapper.isTrackingContinuityArea()) {
|
||||
LOG.info("Component " + component.getName() + " of type "
|
||||
+ component.getClass().getName() + " did not stop tracking continuity area.");
|
||||
if (g2dWrapper.isTrackingContinuityArea()) {
|
||||
LOG.info("Component " + component.getName() + " of type "
|
||||
+ component.getClass().getName() + " did not stop tracking continuity area.");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error drawing " + component.getName(), e);
|
||||
@@ -324,19 +338,15 @@ public class DrawingManager {
|
||||
g2d.setComposite(oldComposite);
|
||||
g2d.setFont(oldFont);
|
||||
// record render stats
|
||||
long componentEnd = System.nanoTime();
|
||||
Counter stats = null;
|
||||
String key = component.getClass().getCanonicalName().replace("org.diylc.components.", "");
|
||||
if (renderStatsByType.containsKey(key)) {
|
||||
stats = renderStatsByType.get(key);
|
||||
} else {
|
||||
stats = new Counter();
|
||||
renderStatsByType.put(key, stats);
|
||||
if (!offScreen) {
|
||||
String key =
|
||||
component.getClass().getCanonicalName().replace("org.diylc.components.", "");
|
||||
renderStatsByType.computeIfAbsent(key, k -> new Counter())
|
||||
.add(System.nanoTime() - componentStart);
|
||||
}
|
||||
stats.add(componentEnd - componentStart);
|
||||
}
|
||||
ComponentArea area = g2dWrapper.finishedDrawingComponent();
|
||||
if (trackArea && area != null && !area.getOutlineArea().isEmpty()) {
|
||||
if (trackArea && area != null && area.hasOutline()) {
|
||||
componentAreaMap.put(component, area);
|
||||
lastDrawnStateMap.put(component, state);
|
||||
}
|
||||
@@ -608,14 +618,28 @@ public class DrawingManager {
|
||||
|
||||
private static void drawComponent(Project project, Set<DrawOption> drawOptions, Double scaleFactor,
|
||||
Rectangle2D visibleRect, IDIYComponent<?> component, G2DWrapper g2dWrapper, ComponentState state,
|
||||
double zoom, int i, boolean outlineMode) {
|
||||
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) // go through the DrawingCache
|
||||
double zoom, int i, boolean outlineMode, boolean cacheable) {
|
||||
if (cacheable && drawOptions.contains(DrawOption.ENABLE_CACHING)) // go through the DrawingCache
|
||||
DrawingCache.Instance.draw(component, g2dWrapper, state,
|
||||
drawOptions.contains(DrawOption.OUTLINE_MODE), project, zoom, scaleFactor, i, visibleRect, drawOptions);
|
||||
else // go straight to the wrapper
|
||||
component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper);
|
||||
}
|
||||
|
||||
private boolean isOffScreen(IDIYComponent<?> component, Rectangle2D visibleRect) {
|
||||
if (visibleRect == null) {
|
||||
return false;
|
||||
}
|
||||
ComponentArea area = componentAreaMap.get(component);
|
||||
if (area == null || area.getDrawnBounds() == null) {
|
||||
return false;
|
||||
}
|
||||
Rectangle2D bounds = area.getDrawnBounds();
|
||||
return !visibleRect.intersects(bounds.getX() - OFF_SCREEN_MARGIN,
|
||||
bounds.getY() - OFF_SCREEN_MARGIN, bounds.getWidth() + 2 * OFF_SCREEN_MARGIN,
|
||||
bounds.getHeight() + 2 * OFF_SCREEN_MARGIN);
|
||||
}
|
||||
|
||||
private void configureRenderingHints(Graphics2D g2d, Set<DrawOption> drawOptions) {
|
||||
if (drawOptions.contains(DrawOption.ANTIALIASING)) {
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
@@ -651,77 +675,15 @@ public class DrawingManager {
|
||||
RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
|
||||
}
|
||||
|
||||
private double zoomCached = 0;
|
||||
private Dimension canvasDimensionCached;
|
||||
private double extraSpaceCached;
|
||||
private GridType gridTypeCached;
|
||||
private BufferedImage cachedGridImage;
|
||||
|
||||
private void drawGrid(Project project, Graphics2D g2dIn, double zoom, double extraSpace,
|
||||
Dimension canvasDimension, Rectangle2D visibleRect, Set<DrawOption> drawOptions,
|
||||
Rectangle2D extraSpaceRect, AffineTransform extraSpaceTx) {
|
||||
|
||||
GridType gridType = GridType.LINES;
|
||||
private void drawGridAndExtraSpace(Project project, Graphics2D g2dIn, double zoom,
|
||||
double extraSpace, Dimension canvasDimension, Rectangle2D visibleRect,
|
||||
Set<DrawOption> drawOptions, Rectangle2D extraSpaceRect, AffineTransform extraSpaceTx) {
|
||||
|
||||
Dimension innerCanvasDimension = getCanvasDimensions(project, zoom, false);
|
||||
|
||||
// draw from cache
|
||||
if (zoom != zoomCached || !canvasDimension.equals(canvasDimensionCached)
|
||||
|| extraSpace != extraSpaceCached || gridType != gridTypeCached) {
|
||||
try {
|
||||
cachedGridImage = new BufferedImage((int) canvasDimension.getWidth(),
|
||||
(int) canvasDimension.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
// create graphics
|
||||
Graphics2D g2d = cachedGridImage.createGraphics();
|
||||
|
||||
if (drawOptions.contains(DrawOption.GRID) && gridType != GridType.NONE) {
|
||||
double zoomStep = project.getGridSpacing().convertToPixels() * zoom;
|
||||
float gridThickness = (float) (1f * (zoom > 1 ? 1 : zoom));
|
||||
if (gridType == GridType.CROSSHAIR) {
|
||||
g2d.setStroke(new BasicStroke(gridThickness, BasicStroke.CAP_BUTT,
|
||||
BasicStroke.JOIN_MITER, 10f,
|
||||
new float[] {(float) zoomStep / 2, (float) zoomStep / 2}, (float) zoomStep / 4));
|
||||
} else if (gridType == GridType.DOT) {
|
||||
g2d.setStroke(new BasicStroke(gridThickness, BasicStroke.CAP_BUTT,
|
||||
BasicStroke.JOIN_MITER, 10f, new float[] {1f, (float) zoomStep - 1}, 0f));
|
||||
} else {
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchZoomableStroke(gridThickness));
|
||||
}
|
||||
|
||||
g2d.setColor(theme.getGridColor());
|
||||
for (double i = zoomStep; i < canvasDimension.width; i += zoomStep) {
|
||||
g2d.draw(new Line2D.Double(i, 0, i, canvasDimension.height - 1));
|
||||
}
|
||||
for (double j = zoomStep; j < canvasDimension.height; j += zoomStep) {
|
||||
g2d.draw(new Line2D.Double(0, j, canvasDimension.width - 1, j));
|
||||
}
|
||||
// draw dots if needed
|
||||
if (project.getDotSpacing() > 1 && zoomStep > 8) {
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchZoomableStroke(gridThickness * 3));
|
||||
g2d.setColor(theme.getDotColor());
|
||||
for (double i = extraSpace + zoomStep * project.getDotSpacing(); i < extraSpace
|
||||
+ innerCanvasDimension.width; i += zoomStep * project.getDotSpacing())
|
||||
for (double j = extraSpace + zoomStep * project.getDotSpacing(); j < extraSpace
|
||||
+ innerCanvasDimension.height; j += zoomStep * project.getDotSpacing()) {
|
||||
g2d.fillOval((int) Math.round(i - 1), (int) Math.round(j - 1), 3, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
zoomCached = zoom;
|
||||
canvasDimensionCached = (Dimension) canvasDimension.clone();
|
||||
extraSpaceCached = extraSpace;
|
||||
gridTypeCached = gridType;
|
||||
}
|
||||
}
|
||||
|
||||
if (cachedGridImage == null) {
|
||||
LOG.warn("Cached grid image is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (drawOptions.contains(DrawOption.GRID) && gridType != GridType.NONE) {
|
||||
g2dIn.drawImage(cachedGridImage, 0, 0, null);
|
||||
if (drawOptions.contains(DrawOption.GRID)) {
|
||||
GridRenderer.draw(g2dIn, project, theme, zoom, extraSpace, canvasDimension,
|
||||
innerCanvasDimension, visibleRect);
|
||||
}
|
||||
|
||||
// manage extra space
|
||||
@@ -793,7 +755,9 @@ public class DrawingManager {
|
||||
List<IDIYComponent<?>> components = new ArrayList<IDIYComponent<?>>();
|
||||
for (int i = 0; i < project.getComponents().size(); i++) {
|
||||
ComponentArea area = componentAreaMap.get(project.getComponents().get(i));
|
||||
if (area != null && area.getOutlineArea().contains(point)) {
|
||||
// the bounds rule most components out without merging their outline into an area
|
||||
if (area != null && area.getOutlineBounds().contains(point)
|
||||
&& area.getOutlineArea().contains(point)) {
|
||||
components.add(0, project.getComponents().get(i));
|
||||
}
|
||||
}
|
||||
@@ -855,7 +819,7 @@ public class DrawingManager {
|
||||
for (Map.Entry<IDIYComponent<?>, ComponentArea> entry : this.componentAreaMap.entrySet()) {
|
||||
ComponentArea a = entry.getValue();
|
||||
|
||||
if (a == null || a.getOutlineArea() == null)
|
||||
if (a == null)
|
||||
continue;
|
||||
|
||||
IDIYComponent<?> component = entry.getKey();
|
||||
@@ -871,7 +835,9 @@ public class DrawingManager {
|
||||
Collection<Area> positiveAreas = a.getContinuityPositiveAreas();
|
||||
if (positiveAreas != null) {
|
||||
for (Area a1 : positiveAreas) {
|
||||
preliminaryAreas.add(new ContinuityArea(layerId, a1));
|
||||
// a copy, because negative areas are subtracted from it below and the recorded area must
|
||||
// stay whole for when they move
|
||||
preliminaryAreas.add(new ContinuityArea(layerId, new Area(a1)));
|
||||
checkBreakout.add(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,11 +101,16 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
private Font originalFont;
|
||||
private AffineTransform currentTx;
|
||||
private AffineTransform initialTx;
|
||||
private Area currentArea;
|
||||
private Map<String, Area> continuityPositiveAreas;
|
||||
private Map<String, Area> continuityNegativeAreas;
|
||||
// shapes recorded for the outline and continuity areas, merged only when an area is asked for
|
||||
private List<Shape> outlineParts;
|
||||
private Map<String, List<Shape>> continuityPositiveParts;
|
||||
private Map<String, List<Shape>> continuityNegativeParts;
|
||||
// only kept with debugAreaPerformance, to spot shapes that add nothing to the outline
|
||||
private Area debugOutline;
|
||||
private String continuityMarker;
|
||||
private Shape lastShape;
|
||||
private boolean recordingDrawnBounds = false;
|
||||
private Rectangle2D drawnBounds;
|
||||
|
||||
public enum MirrorDirection {
|
||||
NONE, HORIZONTAL, VERTICAL
|
||||
@@ -129,9 +134,10 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
String debugAreaPerformanceStr = System.getProperty(DEBUG_AREA_PERFORMANCE);
|
||||
debugAreaPerformance = "true".equalsIgnoreCase(debugAreaPerformanceStr);
|
||||
this.zoom = zoom;
|
||||
currentArea = new Area();
|
||||
continuityPositiveAreas = new HashMap<String, Area>();
|
||||
continuityNegativeAreas = new HashMap<String, Area>();
|
||||
outlineParts = new ArrayList<Shape>();
|
||||
continuityPositiveParts = new HashMap<String, List<Shape>>();
|
||||
continuityNegativeParts = new HashMap<String, List<Shape>>();
|
||||
debugOutline = new Area();
|
||||
continuityMarker = null;
|
||||
currentTx = new AffineTransform();
|
||||
}
|
||||
@@ -156,9 +162,10 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
public void startedDrawingComponent(int zOrder) {
|
||||
this.zOrder = zOrder;
|
||||
this.drawingComponent = true;
|
||||
this.currentArea = new Area();
|
||||
this.continuityPositiveAreas = new HashMap<String, Area>();
|
||||
this.continuityNegativeAreas = new HashMap<String, Area>();
|
||||
this.outlineParts = new ArrayList<Shape>();
|
||||
this.continuityPositiveParts = new HashMap<String, List<Shape>>();
|
||||
this.continuityNegativeParts = new HashMap<String, List<Shape>>();
|
||||
this.debugOutline = new Area();
|
||||
// trackingStacks.clear();
|
||||
this.continuityMarker = null;
|
||||
this.originalStroke = canvasGraphics.getStroke();
|
||||
@@ -169,6 +176,8 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
this.currentTx = new AffineTransform();
|
||||
this.initialTx = canvasGraphics.getTransform();
|
||||
this.lastShape = null;
|
||||
this.recordingDrawnBounds = false;
|
||||
this.drawnBounds = null;
|
||||
startTracking();
|
||||
}
|
||||
|
||||
@@ -179,36 +188,65 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
*/
|
||||
public ComponentArea finishedDrawingComponent() {
|
||||
drawingComponent = false;
|
||||
recordingDrawnBounds = false;
|
||||
canvasGraphics.setStroke(originalStroke);
|
||||
canvasGraphics.setColor(originalColor);
|
||||
canvasGraphics.setTransform(originalTx);
|
||||
canvasGraphics.setComposite(originalComposite);
|
||||
canvasGraphics.setFont(originalFont);
|
||||
// Map<String, Integer> sorted = sortByComparator(trackingStacks, false);
|
||||
return new ComponentArea(zOrder,currentArea, new ArrayList<Area>(continuityPositiveAreas.values()),
|
||||
new ArrayList<Area>(continuityNegativeAreas.values()));
|
||||
return new ComponentArea(zOrder, outlineParts,
|
||||
new ArrayList<List<Shape>>(continuityPositiveParts.values()),
|
||||
new ArrayList<List<Shape>>(continuityNegativeParts.values()), drawnBounds);
|
||||
}
|
||||
|
||||
// start - used for caching
|
||||
|
||||
public Area getCurrentArea() {
|
||||
return currentArea;
|
||||
public List<Shape> getOutlineParts() {
|
||||
return outlineParts;
|
||||
}
|
||||
|
||||
public Map<String, Area> getContinuityPositiveAreas() {
|
||||
return continuityPositiveAreas;
|
||||
public Map<String, List<Shape>> getContinuityPositiveParts() {
|
||||
return continuityPositiveParts;
|
||||
}
|
||||
|
||||
public Map<String, Area> getContinuityNegativeAreas() {
|
||||
return continuityNegativeAreas;
|
||||
public Map<String, List<Shape>> getContinuityNegativeParts() {
|
||||
return continuityNegativeParts;
|
||||
}
|
||||
|
||||
// end - used for caching
|
||||
|
||||
public void merge(Area currentArea, Map<String, Area> continuityPositiveAreas, Map<String, Area> continuityNegativeAreas) {
|
||||
this.currentArea = currentArea;
|
||||
this.continuityPositiveAreas = continuityPositiveAreas;
|
||||
this.continuityNegativeAreas = continuityNegativeAreas;
|
||||
/**
|
||||
* Takes over shapes recorded earlier, for a component drawn from a cached image instead of
|
||||
* through this wrapper. The lists are shared rather than copied: nothing is recorded for the
|
||||
* component after this, and a new component starts with new lists.
|
||||
*/
|
||||
public void merge(List<Shape> outlineParts, Map<String, List<Shape>> continuityPositiveParts,
|
||||
Map<String, List<Shape>> continuityNegativeParts) {
|
||||
this.outlineParts = outlineParts;
|
||||
this.continuityPositiveParts = continuityPositiveParts;
|
||||
this.continuityNegativeParts = continuityNegativeParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts collecting the bounds of everything the current component draws, regardless of whether
|
||||
* its area is being tracked. They are returned with the area from
|
||||
* {@link #finishedDrawingComponent()}.
|
||||
*/
|
||||
public void startRecordingDrawnBounds() {
|
||||
this.recordingDrawnBounds = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds bounds that are already in project coordinates, for a component that was drawn from a
|
||||
* cached image instead of through this wrapper.
|
||||
*
|
||||
* @param bounds
|
||||
*/
|
||||
public void mergeDrawnBounds(Rectangle2D bounds) {
|
||||
if (recordingDrawnBounds && bounds != null) {
|
||||
unionDrawnBounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -254,45 +292,107 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
Rectangle2D bounds = s.getBounds2D();
|
||||
// Only cache the shape if it's not 1D.
|
||||
if (bounds.getWidth() > 1 && bounds.getHeight() > 1) {
|
||||
Area area = new Area(s);
|
||||
area.transform(currentTx);
|
||||
|
||||
Area areaBefore = null;
|
||||
if (debugAreaPerformance) {
|
||||
areaBefore = new Area(currentArea);
|
||||
}
|
||||
Shape part = copyTransformed(s);
|
||||
|
||||
if (trackingAllowed) {
|
||||
currentArea.add(area);
|
||||
outlineParts.add(part);
|
||||
|
||||
if (debugAreaPerformance && currentArea.equals(areaBefore)) {
|
||||
LOG.warn("Detected unnecessary shape tracking at", new Throwable());
|
||||
if (debugAreaPerformance) {
|
||||
Area areaBefore = new Area(debugOutline);
|
||||
debugOutline.add(new Area(part));
|
||||
if (debugOutline.equals(areaBefore)) {
|
||||
LOG.warn("Detected unnecessary shape tracking at", new Throwable());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Area> toAdd;
|
||||
if (trackingContinuityAllowed) {
|
||||
if (trackingContinuityPositive)
|
||||
toAdd = continuityPositiveAreas;
|
||||
else
|
||||
toAdd = continuityNegativeAreas;
|
||||
Map<String, List<Shape>> toAdd =
|
||||
trackingContinuityPositive ? continuityPositiveParts : continuityNegativeParts;
|
||||
// without a marker every shape is a continuity area of its own
|
||||
String marker = continuityMarker == null ? ("internalUnique" + uniqueCounter++) : continuityMarker;
|
||||
if (continuityMarker == null) {
|
||||
toAdd.put(marker, area);
|
||||
// markStack();
|
||||
} else {
|
||||
Area current = toAdd.getOrDefault(marker, null);
|
||||
if (current == null) {
|
||||
toAdd.put(marker, area);
|
||||
// markStack();
|
||||
} else
|
||||
current.add(area);
|
||||
}
|
||||
toAdd.computeIfAbsent(marker, key -> new ArrayList<Shape>()).add(part);
|
||||
}
|
||||
lastShape = s;
|
||||
}
|
||||
}
|
||||
|
||||
// a copy, because a component may reuse and change a shape after drawing it; copying an Area
|
||||
// shares its curves, which Area operations replace rather than change
|
||||
private Shape copyTransformed(Shape s) {
|
||||
if (!currentTx.isIdentity()) {
|
||||
return currentTx.createTransformedShape(s);
|
||||
}
|
||||
return s instanceof Area ? new Area(s) : new Path2D.Double(s);
|
||||
}
|
||||
|
||||
private void appendDrawnBounds(Shape s) {
|
||||
if (!recordingDrawnBounds || s == null) {
|
||||
return;
|
||||
}
|
||||
unionDrawnBounds(currentTx.createTransformedShape(s.getBounds2D()).getBounds2D());
|
||||
}
|
||||
|
||||
private void appendDrawnOutlineBounds(Shape s) {
|
||||
if (!recordingDrawnBounds) {
|
||||
return;
|
||||
}
|
||||
Stroke stroke = getStroke();
|
||||
if (stroke instanceof BasicStroke) {
|
||||
BasicStroke basicStroke = (BasicStroke) stroke;
|
||||
// square caps reach sqrt(2) half-widths past the outline, miter joins up to the miter limit
|
||||
double reach = basicStroke.getLineJoin() == BasicStroke.JOIN_MITER
|
||||
? Math.max(Math.sqrt(2), basicStroke.getMiterLimit()) : Math.sqrt(2);
|
||||
double pad = basicStroke.getLineWidth() / 2 * reach;
|
||||
Rectangle2D bounds = s.getBounds2D();
|
||||
appendDrawnBounds(new Rectangle2D.Double(bounds.getX() - pad, bounds.getY() - pad,
|
||||
bounds.getWidth() + 2 * pad, bounds.getHeight() + 2 * pad));
|
||||
} else {
|
||||
appendDrawnBounds(stroke.createStrokedShape(s).getBounds2D());
|
||||
}
|
||||
}
|
||||
|
||||
private void appendDrawnStringBounds(String str, float x, float y) {
|
||||
if (!recordingDrawnBounds) {
|
||||
return;
|
||||
}
|
||||
Rectangle2D rect = canvasGraphics.getFontMetrics().getStringBounds(str, canvasGraphics);
|
||||
appendDrawnBounds(new Rectangle2D.Double(rect.getX() + x, rect.getY() + y, rect.getWidth(),
|
||||
rect.getHeight()));
|
||||
}
|
||||
|
||||
private void appendDrawnStringBounds(AttributedCharacterIterator iterator, float x, float y) {
|
||||
if (!recordingDrawnBounds || iterator.getBeginIndex() == iterator.getEndIndex()) {
|
||||
return;
|
||||
}
|
||||
Rectangle2D rect = new TextLayout(iterator, canvasGraphics.getFontRenderContext()).getBounds();
|
||||
appendDrawnBounds(new Rectangle2D.Double(rect.getX() + x, rect.getY() + y, rect.getWidth(),
|
||||
rect.getHeight()));
|
||||
}
|
||||
|
||||
private void appendDrawnImageBounds(Image img, AffineTransform xform, ImageObserver observer) {
|
||||
if (!recordingDrawnBounds || img == null) {
|
||||
return;
|
||||
}
|
||||
int width = img.getWidth(observer);
|
||||
int height = img.getHeight(observer);
|
||||
// the size is unknown until the image has loaded, and nothing is drawn before that
|
||||
if (width < 0 || height < 0) {
|
||||
return;
|
||||
}
|
||||
Rectangle rect = new Rectangle(0, 0, width, height);
|
||||
appendDrawnBounds(xform == null ? rect : xform.createTransformedShape(rect).getBounds2D());
|
||||
}
|
||||
|
||||
private void unionDrawnBounds(Rectangle2D bounds) {
|
||||
if (drawnBounds == null) {
|
||||
drawnBounds = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(),
|
||||
bounds.getHeight());
|
||||
} else {
|
||||
drawnBounds.add(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private void markStack() {
|
||||
StringWriter sw = new StringWriter();
|
||||
@@ -340,6 +440,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void draw(Shape s) {
|
||||
canvasGraphics.draw(s);
|
||||
appendDrawnOutlineBounds(s);
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShapeOutline(s);
|
||||
}
|
||||
@@ -376,30 +477,52 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
} else {
|
||||
canvasGraphics.drawGlyphVector(g, x, y);
|
||||
}
|
||||
if (recordingDrawnBounds) {
|
||||
Rectangle2D bounds = g.getVisualBounds();
|
||||
appendDrawnBounds(new Rectangle2D.Double(bounds.getX() + x, bounds.getY() + y,
|
||||
bounds.getWidth(), bounds.getHeight()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) {
|
||||
// FIXME: process map
|
||||
return canvasGraphics.drawImage(img, xform, obs);
|
||||
boolean result = canvasGraphics.drawImage(img, xform, obs);
|
||||
appendDrawnImageBounds(img, xform, obs);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {
|
||||
canvasGraphics.drawImage(img, op, x, y);
|
||||
// FIXME: process map
|
||||
if (recordingDrawnBounds && img != null) {
|
||||
Rectangle2D bounds = op == null ? img.getRaster().getBounds() : op.getBounds2D(img);
|
||||
appendDrawnBounds(new Rectangle2D.Double(bounds.getX() + x, bounds.getY() + y,
|
||||
bounds.getWidth(), bounds.getHeight()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawRenderableImage(RenderableImage img, AffineTransform xform) {
|
||||
canvasGraphics.drawRenderableImage(img, xform);
|
||||
// FIXME: process map
|
||||
if (recordingDrawnBounds && img != null) {
|
||||
Rectangle2D bounds =
|
||||
new Rectangle2D.Double(img.getMinX(), img.getMinY(), img.getWidth(), img.getHeight());
|
||||
appendDrawnBounds(xform.createTransformedShape(bounds).getBounds2D());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawRenderedImage(RenderedImage img, AffineTransform xform) {
|
||||
canvasGraphics.drawRenderedImage(img, xform);
|
||||
// FIXME: process map
|
||||
if (recordingDrawnBounds && img != null) {
|
||||
Rectangle bounds =
|
||||
new Rectangle(img.getMinX(), img.getMinY(), img.getWidth(), img.getHeight());
|
||||
appendDrawnBounds(xform.createTransformedShape(bounds).getBounds2D());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -412,6 +535,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
} else {
|
||||
canvasGraphics.drawString(str, x, y);
|
||||
}
|
||||
appendDrawnStringBounds(str, x, y);
|
||||
if (drawingComponent && trackingAllowed) {
|
||||
FontMetrics fontMetrics = canvasGraphics.getFontMetrics();
|
||||
Rectangle2D rect = fontMetrics.getStringBounds(str, canvasGraphics);
|
||||
@@ -434,6 +558,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
} else {
|
||||
canvasGraphics.drawString(str, x, y);
|
||||
}
|
||||
appendDrawnStringBounds(str, x, y);
|
||||
if (drawingComponent && trackingAllowed) {
|
||||
FontMetrics fontMetrics = canvasGraphics.getFontMetrics();
|
||||
Rectangle2D rect = fontMetrics.getStringBounds(str, canvasGraphics);
|
||||
@@ -451,6 +576,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
} else {
|
||||
canvasGraphics.drawString(iterator, x, y);
|
||||
}
|
||||
appendDrawnStringBounds(iterator, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -463,6 +589,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
} else {
|
||||
canvasGraphics.drawString(iterator, x, y);
|
||||
}
|
||||
appendDrawnStringBounds(iterator, x, y);
|
||||
}
|
||||
|
||||
private void drawMirroredString(String str, float x, float y) {
|
||||
@@ -529,6 +656,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void fill(Shape s) {
|
||||
canvasGraphics.fill(s);
|
||||
appendDrawnBounds(s);
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShape(s);
|
||||
}
|
||||
@@ -690,6 +818,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void clearRect(int x, int y, int width, int height) {
|
||||
canvasGraphics.clearRect(x, y, width, height);
|
||||
appendDrawnBounds(new Rectangle(x, y, width, height));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -700,6 +829,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void copyArea(int x, int y, int width, int height, int dx, int dy) {
|
||||
canvasGraphics.copyArea(x, y, width, height, dx, dy);
|
||||
appendDrawnBounds(new Rectangle(x + dx, y + dy, width, height));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -715,6 +845,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
|
||||
canvasGraphics.drawArc(x, y, width, height, startAngle, arcAngle);
|
||||
appendDrawnOutlineBounds(new Rectangle(x, y, width, height));
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShapeOutline(new Arc2D.Double(x, y, width, height, startAngle, arcAngle, Arc2D.OPEN));
|
||||
}
|
||||
@@ -724,44 +855,60 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
|
||||
boolean result = canvasGraphics.drawImage(img, x, y, observer);
|
||||
appendShape(new Rectangle2D.Double(x, y, img.getWidth(observer), img.getHeight(observer)));
|
||||
appendDrawnImageBounds(img, AffineTransform.getTranslateInstance(x, y), observer);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) {
|
||||
// FIXME: map
|
||||
return canvasGraphics.drawImage(img, x, y, bgcolor, observer);
|
||||
boolean result = canvasGraphics.drawImage(img, x, y, bgcolor, observer);
|
||||
appendDrawnImageBounds(img, AffineTransform.getTranslateInstance(x, y), observer);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) {
|
||||
// FIXME: map
|
||||
return canvasGraphics.drawImage(img, x, y, width, height, observer);
|
||||
boolean result = canvasGraphics.drawImage(img, x, y, width, height, observer);
|
||||
appendDrawnBounds(new Rectangle(x, y, width, height));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) {
|
||||
// FIXME: map
|
||||
return canvasGraphics.drawImage(img, x, y, width, height, bgcolor, observer);
|
||||
boolean result = canvasGraphics.drawImage(img, x, y, width, height, bgcolor, observer);
|
||||
appendDrawnBounds(new Rectangle(x, y, width, height));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2,
|
||||
ImageObserver observer) {
|
||||
// FIXME: map
|
||||
return canvasGraphics.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer);
|
||||
boolean result =
|
||||
canvasGraphics.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer);
|
||||
appendDrawnBounds(new Rectangle(Math.min(dx1, dx2), Math.min(dy1, dy2), Math.abs(dx2 - dx1),
|
||||
Math.abs(dy2 - dy1)));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2,
|
||||
Color bgcolor, ImageObserver observer) {
|
||||
// FIXME: map
|
||||
return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer);
|
||||
boolean result =
|
||||
canvasGraphics.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer);
|
||||
appendDrawnBounds(new Rectangle(Math.min(dx1, dx2), Math.min(dy1, dy2), Math.abs(dx2 - dx1),
|
||||
Math.abs(dy2 - dy1)));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawLine(int x1, int y1, int x2, int y2) {
|
||||
canvasGraphics.drawLine(x1, y1, x2, y2);
|
||||
appendDrawnOutlineBounds(new Line2D.Double(x1, y1, x2, y2));
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShapeOutline(new Line2D.Double(x1, y1, x2, y2));
|
||||
}
|
||||
@@ -770,6 +917,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void drawOval(int x, int y, int width, int height) {
|
||||
canvasGraphics.drawOval(x, y, width, height);
|
||||
appendDrawnOutlineBounds(new Rectangle(x, y, width, height));
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShapeOutline(new Ellipse2D.Double(x, y, width, height));
|
||||
}
|
||||
@@ -778,6 +926,9 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void drawPolygon(int[] points, int[] points2, int points3) {
|
||||
canvasGraphics.drawPolygon(points, points2, points3);
|
||||
if (points3 > 0) {
|
||||
appendDrawnOutlineBounds(new Polygon(points, points2, points3));
|
||||
}
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShapeOutline(new Polygon(points, points2, points3));
|
||||
}
|
||||
@@ -786,6 +937,9 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void drawPolyline(int[] pointsX, int[] pointsY, int pointCount) {
|
||||
canvasGraphics.drawPolyline(pointsX, pointsY, pointCount);
|
||||
if (pointCount > 0) {
|
||||
appendDrawnOutlineBounds(new Polygon(pointsX, pointsY, pointCount));
|
||||
}
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed) && pointCount > 1) {
|
||||
Path2D path = new Path2D.Double();
|
||||
path.moveTo(pointsX[0], pointsY[0]);
|
||||
@@ -798,6 +952,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {
|
||||
canvasGraphics.drawRoundRect(x, y, width, height, arcWidth, arcHeight);
|
||||
appendDrawnOutlineBounds(new Rectangle(x, y, width, height));
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShapeOutline(new RoundRectangle2D.Double(x, y, width, height, arcWidth, arcHeight));
|
||||
}
|
||||
@@ -806,6 +961,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
|
||||
canvasGraphics.fillArc(x, y, width, height, startAngle, arcAngle);
|
||||
appendDrawnBounds(new Rectangle(x, y, width, height));
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShape(new Arc2D.Double(x, y, width, height, startAngle, arcAngle, Arc2D.PIE));
|
||||
}
|
||||
@@ -814,6 +970,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void fillOval(int x, int y, int width, int height) {
|
||||
canvasGraphics.fillOval(x, y, width, height);
|
||||
appendDrawnBounds(new Rectangle(x, y, width, height));
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShape(new Ellipse2D.Double(x, y, width, height));
|
||||
}
|
||||
@@ -822,6 +979,9 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void fillPolygon(int[] points, int[] points2, int points3) {
|
||||
canvasGraphics.fillPolygon(points, points2, points3);
|
||||
if (points3 > 0) {
|
||||
appendDrawnBounds(new Polygon(points, points2, points3));
|
||||
}
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShape(new Polygon(points, points2, points3));
|
||||
}
|
||||
@@ -830,6 +990,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void drawRect(int x, int y, int width, int height) {
|
||||
canvasGraphics.drawRect(x, y, width, height);
|
||||
appendDrawnOutlineBounds(new Rectangle(x, y, width, height));
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShapeOutline(new Rectangle(x, y, width, height));
|
||||
}
|
||||
@@ -838,6 +999,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void fillRect(int x, int y, int width, int height) {
|
||||
canvasGraphics.fillRect(x, y, width, height);
|
||||
appendDrawnBounds(new Rectangle(x, y, width, height));
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShape(new Rectangle(x, y, width, height));
|
||||
}
|
||||
@@ -846,6 +1008,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
@Override
|
||||
public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {
|
||||
canvasGraphics.fillRoundRect(x, y, width, height, arcWidth, arcHeight);
|
||||
appendDrawnBounds(new Rectangle(x, y, width, height));
|
||||
if (drawingComponent && (trackingAllowed || trackingContinuityAllowed)) {
|
||||
appendShape(new RoundRectangle2D.Double(x, y, width, height, arcWidth, arcHeight));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC 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.
|
||||
|
||||
DIYLC 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 DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.geom.Line2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
import org.diylc.common.GridType;
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.Theme;
|
||||
|
||||
/**
|
||||
* Draws the canvas grid lines and dots directly, limited to the visible part of the canvas.
|
||||
*/
|
||||
public class GridRenderer {
|
||||
|
||||
// closest that grid lines are drawn together, in canvas pixels at the current zoom, before a
|
||||
// dense grid is thinned out
|
||||
public static double MIN_GRID_LINE_SPACING = 3;
|
||||
|
||||
/**
|
||||
* Draws the grid in zoomed canvas coordinates, before the zoom transform is applied.
|
||||
*
|
||||
* @param g2d
|
||||
* @param project
|
||||
* @param theme
|
||||
* @param zoom
|
||||
* @param extraSpace extra space around the project, already multiplied by the zoom
|
||||
* @param canvasDimension canvas size including the extra space
|
||||
* @param innerCanvasDimension canvas size without the extra space
|
||||
* @param visibleRect part of the canvas to draw, or null to draw all of it as exports and
|
||||
* printing do
|
||||
*/
|
||||
public static void draw(Graphics2D g2d, Project project, Theme theme, double zoom,
|
||||
double extraSpace, Dimension canvasDimension, Dimension innerCanvasDimension,
|
||||
Rectangle2D visibleRect) {
|
||||
GridType gridType = GridType.LINES;
|
||||
|
||||
Rectangle2D area =
|
||||
new Rectangle2D.Double(0, 0, canvasDimension.width, canvasDimension.height);
|
||||
if (visibleRect != null) {
|
||||
Rectangle2D.intersect(area, visibleRect, area);
|
||||
}
|
||||
|
||||
double zoomStep = project.getGridSpacing().convertToPixels() * zoom;
|
||||
// a dense grid drawn zoomed out is reduced to every 5th line, every 25th and so on, once its
|
||||
// lines are too close together to be told apart
|
||||
int lineMultiple = 1;
|
||||
while (zoomStep * lineMultiple < MIN_GRID_LINE_SPACING) {
|
||||
lineMultiple *= 5;
|
||||
}
|
||||
double lineStep = zoomStep * lineMultiple;
|
||||
float gridThickness = (float) (1f * (zoom > 1 ? 1 : zoom));
|
||||
|
||||
// grid lines are axis-aligned, so without antialiasing each one is snapped to whole pixels
|
||||
// instead of being smeared across two
|
||||
Object antialiasing = g2d.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
|
||||
try {
|
||||
if (gridType == GridType.CROSSHAIR) {
|
||||
g2d.setStroke(new BasicStroke(gridThickness, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
|
||||
10f, new float[] {(float) zoomStep / 2, (float) zoomStep / 2}, (float) zoomStep / 4));
|
||||
} else if (gridType == GridType.DOT) {
|
||||
g2d.setStroke(new BasicStroke(gridThickness, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
|
||||
10f, new float[] {1f, (float) zoomStep - 1}, 0f));
|
||||
} else {
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchZoomableStroke(gridThickness));
|
||||
}
|
||||
|
||||
g2d.setColor(theme.getGridColor());
|
||||
Line2D line = new Line2D.Double();
|
||||
double maxX = Math.min(area.getMaxX(), canvasDimension.width - 1);
|
||||
double maxY = Math.min(area.getMaxY(), canvasDimension.height - 1);
|
||||
// positions are computed from the line index rather than accumulated, so they do not drift
|
||||
for (long k = Math.max(1, (long) Math.ceil(area.getMinX() / lineStep));
|
||||
k * lineStep <= maxX; k++) {
|
||||
line.setLine(k * lineStep, area.getMinY(), k * lineStep, maxY);
|
||||
g2d.draw(line);
|
||||
}
|
||||
for (long k = Math.max(1, (long) Math.ceil(area.getMinY() / lineStep));
|
||||
k * lineStep <= maxY; k++) {
|
||||
line.setLine(area.getMinX(), k * lineStep, maxX, k * lineStep);
|
||||
g2d.draw(line);
|
||||
}
|
||||
|
||||
if (project.getDotSpacing() > 1 && zoomStep > 8) {
|
||||
g2d.setColor(theme.getDotColor());
|
||||
double dotStep = zoomStep * project.getDotSpacing();
|
||||
// dots are 3 pixels wide, so the ones just outside the visible rect still show partly
|
||||
double dotMinX = area.getMinX() - extraSpace - 2;
|
||||
double dotMinY = area.getMinY() - extraSpace - 2;
|
||||
double dotMaxX = Math.min(area.getMaxX() - extraSpace + 2, innerCanvasDimension.width);
|
||||
double dotMaxY = Math.min(area.getMaxY() - extraSpace + 2, innerCanvasDimension.height);
|
||||
for (long i = Math.max(1, (long) Math.ceil(dotMinX / dotStep)); i * dotStep < dotMaxX;
|
||||
i++) {
|
||||
for (long j = Math.max(1, (long) Math.ceil(dotMinY / dotStep)); j * dotStep < dotMaxY;
|
||||
j++) {
|
||||
g2d.fillOval((int) Math.round(extraSpace + i * dotStep - 1),
|
||||
(int) Math.round(extraSpace + j * dotStep - 1), 3, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC 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.
|
||||
|
||||
DIYLC 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 DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.core.ComponentGroup;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.Project;
|
||||
|
||||
/**
|
||||
* Answers which components are grouped together and which are locked. Both are read on every
|
||||
* repaint and every mouse move, so they are worked out once and kept until {@link #invalidate()}
|
||||
* is called, which must happen wherever the project's components, groups or locks may have
|
||||
* changed. Asking about a different project than last time rebuilds them as well.
|
||||
*/
|
||||
public class GroupAndLockIndex {
|
||||
|
||||
private Project project;
|
||||
private Map<UUID, Set<IDIYComponent<?>>> groups;
|
||||
private Set<IDIYComponent<?>> lockedComponents;
|
||||
|
||||
public void invalidate() {
|
||||
groups = null;
|
||||
lockedComponents = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param project
|
||||
* @param component
|
||||
* @return the project components that share a group with the specified component, matched by
|
||||
* id, or just the component itself when it is not grouped. The set is read-only.
|
||||
*/
|
||||
public Set<IDIYComponent<?>> findGroup(Project project, IDIYComponent<?> component) {
|
||||
checkProject(project);
|
||||
if (groups == null) {
|
||||
groups = buildGroups(project);
|
||||
}
|
||||
Set<IDIYComponent<?>> group = groups.get(component.getId());
|
||||
return group == null ? Collections.<IDIYComponent<?>>singleton(component) : group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param project
|
||||
* @return the project components that are locked, either one by one or through their layer. The
|
||||
* set is read-only.
|
||||
*/
|
||||
public Set<IDIYComponent<?>> getLockedComponents(Project project) {
|
||||
checkProject(project);
|
||||
if (lockedComponents == null) {
|
||||
Set<IDIYComponent<?>> locked = new HashSet<IDIYComponent<?>>();
|
||||
for (IDIYComponent<?> component : project.getComponents()) {
|
||||
if (isLocked(project, component)) {
|
||||
locked.add(component);
|
||||
}
|
||||
}
|
||||
lockedComponents = Collections.unmodifiableSet(locked);
|
||||
}
|
||||
return lockedComponents;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static boolean isLocked(Project project, IDIYComponent<?> component) {
|
||||
ComponentType componentType = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) component.getClass());
|
||||
// for internal-use components
|
||||
if (componentType == null) {
|
||||
return false;
|
||||
}
|
||||
return project.getLockedLayers().contains((int) Math.round(componentType.getZOrder()))
|
||||
|| project.getLockedComponents().contains(component);
|
||||
}
|
||||
|
||||
private void checkProject(Project project) {
|
||||
if (project != this.project) {
|
||||
this.project = project;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
// a component listed in more than one group keeps the first, as a search through the groups in
|
||||
// order would find it
|
||||
private static Map<UUID, Set<IDIYComponent<?>>> buildGroups(Project project) {
|
||||
Map<UUID, List<IDIYComponent<?>>> componentsById = new HashMap<UUID, List<IDIYComponent<?>>>();
|
||||
for (IDIYComponent<?> component : project.getComponents()) {
|
||||
componentsById.computeIfAbsent(component.getId(), id -> new ArrayList<IDIYComponent<?>>())
|
||||
.add(component);
|
||||
}
|
||||
Map<UUID, Set<IDIYComponent<?>>> groups = new HashMap<UUID, Set<IDIYComponent<?>>>();
|
||||
for (ComponentGroup group : project.getGroupsEx()) {
|
||||
if (group.getComponentIds() == null) {
|
||||
continue;
|
||||
}
|
||||
Set<IDIYComponent<?>> members = new HashSet<IDIYComponent<?>>();
|
||||
for (UUID id : group.getComponentIds()) {
|
||||
members.addAll(componentsById.getOrDefault(id, Collections.emptyList()));
|
||||
}
|
||||
Set<IDIYComponent<?>> readOnlyMembers = Collections.unmodifiableSet(members);
|
||||
for (UUID id : group.getComponentIds()) {
|
||||
groups.putIfAbsent(id, readOnlyMembers);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ public class Presenter implements IPlugInPort {
|
||||
// Maps components that have at least one dragged point to set of indices
|
||||
// that designate which of their control points are being dragged.
|
||||
private Map<IDIYComponent<?>, Set<Integer>> controlPointMap;
|
||||
private Set<IDIYComponent<?>> lockedComponents;
|
||||
private GroupAndLockIndex groupAndLockIndex;
|
||||
|
||||
// Utilities
|
||||
// private Cloner cloner;
|
||||
@@ -192,7 +192,7 @@ public class Presenter implements IPlugInPort {
|
||||
this.messageDispatcher = messageDispatcher;
|
||||
plugIns = new ArrayList<IPlugIn>();
|
||||
selectedComponents = new HashSet<IDIYComponent<?>>();
|
||||
lockedComponents = new HashSet<IDIYComponent<?>>();
|
||||
groupAndLockIndex = new GroupAndLockIndex();
|
||||
currentProject = new Project();
|
||||
drawingManager = new DrawingManager(messageDispatcher, configManager);
|
||||
projectFileManager = new ProjectFileManager(messageDispatcher);
|
||||
@@ -280,7 +280,9 @@ public class Presenter implements IPlugInPort {
|
||||
if (!isComponentLocked(component) && isComponentVisible(component)
|
||||
&& operationMode != OperationMode.HIGHLIGHT_CONNECTED_AREAS) {
|
||||
ComponentArea area = drawingManager.getComponentArea(component);
|
||||
if (area != null && area.getOutlineArea() != null && scaledPoint != null && area.getOutlineArea().contains(scaledPoint)) {
|
||||
// the bounds rule most components out without merging their outline into an area
|
||||
if (area != null && scaledPoint != null && area.getOutlineBounds().contains(scaledPoint)
|
||||
&& area.getOutlineArea().contains(scaledPoint)) {
|
||||
return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
|
||||
}
|
||||
}
|
||||
@@ -305,6 +307,7 @@ public class Presenter implements IPlugInPort {
|
||||
public void loadProject(Project project, boolean freshStart, String filename) {
|
||||
LOG.info(String.format("loadProject(%s, %s)", project.getTitle(), freshStart));
|
||||
this.currentProject = project;
|
||||
groupAndLockIndex.invalidate();
|
||||
drawingManager.clearComponentAreaMap();
|
||||
drawingManager.clearContinuityArea();
|
||||
DrawingCache.Instance.clear();
|
||||
@@ -535,7 +538,8 @@ public class Presenter implements IPlugInPort {
|
||||
getLockedComponents(),
|
||||
groupedComponents,
|
||||
Arrays.asList(instantiationManager.getFirstControlPoint(),
|
||||
instantiationManager.getPotentialControlPoint()), componentSlotToDraw, dragInProgress, externalZoom, scaleFactor,
|
||||
instantiationManager.getPotentialControlPoint()), componentSlotToDraw, dragInProgress,
|
||||
getMovingComponents(), externalZoom, scaleFactor,
|
||||
visibleRect);
|
||||
List<String> failedComponentNames = new ArrayList<String>();
|
||||
for (IDIYComponent<?> component : failedComponents) {
|
||||
@@ -719,8 +723,10 @@ public class Presenter implements IPlugInPort {
|
||||
}
|
||||
|
||||
private void notifyProjectModifiedIfNeeded(Project oldProject, String action, boolean clearContinuityArea, boolean repaint) {
|
||||
groupAndLockIndex.invalidate();
|
||||
// Notify the listeners.
|
||||
if (oldProject == null || !currentProject.equals(oldProject)) {
|
||||
invalidateChangedComponents(oldProject);
|
||||
messageDispatcher.dispatchMessage(EventType.PROJECT_MODIFIED, oldProject, currentProject, action);
|
||||
if (clearContinuityArea) {
|
||||
drawingManager.clearContinuityArea();
|
||||
@@ -732,6 +738,41 @@ public class Presenter implements IPlugInPort {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards what was derived from the last drawing of the component, its areas and its cached
|
||||
* image, so that the next repaint draws it afresh.
|
||||
*
|
||||
* @param component
|
||||
*/
|
||||
private void componentChanged(IDIYComponent<?> component) {
|
||||
drawingManager.invalidateComponent(component);
|
||||
DrawingCache.Instance.invalidate(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link #componentChanged(IDIYComponent)} for every component that differs from its
|
||||
* counterpart in the specified snapshot, matched by id. The drawing cache relies on this rather
|
||||
* than comparing each component on every repaint, so every edit must end up here.
|
||||
*
|
||||
* @param oldProject snapshot taken before the edit, or null when there is none
|
||||
*/
|
||||
private void invalidateChangedComponents(Project oldProject) {
|
||||
if (oldProject == null) {
|
||||
DrawingCache.Instance.clear();
|
||||
return;
|
||||
}
|
||||
Map<UUID, IDIYComponent<?>> oldComponents = new HashMap<UUID, IDIYComponent<?>>();
|
||||
for (IDIYComponent<?> component : oldProject.getComponents()) {
|
||||
oldComponents.put(component.getId(), component);
|
||||
}
|
||||
for (IDIYComponent<?> component : currentProject.getComponents()) {
|
||||
IDIYComponent<?> oldComponent = oldComponents.get(component.getId());
|
||||
if (oldComponent == null || !oldComponent.equalsTo(component)) {
|
||||
componentChanged(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addPendingComponentsToProject(Point2D scaledPoint, ComponentType componentTypeSlot, Template template, String[] model,
|
||||
Project oldProject) {
|
||||
List<IDIYComponent<?>> componentSlot = instantiationManager.getComponentSlot();
|
||||
@@ -1060,12 +1101,12 @@ public class Presenter implements IPlugInPort {
|
||||
for (IDIYComponent<?> c : components) {
|
||||
|
||||
ComponentArea compArea = drawingManager.getComponentArea(c);
|
||||
if (compArea == null || compArea.getOutlineArea() == null) {
|
||||
if (compArea == null) {
|
||||
LOG.debug("Area is null for " + c.getName() + " of type " + c.getClass().getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
Rectangle rect = compArea.getOutlineArea().getBounds();
|
||||
Rectangle rect = compArea.getOutlineBounds().getBounds();
|
||||
if (rect.x < minX)
|
||||
minX = rect.x;
|
||||
if (rect.x + rect.width > maxX)
|
||||
@@ -1216,13 +1257,13 @@ public class Presenter implements IPlugInPort {
|
||||
Rectangle2D bounds = null;
|
||||
for (IDIYComponent<?> c : unit) {
|
||||
ComponentArea area = drawingManager.getComponentArea(c);
|
||||
if (area == null || area.getOutlineArea() == null) {
|
||||
if (area == null) {
|
||||
return null;
|
||||
}
|
||||
if (bounds == null) {
|
||||
bounds = area.getOutlineArea().getBounds2D();
|
||||
bounds = area.getOutlineBounds();
|
||||
} else {
|
||||
bounds.add(area.getOutlineArea().getBounds2D());
|
||||
bounds.add(area.getOutlineBounds());
|
||||
}
|
||||
}
|
||||
return bounds;
|
||||
@@ -1769,9 +1810,10 @@ public class Presenter implements IPlugInPort {
|
||||
}
|
||||
|
||||
// only the moved components need their areas re-tracked, rebuilding every area on each drag
|
||||
// step makes dragging slow in large projects
|
||||
// step makes dragging slow in large projects; drag steps do not go through
|
||||
// notifyProjectModifiedIfNeeded, so they invalidate explicitly
|
||||
for (IDIYComponent<?> c : controlPointMap.keySet()) {
|
||||
drawingManager.invalidateComponent(c);
|
||||
componentChanged(c);
|
||||
}
|
||||
drawingManager.clearContinuityArea();
|
||||
|
||||
@@ -1945,7 +1987,8 @@ public class Presenter implements IPlugInPort {
|
||||
for (IDIYComponent<?> component : currentProject.getComponents()) {
|
||||
if (!isComponentLocked(component) && isComponentVisible(component)) {
|
||||
ComponentArea area = drawingManager.getComponentArea(component);
|
||||
if ((area != null && area.getOutlineArea() != null) && (selectionRect != null)
|
||||
if (area != null && selectionRect != null
|
||||
&& area.getOutlineBounds().intersects(selectionRect)
|
||||
&& area.getOutlineArea().intersects(selectionRect)) {
|
||||
newSelection.addAll(findAllGroupedComponents(component));
|
||||
}
|
||||
@@ -1967,6 +2010,7 @@ public class Presenter implements IPlugInPort {
|
||||
// There is selection, so we need to finalize the drag&drop
|
||||
// operation.
|
||||
if (!preDragProject.equals(currentProject)) {
|
||||
invalidateChangedComponents(preDragProject);
|
||||
messageDispatcher.dispatchMessage(EventType.PROJECT_MODIFIED, preDragProject, currentProject, "Drag");
|
||||
drawingManager.clearContinuityArea();
|
||||
projectFileManager.notifyFileChange();
|
||||
@@ -1984,6 +2028,8 @@ public class Presenter implements IPlugInPort {
|
||||
LOG.info(String.format("pasteComponents(%s, %s, %s)", componentTransferable, autoGroup, assignNewNames));
|
||||
instantiationManager.pasteComponents(componentTransferable, this.previousScaledPoint, isSnapToGrid(),
|
||||
currentProject.getGridSpacing(), autoGroup, this.currentProject, assignNewNames);
|
||||
// pasting copies the clipboard's groups into the project straight away
|
||||
groupAndLockIndex.invalidate();
|
||||
messageDispatcher.dispatchMessage(EventType.REPAINT);
|
||||
messageDispatcher.dispatchMessage(EventType.SLOT_CHANGED, instantiationManager.getComponentTypeSlot(),
|
||||
instantiationManager.getFirstControlPoint());
|
||||
@@ -2120,6 +2166,7 @@ public class Presenter implements IPlugInPort {
|
||||
} else {
|
||||
currentProject.getLockedLayers().remove(layerZOrder);
|
||||
}
|
||||
groupAndLockIndex.invalidate();
|
||||
updateSelection(EMPTY_SELECTION);
|
||||
messageDispatcher.dispatchMessage(EventType.LAYER_STATE_CHANGED, currentProject.getLockedLayers());
|
||||
|
||||
@@ -2480,8 +2527,8 @@ public class Presenter implements IPlugInPort {
|
||||
|
||||
// do a rough check, if the outlines don't intersect there's no chance of a match
|
||||
// so don't waste time doing the precise check
|
||||
if (area.getOutlineArea() != null && selectedArea.getOutlineArea() != null &&
|
||||
!area.getOutlineArea().getBounds().intersects(selectedArea.getOutlineArea().getBounds()))
|
||||
if (!area.getOutlineBounds().getBounds()
|
||||
.intersects(selectedArea.getOutlineBounds().getBounds()))
|
||||
continue;
|
||||
|
||||
// create a unified continuity area for both components
|
||||
@@ -2560,6 +2607,15 @@ public class Presenter implements IPlugInPort {
|
||||
currentProject.getGroupsEx().removeIf(
|
||||
group -> group.getComponentIds() != null && group.getComponentIds().stream()
|
||||
.anyMatch(id -> id != null && componentIds.contains(id)));
|
||||
groupAndLockIndex.invalidate();
|
||||
}
|
||||
|
||||
// the components a drag in progress moves: the dragged ones and those stuck to them
|
||||
private Collection<IDIYComponent<?>> getMovingComponents() {
|
||||
if (!dragInProgress || controlPointMap == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return controlPointMap.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2571,16 +2627,7 @@ public class Presenter implements IPlugInPort {
|
||||
* the minimum, set contains that single component.
|
||||
*/
|
||||
private Set<IDIYComponent<?>> findAllGroupedComponents(IDIYComponent<?> component) {
|
||||
|
||||
for (ComponentGroup group : currentProject.getGroupsEx()) {
|
||||
if (group.getComponentIds().stream().anyMatch(id -> Objects.equals(id, component.getId()))) {
|
||||
return currentProject.getComponents().stream()
|
||||
.filter(comp -> group.getComponentIds().contains(comp.getId()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
||||
return new HashSet<>(Set.of(component));
|
||||
return groupAndLockIndex.findGroup(currentProject, component);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3040,13 +3087,7 @@ public class Presenter implements IPlugInPort {
|
||||
}
|
||||
|
||||
private Set<IDIYComponent<?>> getLockedComponents() {
|
||||
lockedComponents.clear();
|
||||
for (IDIYComponent<?> component : currentProject.getComponents()) {
|
||||
if (isComponentLocked(component)) {
|
||||
lockedComponents.add(component);
|
||||
}
|
||||
}
|
||||
return lockedComponents;
|
||||
return groupAndLockIndex.getLockedComponents(currentProject);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3060,6 +3101,7 @@ public class Presenter implements IPlugInPort {
|
||||
} else {
|
||||
currentProject.getLockedComponents().remove(c);
|
||||
}
|
||||
groupAndLockIndex.invalidate();
|
||||
} finally {
|
||||
notifyProjectModifiedIfNeeded(oldProject, locked ? "Lock Component" : "Unlock Component", false, true);
|
||||
|
||||
@@ -3068,15 +3110,8 @@ public class Presenter implements IPlugInPort {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean isComponentLocked(IDIYComponent<?> component) {
|
||||
ComponentType componentType =
|
||||
ComponentProcessor.getInstance().extractComponentTypeFrom(
|
||||
(Class<? extends IDIYComponent<?>>) component.getClass());
|
||||
// for internal-use components
|
||||
if (componentType == null)
|
||||
return false;
|
||||
return currentProject.getLockedLayers().contains((int) Math.round(componentType.getZOrder())) || currentProject.getLockedComponents().contains(component);
|
||||
return GroupAndLockIndex.isLocked(currentProject, component);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -617,10 +617,26 @@
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved performance of 'Project Explorer' pane</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved memory management when caching rendered components</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved grid rendering on screen and when exporting; PDF export of the grid is now vector-based and does not get blurry on zooming in</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed the issue of disappearing components in some edge cases</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed netlists and 'Highlight Connected Areas' giving inconsistent results on boards with trace cuts - running them again could join strips across a cut, and a moved or deleted cut could leave the strip broken where it used to be</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>'Twisted Leads' - the last few crossings near the end of a wire were drawn with the leads overlapping instead of one passing over the other, and crossings could come out slightly different between redraws</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>When a component that belongs to a group is stuck to another component, moving that other component now drags the whole group along instead of only the stuck component</description>
|
||||
|
||||
@@ -6,6 +6,7 @@ import static org.junit.Assert.assertTrue;
|
||||
import java.awt.AlphaComposite;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Area;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
@@ -13,6 +14,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.diylc.appframework.miscutils.InMemoryConfigurationManager;
|
||||
import org.diylc.appframework.simplemq.MessageDispatcher;
|
||||
@@ -151,7 +153,8 @@ public class DrawingManagerTest {
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
|
||||
drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM), null, null,
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null);
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false,
|
||||
Collections.emptySet(), 1.0, 1.0, null);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
@@ -196,7 +199,8 @@ public class DrawingManagerTest {
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
|
||||
drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM), null, null,
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null);
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false,
|
||||
Collections.emptySet(), 1.0, 1.0, null);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
@@ -230,7 +234,8 @@ public class DrawingManagerTest {
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
|
||||
drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM), null, null,
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null);
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false,
|
||||
Collections.emptySet(), 1.0, 1.0, null);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
@@ -261,7 +266,8 @@ public class DrawingManagerTest {
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
|
||||
drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM), null, null,
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null);
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false,
|
||||
Collections.emptySet(), 1.0, 1.0, null);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
@@ -290,7 +296,8 @@ public class DrawingManagerTest {
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
|
||||
drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM), null, null,
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null);
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false,
|
||||
Collections.emptySet(), 1.0, 1.0, null);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
@@ -379,6 +386,352 @@ public class DrawingManagerTest {
|
||||
assertEquals(-1.0, wrapper.getTransform().getScaleY(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOffScreenComponentSkippedOnceAreaIsTracked() {
|
||||
Project project = new Project();
|
||||
DrawCountingComponent component = new DrawCountingComponent();
|
||||
project.getComponents().add(component);
|
||||
DrawingManager drawingManager = createDrawingManager();
|
||||
|
||||
drawVisibleRect(drawingManager, project, new Rectangle2D.Double(0, 0, 100, 100));
|
||||
// the first pass records the area, so it has to draw even though the component is off-screen
|
||||
assertEquals(1, component.drawCount);
|
||||
|
||||
drawVisibleRect(drawingManager, project, new Rectangle2D.Double(0, 0, 100, 100));
|
||||
assertEquals(1, component.drawCount);
|
||||
|
||||
drawVisibleRect(drawingManager, project, new Rectangle2D.Double(400, 400, 200, 200));
|
||||
assertEquals(2, component.drawCount);
|
||||
|
||||
drawVisibleRect(drawingManager, project, null);
|
||||
assertEquals(3, component.drawCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOffScreenComponentDrawnAgainAfterInvalidation() {
|
||||
Project project = new Project();
|
||||
DrawCountingComponent component = new DrawCountingComponent();
|
||||
project.getComponents().add(component);
|
||||
DrawingManager drawingManager = createDrawingManager();
|
||||
|
||||
drawVisibleRect(drawingManager, project, new Rectangle2D.Double(0, 0, 100, 100));
|
||||
drawingManager.invalidateComponent(component);
|
||||
drawVisibleRect(drawingManager, project, new Rectangle2D.Double(0, 0, 100, 100));
|
||||
|
||||
assertEquals(2, component.drawCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUntrackedDrawingKeepsComponentOnScreen() {
|
||||
Project project = new Project();
|
||||
DrawCountingComponent component = new DrawCountingComponent();
|
||||
component.labelPosition = new Point2D.Double(50, 50);
|
||||
project.getComponents().add(component);
|
||||
DrawingManager drawingManager = createDrawingManager();
|
||||
|
||||
drawVisibleRect(drawingManager, project, new Rectangle2D.Double(0, 0, 100, 100));
|
||||
drawVisibleRect(drawingManager, project, new Rectangle2D.Double(0, 0, 100, 100));
|
||||
|
||||
// the label lies outside the selection area but inside the visible rect
|
||||
assertEquals(2, component.drawCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOffScreenCachedComponentContributesContinuity() {
|
||||
Project project = new Project();
|
||||
CachedConductorComponent component = new CachedConductorComponent();
|
||||
project.getComponents().add(component);
|
||||
DrawingManager drawingManager = createDrawingManager();
|
||||
|
||||
drawCachedAndClipped(drawingManager, project, 100);
|
||||
// the netlist and continuity checks are built from the areas recorded while painting
|
||||
assertEquals(1, drawingManager.getContinuityAreas().size());
|
||||
assertEquals(1, component.drawCount);
|
||||
|
||||
drawCachedAndClipped(drawingManager, project, 100);
|
||||
assertEquals(1, component.drawCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCachedComponentRedrawnOnlyAfterInvalidation() {
|
||||
Project project = new Project();
|
||||
CachedConductorComponent component = new CachedConductorComponent();
|
||||
project.getComponents().add(component);
|
||||
DrawingManager drawingManager = createDrawingManager();
|
||||
|
||||
drawCachedAndClipped(drawingManager, project, 1000);
|
||||
drawCachedAndClipped(drawingManager, project, 1000);
|
||||
assertEquals(1, component.drawCount);
|
||||
|
||||
// the cache does not look for changes itself, whoever changes a component invalidates it
|
||||
component.setTestField("changed");
|
||||
drawCachedAndClipped(drawingManager, project, 1000);
|
||||
assertEquals(1, component.drawCount);
|
||||
|
||||
DrawingCache.Instance.invalidate(component);
|
||||
drawCachedAndClipped(drawingManager, project, 1000);
|
||||
assertEquals(2, component.drawCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMovingComponentBypassesCache() {
|
||||
Project project = new Project();
|
||||
CachedConductorComponent component = new CachedConductorComponent();
|
||||
project.getComponents().add(component);
|
||||
DrawingManager drawingManager = createDrawingManager();
|
||||
|
||||
// a component moved along by a drag is drawn directly on every frame...
|
||||
drawCachedAndClipped(drawingManager, project, 1000, Set.of(component));
|
||||
drawCachedAndClipped(drawingManager, project, 1000, Set.of(component));
|
||||
assertEquals(2, component.drawCount);
|
||||
|
||||
// ...and leaves nothing in the cache, so the first ordinary repaint renders it afresh
|
||||
drawCachedAndClipped(drawingManager, project, 1000);
|
||||
assertEquals(3, component.drawCount);
|
||||
}
|
||||
|
||||
private void drawCachedAndClipped(DrawingManager drawingManager, Project project, int size) {
|
||||
drawCachedAndClipped(drawingManager, project, size, Collections.emptySet());
|
||||
}
|
||||
|
||||
private void drawCachedAndClipped(DrawingManager drawingManager, Project project, int size,
|
||||
Set<IDIYComponent<?>> movingComponents) {
|
||||
BufferedImage img = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
// CanvasPanel clips its buffer to the visible rect before drawing
|
||||
g2d.setClip(0, 0, size, size);
|
||||
drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM, DrawOption.ENABLE_CACHING),
|
||||
null, null, Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null,
|
||||
null, false, movingComponents, 1.0, 1.0, new Rectangle2D.Double(0, 0, size, size));
|
||||
g2d.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOutlineAreaUnaffectedByLaterContinuityShapes() {
|
||||
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
|
||||
G2DWrapper wrapper =
|
||||
new G2DWrapper(img.createGraphics(), 1.0, EnumSet.noneOf(DrawOption.class));
|
||||
|
||||
wrapper.startedDrawingComponent(0);
|
||||
wrapper.startTrackingContinuityArea(true);
|
||||
wrapper.setContinuityMarker("lead");
|
||||
// the first shape counts towards both the outline and the marker's continuity area, the second
|
||||
// only towards the continuity area
|
||||
wrapper.fill(new Rectangle2D.Double(0, 0, 10, 10));
|
||||
wrapper.stopTracking();
|
||||
wrapper.fill(new Rectangle2D.Double(50, 50, 10, 10));
|
||||
wrapper.stopTrackingContinuityArea();
|
||||
ComponentArea area = wrapper.finishedDrawingComponent();
|
||||
|
||||
assertEquals(new Rectangle2D.Double(0, 0, 10, 10), area.getOutlineArea().getBounds2D());
|
||||
assertEquals(1, area.getContinuityPositiveAreas().size());
|
||||
assertEquals(new Rectangle2D.Double(0, 0, 60, 60),
|
||||
area.getContinuityPositiveAreas().iterator().next().getBounds2D());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOutlineAreaFollowsComponentTransform() {
|
||||
BufferedImage img = new BufferedImage(200, 200, BufferedImage.TYPE_INT_ARGB);
|
||||
G2DWrapper wrapper =
|
||||
new G2DWrapper(img.createGraphics(), 1.0, EnumSet.noneOf(DrawOption.class));
|
||||
|
||||
wrapper.startedDrawingComponent(0);
|
||||
wrapper.translate(100, 50);
|
||||
wrapper.rotate(Math.PI / 2);
|
||||
wrapper.fill(new Rectangle2D.Double(0, 0, 20, 10));
|
||||
ComponentArea area = wrapper.finishedDrawingComponent();
|
||||
|
||||
// rotated a quarter turn about the translated origin, the 20 x 10 rectangle spans
|
||||
// x 90..100 and y 50..70
|
||||
Rectangle2D bounds = area.getOutlineArea().getBounds2D();
|
||||
assertEquals(90, bounds.getMinX(), 1e-6);
|
||||
assertEquals(100, bounds.getMaxX(), 1e-6);
|
||||
assertEquals(50, bounds.getMinY(), 1e-6);
|
||||
assertEquals(70, bounds.getMaxY(), 1e-6);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecordedShapeUnaffectedByLaterChanges() {
|
||||
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
|
||||
G2DWrapper wrapper =
|
||||
new G2DWrapper(img.createGraphics(), 1.0, EnumSet.noneOf(DrawOption.class));
|
||||
|
||||
wrapper.startedDrawingComponent(0);
|
||||
Rectangle2D shape = new Rectangle2D.Double(0, 0, 10, 10);
|
||||
wrapper.fill(shape);
|
||||
// shapes are merged into areas only when asked for, so the recorded one must be a copy
|
||||
shape.setRect(50, 50, 10, 10);
|
||||
ComponentArea area = wrapper.finishedDrawingComponent();
|
||||
|
||||
assertEquals(new Rectangle2D.Double(0, 0, 10, 10), area.getOutlineArea().getBounds2D());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOutlineBoundsMatchMergedArea() {
|
||||
BufferedImage img = new BufferedImage(200, 200, BufferedImage.TYPE_INT_ARGB);
|
||||
G2DWrapper wrapper =
|
||||
new G2DWrapper(img.createGraphics(), 1.0, EnumSet.noneOf(DrawOption.class));
|
||||
|
||||
wrapper.startedDrawingComponent(0);
|
||||
wrapper.fill(new Rectangle2D.Double(0, 0, 10, 10));
|
||||
wrapper.translate(100, 50);
|
||||
wrapper.fill(new java.awt.geom.Ellipse2D.Double(0, 0, 30, 20));
|
||||
ComponentArea area = wrapper.finishedDrawingComponent();
|
||||
|
||||
Rectangle2D merged = area.getOutlineArea().getBounds2D();
|
||||
Rectangle2D bounds = area.getOutlineBounds();
|
||||
assertEquals(merged.getMinX(), bounds.getMinX(), 1e-6);
|
||||
assertEquals(merged.getMinY(), bounds.getMinY(), 1e-6);
|
||||
assertEquals(merged.getMaxX(), bounds.getMaxX(), 1e-6);
|
||||
assertEquals(merged.getMaxY(), bounds.getMaxY(), 1e-6);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContinuityShapesMergedPerMarker() {
|
||||
BufferedImage img = new BufferedImage(200, 200, BufferedImage.TYPE_INT_ARGB);
|
||||
G2DWrapper wrapper =
|
||||
new G2DWrapper(img.createGraphics(), 1.0, EnumSet.noneOf(DrawOption.class));
|
||||
|
||||
wrapper.startedDrawingComponent(0);
|
||||
wrapper.startTrackingContinuityArea(true);
|
||||
wrapper.setContinuityMarker("pad");
|
||||
wrapper.fill(new Rectangle2D.Double(0, 0, 10, 10));
|
||||
wrapper.fill(new Rectangle2D.Double(5, 0, 10, 10));
|
||||
// without a marker each shape is an area of its own
|
||||
wrapper.setContinuityMarker(null);
|
||||
wrapper.fill(new Rectangle2D.Double(100, 0, 10, 10));
|
||||
wrapper.fill(new Rectangle2D.Double(150, 0, 10, 10));
|
||||
wrapper.stopTrackingContinuityArea();
|
||||
ComponentArea area = wrapper.finishedDrawingComponent();
|
||||
|
||||
assertEquals(3, area.getContinuityPositiveAreas().size());
|
||||
boolean merged = false;
|
||||
for (Area a : area.getContinuityPositiveAreas()) {
|
||||
merged |= a.contains(2, 5) && a.contains(13, 5);
|
||||
}
|
||||
assertTrue(merged);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOutlineAreaIsUnionOfTrackedShapes() {
|
||||
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
|
||||
G2DWrapper wrapper =
|
||||
new G2DWrapper(img.createGraphics(), 1.0, EnumSet.noneOf(DrawOption.class));
|
||||
|
||||
wrapper.startedDrawingComponent(0);
|
||||
wrapper.fill(new Rectangle2D.Double(0, 0, 10, 10));
|
||||
wrapper.fill(new Rectangle2D.Double(50, 50, 10, 10));
|
||||
ComponentArea area = wrapper.finishedDrawingComponent();
|
||||
|
||||
assertTrue(area.getOutlineArea().contains(5, 5));
|
||||
assertTrue(area.getOutlineArea().contains(55, 55));
|
||||
assertTrue(!area.getOutlineArea().contains(30, 30));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContinuityCalculationLeavesRecordedAreasIntact() {
|
||||
Project project = new Project();
|
||||
ConductorComponent strip = new ConductorComponent(new Rectangle2D.Double(0, 0, 100, 10), true);
|
||||
ConductorComponent cut = new ConductorComponent(new Rectangle2D.Double(40, 0, 20, 10), false);
|
||||
project.getComponents().add(strip);
|
||||
project.getComponents().add(cut);
|
||||
DrawingManager drawingManager = createDrawingManager();
|
||||
drawVisibleRect(drawingManager, project, null);
|
||||
|
||||
// the cut splits the strip in two for the netlist...
|
||||
assertEquals(2, drawingManager.getContinuityAreas().size());
|
||||
// ...but the strip's own recorded area must stay whole, so that moving or deleting the cut
|
||||
// does not leave a hole behind
|
||||
Area recorded =
|
||||
drawingManager.getComponentArea(strip).getContinuityPositiveAreas().iterator().next();
|
||||
assertTrue(recorded.contains(50, 5));
|
||||
}
|
||||
|
||||
private void drawVisibleRect(DrawingManager drawingManager, Project project,
|
||||
Rectangle2D visibleRect) {
|
||||
BufferedImage img = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM), null, null,
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false,
|
||||
Collections.emptySet(), 1.0, 1.0, visibleRect);
|
||||
g2d.dispose();
|
||||
}
|
||||
|
||||
public static class DrawCountingComponent extends MockDIYComponent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public int drawCount = 0;
|
||||
public Point2D labelPosition = null;
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
|
||||
Project project, IDrawingObserver drawingObserver) {
|
||||
drawCount++;
|
||||
g2d.fill(new Rectangle2D.Double(500, 500, 20, 20));
|
||||
if (labelPosition != null) {
|
||||
drawingObserver.stopTracking();
|
||||
g2d.drawString("R1", (float) labelPosition.getX(), (float) labelPosition.getY());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ComponentDescriptor(name = "CachedConductor", author = "test", category = "Test",
|
||||
instanceNamePrefix = "C", description = "Cached conductive component for testing",
|
||||
zOrder = IDIYComponent.COMPONENT, enableCache = true)
|
||||
public static class CachedConductorComponent extends MockDIYComponent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public int drawCount = 0;
|
||||
private Point2D controlPoint = new Point2D.Double(500, 500);
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
return controlPoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rectangle2D getCachingBounds() {
|
||||
return new Rectangle2D.Double(490, 490, 40, 40);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
|
||||
Project project, IDrawingObserver drawingObserver) {
|
||||
drawCount++;
|
||||
drawingObserver.startTrackingContinuityArea(true);
|
||||
g2d.fill(new Rectangle2D.Double(500, 500, 20, 20));
|
||||
drawingObserver.stopTrackingContinuityArea();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConductorComponent extends MockDIYComponent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Rectangle2D shape;
|
||||
private boolean positive;
|
||||
|
||||
public ConductorComponent(Rectangle2D shape, boolean positive) {
|
||||
this.shape = shape;
|
||||
this.positive = positive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
|
||||
Project project, IDrawingObserver drawingObserver) {
|
||||
drawingObserver.startTrackingContinuityArea(positive);
|
||||
g2d.fill(shape);
|
||||
drawingObserver.stopTrackingContinuityArea();
|
||||
}
|
||||
}
|
||||
|
||||
// Simple delegating Graphics2D proxy for testing
|
||||
private static class Graphics2DProxy extends Graphics2D {
|
||||
private final Graphics2D delegate;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC 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.
|
||||
|
||||
DIYLC 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 DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.utils.Constants;
|
||||
import org.junit.Test;
|
||||
|
||||
public class GridRendererTest {
|
||||
|
||||
@Test
|
||||
public void testEveryLineDrawnWhenSpacedApart() {
|
||||
// 0.5 mm is 3.94 px at 100%, so 101 lines fit in 400 px
|
||||
assertEquals(101, countVerticalLines(1, new Rectangle2D.Double(0, 0, 400, 300)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDenseGridThinnedToEveryFifthLine() {
|
||||
// 0.5 mm is 2.95 px at 75%, below the minimum, so lines are 14.76 px apart
|
||||
assertEquals(27, countVerticalLines(0.75, new Rectangle2D.Double(0, 0, 400, 300)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlyVisibleRectDrawn() {
|
||||
assertEquals(25, countVerticalLines(1, new Rectangle2D.Double(0, 0, 100, 300)));
|
||||
}
|
||||
|
||||
private int countVerticalLines(double zoom, Rectangle2D visibleRect) {
|
||||
Project project = new Project();
|
||||
project.setGridSpacing(new Size(0.5, SizeUnit.mm));
|
||||
BufferedImage img = new BufferedImage(400, 300, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
GridRenderer.draw(g2d, project, Constants.DEFAULT_THEME, zoom, 0, new Dimension(1000, 1000),
|
||||
new Dimension(1000, 1000), visibleRect);
|
||||
g2d.dispose();
|
||||
|
||||
// the first horizontal line is further down than this row
|
||||
int lines = 0;
|
||||
for (int x = 0; x < img.getWidth(); x++) {
|
||||
if ((img.getRGB(x, 1) >>> 24) != 0) {
|
||||
lines++;
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC 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.
|
||||
|
||||
DIYLC 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 DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.diylc.core.ComponentGroup;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.testcomponents.MockDIYComponent;
|
||||
import org.junit.Test;
|
||||
|
||||
public class GroupAndLockIndexTest {
|
||||
|
||||
@Test
|
||||
public void testGroupMembersFoundById() {
|
||||
Project project = new Project();
|
||||
MockDIYComponent a = add(project);
|
||||
MockDIYComponent b = add(project);
|
||||
MockDIYComponent single = add(project);
|
||||
group(project, a, b);
|
||||
GroupAndLockIndex index = new GroupAndLockIndex();
|
||||
|
||||
assertEquals(Set.of(a, b), index.findGroup(project, a));
|
||||
assertEquals(Set.of(a, b), index.findGroup(project, b));
|
||||
assertEquals(Set.of(single), index.findGroup(project, single));
|
||||
|
||||
// a component outside the project, such as one being placed, is matched by its id
|
||||
MockDIYComponent copy = new MockDIYComponent();
|
||||
copy.setId(a.getId());
|
||||
assertEquals(Set.of(a, b), index.findGroup(project, copy));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGroupChangesSeenAfterInvalidation() {
|
||||
Project project = new Project();
|
||||
MockDIYComponent a = add(project);
|
||||
MockDIYComponent b = add(project);
|
||||
group(project, a, b);
|
||||
GroupAndLockIndex index = new GroupAndLockIndex();
|
||||
assertEquals(Set.of(a, b), index.findGroup(project, a));
|
||||
|
||||
project.getGroupsEx().clear();
|
||||
index.invalidate();
|
||||
|
||||
assertEquals(Set.of(a), index.findGroup(project, a));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnotherProjectRebuildsWithoutInvalidation() {
|
||||
Project first = new Project();
|
||||
MockDIYComponent a = add(first);
|
||||
MockDIYComponent b = add(first);
|
||||
group(first, a, b);
|
||||
GroupAndLockIndex index = new GroupAndLockIndex();
|
||||
assertEquals(Set.of(a, b), index.findGroup(first, a));
|
||||
|
||||
Project second = new Project();
|
||||
second.getComponents().add(a);
|
||||
|
||||
assertEquals(Set.of(a), index.findGroup(second, a));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockedComponentsIncludeLockedLayers() {
|
||||
Project project = new Project();
|
||||
MockDIYComponent a = add(project);
|
||||
MockDIYComponent b = add(project);
|
||||
GroupAndLockIndex index = new GroupAndLockIndex();
|
||||
assertTrue(index.getLockedComponents(project).isEmpty());
|
||||
|
||||
project.getLockedComponents().add(a);
|
||||
index.invalidate();
|
||||
assertEquals(Set.of(a), index.getLockedComponents(project));
|
||||
|
||||
project.getLockedLayers().add(IDIYComponent.COMPONENT);
|
||||
index.invalidate();
|
||||
assertEquals(Set.of(a, b), index.getLockedComponents(project));
|
||||
}
|
||||
|
||||
private MockDIYComponent add(Project project) {
|
||||
MockDIYComponent component = new MockDIYComponent();
|
||||
project.getComponents().add(component);
|
||||
return component;
|
||||
}
|
||||
|
||||
private void group(Project project, IDIYComponent<?>... components) {
|
||||
Set<UUID> ids = new HashSet<UUID>();
|
||||
for (IDIYComponent<?> component : components) {
|
||||
ids.add(component.getId());
|
||||
}
|
||||
project.getGroupsEx().add(new ComponentGroup(ids));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user