mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
wiring
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-57
@@ -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);
|
||||
}
|
||||
}
|
||||
-46
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user