mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
schematic improvements
This commit is contained in:
@@ -123,7 +123,7 @@ public class SchematicBuilder {
|
||||
List<OrthogonalLine> wires = createWires(netlist, entriesByPhysicalId);
|
||||
schematicComponents.addAll(wires);
|
||||
|
||||
schematicComponents.sort(Comparator.comparingDouble(SchematicBuilder::zOrderOf));
|
||||
schematicComponents.sort(SCHEMATIC_ORDER);
|
||||
|
||||
SchematicView view = project.getOrCreateSchematicView();
|
||||
view.getComponents().clear();
|
||||
@@ -441,6 +441,17 @@ public class SchematicBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Paint and hit-test order for the schematic view. Wires go underneath the symbols, and not where
|
||||
* their {@link IDIYComponent#WIRING} z-order would otherwise put them, because the canvas hands a
|
||||
* click to the last matching component in this list. A wire runs right up to the pin it connects
|
||||
* to and its hit area is widened to three pixels, so a wire drawn on top would swallow the clicks
|
||||
* meant for the symbol underneath it and the user could not drag the symbol at all.
|
||||
*/
|
||||
static final Comparator<IDIYComponent<?>> SCHEMATIC_ORDER =
|
||||
Comparator.comparingInt((IDIYComponent<?> c) -> c instanceof OrthogonalLine ? 0 : 1)
|
||||
.thenComparingDouble(SchematicBuilder::zOrderOf);
|
||||
|
||||
static double zOrderOf(IDIYComponent<?> component) {
|
||||
ComponentType type = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) component.getClass());
|
||||
|
||||
@@ -157,7 +157,7 @@ public class SchematicSynchronizer {
|
||||
|
||||
List<IDIYComponent<?>> result = new ArrayList<IDIYComponent<?>>(keptSymbols);
|
||||
result.addAll(wires);
|
||||
result.sort(java.util.Comparator.comparingDouble(SchematicBuilder::zOrderOf));
|
||||
result.sort(SchematicBuilder.SCHEMATIC_ORDER);
|
||||
|
||||
view.getComponents().clear();
|
||||
view.getComponents().addAll(result);
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
|
||||
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.swing.plugins.schematic;
|
||||
|
||||
import org.diylc.appframework.miscutils.IConfigListener;
|
||||
import org.diylc.appframework.miscutils.IConfigurationManager;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
|
||||
/**
|
||||
* The application's configuration as the schematic canvas sees it: everything is delegated, except
|
||||
* that locked components are never dimmed.
|
||||
* <p>
|
||||
* The schematic keeps its whole wiring layer locked so that generated wires cannot be selected,
|
||||
* dragged or deleted. Locked components are normally painted at half alpha to signal that they are
|
||||
* untouchable, which here would wash out every wire on the drawing and read as a rendering fault
|
||||
* rather than a signal. Overriding the preference for this canvas alone leaves the user's own
|
||||
* setting untouched on the layout canvas.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class SchematicConfigurationManager<T> implements IConfigurationManager<T> {
|
||||
|
||||
private final IConfigurationManager<T> delegate;
|
||||
|
||||
public static <T> IConfigurationManager<T> wrap(IConfigurationManager<T> delegate) {
|
||||
return new SchematicConfigurationManager<T>(delegate);
|
||||
}
|
||||
|
||||
private SchematicConfigurationManager(IConfigurationManager<T> delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readBoolean(String key, boolean defaultValue) {
|
||||
if (IPlugInPort.LOCKED_ALPHA.equals(key)) {
|
||||
return false;
|
||||
}
|
||||
return delegate.readBoolean(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addConfigListener(String key, IConfigListener listener) {
|
||||
delegate.addConfigListener(key, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readString(String key, String defaultValue) {
|
||||
return delegate.readString(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readInt(String key, int defaultValue) {
|
||||
return delegate.readInt(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float readFloat(String key, float defaultValue) {
|
||||
return delegate.readFloat(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double readDouble(String key, double defaultValue) {
|
||||
return delegate.readDouble(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object readObject(String key, Object defaultValue) {
|
||||
return delegate.readObject(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeValue(String key, Object value) {
|
||||
delegate.writeValue(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getSerializer() {
|
||||
return delegate.getSerializer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(String appName) {
|
||||
delegate.initialize(appName);
|
||||
}
|
||||
}
|
||||
+61
-4
@@ -43,6 +43,7 @@ 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;
|
||||
@@ -63,8 +64,10 @@ import org.diylc.swing.plugins.canvas.CanvasPlugin;
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The schematic canvas is not restricted yet, so nothing prevents selecting or nudging symbols; a
|
||||
* dedicated restricted mode (no add / delete / paste, shared undo) is a planned follow-up.
|
||||
* Symbols are movable; the generated wires are not. The whole wiring layer is locked so that a wire
|
||||
* cannot be selected, dragged or deleted, and because a locked component is skipped by the sticky
|
||||
* point expansion that would otherwise drag its endpoints along, the wires are regenerated from the
|
||||
* symbols' current pin positions after every change.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
@@ -84,6 +87,7 @@ public class SchematicTabPlugin implements IPlugIn {
|
||||
private JComponent schematicScroll;
|
||||
private JToggleButton schematicTab;
|
||||
private Project schematicProject;
|
||||
private boolean rewiring;
|
||||
|
||||
public SchematicTabPlugin(ISwingUI swingUI, IConfigurationManager<?> configManager,
|
||||
CanvasPlugin layoutCanvasPlugin) {
|
||||
@@ -99,9 +103,11 @@ public class SchematicTabPlugin implements IPlugIn {
|
||||
|
||||
// A second presenter + canvas plugin gives the schematic the exact same rendering, scrolling
|
||||
// and zoom behaviour as the layout canvas.
|
||||
this.schematicPresenter = new Presenter(swingUI, configManager, false);
|
||||
this.schematicCanvasPlugin = new CanvasPlugin(swingUI, configManager);
|
||||
IConfigurationManager<?> schematicConfig = SchematicConfigurationManager.wrap(configManager);
|
||||
this.schematicPresenter = new Presenter(swingUI, schematicConfig, false);
|
||||
this.schematicCanvasPlugin = new CanvasPlugin(swingUI, schematicConfig);
|
||||
this.schematicPresenter.installPlugin(() -> schematicCanvasPlugin);
|
||||
this.schematicPresenter.installPlugin(SchematicChangeListener::new);
|
||||
this.schematicScroll = schematicCanvasPlugin.getCanvasScrollComponent();
|
||||
this.schematicScroll.setVisible(false);
|
||||
|
||||
@@ -184,6 +190,10 @@ public class SchematicTabPlugin implements IPlugIn {
|
||||
wrapper.setGridSpacing(view.getGridSpacing());
|
||||
wrapper.setFont(layoutProject.getFont());
|
||||
wrapper.getComponents().addAll(view.getComponents());
|
||||
// Generated wires 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;
|
||||
}
|
||||
|
||||
@@ -197,6 +207,32 @@ public class SchematicTabPlugin implements IPlugIn {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerates the wires from the symbols' current pin positions. A locked wire is invisible to the
|
||||
* sticky point expansion that moves attached endpoints along with a dragged component, so this is
|
||||
* what keeps the drawing connected after the user moves a symbol.
|
||||
*/
|
||||
private void onSchematicModified() {
|
||||
if (rewiring || schematicProject == null) {
|
||||
return;
|
||||
}
|
||||
rewiring = true;
|
||||
try {
|
||||
Project layoutProject = plugInPort.getCurrentProject();
|
||||
new SchematicSynchronizer().synchronize(layoutProject, plugInPort.getContinuityAreas());
|
||||
SchematicView view = layoutProject.getOrCreateSchematicView();
|
||||
schematicProject.setWidth(view.getWidth());
|
||||
schematicProject.setHeight(view.getHeight());
|
||||
schematicProject.getComponents().clear();
|
||||
schematicProject.getComponents().addAll(view.getComponents());
|
||||
schematicPresenter.refresh();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Could not re-wire the schematic", e);
|
||||
} finally {
|
||||
rewiring = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EnumSet<EventType> getSubscribedEventTypes() {
|
||||
return EnumSet.noneOf(EventType.class);
|
||||
@@ -206,4 +242,25 @@ public class SchematicTabPlugin implements IPlugIn {
|
||||
public void processMessage(EventType eventType, Object... params) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
/** Listens on the schematic presenter for symbol moves and triggers the re-wiring. */
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import org.diylc.components.connectivity.OrthogonalLine;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.SchematicView;
|
||||
import org.diylc.schematic.SchematicBuilder;
|
||||
@@ -59,6 +61,84 @@ public class SchematicViewIntegrationTest extends TestBase {
|
||||
assertEquals(1, project.getSchematicView().getPhysicalToSchematicMap().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wiresAreOrderedUnderneathTheSymbols() {
|
||||
instantiateTwoClick("Passive", "Resistor", new Point(1000, 1000), new Point(1200, 1000));
|
||||
instantiateTwoClick("Passive", "Resistor", new Point(1200, 1000), new Point(1400, 1000));
|
||||
|
||||
Project project = presenter.getCurrentProject();
|
||||
new SchematicBuilder().build(project, presenter.getContinuityAreas());
|
||||
|
||||
// the canvas paints in list order and hands a click to the last match, so wires belong first
|
||||
boolean seenSymbol = false;
|
||||
for (IDIYComponent<?> component : project.getSchematicView().getComponents()) {
|
||||
if (component instanceof OrthogonalLine) {
|
||||
assertTrue("wire found after a symbol", !seenSymbol);
|
||||
} else {
|
||||
seenSymbol = true;
|
||||
}
|
||||
}
|
||||
assertTrue(seenSymbol);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchingASymbolReconnectsTheWiresOnSync() {
|
||||
instantiateTwoClick("Passive", "Resistor", new Point(1000, 1000), new Point(1200, 1000));
|
||||
instantiateTwoClick("Passive", "Resistor", new Point(1200, 1000), new Point(1400, 1000));
|
||||
|
||||
Project project = presenter.getCurrentProject();
|
||||
new SchematicBuilder().build(project, presenter.getContinuityAreas());
|
||||
SchematicView view = project.getSchematicView();
|
||||
assertTrue(countWires(view) > 0);
|
||||
assertEndpointsSitOnPins(view);
|
||||
|
||||
// stretch a symbol by dragging a single pin, which is the case that used to leave the wire
|
||||
// behind: the pin the wire was drawn to is now somewhere else
|
||||
IDIYComponent<?> moved = null;
|
||||
for (IDIYComponent<?> component : view.getComponents()) {
|
||||
if (!(component instanceof OrthogonalLine)) {
|
||||
moved = component;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(moved);
|
||||
Point2D pin = moved.getControlPoint(1);
|
||||
moved.setControlPoint(new Point2D.Double(pin.getX() + 300, pin.getY() + 400), 1);
|
||||
|
||||
new SchematicSynchronizer().synchronize(project, presenter.getContinuityAreas());
|
||||
|
||||
view = project.getSchematicView();
|
||||
assertTrue(countWires(view) > 0);
|
||||
assertEndpointsSitOnPins(view);
|
||||
}
|
||||
|
||||
private static long countWires(SchematicView view) {
|
||||
return view.getComponents().stream().filter(c -> c instanceof OrthogonalLine).count();
|
||||
}
|
||||
|
||||
private static void assertEndpointsSitOnPins(SchematicView view) {
|
||||
for (IDIYComponent<?> wire : view.getComponents()) {
|
||||
if (!(wire instanceof OrthogonalLine)) {
|
||||
continue;
|
||||
}
|
||||
for (int end : new int[] {0, wire.getControlPointCount() - 1}) {
|
||||
Point2D endpoint = wire.getControlPoint(end);
|
||||
boolean onPin = false;
|
||||
for (IDIYComponent<?> symbol : view.getComponents()) {
|
||||
if (symbol instanceof OrthogonalLine) {
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < symbol.getControlPointCount(); i++) {
|
||||
if (symbol.getControlPoint(i).distance(endpoint) < 0.6) {
|
||||
onPin = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue("wire endpoint " + endpoint + " is not on any symbol pin", onPin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void viewerPanelRendersWithoutError() {
|
||||
instantiateTwoClick("Passive", "Resistor", new Point(1000, 1000), new Point(1200, 1000));
|
||||
|
||||
@@ -233,15 +233,36 @@ 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.
|
||||
- **Wires stay locked, and that is what forces the regeneration.** The whole
|
||||
`WIRING` layer is locked in the schematic wrapper project so a wire cannot be
|
||||
selected, dragged or deleted. `Presenter.includeStuckComponents` skips locked
|
||||
components, so a locked wire is invisible to the sticky-point expansion that
|
||||
drags attached endpoints along with a moved symbol. Locking and automatic
|
||||
reconnection are therefore coupled: the wires have to be regenerated explicitly.
|
||||
`SchematicTabPlugin` re-runs `SchematicSynchronizer` on `PROJECT_MODIFIED`,
|
||||
which keeps the placed symbols and rebuilds every wire from their current pin
|
||||
positions. That covers moving a whole symbol and, importantly, dragging a single
|
||||
pin of one.
|
||||
- **Locked components are painted at half alpha.** On a schematic where every wire
|
||||
is locked that washes out the entire drawing.
|
||||
`SchematicConfigurationManager` wraps the application's configuration for the
|
||||
schematic canvas alone and reports `lockedAlpha` as false, leaving the user's own
|
||||
setting untouched on the layout canvas. This replaces the opaque-composite
|
||||
workaround the old wire class carried inside its `draw`.
|
||||
- **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.
|
||||
dense schematic can produce wires that run over each other.
|
||||
|
||||
### Wires are ordered under the symbols
|
||||
|
||||
The schematic used to be sorted by z-order, which puts `WIRING` above `COMPONENT`.
|
||||
`SchematicBuilder.SCHEMATIC_ORDER` replaces that sort in both the builder and the
|
||||
synchronizer, ranking wires first and falling back to z-order for everything else.
|
||||
It is the right paint order for a schematic, symbols drawing over the wires behind
|
||||
them, and it keeps clicks reaching the symbols even if the wiring layer is ever
|
||||
unlocked, since the canvas hands a click to the last matching component in the
|
||||
list.
|
||||
|
||||
Three tests in `SchematicViewIntegrationTest` pin the behaviour down: that wires
|
||||
are ordered before symbols, that the generated wires' endpoints sit on symbol pins,
|
||||
and that stretching a symbol by one pin and re-syncing puts them back on pins.
|
||||
|
||||
Reference in New Issue
Block a user