This commit is contained in:
Bane Stojkovic
2026-09-12 23:01:12 +02:00
parent 333af398dd
commit 078d03d59b
9 changed files with 80 additions and 737 deletions
@@ -1,233 +0,0 @@
/*
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.components.schematic;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.diylc.common.ObjectCache;
import org.diylc.components.AbstractComponent;
import org.diylc.core.ComponentState;
import org.diylc.core.IContinuity;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
/**
* Auto-routed Manhattan wire that renders a single connection in the schematic view. The endpoints
* stick to the pins of the schematic symbols they connect; the intermediate way-points are
* calculated by the router and are not meant to be edited by the user.
*
* @author Branislav Stojkovic
*/
@ComponentDescriptor(name = "Schematic Wire", author = "DIYLC", category = "Schematic Symbols",
instanceNamePrefix = "W", description = "Auto-routed Manhattan wire for schematic connections",
zOrder = IDIYComponent.WIRING, bomPolicy = BomPolicy.NEVER_SHOW, autoEdit = false)
public class SchematicWire extends AbstractComponent<Void> implements IContinuity {
private static final long serialVersionUID = 1L;
public static Color COLOR = Color.black;
private UUID sourceComponentId;
private int sourcePinIndex;
private UUID targetComponentId;
private int targetPinIndex;
private List<Point2D> routePoints;
private Color color = COLOR;
private int thickness = 1;
public SchematicWire() {
super();
routePoints = new ArrayList<Point2D>();
routePoints.add(new Point2D.Double(0, 0));
routePoints.add(new Point2D.Double(0, 0));
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip()))
return;
List<Point2D> points = getRoutePoints();
if (points.size() < 2)
return;
// The schematic renders wires on a locked layer (so they cannot be selected or detached), but
// locked components are painted at reduced alpha. A wire must stay solid, so force an opaque
// composite for its own drawing and restore it afterwards.
Composite oldComposite = g2d.getComposite();
g2d.setComposite(AlphaComposite.SrcOver);
g2d.setColor(
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
: color);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(thickness));
GeneralPath path = new GeneralPath();
path.moveTo(points.get(0).getX(), points.get(0).getY());
for (int i = 1; i < points.size(); i++) {
path.lineTo(points.get(i).getX(), points.get(i).getY());
}
g2d.draw(path);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(COLOR);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.drawLine(2, height / 3, width / 2, height / 3);
g2d.drawLine(width / 2, height / 3, width / 2, height * 2 / 3);
g2d.drawLine(width / 2, height * 2 / 3, width - 2, height * 2 / 3);
}
public List<Point2D> getRoutePoints() {
if (routePoints == null)
routePoints = new ArrayList<Point2D>();
return routePoints;
}
public void setRoutePoints(List<Point2D> routePoints) {
this.routePoints = routePoints;
}
@Override
public int getControlPointCount() {
return getRoutePoints().size();
}
@Override
public Point2D getControlPoint(int index) {
return getRoutePoints().get(index);
}
@Override
public void setControlPoint(Point2D point, int index) {
getRoutePoints().get(index).setLocation(point);
}
@Override
public boolean isControlPointSticky(int index) {
// only the two endpoints attach to symbol pins
return index == 0 || index == getRoutePoints().size() - 1;
}
@Override
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
return VisibilityPolicy.NEVER;
}
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
}
@Override
public String getControlPointNodeName(int index) {
// wires are continuity, not graph nodes
return null;
}
@Override
public boolean arePointsConnected(int index1, int index2) {
return true;
}
public UUID getSourceComponentId() {
return sourceComponentId;
}
public void setSourceComponentId(UUID sourceComponentId) {
this.sourceComponentId = sourceComponentId;
}
public int getSourcePinIndex() {
return sourcePinIndex;
}
public void setSourcePinIndex(int sourcePinIndex) {
this.sourcePinIndex = sourcePinIndex;
}
public UUID getTargetComponentId() {
return targetComponentId;
}
public void setTargetComponentId(UUID targetComponentId) {
this.targetComponentId = targetComponentId;
}
public int getTargetPinIndex() {
return targetPinIndex;
}
public void setTargetPinIndex(int targetPinIndex) {
this.targetPinIndex = targetPinIndex;
}
@EditableProperty
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
@EditableProperty
public int getThickness() {
return thickness;
}
public void setThickness(int thickness) {
this.thickness = thickness;
}
@Override
public Void getValue() {
return null;
}
@Override
public void setValue(Void value) {}
@Override
public IDIYComponent<Void> clone() throws CloneNotSupportedException {
SchematicWire clone = (SchematicWire) super.clone();
List<Point2D> pointsCopy = new ArrayList<Point2D>();
for (Point2D p : getRoutePoints()) {
pointsCopy.add(new Point2D.Double(p.getX(), p.getY()));
}
clone.routePoints = pointsCopy;
return clone;
}
}
@@ -1,235 +0,0 @@
/*
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.schematic;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Computes Manhattan-style (axis-aligned) routes between two schematic pins. The router decides
* whether a straight horizontal or vertical segment is enough, or whether the connection needs a
* single bend (L shape) or a double bend (horizontal-vertical-horizontal or
* vertical-horizontal-vertical). Among the candidate routes it picks the one that overlaps and
* crosses existing wires the least.
*
* @author Branislav Stojkovic
*/
public class ManhattanRouter {
/** Coordinates closer than this are considered aligned. */
public static final double EPS = 1.5;
public enum Direction {
LEFT, RIGHT, UP, DOWN, NONE
}
private ManhattanRouter() {}
public static List<Point2D> route(Point2D start, Point2D end) {
return route(start, end, Direction.NONE, Direction.NONE, Collections.<Line2D>emptyList(), 0);
}
/**
* @param start start pin location
* @param end end pin location
* @param startDir direction the wire should leave the start pin, or {@link Direction#NONE}
* @param endDir direction the wire should leave the end pin, or {@link Direction#NONE}
* @param obstacles existing wire segments to avoid overlapping/crossing
* @param grid grid step used to generate alternative bend positions; 0 disables it
* @return ordered list of way-points, first equal to {@code start}, last equal to {@code end}
*/
public static List<Point2D> route(Point2D start, Point2D end, Direction startDir, Direction endDir,
Collection<Line2D> obstacles, double grid) {
double dx = end.getX() - start.getX();
double dy = end.getY() - start.getY();
if (Math.abs(dx) < EPS && Math.abs(dy) < EPS) {
return list(start, end);
}
if (Math.abs(dy) < EPS) {
return list(start, new Point2D.Double(end.getX(), start.getY()));
}
if (Math.abs(dx) < EPS) {
return list(start, new Point2D.Double(start.getX(), end.getY()));
}
List<List<Point2D>> candidates = new ArrayList<List<Point2D>>();
// L shapes
candidates.add(list(start, new Point2D.Double(end.getX(), start.getY()), end));
candidates.add(list(start, new Point2D.Double(start.getX(), end.getY()), end));
// HVH with a vertical middle leg at x = bx
for (double bx : bendCoordinates(start.getX(), end.getX(), grid)) {
candidates.add(list(start, new Point2D.Double(bx, start.getY()), new Point2D.Double(bx, end.getY()), end));
}
// VHV with a horizontal middle leg at y = by
for (double by : bendCoordinates(start.getY(), end.getY(), grid)) {
candidates.add(list(start, new Point2D.Double(start.getX(), by), new Point2D.Double(end.getX(), by), end));
}
List<Point2D> best = null;
double bestCost = Double.MAX_VALUE;
for (List<Point2D> candidate : candidates) {
double cost = cost(candidate, startDir, endDir, obstacles);
if (cost < bestCost) {
bestCost = cost;
best = candidate;
}
}
return best == null ? list(start, new Point2D.Double(end.getX(), start.getY()), end) : dedupe(best);
}
private static List<Double> bendCoordinates(double a, double b, double grid) {
List<Double> result = new ArrayList<Double>();
result.add((a + b) / 2);
result.add((a * 3 + b) / 4);
result.add((a + b * 3) / 4);
if (grid > 0) {
double mid = (a + b) / 2;
for (int k = 1; k <= 3; k++) {
result.add(mid + k * grid);
result.add(mid - k * grid);
}
}
return result;
}
private static double cost(List<Point2D> path, Direction startDir, Direction endDir,
Collection<Line2D> obstacles) {
double length = 0;
int bends = Math.max(0, path.size() - 2);
double crossings = 0;
double overlap = 0;
for (int i = 0; i < path.size() - 1; i++) {
Point2D p1 = path.get(i);
Point2D p2 = path.get(i + 1);
length += p1.distance(p2);
Line2D seg = new Line2D.Double(p1, p2);
for (Line2D obs : obstacles) {
if (segmentsOverlap(seg, obs)) {
overlap += overlapLength(seg, obs);
} else if (seg.intersectsLine(obs)) {
crossings++;
}
}
}
double dirPenalty = 0;
if (startDir != Direction.NONE && path.size() >= 2
&& !matchesDirection(path.get(0), path.get(1), startDir)) {
dirPenalty += 40;
}
if (endDir != Direction.NONE && path.size() >= 2
&& !matchesDirection(path.get(path.size() - 1), path.get(path.size() - 2), endDir)) {
dirPenalty += 40;
}
return length * 0.01 + bends * 6 + crossings * 15 + overlap * 0.5 + dirPenalty;
}
private static boolean matchesDirection(Point2D from, Point2D to, Direction dir) {
double dx = to.getX() - from.getX();
double dy = to.getY() - from.getY();
switch (dir) {
case LEFT:
return dx < -EPS && Math.abs(dy) < EPS;
case RIGHT:
return dx > EPS && Math.abs(dy) < EPS;
case UP:
return dy < -EPS && Math.abs(dx) < EPS;
case DOWN:
return dy > EPS && Math.abs(dx) < EPS;
default:
return true;
}
}
private static boolean isHorizontal(Line2D l) {
return Math.abs(l.getY1() - l.getY2()) < EPS;
}
private static boolean isVertical(Line2D l) {
return Math.abs(l.getX1() - l.getX2()) < EPS;
}
private static boolean segmentsOverlap(Line2D a, Line2D b) {
if (isHorizontal(a) && isHorizontal(b) && Math.abs(a.getY1() - b.getY1()) < EPS) {
return rangesOverlap(a.getX1(), a.getX2(), b.getX1(), b.getX2());
}
if (isVertical(a) && isVertical(b) && Math.abs(a.getX1() - b.getX1()) < EPS) {
return rangesOverlap(a.getY1(), a.getY2(), b.getY1(), b.getY2());
}
return false;
}
private static double overlapLength(Line2D a, Line2D b) {
if (isHorizontal(a)) {
return rangeOverlap(a.getX1(), a.getX2(), b.getX1(), b.getX2());
}
return rangeOverlap(a.getY1(), a.getY2(), b.getY1(), b.getY2());
}
private static boolean rangesOverlap(double a1, double a2, double b1, double b2) {
return rangeOverlap(a1, a2, b1, b2) > EPS;
}
private static double rangeOverlap(double a1, double a2, double b1, double b2) {
double lo = Math.max(Math.min(a1, a2), Math.min(b1, b2));
double hi = Math.min(Math.max(a1, a2), Math.max(b1, b2));
return Math.max(0, hi - lo);
}
private static List<Point2D> list(Point2D... points) {
List<Point2D> result = new ArrayList<Point2D>();
for (Point2D p : points) {
result.add(new Point2D.Double(p.getX(), p.getY()));
}
return result;
}
private static List<Point2D> dedupe(List<Point2D> points) {
List<Point2D> result = new ArrayList<Point2D>();
for (Point2D p : points) {
if (result.isEmpty() || result.get(result.size() - 1).distance(p) > EPS) {
result.add(p);
}
}
// drop collinear middle points
int i = 1;
while (i < result.size() - 1) {
Point2D a = result.get(i - 1);
Point2D b = result.get(i);
Point2D c = result.get(i + 1);
boolean collinearH = Math.abs(a.getY() - b.getY()) < EPS && Math.abs(b.getY() - c.getY()) < EPS;
boolean collinearV = Math.abs(a.getX() - b.getX()) < EPS && Math.abs(b.getX() - c.getX()) < EPS;
if (collinearH || collinearV) {
result.remove(i);
} else {
i++;
}
}
return result;
}
}
@@ -21,7 +21,6 @@
*/
package org.diylc.schematic;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
@@ -34,7 +33,8 @@ import java.util.UUID;
import org.apache.log4j.Logger;
import org.diylc.common.ComponentType;
import org.diylc.components.schematic.SchematicWire;
import org.diylc.common.OrientationHV;
import org.diylc.components.connectivity.OrthogonalLine;
import org.diylc.core.IContinuity;
import org.diylc.core.ICommonNode;
import org.diylc.core.IDIYComponent;
@@ -53,7 +53,7 @@ import org.diylc.presenter.ContinuityArea;
* Generates the initial {@link SchematicView} for a layout {@link Project}. Every eligible physical
* component is turned into one or more schematic symbols via its {@link ISchematicFactory} (or the
* {@link GenericBoxSchematicFactory} fallback), symbols are placed on a connection-density aware
* grid and the nets from the layout netlist are drawn as auto-routed {@link SchematicWire}s.
* grid and the nets from the layout netlist are drawn as {@link OrthogonalLine}s.
*
* @author Branislav Stojkovic
*/
@@ -120,7 +120,7 @@ public class SchematicBuilder {
placeSymbols(eligible, entriesByPhysicalId, netlist);
List<SchematicWire> wires = createWires(netlist, entriesByPhysicalId);
List<OrthogonalLine> wires = createWires(netlist, entriesByPhysicalId);
schematicComponents.addAll(wires);
schematicComponents.sort(Comparator.comparingDouble(SchematicBuilder::zOrderOf));
@@ -311,13 +311,12 @@ public class SchematicBuilder {
/* ------------------------------------------------------------------ wiring */
List<SchematicWire> createWires(Netlist netlist,
List<OrthogonalLine> createWires(Netlist netlist,
Map<UUID, List<SymbolEntry>> entriesByPhysicalId) {
List<SchematicWire> wires = new ArrayList<SchematicWire>();
List<OrthogonalLine> wires = new ArrayList<OrthogonalLine>();
if (netlist == null) {
return wires;
}
List<Line2D> obstacles = new ArrayList<Line2D>();
for (Group group : netlist.getSortedGroups()) {
List<Pin> pins = new ArrayList<Pin>();
@@ -346,19 +345,7 @@ public class SchematicBuilder {
for (int i = 0; i < pins.size() - 1; i++) {
Pin a = pins.get(i);
Pin b = pins.get(i + 1);
SchematicWire wire = new SchematicWire();
wire.setId(UUID.randomUUID());
wire.setSourceComponentId(a.symbol.getId());
wire.setSourcePinIndex(a.pinIndex);
wire.setTargetComponentId(b.symbol.getId());
wire.setTargetPinIndex(b.pinIndex);
List<Point2D> route = ManhattanRouter.route(a.location, b.location, exitDirection(a),
exitDirection(b), obstacles, GRID);
wire.setRoutePoints(route);
for (int s = 0; s < route.size() - 1; s++) {
obstacles.add(new Line2D.Double(route.get(s), route.get(s + 1)));
}
wires.add(wire);
wires.add(createWire(a, b));
}
}
return wires;
@@ -376,20 +363,46 @@ public class SchematicBuilder {
}
}
private static ManhattanRouter.Direction exitDirection(Pin pin) {
return exitDirection(pin.symbol, pin.location);
/**
* Connects two pins with a wire whose single bend point is placed so that the wire leaves the
* first pin along the axis that pin faces. {@link OrthogonalLine} derives the rest of the route
* from that one point, and keeps deriving it as the sticky endpoints follow the symbols around.
*/
private static OrthogonalLine createWire(Pin a, Pin b) {
OrthogonalLine wire = new OrthogonalLine();
wire.setId(UUID.randomUUID());
wire.setControlPoint(copyOf(a.location), 0);
wire.setControlPoint(bendPoint(a, b), 1);
wire.setControlPoint(copyOf(b.location), 2);
return wire;
}
private static Point2D bendPoint(Pin a, Pin b) {
Point2D from = a.location;
Point2D to = b.location;
// pins that already share a row or a column need no bend; a handle sitting on the first pin
// collapses the route to a straight run
if (Math.abs(from.getX() - to.getX()) < 1 || Math.abs(from.getY() - to.getY()) < 1) {
return copyOf(from);
}
if (exitDirection(a.symbol, from) == OrientationHV.VERTICAL) {
// on the first pin's column, which swallows the leading horizontal run
return new Point2D.Double(from.getX(), snap((from.getY() + to.getY()) / 2));
}
// on the second pin's row, which swallows the trailing vertical run
return new Point2D.Double(snap((from.getX() + to.getX()) / 2), to.getY());
}
/**
* Guesses the direction a wire should leave the given pin: away from the symbol's centroid, along
* the dominant axis.
* Guesses whether a wire should leave the given pin horizontally or vertically: away from the
* symbol's centroid, along the dominant axis.
*/
static ManhattanRouter.Direction exitDirection(IDIYComponent<?> symbol, Point2D pinLocation) {
static OrientationHV exitDirection(IDIYComponent<?> symbol, Point2D pinLocation) {
double cx = 0;
double cy = 0;
int n = symbol.getControlPointCount();
if (n == 0) {
return ManhattanRouter.Direction.NONE;
return OrientationHV.HORIZONTAL;
}
for (int i = 0; i < n; i++) {
cx += symbol.getControlPoint(i).getX();
@@ -399,82 +412,13 @@ public class SchematicBuilder {
cy /= n;
double dx = pinLocation.getX() - cx;
double dy = pinLocation.getY() - cy;
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
return ManhattanRouter.Direction.NONE;
}
if (Math.abs(dx) >= Math.abs(dy)) {
return dx < 0 ? ManhattanRouter.Direction.LEFT : ManhattanRouter.Direction.RIGHT;
}
return dy < 0 ? ManhattanRouter.Direction.UP : ManhattanRouter.Direction.DOWN;
}
/**
* Recomputes the route of every {@link SchematicWire} in the list from the current positions of
* the symbols it connects (looked up by component id). Used to re-route wires after the user moves
* a symbol on the schematic. Wires whose endpoints can no longer be resolved are left untouched.
*
* @return {@code true} if any wire route changed
*/
public static boolean rerouteWires(List<IDIYComponent<?>> schematicComponents) {
Map<UUID, IDIYComponent<?>> symbolsById = new HashMap<UUID, IDIYComponent<?>>();
List<SchematicWire> wires = new ArrayList<SchematicWire>();
for (IDIYComponent<?> component : schematicComponents) {
if (component instanceof SchematicWire) {
wires.add((SchematicWire) component);
} else {
symbolsById.put(component.getId(), component);
}
}
// deterministic order so obstacle accumulation is stable
wires.sort(Comparator.comparing(w -> w.getId().toString()));
boolean changed = false;
List<Line2D> obstacles = new ArrayList<Line2D>();
for (SchematicWire wire : wires) {
IDIYComponent<?> source = symbolsById.get(wire.getSourceComponentId());
IDIYComponent<?> target = symbolsById.get(wire.getTargetComponentId());
if (source == null || target == null
|| wire.getSourcePinIndex() >= source.getControlPointCount()
|| wire.getTargetPinIndex() >= target.getControlPointCount()) {
// keep the existing route as an obstacle so other wires still avoid it
addSegments(obstacles, wire.getRoutePoints());
continue;
}
Point2D a = copyOf(source.getControlPoint(wire.getSourcePinIndex()));
Point2D b = copyOf(target.getControlPoint(wire.getTargetPinIndex()));
List<Point2D> route = ManhattanRouter.route(a, b, exitDirection(source, a),
exitDirection(target, b), obstacles, GRID);
if (!sameRoute(route, wire.getRoutePoints())) {
wire.setRoutePoints(route);
changed = true;
}
addSegments(obstacles, route);
}
return changed;
return Math.abs(dx) >= Math.abs(dy) ? OrientationHV.HORIZONTAL : OrientationHV.VERTICAL;
}
private static Point2D copyOf(Point2D p) {
return new Point2D.Double(p.getX(), p.getY());
}
private static void addSegments(List<Line2D> obstacles, List<Point2D> route) {
for (int i = 0; i < route.size() - 1; i++) {
obstacles.add(new Line2D.Double(route.get(i), route.get(i + 1)));
}
}
private static boolean sameRoute(List<Point2D> a, List<Point2D> b) {
if (a.size() != b.size()) {
return false;
}
for (int i = 0; i < a.size(); i++) {
if (a.get(i).distance(b.get(i)) > 0.5) {
return false;
}
}
return true;
}
/* ------------------------------------------------------------------ misc */
static void resizeCanvasToFit(SchematicView view) {
@@ -32,7 +32,7 @@ import java.util.Set;
import java.util.UUID;
import org.apache.log4j.Logger;
import org.diylc.components.schematic.SchematicWire;
import org.diylc.components.connectivity.OrthogonalLine;
import org.diylc.core.IDIYComponent;
import org.diylc.core.Project;
import org.diylc.core.SchematicView;
@@ -75,7 +75,7 @@ public class SchematicSynchronizer {
Map<UUID, List<UUID>> map = view.getPhysicalToSchematicMap();
Map<UUID, IDIYComponent<?>> symbolsById = new HashMap<UUID, IDIYComponent<?>>();
for (IDIYComponent<?> component : view.getComponents()) {
if (!(component instanceof SchematicWire)) {
if (!(component instanceof OrthogonalLine)) {
symbolsById.put(component.getId(), component);
}
}
@@ -153,7 +153,7 @@ public class SchematicSynchronizer {
Netlist netlist = builder.extractNetlist(project,
continuityAreas == null ? new ArrayList<ContinuityArea>() : continuityAreas);
List<SchematicWire> wires = builder.createWires(netlist, entriesByPhysicalId);
List<OrthogonalLine> wires = builder.createWires(netlist, entriesByPhysicalId);
List<IDIYComponent<?>> result = new ArrayList<IDIYComponent<?>>(keptSymbols);
result.addAll(wires);
@@ -1,64 +0,0 @@
/*
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.schematic;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.awt.geom.Point2D;
import java.util.List;
import org.junit.Test;
public class ManhattanRouterTest {
@Test
public void straightHorizontalWhenAligned() {
List<Point2D> route =
ManhattanRouter.route(new Point2D.Double(0, 10), new Point2D.Double(100, 10));
assertEquals(2, route.size());
assertEquals(10d, route.get(1).getY(), 0.001);
}
@Test
public void straightVerticalWhenAligned() {
List<Point2D> route =
ManhattanRouter.route(new Point2D.Double(20, 0), new Point2D.Double(20, 80));
assertEquals(2, route.size());
assertEquals(20d, route.get(1).getX(), 0.001);
}
@Test
public void bendsForDiagonalOffset() {
List<Point2D> route =
ManhattanRouter.route(new Point2D.Double(0, 0), new Point2D.Double(100, 60));
assertTrue("expected at least one bend", route.size() >= 3);
// every segment must be axis-aligned
for (int i = 0; i < route.size() - 1; i++) {
Point2D a = route.get(i);
Point2D b = route.get(i + 1);
boolean axisAligned = Math.abs(a.getX() - b.getX()) < ManhattanRouter.EPS
|| Math.abs(a.getY() - b.getY()) < ManhattanRouter.EPS;
assertTrue("segment " + i + " is not axis-aligned", axisAligned);
}
assertEquals(0d, route.get(0).distance(new Point2D.Double(0, 0)), 0.001);
assertEquals(0d, route.get(route.size() - 1).distance(new Point2D.Double(100, 60)), 0.001);
}
}
@@ -20,59 +20,13 @@
package org.diylc.schematic;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.diylc.components.passive.ResistorSymbol;
import org.diylc.components.schematic.SchematicBox;
import org.diylc.components.schematic.SchematicWire;
import org.diylc.core.IDIYComponent;
import org.junit.Test;
public class SchematicRerouteTest {
private static ResistorSymbol symbolAt(double x1, double y1, double x2, double y2) {
ResistorSymbol symbol = new ResistorSymbol();
symbol.setId(UUID.randomUUID());
symbol.setControlPoint(new Point2D.Double(x1, y1), 0);
symbol.setControlPoint(new Point2D.Double(x2, y2), 1);
return symbol;
}
@Test
public void wireEndpointsFollowMovedSymbol() {
ResistorSymbol a = symbolAt(100, 100, 140, 100);
ResistorSymbol b = symbolAt(400, 100, 440, 100);
SchematicWire wire = new SchematicWire();
wire.setId(UUID.randomUUID());
wire.setSourceComponentId(a.getId());
wire.setSourcePinIndex(1);
wire.setTargetComponentId(b.getId());
wire.setTargetPinIndex(0);
wire.setRoutePoints(new ArrayList<Point2D>(List.of(new Point2D.Double(0, 0), new Point2D.Double(1, 1))));
List<IDIYComponent<?>> components = new ArrayList<IDIYComponent<?>>(List.of(a, wire, b));
assertTrue(SchematicBuilder.rerouteWires(components));
assertEndpoint(wire, 0, a.getControlPoint(1));
assertEndpoint(wire, last(wire), b.getControlPoint(0));
// move b down; re-route and check the wire still ends on b's pin
b.setControlPoint(new Point2D.Double(400, 400), 0);
b.setControlPoint(new Point2D.Double(440, 400), 1);
assertTrue(SchematicBuilder.rerouteWires(components));
assertEndpoint(wire, 0, a.getControlPoint(1));
assertEndpoint(wire, last(wire), b.getControlPoint(0));
// a second re-route with nothing moved should be a no-op
assertTrue(!SchematicBuilder.rerouteWires(components));
}
public class SchematicBoxGeometryTest {
@Test
public void moveAnchorToTranslatesEverySchematicBoxPinExactlyOnce() {
@@ -134,14 +88,4 @@ public class SchematicRerouteTest {
}
}
}
private static int last(SchematicWire wire) {
return wire.getRoutePoints().size() - 1;
}
private static void assertEndpoint(SchematicWire wire, int index, Point2D expected) {
Point2D actual = wire.getRoutePoints().get(index);
assertEquals("x", expected.getX(), actual.getX(), 0.6);
assertEquals("y", expected.getY(), actual.getY(), 0.6);
}
}
@@ -43,12 +43,10 @@ import org.diylc.common.BadPositionException;
import org.diylc.common.EventType;
import org.diylc.common.IPlugIn;
import org.diylc.common.IPlugInPort;
import org.diylc.core.IDIYComponent;
import org.diylc.core.Project;
import org.diylc.core.SchematicView;
import org.diylc.presenter.ContinuityArea;
import org.diylc.presenter.Presenter;
import org.diylc.schematic.SchematicBuilder;
import org.diylc.schematic.SchematicSynchronizer;
import org.diylc.swing.ISwingUI;
import org.diylc.swing.plugins.canvas.CanvasPlugin;
@@ -86,7 +84,6 @@ public class SchematicTabPlugin implements IPlugIn {
private JComponent schematicScroll;
private JToggleButton schematicTab;
private Project schematicProject;
private boolean rerouting;
public SchematicTabPlugin(ISwingUI swingUI, IConfigurationManager<?> configManager,
CanvasPlugin layoutCanvasPlugin) {
@@ -105,7 +102,6 @@ public class SchematicTabPlugin implements IPlugIn {
this.schematicPresenter = new Presenter(swingUI, configManager, false);
this.schematicCanvasPlugin = new CanvasPlugin(swingUI, configManager);
this.schematicPresenter.installPlugin(() -> schematicCanvasPlugin);
this.schematicPresenter.installPlugin(SchematicChangeListener::new);
this.schematicScroll = schematicCanvasPlugin.getCanvasScrollComponent();
this.schematicScroll.setVisible(false);
@@ -188,10 +184,6 @@ public class SchematicTabPlugin implements IPlugIn {
wrapper.setGridSpacing(view.getGridSpacing());
wrapper.setFont(layoutProject.getFont());
wrapper.getComponents().addAll(view.getComponents());
// Wires are auto-routed and must never be touched by the user: lock the whole WIRING layer so
// they cannot be selected, dragged or detached from symbols. Symbols live on the COMPONENT
// layer and stay fully movable.
wrapper.getLockedLayers().add(IDIYComponent.WIRING);
return wrapper;
}
@@ -205,23 +197,6 @@ public class SchematicTabPlugin implements IPlugIn {
}
}
/** Re-routes the wires attached to any symbol the user just moved on the schematic canvas. */
private void onSchematicModified() {
if (rerouting || schematicProject == null) {
return;
}
rerouting = true;
try {
if (SchematicBuilder.rerouteWires(schematicProject.getComponents())) {
schematicPresenter.refresh();
}
} catch (Exception e) {
LOG.error("Could not re-route schematic wires", e);
} finally {
rerouting = false;
}
}
@Override
public EnumSet<EventType> getSubscribedEventTypes() {
return EnumSet.noneOf(EventType.class);
@@ -231,25 +206,4 @@ public class SchematicTabPlugin implements IPlugIn {
public void processMessage(EventType eventType, Object... params) {
// no-op
}
/** Listens on the schematic presenter for symbol moves and triggers wire re-routing. */
private class SchematicChangeListener implements IPlugIn {
@Override
public void connect(IPlugInPort plugInPort) {
// nothing to do; we only need the message subscription
}
@Override
public EnumSet<EventType> getSubscribedEventTypes() {
return EnumSet.of(EventType.PROJECT_MODIFIED);
}
@Override
public void processMessage(EventType eventType, Object... params) {
if (eventType == EventType.PROJECT_MODIFIED) {
SwingUtilities.invokeLater(SchematicTabPlugin.this::onSchematicModified);
}
}
}
}
@@ -7,7 +7,7 @@ import static org.junit.Assert.assertTrue;
import java.awt.Point;
import java.awt.image.BufferedImage;
import org.diylc.components.schematic.SchematicWire;
import org.diylc.components.connectivity.OrthogonalLine;
import org.diylc.core.Project;
import org.diylc.core.SchematicView;
import org.diylc.schematic.SchematicBuilder;
@@ -36,8 +36,8 @@ public class SchematicViewIntegrationTest extends TestBase {
assertEquals(2, view.getPhysicalToSchematicMap().size());
long symbolCount =
view.getComponents().stream().filter(c -> !(c instanceof SchematicWire)).count();
long wireCount = view.getComponents().stream().filter(c -> c instanceof SchematicWire).count();
view.getComponents().stream().filter(c -> !(c instanceof OrthogonalLine)).count();
long wireCount = view.getComponents().stream().filter(c -> c instanceof OrthogonalLine).count();
assertEquals(2, symbolCount);
assertTrue("expected the shared node to produce a wire", wireCount >= 1);
}
+33
View File
@@ -212,3 +212,36 @@ The test covers the routing template in both start directions, the degenerate
shapes, the invariant that every produced segment is axis aligned, point-count
changes, and the transformer contract that rotating the component yields exactly
the rotated route.
## 9. The schematic generator now uses it
`Right Angle Line` replaced the generated-schematic wire, and two classes came
out with it.
`SchematicWire` stored a full point list plus the ids of the two symbols and pins
it joined, because it had no other way to find its endpoints again. `ManhattanRouter`
picked a route for it by scoring candidate paths against an obstacle list. When the
user moved a symbol, `SchematicBuilder.rerouteWires` walked every wire, looked its
symbols up by id and asked the router for a fresh path.
None of that is needed now. `Right Angle Line` has sticky endpoints, so the
framework moves them with the symbol they sit on, and the component re-derives its
own route from the bend point on every repaint. `SchematicBuilder` picks that one
bend point when it creates the wire, placing it on the first pin's column or the
second pin's row according to which way the pin faces, and never touches the wire
again.
Three consequences worth knowing:
- **Wires are editable now.** `SchematicTabPlugin` used to lock the whole `WIRING`
layer so that auto-routed wires could not be selected or dragged. That lock is
gone, both because sticky endpoints need it gone and because a hand-adjustable
wire is the point of the swap. It also retires the opaque-composite workaround
`SchematicWire.draw` needed to escape the reduced alpha that locked components
are painted with.
- **Obstacle avoidance is gone.** The router used to steer a new route away from
overlapping or crossing the wires already placed. Nothing replaces that, so a
dense schematic can produce wires that run over each other. The user can now drag
them apart, which was impossible before.
- **The reroute listener is gone.** `SchematicChangeListener` existed only to call
`rerouteWires` after every project modification. Nothing subscribes in its place.