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:
@@ -42,7 +42,7 @@ import org.diylc.utils.Constants;
|
||||
@ComponentDescriptor(name = "Hookup Wire", author = "Branislav Stojkovic", category = "Connectivity",
|
||||
instanceNamePrefix = "W", description = "Flexible wire with two control points", zOrder = IDIYComponent.WIRING,
|
||||
flexibleZOrder = true, bomPolicy = BomPolicy.NEVER_SHOW, creationMethod = CreationMethod.POINT_BY_POINT,
|
||||
transformer = SimpleComponentTransformer.class, enableCache = false)
|
||||
transformer = SimpleComponentTransformer.class, enableCache = true)
|
||||
public class HookupWire extends AbstractCurvedComponent<AWG> implements IContinuity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ import org.diylc.utils.Constants;
|
||||
@ComponentDescriptor(name = "Right Angle Wire", author = "Branislav Stojkovic", category = "Connectivity",
|
||||
instanceNamePrefix = "W", description = "Wire that runs horizontally and vertically and turns at right angles",
|
||||
zOrder = IDIYComponent.WIRING, flexibleZOrder = true, bomPolicy = BomPolicy.NEVER_SHOW,
|
||||
transformer = OrthogonalComponentTransformer.class, enableCache = false, creationMethod = CreationMethod.POINT_BY_POINT)
|
||||
transformer = OrthogonalComponentTransformer.class, enableCache = true, creationMethod = CreationMethod.POINT_BY_POINT)
|
||||
public class OrthogonalWire extends AbstractOrthogonalComponent<AWG> implements IContinuity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
+182
-66
@@ -20,6 +20,7 @@ package org.diylc.components.connectivity;
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Shape;
|
||||
import java.awt.Stroke;
|
||||
import java.awt.geom.Area;
|
||||
import java.awt.geom.CubicCurve2D;
|
||||
@@ -29,9 +30,7 @@ import java.awt.geom.PathIterator;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.diylc.common.LineStyle;
|
||||
import org.diylc.common.ObjectCache;
|
||||
@@ -45,6 +44,7 @@ import org.diylc.core.IDrawingObserver;
|
||||
import org.diylc.core.annotations.BomPolicy;
|
||||
import org.diylc.core.annotations.ComponentDescriptor;
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
import org.diylc.presenter.AreaUtils;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
@ComponentDescriptor(name = "Twisted Leads", author = "Branislav Stojkovic",
|
||||
@@ -78,6 +78,8 @@ public class TwistedWire extends AbstractCurvedComponent<Void> implements IConti
|
||||
transient private Area firstLeadStripeArea = null;
|
||||
transient private Area secondLeadStripeArea = null;
|
||||
|
||||
transient private LeadGeometry leadGeometry = null;
|
||||
|
||||
@Override
|
||||
protected Color getDefaultColor() {
|
||||
return COLOR;
|
||||
@@ -97,6 +99,89 @@ public class TwistedWire extends AbstractCurvedComponent<Void> implements IConti
|
||||
@Override
|
||||
protected void drawCurve(Path2D curve, Graphics2D g2d, ComponentState componentState,
|
||||
IDrawingObserver drawingObserver) {
|
||||
// stroking paints some pixels more than once, which would show through on a translucent wire,
|
||||
// so that case builds each lead as an area and paints it once instead
|
||||
if (getAlpha().getValue().doubleValue() < MAX_ALPHA) {
|
||||
drawCurveWithAreas(curve, g2d, componentState, drawingObserver);
|
||||
} else {
|
||||
drawCurveWithStrokes(curve, g2d, componentState, drawingObserver);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawCurveWithStrokes(Path2D curve, Graphics2D g2d, ComponentState componentState,
|
||||
IDrawingObserver drawingObserver) {
|
||||
int thickness = getThickness();
|
||||
if (leadGeometry == null) {
|
||||
leadGeometry = buildLeadGeometry(curve, thickness);
|
||||
}
|
||||
boolean selected =
|
||||
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING;
|
||||
boolean normal = componentState == ComponentState.NORMAL;
|
||||
Color firstColor = selected ? SELECTION_COLOR : color;
|
||||
Color secondColor = selected ? SELECTION_COLOR : color2;
|
||||
|
||||
float outlineWidth = 0;
|
||||
if (normal) {
|
||||
// as wide as the 1px outline of a regular wire: the canvas narrows a thin stroke to keep it
|
||||
// one pixel wide when zoomed in, so the stroke it ends up with gives that width
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
|
||||
outlineWidth = ((BasicStroke) g2d.getStroke()).getLineWidth();
|
||||
}
|
||||
|
||||
drawLead(g2d, leadGeometry.firstLead, firstColor, normal && stripe1, stripeColor,
|
||||
outlineWidth, thickness, drawingObserver);
|
||||
drawLead(g2d, leadGeometry.secondLead, secondColor, normal && stripe2, stripeColor2,
|
||||
outlineWidth, thickness, drawingObserver);
|
||||
|
||||
// the second lead was drawn last and lies on top everywhere; redrawing the first lead inside
|
||||
// its own crossings puts it on top there. Both leads share one colour when selected.
|
||||
if (!selected) {
|
||||
Shape oldClip = g2d.getClip();
|
||||
g2d.clip(leadGeometry.firstOnTop);
|
||||
drawLead(g2d, leadGeometry.firstLead, firstColor, normal && stripe1, stripeColor,
|
||||
outlineWidth, thickness, null);
|
||||
g2d.setClip(oldClip);
|
||||
}
|
||||
}
|
||||
|
||||
// drawingObserver is null for a redraw that must not be recorded again; outlineWidth is 0 for
|
||||
// no outline
|
||||
private void drawLead(Graphics2D g2d, Path2D lead, Color leadColor, boolean stripe,
|
||||
Color leadStripeColor, float outlineWidth, int thickness, IDrawingObserver drawingObserver) {
|
||||
boolean outline = outlineWidth > 0;
|
||||
float innerWidth = Math.max(thickness - outlineWidth, 1);
|
||||
// the first stroke covers the whole lead, so it is the one recorded as the component's area
|
||||
if (drawingObserver != null) {
|
||||
drawingObserver.startTracking();
|
||||
}
|
||||
if (outline) {
|
||||
// a dark band centred on the lead's edge, like an outline drawn around an area
|
||||
g2d.setColor(leadColor.darker());
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(thickness + outlineWidth));
|
||||
g2d.draw(lead);
|
||||
if (drawingObserver != null) {
|
||||
drawingObserver.stopTracking();
|
||||
}
|
||||
g2d.setColor(leadColor);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(innerWidth));
|
||||
g2d.draw(lead);
|
||||
} else {
|
||||
g2d.setColor(leadColor);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(thickness));
|
||||
g2d.draw(lead);
|
||||
if (drawingObserver != null) {
|
||||
drawingObserver.stopTracking();
|
||||
}
|
||||
}
|
||||
if (stripe) {
|
||||
g2d.setColor(leadStripeColor);
|
||||
g2d.setStroke(getStripeStroke(thickness, outline ? innerWidth : thickness));
|
||||
g2d.draw(lead);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawCurveWithAreas(Path2D curve, Graphics2D g2d, ComponentState componentState,
|
||||
IDrawingObserver drawingObserver) {
|
||||
int thickness = getThickness();
|
||||
Color curveColor1 =
|
||||
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
|
||||
@@ -188,6 +273,7 @@ public class TwistedWire extends AbstractCurvedComponent<Void> implements IConti
|
||||
this.secondLeadArea = null;
|
||||
this.firstLeadStripeArea = null;
|
||||
this.secondLeadStripeArea = null;
|
||||
this.leadGeometry = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -199,6 +285,7 @@ public class TwistedWire extends AbstractCurvedComponent<Void> implements IConti
|
||||
this.secondLeadArea = null;
|
||||
this.firstLeadStripeArea = null;
|
||||
this.secondLeadStripeArea = null;
|
||||
this.leadGeometry = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -210,6 +297,7 @@ public class TwistedWire extends AbstractCurvedComponent<Void> implements IConti
|
||||
this.secondLeadArea = null;
|
||||
this.firstLeadStripeArea = null;
|
||||
this.secondLeadStripeArea = null;
|
||||
this.leadGeometry = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -272,6 +360,7 @@ public class TwistedWire extends AbstractCurvedComponent<Void> implements IConti
|
||||
this.secondLeadArea = null;
|
||||
this.firstLeadStripeArea = null;
|
||||
this.secondLeadStripeArea = null;
|
||||
this.leadGeometry = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +378,7 @@ public class TwistedWire extends AbstractCurvedComponent<Void> implements IConti
|
||||
this.secondLeadArea = null;
|
||||
this.firstLeadStripeArea = null;
|
||||
this.secondLeadStripeArea = null;
|
||||
this.leadGeometry = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +386,44 @@ public class TwistedWire extends AbstractCurvedComponent<Void> implements IConti
|
||||
|
||||
public void recalculate(Path2D path, float thickness, Area firstLeadArea, Area secondLeadArea,
|
||||
boolean stripe) {
|
||||
LeadGeometry geometry = buildLeadGeometry(path, thickness);
|
||||
Stroke stroke = stripe ? getStripeStroke(thickness, thickness)
|
||||
: ObjectCache.getInstance().fetchBasicStroke(thickness);
|
||||
|
||||
Area[] outputAreas = new Area[2];
|
||||
List<List<Path2D>> segments = List.of(geometry.firstSegments, geometry.secondSegments);
|
||||
for (int j = 0; j < 2; j++) {
|
||||
List<Area> segmentAreas = new ArrayList<Area>();
|
||||
for (Path2D p : segments.get(j)) {
|
||||
segmentAreas.add(new Area(stroke.createStrokedShape(p)));
|
||||
}
|
||||
outputAreas[j] = AreaUtils.union(segmentAreas);
|
||||
}
|
||||
|
||||
// at overlapping points, decide which lead goes on top and clear the overlapping area from the
|
||||
// lead below; all squares of one kind are cut in a single operation, because cutting them one
|
||||
// at a time costs an operation over the whole lead for each
|
||||
Area firstOnTopArea = new Area(geometry.firstOnTop);
|
||||
firstOnTopArea.intersect(outputAreas[0]);
|
||||
outputAreas[1].subtract(firstOnTopArea);
|
||||
// taken after the first cut, so where squares of both kinds overlap only one lead loses it
|
||||
Area secondOnTopArea = new Area(geometry.secondOnTop);
|
||||
secondOnTopArea.intersect(outputAreas[1]);
|
||||
outputAreas[0].subtract(secondOnTopArea);
|
||||
|
||||
// dump to the output areas
|
||||
firstLeadArea.add(outputAreas[0]);
|
||||
secondLeadArea.add(outputAreas[1]);
|
||||
}
|
||||
|
||||
private Stroke getStripeStroke(float thickness, float width) {
|
||||
return ObjectCache.getInstance().fetchStroke(width,
|
||||
new float[] {thickness / 2, thickness * 2}, thickness * 10, BasicStroke.CAP_BUTT);
|
||||
}
|
||||
|
||||
// works out the path of each lead as it winds around the curve, and the squares around the
|
||||
// crossings where each lead goes on top
|
||||
private LeadGeometry buildLeadGeometry(Path2D path, float thickness) {
|
||||
PathIterator iterator = path.getPathIterator(null);
|
||||
float[] coords = new float[6];
|
||||
Point2D current = new Point2D.Double();
|
||||
@@ -329,14 +457,6 @@ public class TwistedWire extends AbstractCurvedComponent<Void> implements IConti
|
||||
iterator.next();
|
||||
}
|
||||
|
||||
Stroke stroke;
|
||||
|
||||
if (stripe)
|
||||
stroke = ObjectCache.getInstance().fetchStroke(thickness,
|
||||
new float[] {thickness / 2, thickness * 2}, thickness * 10, BasicStroke.CAP_BUTT);
|
||||
else
|
||||
stroke = ObjectCache.getInstance().fetchBasicStroke(thickness);
|
||||
|
||||
double segmentLength = thickness * 6; // chosen empirically
|
||||
|
||||
// Convert to polygon
|
||||
@@ -368,76 +488,51 @@ public class TwistedWire extends AbstractCurvedComponent<Void> implements IConti
|
||||
double sign = i % 2 == 0 ? 1 : -1;
|
||||
double[] thetas = new double[] {theta - sign * Math.PI / 2, theta + sign * Math.PI / 2};
|
||||
|
||||
final int finalI = i;
|
||||
|
||||
Arrays.stream(new Integer[] { 0, 1 }).parallel().forEach((j) -> {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
currentPaths[j].quadTo(centerX + offset * Math.cos(thetas[j]),
|
||||
centerY + offset * Math.sin(thetas[j]), line.getX2(), line.getY2());
|
||||
if (finalI % 2 == 0 || finalI == polygon.size() - 1) {
|
||||
if (i % 2 == 0 || i == polygon.size() - 1) {
|
||||
segments.get(j).add(currentPaths[j]);
|
||||
currentPaths[j] = new Path2D.Double();
|
||||
currentPaths[j].moveTo(line.getX2(), line.getY2());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Area[] outputAreas = new Area[] {new Area(), new Area()};
|
||||
|
||||
// create stroked areas in parallel
|
||||
Arrays.stream(new Integer[] {0, 1}).parallel().forEach((j) -> {
|
||||
for (Path2D p : segments.get(j)) {
|
||||
outputAreas[j].add(new Area(stroke.createStrokedShape(p)));
|
||||
}
|
||||
});
|
||||
|
||||
int threadCount = 1;
|
||||
while (threadCount * 4 < polygon.size() && threadCount <= 8) {
|
||||
threadCount++;
|
||||
}
|
||||
int chunkSize = polygon.size() / threadCount;
|
||||
|
||||
// at overlapping points, decide which lead goes on top and clear the overlapping area from the
|
||||
// lead below
|
||||
IntStream.range(0, threadCount).parallel().forEach(threadIdx -> {
|
||||
for (int i = threadIdx * chunkSize; i < (threadIdx + 1) * chunkSize
|
||||
&& i < polygon.size(); i++) {
|
||||
Line2D line = polygon.get(i);
|
||||
Area pointRect1 = new Area(new Rectangle2D.Double(line.getX1() - rectSize / 2,
|
||||
line.getY1() - rectSize / 2, rectSize, rectSize));
|
||||
Path2D firstOnTop = new Path2D.Double();
|
||||
Path2D secondOnTop = new Path2D.Double();
|
||||
for (int i = 0; i < polygon.size(); i++) {
|
||||
Line2D line = polygon.get(i);
|
||||
Rectangle2D pointRect1 = new Rectangle2D.Double(line.getX1() - rectSize / 2,
|
||||
line.getY1() - rectSize / 2, rectSize, rectSize);
|
||||
if (i % 2 == 1) {
|
||||
firstOnTop.append(pointRect1, false);
|
||||
} else {
|
||||
secondOnTop.append(pointRect1, false);
|
||||
}
|
||||
|
||||
if (i % 2 == 1) {
|
||||
pointRect1.intersect(outputAreas[0]);
|
||||
synchronized (outputAreas[1]) {
|
||||
outputAreas[1].subtract(pointRect1);
|
||||
}
|
||||
if (i == polygon.size() - 1) {
|
||||
Rectangle2D pointRect2 = new Rectangle2D.Double(line.getX2() - rectSize / 2,
|
||||
line.getY2() - rectSize / 2, rectSize, rectSize);
|
||||
if (i % 2 == 0) {
|
||||
firstOnTop.append(pointRect2, false);
|
||||
} else {
|
||||
pointRect1.intersect(outputAreas[1]);
|
||||
synchronized (outputAreas[0]) {
|
||||
outputAreas[0].subtract(pointRect1);
|
||||
}
|
||||
}
|
||||
|
||||
if (i == polygon.size() - 1) {
|
||||
Area pointRect2 = new Area(new Rectangle2D.Double(line.getX2() - rectSize / 2,
|
||||
line.getY2() - rectSize / 2, rectSize, rectSize));
|
||||
if (i % 2 == 0) {
|
||||
pointRect2.intersect(outputAreas[0]);
|
||||
synchronized (outputAreas[1]) {
|
||||
outputAreas[1].subtract(pointRect2);
|
||||
}
|
||||
} else {
|
||||
pointRect2.intersect(outputAreas[1]);
|
||||
synchronized (outputAreas[0]) {
|
||||
outputAreas[0].subtract(pointRect2);
|
||||
}
|
||||
}
|
||||
secondOnTop.append(pointRect2, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// dump to the output areas
|
||||
firstLeadArea.add(outputAreas[0]);
|
||||
secondLeadArea.add(outputAreas[1]);
|
||||
// each lead also as a single path, with its segments as separate sub-paths, for stroking
|
||||
Path2D[] leads = new Path2D[2];
|
||||
for (int j = 0; j < 2; j++) {
|
||||
leads[j] = new Path2D.Double();
|
||||
for (Path2D p : segments.get(j)) {
|
||||
leads[j].append(p, false);
|
||||
}
|
||||
}
|
||||
|
||||
return new LeadGeometry(leads[0], leads[1], segments.get(0), segments.get(1), firstOnTop,
|
||||
secondOnTop);
|
||||
}
|
||||
|
||||
// splits a curve into a series of lines
|
||||
@@ -484,4 +579,25 @@ public class TwistedWire extends AbstractCurvedComponent<Void> implements IConti
|
||||
public boolean arePointsConnected(int index1, int index2) {
|
||||
return Math.abs(index1 - index2) == getControlPointCount() - 1;
|
||||
}
|
||||
|
||||
private static class LeadGeometry {
|
||||
|
||||
final Path2D firstLead;
|
||||
final Path2D secondLead;
|
||||
final List<Path2D> firstSegments;
|
||||
final List<Path2D> secondSegments;
|
||||
// squares around the crossings where the first or the second lead goes on top
|
||||
final Path2D firstOnTop;
|
||||
final Path2D secondOnTop;
|
||||
|
||||
LeadGeometry(Path2D firstLead, Path2D secondLead, List<Path2D> firstSegments,
|
||||
List<Path2D> secondSegments, Path2D firstOnTop, Path2D secondOnTop) {
|
||||
this.firstLead = firstLead;
|
||||
this.secondLead = secondLead;
|
||||
this.firstSegments = firstSegments;
|
||||
this.secondSegments = secondSegments;
|
||||
this.firstOnTop = firstOnTop;
|
||||
this.secondOnTop = secondOnTop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user