mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
Compare commits
22 Commits
ui-theme
...
schem-view
| Author | SHA1 | Date | |
|---|---|---|---|
| a8b8f478dc | |||
| 5eebb055f8 | |||
| 636a33fae4 | |||
| 8bfcb51548 | |||
| a5739d2dcf | |||
| 54f4837b86 | |||
| b5ba302e93 | |||
| 078d03d59b | |||
| 333af398dd | |||
| 6f011bc66e | |||
| d3bb0e88b1 | |||
| 129ca2a304 | |||
| 17b5a65926 | |||
| 7cd2f11bda | |||
| 1669feafe4 | |||
| 5997fcfe9c | |||
| 20116ec98c | |||
| f229ef2c1a | |||
| fb3f1a82aa | |||
| 0ebd7e012a | |||
| 368d9bfdcb | |||
| b9740338ce |
+89
-2
@@ -7837,7 +7837,7 @@
|
||||
"className" : "electromechanical.MiniRelay",
|
||||
"category" : "Electro-Mechanical",
|
||||
"namePrefix" : "RY",
|
||||
"description" : "Miniature PCB mount relay, like Omron G5V-1 or G5V-2",
|
||||
"description" : "Miniature PCB mount relay, like Omron G5V-1/G5V-2 or Fujitsu FTR-B3",
|
||||
"creationMethod" : "SINGLE_CLICK",
|
||||
"notes" : [ "The first control point is the anchor; with 'Orientation' DEFAULT the first column of pins runs down from it, the second column sits to the right and the indent is at the anchor's end. The other orientations rotate this around the anchor", "Pin offsets down each column from the anchor: Miniature DPDT 0, 0.3, 0.5 and 0.7in, SPDT 0, 0.5 and 0.7in, columns 0.3in apart; Ultra_miniature DPDT 0, 0.2, 0.3 and 0.4in, SPDT 0, 0.3 and 0.4in, columns 0.2in apart" ],
|
||||
"properties" : [ {
|
||||
@@ -7994,7 +7994,7 @@
|
||||
}, {
|
||||
"name" : "Type",
|
||||
"type" : "ToggleSwitchType",
|
||||
"possibleValues" : [ "SPST", "SPDT", "SPDT_off", "DPDT", "DPDT_off", "DPDT_ononon_1", "DPDT_ononon_2", "_3PDT", "_3PDT_off", "_4PDT", "_4PDT_off", "_4PDT_ononon_1", "_4PDT_ononon_2", "_5PDT", "_5PDT_off" ],
|
||||
"possibleValues" : [ "SPST", "SPDT", "SPDT_off", "DPDT", "DPDT_off", "DPDT_ononon_1", "DPDT_ononon_2", "_3PDT", "_3PDT_off", "_4PDT", "_4PDT_off", "_4PDT_ononon_1", "_4PDT_ononon_2", "_5PDT", "_5PDT_off", "_6PDT", "_6PDT_off" ],
|
||||
"defaultValue" : "DPDT"
|
||||
} ],
|
||||
"pinout" : {
|
||||
@@ -8266,6 +8266,41 @@
|
||||
"connected" : [ [ 1, 2 ], [ 4, 5 ], [ 7, 8 ], [ 10, 11 ], [ 13, 14 ] ]
|
||||
} ]
|
||||
}
|
||||
}, {
|
||||
"when" : {
|
||||
"Type" : "_6PDT"
|
||||
},
|
||||
"pinCount" : 18,
|
||||
"labels" : "sequential",
|
||||
"switching" : {
|
||||
"positionCount" : 2,
|
||||
"positions" : [ {
|
||||
"name" : "ON1",
|
||||
"connected" : [ [ 0, 1 ], [ 3, 4 ], [ 6, 7 ], [ 9, 10 ], [ 12, 13 ], [ 15, 16 ] ]
|
||||
}, {
|
||||
"name" : "ON2",
|
||||
"connected" : [ [ 1, 2 ], [ 4, 5 ], [ 7, 8 ], [ 10, 11 ], [ 13, 14 ], [ 16, 17 ] ]
|
||||
} ]
|
||||
}
|
||||
}, {
|
||||
"when" : {
|
||||
"Type" : "_6PDT_off"
|
||||
},
|
||||
"pinCount" : 18,
|
||||
"labels" : "sequential",
|
||||
"switching" : {
|
||||
"positionCount" : 3,
|
||||
"positions" : [ {
|
||||
"name" : "ON1",
|
||||
"connected" : [ [ 0, 1 ], [ 3, 4 ], [ 6, 7 ], [ 9, 10 ], [ 12, 13 ], [ 15, 16 ] ]
|
||||
}, {
|
||||
"name" : "OFF",
|
||||
"connected" : [ ]
|
||||
}, {
|
||||
"name" : "ON2",
|
||||
"connected" : [ [ 1, 2 ], [ 4, 5 ], [ 7, 8 ], [ 10, 11 ], [ 13, 14 ], [ 16, 17 ] ]
|
||||
} ]
|
||||
}
|
||||
} ]
|
||||
}
|
||||
}, {
|
||||
@@ -13969,6 +14004,58 @@
|
||||
"label" : "-"
|
||||
} ]
|
||||
}
|
||||
}, {
|
||||
"name" : "Box (Schematic)",
|
||||
"className" : "schematic.SchematicBox",
|
||||
"category" : "Schematic Symbols",
|
||||
"namePrefix" : "U",
|
||||
"description" : "Generic schematic box with configurable terminals",
|
||||
"creationMethod" : "SINGLE_CLICK",
|
||||
"properties" : [ {
|
||||
"name" : "Alpha",
|
||||
"type" : "Percentage",
|
||||
"format" : "number without the % sign, e.g. 100",
|
||||
"defaultValue" : "100"
|
||||
}, {
|
||||
"name" : "Body",
|
||||
"type" : "Color",
|
||||
"format" : "hex",
|
||||
"defaultValue" : "#FFFFFF"
|
||||
}, {
|
||||
"name" : "Border",
|
||||
"type" : "Color",
|
||||
"format" : "hex",
|
||||
"defaultValue" : "#000000"
|
||||
}, {
|
||||
"name" : "Bottom Nodes",
|
||||
"type" : "String"
|
||||
}, {
|
||||
"name" : "Display",
|
||||
"type" : "Display",
|
||||
"possibleValues" : [ "NAME", "VALUE", "NONE", "BOTH" ],
|
||||
"defaultValue" : "BOTH"
|
||||
}, {
|
||||
"name" : "Left Nodes",
|
||||
"type" : "String",
|
||||
"defaultValue" : "1,2"
|
||||
}, {
|
||||
"name" : "Name",
|
||||
"type" : "String"
|
||||
}, {
|
||||
"name" : "Right Nodes",
|
||||
"type" : "String",
|
||||
"defaultValue" : "3,4"
|
||||
}, {
|
||||
"name" : "Top Nodes",
|
||||
"type" : "String"
|
||||
}, {
|
||||
"name" : "Value",
|
||||
"type" : "String"
|
||||
} ],
|
||||
"pinout" : {
|
||||
"pinCount" : 4,
|
||||
"labels" : "sequential"
|
||||
}
|
||||
}, {
|
||||
"name" : "Bulb (Schematic)",
|
||||
"className" : "misc.BulbSchematicSymbol",
|
||||
|
||||
@@ -389,6 +389,9 @@
|
||||
}, {
|
||||
"name" : "Battery (Schematic)",
|
||||
"className" : "misc.BatterySymbol"
|
||||
}, {
|
||||
"name" : "Box (Schematic)",
|
||||
"className" : "schematic.SchematicBox"
|
||||
}, {
|
||||
"name" : "Bulb (Schematic)",
|
||||
"className" : "misc.BulbSchematicSymbol"
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.diylc.core.CreationMethod;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.annotations.BomPolicy;
|
||||
import org.diylc.core.annotations.KeywordPolicy;
|
||||
import org.diylc.schematic.ISchematicFactory;
|
||||
|
||||
/**
|
||||
* Entity class used to describe a component type.
|
||||
@@ -56,6 +57,7 @@ public class ComponentType implements Serializable {
|
||||
private boolean enableCache;
|
||||
private List<String[]> datasheet;
|
||||
private int datasheetCreationStepCount;
|
||||
private Class<? extends ISchematicFactory> schematicFactoryClass;
|
||||
|
||||
public ComponentType() {
|
||||
}
|
||||
@@ -158,6 +160,18 @@ public class ComponentType implements Serializable {
|
||||
return datasheetCreationStepCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the declared {@link ISchematicFactory} class for this type, or {@code null} when the
|
||||
* type relies on the generic box fallback.
|
||||
*/
|
||||
public Class<? extends ISchematicFactory> getSchematicFactoryClass() {
|
||||
return schematicFactoryClass;
|
||||
}
|
||||
|
||||
public void setSchematicFactoryClass(Class<? extends ISchematicFactory> schematicFactoryClass) {
|
||||
this.schematicFactoryClass = schematicFactoryClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.List;
|
||||
import org.diylc.netlist.INetlistParser;
|
||||
import org.diylc.netlist.Netlist;
|
||||
import org.diylc.netlist.NetlistException;
|
||||
import org.diylc.presenter.ContinuityArea;
|
||||
|
||||
/**
|
||||
* Interface for {@link Netlist} related operations.
|
||||
@@ -42,7 +43,14 @@ public interface INetlistProcessor {
|
||||
* @throws NetlistException
|
||||
*/
|
||||
List<Netlist> extractNetlists(boolean includeSwitches) throws NetlistException;
|
||||
|
||||
|
||||
/**
|
||||
* @return the current continuity areas as computed by the drawing manager. Needed to build a
|
||||
* netlist outside of the presenter, e.g. for schematic generation.
|
||||
*/
|
||||
List<ContinuityArea> getContinuityAreas();
|
||||
|
||||
|
||||
/**
|
||||
* Finds all available {@link INetlistAnalyzer} implementations.
|
||||
*
|
||||
|
||||
@@ -68,6 +68,7 @@ public class Project implements Serializable, Cloneable {
|
||||
private Set<IDIYComponent<?>> lockedComponents;
|
||||
private Set<Integer> hiddenLayers;
|
||||
private Font font = DEFAULT_FONT;
|
||||
private SchematicView schematicView;
|
||||
|
||||
public Project() {
|
||||
components = new ArrayList<IDIYComponent<?>>();
|
||||
@@ -196,6 +197,30 @@ public class Project implements Serializable, Cloneable {
|
||||
return hiddenLayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the schematic representation of this project, or {@code null} if the user has never
|
||||
* opened the schematic view. Use {@link #getOrCreateSchematicView()} when a non-null
|
||||
* instance is required.
|
||||
*/
|
||||
public SchematicView getSchematicView() {
|
||||
return schematicView;
|
||||
}
|
||||
|
||||
public void setSchematicView(SchematicView schematicView) {
|
||||
this.schematicView = schematicView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily initializes and returns the {@link SchematicView}. Calling this method causes the
|
||||
* schematic data to be persisted with the project from now on.
|
||||
*/
|
||||
public SchematicView getOrCreateSchematicView() {
|
||||
if (schematicView == null) {
|
||||
schematicView = new SchematicView();
|
||||
}
|
||||
return schematicView;
|
||||
}
|
||||
|
||||
public VersionNumber getFileVersion() {
|
||||
return fileVersion;
|
||||
}
|
||||
@@ -323,6 +348,11 @@ public class Project implements Serializable, Cloneable {
|
||||
return false;
|
||||
} else if (!hiddenLayers.equals(other.hiddenLayers))
|
||||
return false;
|
||||
if (schematicView == null) {
|
||||
if (other.schematicView != null)
|
||||
return false;
|
||||
} else if (!schematicView.equals(other.schematicView))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -358,7 +388,10 @@ public class Project implements Serializable, Cloneable {
|
||||
project.setWidth(this.getWidth());
|
||||
project.getLockedLayers().addAll(this.getLockedLayers());
|
||||
project.getHiddenLayers().addAll(this.getHiddenLayers());
|
||||
project.setFont(this.getFont());
|
||||
project.setFont(this.getFont());
|
||||
if (this.schematicView != null) {
|
||||
project.setSchematicView(this.schematicView.clone());
|
||||
}
|
||||
|
||||
Map<IDIYComponent<?>, IDIYComponent<?>> cloneMap = new HashMap<IDIYComponent<?>, IDIYComponent<?>>();
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
|
||||
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.core;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
|
||||
/**
|
||||
* Schematic representation of a physical layout {@link Project}. It is derived from the layout via
|
||||
* the netlist infrastructure and rendered using the regular DIYLC drawing pipeline. It is embedded
|
||||
* in {@link Project} and serialized alongside the rest of the project data, but only after the user
|
||||
* opens the schematic view for the first time (the field stays {@code null} until then, keeping
|
||||
* files unchanged for users who never use the feature).
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class SchematicView implements Serializable, Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static Size DEFAULT_WIDTH = new Size(42d, SizeUnit.cm);
|
||||
public static Size DEFAULT_HEIGHT = new Size(30d, SizeUnit.cm);
|
||||
public static Size DEFAULT_GRID_SPACING = new Size(0.1d, SizeUnit.in);
|
||||
|
||||
/** Schematic components: symbols and auto-routed wires, sorted by z-order ascending. */
|
||||
private List<IDIYComponent<?>> components;
|
||||
|
||||
/**
|
||||
* Maps a physical component id ({@link IDIYComponent#getId()}) to the ids of the schematic
|
||||
* symbol(s) that were spawned from it. The relation is 1:N to support multi-section components
|
||||
* such as dual triodes or dual op-amps.
|
||||
*/
|
||||
private Map<UUID, List<UUID>> physicalToSchematicMap;
|
||||
|
||||
private Size width;
|
||||
private Size height;
|
||||
private Size gridSpacing;
|
||||
|
||||
public SchematicView() {
|
||||
this.components = new ArrayList<IDIYComponent<?>>();
|
||||
this.physicalToSchematicMap = new LinkedHashMap<UUID, List<UUID>>();
|
||||
this.width = DEFAULT_WIDTH;
|
||||
this.height = DEFAULT_HEIGHT;
|
||||
this.gridSpacing = DEFAULT_GRID_SPACING;
|
||||
}
|
||||
|
||||
public List<IDIYComponent<?>> getComponents() {
|
||||
if (components == null) {
|
||||
components = new ArrayList<IDIYComponent<?>>();
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
public void setComponents(List<IDIYComponent<?>> components) {
|
||||
this.components = components;
|
||||
}
|
||||
|
||||
public Map<UUID, List<UUID>> getPhysicalToSchematicMap() {
|
||||
if (physicalToSchematicMap == null) {
|
||||
physicalToSchematicMap = new LinkedHashMap<UUID, List<UUID>>();
|
||||
}
|
||||
return physicalToSchematicMap;
|
||||
}
|
||||
|
||||
public void setPhysicalToSchematicMap(Map<UUID, List<UUID>> physicalToSchematicMap) {
|
||||
this.physicalToSchematicMap = physicalToSchematicMap;
|
||||
}
|
||||
|
||||
public Size getWidth() {
|
||||
return width == null ? DEFAULT_WIDTH : width;
|
||||
}
|
||||
|
||||
public void setWidth(Size width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public Size getHeight() {
|
||||
return height == null ? DEFAULT_HEIGHT : height;
|
||||
}
|
||||
|
||||
public void setHeight(Size height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public Size getGridSpacing() {
|
||||
return gridSpacing == null ? DEFAULT_GRID_SPACING : gridSpacing;
|
||||
}
|
||||
|
||||
public void setGridSpacing(Size gridSpacing) {
|
||||
this.gridSpacing = gridSpacing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if the schematic has already been generated at least once.
|
||||
*/
|
||||
public boolean isGenerated() {
|
||||
return components != null && !components.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SchematicView clone() {
|
||||
SchematicView clone = new SchematicView();
|
||||
clone.setWidth(this.getWidth());
|
||||
clone.setHeight(this.getHeight());
|
||||
clone.setGridSpacing(this.getGridSpacing());
|
||||
for (IDIYComponent<?> component : this.getComponents()) {
|
||||
try {
|
||||
clone.getComponents().add(component.clone());
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<UUID, List<UUID>> entry : this.getPhysicalToSchematicMap().entrySet()) {
|
||||
clone.getPhysicalToSchematicMap().put(entry.getKey(), new ArrayList<UUID>(entry.getValue()));
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((components == null) ? 0 : components.hashCode());
|
||||
result = prime * result + ((width == null) ? 0 : width.hashCode());
|
||||
result = prime * result + ((height == null) ? 0 : height.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null || getClass() != obj.getClass())
|
||||
return false;
|
||||
SchematicView other = (SchematicView) obj;
|
||||
if (components == null) {
|
||||
if (other.components != null)
|
||||
return false;
|
||||
} else if (other.components == null || components.size() != other.components.size()) {
|
||||
return false;
|
||||
} else {
|
||||
for (int i = 0; i < components.size(); i++) {
|
||||
if (!components.get(i).equalsTo(other.components.get(i)))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (width == null ? other.width != null : !width.equals(other.width))
|
||||
return false;
|
||||
if (height == null ? other.height != null : !height.equals(other.height))
|
||||
return false;
|
||||
if (gridSpacing == null ? other.gridSpacing != null : !gridSpacing.equals(other.gridSpacing))
|
||||
return false;
|
||||
return new HashMap<UUID, List<UUID>>(getPhysicalToSchematicMap())
|
||||
.equals(new HashMap<UUID, List<UUID>>(other.getPhysicalToSchematicMap()));
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import org.diylc.common.DefaultComponentTransformer;
|
||||
import org.diylc.common.IComponentTransformer;
|
||||
import org.diylc.core.CreationMethod;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.schematic.ISchematicFactory;
|
||||
|
||||
/**
|
||||
* Annotation that needs to be used for each {@link IDIYComponent} implementation. Describes
|
||||
@@ -129,6 +130,15 @@ public @interface ComponentDescriptor {
|
||||
*/
|
||||
int datasheetCreationStepCount() default 0;
|
||||
|
||||
/**
|
||||
* Strategy that converts an instance of this component type into one or more schematic symbols
|
||||
* when the schematic view is generated. When left at the default ({@link ISchematicFactory}
|
||||
* itself, used here only as a "none" marker), a generic box symbol is produced instead.
|
||||
*
|
||||
* @return the schematic factory class, or {@code ISchematicFactory.class} when none is declared
|
||||
*/
|
||||
Class<? extends ISchematicFactory> schematicFactory() default ISchematicFactory.class;
|
||||
|
||||
/**
|
||||
* @return true if the component type should not be listed in the palette (toolbox and
|
||||
* component tree), while still being resolvable via
|
||||
|
||||
@@ -133,6 +133,11 @@ public class ComponentProcessor {
|
||||
new ComponentType(name, description, creationMethod, category, namePrefix, author, icon, clazz, zOrder,
|
||||
flexibleZOrder, bomPolicy, autoEdit, transformer, keywordPolicy, keywordTag, cacheDrawing, datasheet,
|
||||
datasheetCreationStepCount);
|
||||
Class<? extends org.diylc.schematic.ISchematicFactory> schematicFactory =
|
||||
clazz.getAnnotation(ComponentDescriptor.class).schematicFactory();
|
||||
if (schematicFactory != null && schematicFactory != org.diylc.schematic.ISchematicFactory.class) {
|
||||
componentType.setSchematicFactoryClass(schematicFactory);
|
||||
}
|
||||
componentTypeMap.put(clazz.getName(), componentType);
|
||||
return componentType;
|
||||
}
|
||||
|
||||
@@ -3208,10 +3208,15 @@ public class Presenter implements IPlugInPort {
|
||||
@Override
|
||||
public List<Netlist> extractNetlists(boolean includeSwitches) throws NetlistException {
|
||||
List<ContinuityArea> continuityAreas = drawingManager.getContinuityAreas();
|
||||
|
||||
|
||||
return NetlistBuilder.extractNetlists(includeSwitches, currentProject, continuityAreas);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ContinuityArea> getContinuityAreas() {
|
||||
return drawingManager.getContinuityAreas();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<INetlistAnalyzer> getNetlistAnalyzers() {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
|
||||
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.util.List;
|
||||
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Strategy that converts a physical layout component into one or more schematic symbols.
|
||||
*
|
||||
* <p>
|
||||
* Implementations are declared per component type through
|
||||
* {@code @ComponentDescriptor(schematicFactory = ...)}. When a type declares no factory, the
|
||||
* schematic generator falls back to a generic box symbol.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Implementations must have a public no-argument constructor. They are instantiated once and reused,
|
||||
* so they must be stateless.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public interface ISchematicFactory {
|
||||
|
||||
/**
|
||||
* Creates fully instantiated and configured schematic symbol instances for the given physical
|
||||
* component. The returned symbols only need to be positioned by the caller.
|
||||
*
|
||||
* @param physicalComponent the physical layout component
|
||||
* @return list of {@link SchematicSymbolMapping} entries, one per schematic symbol (1:1 for simple
|
||||
* components, 1:N for multi-section components). An empty list means the component produces
|
||||
* no symbol and should be treated as excluded.
|
||||
*/
|
||||
List<SchematicSymbolMapping> createSchematicSymbols(IDIYComponent<?> physicalComponent);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
|
||||
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.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Describes a single schematic symbol that an {@link ISchematicFactory} spawned from a physical
|
||||
* layout component. For multi-section physical components (dual triodes, dual op-amps, etc.) a
|
||||
* factory returns more than one of these.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class SchematicSymbolMapping {
|
||||
|
||||
private IDIYComponent<?> schematicSymbol;
|
||||
private Map<Integer, Integer> pinMapping;
|
||||
private String sectionLabel;
|
||||
|
||||
public SchematicSymbolMapping() {
|
||||
this.pinMapping = new HashMap<Integer, Integer>();
|
||||
}
|
||||
|
||||
public SchematicSymbolMapping(IDIYComponent<?> schematicSymbol) {
|
||||
this();
|
||||
this.schematicSymbol = schematicSymbol;
|
||||
}
|
||||
|
||||
public SchematicSymbolMapping(IDIYComponent<?> schematicSymbol, Map<Integer, Integer> pinMapping,
|
||||
String sectionLabel) {
|
||||
this.schematicSymbol = schematicSymbol;
|
||||
this.pinMapping = pinMapping == null ? new HashMap<Integer, Integer>() : pinMapping;
|
||||
this.sectionLabel = sectionLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the fully configured schematic symbol instance. It only needs to be positioned.
|
||||
*/
|
||||
public IDIYComponent<?> getSchematicSymbol() {
|
||||
return schematicSymbol;
|
||||
}
|
||||
|
||||
public void setSchematicSymbol(IDIYComponent<?> schematicSymbol) {
|
||||
this.schematicSymbol = schematicSymbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a physical component control point index to the control point index on
|
||||
* {@link #getSchematicSymbol()}. Physical control points that are not present in this map have no
|
||||
* counterpart on the symbol and any net attached to them is dropped for this symbol.
|
||||
*
|
||||
* @return physical control point index -> schematic control point index
|
||||
*/
|
||||
public Map<Integer, Integer> getPinMapping() {
|
||||
if (pinMapping == null) {
|
||||
pinMapping = new HashMap<Integer, Integer>();
|
||||
}
|
||||
return pinMapping;
|
||||
}
|
||||
|
||||
public void setPinMapping(Map<Integer, Integer> pinMapping) {
|
||||
this.pinMapping = pinMapping;
|
||||
}
|
||||
|
||||
public SchematicSymbolMapping mapPin(int physicalIndex, int schematicIndex) {
|
||||
getPinMapping().put(physicalIndex, schematicIndex);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an optional label identifying the section of a multi-section component, e.g.
|
||||
* "Section A". May be {@code null}.
|
||||
*/
|
||||
public String getSectionLabel() {
|
||||
return sectionLabel;
|
||||
}
|
||||
|
||||
public void setSectionLabel(String sectionLabel) {
|
||||
this.sectionLabel = sectionLabel;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -26,6 +26,7 @@ import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.CapacitorSchematicFactory;
|
||||
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.ObjectCache;
|
||||
@@ -48,7 +49,8 @@ import org.diylc.utils.Constants;
|
||||
|
||||
@AiNote("The two control points are the holes the leads go into and the body is centred between them; 'Length' is the body length, not the hole spacing")
|
||||
@AiNote("It has no standing mode: keep the control points at least 'Length' apart, or the body is drawn past the holes")
|
||||
@ComponentDescriptor(name = "Electrolytic Capacitor (Axial)", author = "Branislav Stojkovic",
|
||||
@ComponentDescriptor(schematicFactory = CapacitorSchematicFactory.class,
|
||||
name = "Electrolytic Capacitor (Axial)", author = "Branislav Stojkovic",
|
||||
category = "Passive", creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "C",
|
||||
description = "Axial electrolytic capacitor, similar to Sprague Atom, F&T, etc",
|
||||
zOrder = IDIYComponent.COMPONENT, transformer = SimpleComponentTransformer.class,
|
||||
|
||||
+3
-1
@@ -24,6 +24,7 @@ import java.awt.Shape;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
import org.diylc.components.passive.CapacitorDatasheetService.CapacitorDatasheet;
|
||||
import org.diylc.schematic.CapacitorSchematicFactory;
|
||||
import org.diylc.components.transform.SimpleComponentTransformer;
|
||||
import org.diylc.core.CreationMethod;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
@@ -34,7 +35,8 @@ import org.diylc.core.annotations.EditableProperty;
|
||||
|
||||
@AiNote("The two control points are the holes the leads go into and the body is centred between them; 'Length' is the body length, not the hole spacing")
|
||||
@AiNote("If the control points are closer together than 'Length' (0.5in by default) the part is drawn standing, its body over the first point, or over the second with 'Reverse (standing)'; to keep it lying on a tighter footprint, reduce 'Length'")
|
||||
@ComponentDescriptor(name = "Film Capacitor (Axial)", author = "Branislav Stojkovic",
|
||||
@ComponentDescriptor(schematicFactory = CapacitorSchematicFactory.class,
|
||||
name = "Film Capacitor (Axial)", author = "Branislav Stojkovic",
|
||||
category = "Passive", creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "C",
|
||||
description = "Axial film capacitor, similar to Mallory 150s", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = SimpleComponentTransformer.class, enableDatasheet = true,
|
||||
|
||||
+3
-1
@@ -12,6 +12,7 @@ import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.PotentiometerSchematicFactory;
|
||||
|
||||
import org.diylc.common.Display;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
@@ -30,7 +31,8 @@ import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
@ComponentDescriptor(name = "Potentiometer (Miniature)", author = "Branislav Stojkovic", category = "Passive",
|
||||
@ComponentDescriptor(schematicFactory = PotentiometerSchematicFactory.class,
|
||||
name = "Potentiometer (Miniature)", author = "Branislav Stojkovic", category = "Passive",
|
||||
creationMethod = CreationMethod.SINGLE_CLICK, instanceNamePrefix = "VR",
|
||||
description = "9mm Alpha style miniature potentiometers", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = MiniaturePotentiometerTransformer.class)
|
||||
|
||||
+3
-1
@@ -33,6 +33,7 @@ import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.PotentiometerSchematicFactory;
|
||||
|
||||
import org.diylc.awt.StringUtils;
|
||||
import org.diylc.common.HorizontalAlignment;
|
||||
@@ -59,7 +60,8 @@ import org.diylc.utils.Constants;
|
||||
@AiNote("The first control point is the anchor; with 'Orientation' DEFAULT the other two lugs run to the right of it 'Spacing' apart, and the other orientations rotate this around the anchor")
|
||||
@AiNote("Orient the pot so its lugs face the rest of the circuit: they face down in DEFAULT, left in _90, up in _180 and right in _270")
|
||||
@AiNote("Unless told otherwise, use one of two standard sizes, picked by the type of circuit and the space available: the 25mm pot, which the defaults already describe ('Diameter' 1in, 'Spacing' 0.3in), or the 16mm pot ('Diameter' 16mm, 'Spacing' 0.2in)")
|
||||
@ComponentDescriptor(name = "Potentiometer", author = "Branislav Stojkovic", category = "Passive",
|
||||
@ComponentDescriptor(schematicFactory = PotentiometerSchematicFactory.class,
|
||||
name = "Potentiometer", author = "Branislav Stojkovic", category = "Passive",
|
||||
creationMethod = CreationMethod.SINGLE_CLICK, instanceNamePrefix = "VR",
|
||||
description = "Metal body panel mount potentiometer with solder lugs or PCB terminals (Alpha, CTS, Bourns)", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = PotentiometerTransformer.class, enableCache = true)
|
||||
|
||||
+3
-1
@@ -27,6 +27,7 @@ import java.awt.Shape;
|
||||
import java.awt.geom.Ellipse2D;
|
||||
|
||||
import org.diylc.components.transform.SimpleComponentTransformer;
|
||||
import org.diylc.schematic.CapacitorSchematicFactory;
|
||||
import org.diylc.core.CreationMethod;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.annotations.AiNote;
|
||||
@@ -38,7 +39,8 @@ import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
|
||||
@AiNote("'Length' and 'Width' are the disc as seen from above: 'Length' is its diameter and 'Width' its thickness")
|
||||
@ComponentDescriptor(name = "Ceramic Capacitor (Radial)", author = "Branislav Stojkovic", category = "Passive",
|
||||
@ComponentDescriptor(schematicFactory = CapacitorSchematicFactory.class,
|
||||
name = "Ceramic Capacitor (Radial)", author = "Branislav Stojkovic", category = "Passive",
|
||||
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "C",
|
||||
description = "Standard radial ceramic capacitor", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = SimpleComponentTransformer.class)
|
||||
|
||||
+3
-1
@@ -29,6 +29,7 @@ import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.CapacitorSchematicFactory;
|
||||
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.ObjectCache;
|
||||
@@ -48,7 +49,8 @@ import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
@AiNote("'Diameter' is the can as seen from above; 'Height' is drawn only when 'Folded' lays the can on its side")
|
||||
@ComponentDescriptor(name = "Electrolytic Capacitor (Radial)", author = "Branislav Stojkovic", category = "Passive",
|
||||
@ComponentDescriptor(schematicFactory = CapacitorSchematicFactory.class,
|
||||
name = "Electrolytic Capacitor (Radial)", author = "Branislav Stojkovic", category = "Passive",
|
||||
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "C",
|
||||
description = "Vertically mounted electrolytic capacitor, polarized or bipolar", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = SimpleComponentTransformer.class, enableDatasheet = true, datasheetCreationStepCount = 3)
|
||||
|
||||
+3
-1
@@ -26,6 +26,7 @@ import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
|
||||
import org.diylc.components.passive.CapacitorDatasheetService.CapacitorDatasheet;
|
||||
import org.diylc.schematic.CapacitorSchematicFactory;
|
||||
import org.diylc.components.transform.SimpleComponentTransformer;
|
||||
import org.diylc.core.CreationMethod;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
@@ -38,7 +39,8 @@ import org.diylc.core.measures.SizeUnit;
|
||||
|
||||
@AiNote("The two control points are the holes; the body's own leads are 'Lead Spacing' apart and centred between them, with lead wire drawn out to each hole. Place the points exactly 'Lead Spacing' apart for straight leads")
|
||||
@AiNote("'Length' and 'Width' are the body as seen from above: 'Length' runs along the line between the leads and 'Width' across it, i.e. the datasheet width and thickness; body height is not drawn")
|
||||
@ComponentDescriptor(name = "Film Capacitor (Radial)", author = "Branislav Stojkovic",
|
||||
@ComponentDescriptor(schematicFactory = CapacitorSchematicFactory.class,
|
||||
name = "Film Capacitor (Radial)", author = "Branislav Stojkovic",
|
||||
category = "Passive", creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "C",
|
||||
description = "Radial film capacitor, similar to Sprague Orange Drop",
|
||||
zOrder = IDIYComponent.COMPONENT, transformer = SimpleComponentTransformer.class,
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.schematic.InductorSchematicFactory;
|
||||
import org.diylc.components.transform.SimpleComponentTransformer;
|
||||
import org.diylc.core.CreationMethod;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
@@ -46,7 +47,8 @@ import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
@AiNote("'Diameter' is the body as seen from above; 'Height' is drawn only when 'Folded' lays the part on its side")
|
||||
@ComponentDescriptor(name = "Inductor (Radial)", author = "Branislav Stojkovic", category = "Passive",
|
||||
@ComponentDescriptor(schematicFactory = InductorSchematicFactory.class,
|
||||
name = "Inductor (Radial)", author = "Branislav Stojkovic", category = "Passive",
|
||||
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "L",
|
||||
description = "Vertically mounted ferrite core inductor", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = SimpleComponentTransformer.class)
|
||||
|
||||
+3
-1
@@ -26,6 +26,7 @@ import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
import org.diylc.components.passive.CapacitorDatasheetService.CapacitorDatasheet;
|
||||
import org.diylc.schematic.CapacitorSchematicFactory;
|
||||
import org.diylc.components.transform.SimpleComponentTransformer;
|
||||
import org.diylc.core.CreationMethod;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
@@ -38,7 +39,8 @@ import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.core.measures.Voltage;
|
||||
|
||||
@ComponentDescriptor(name = "Mica Capacitor (Radial)", author = "Branislav Stojkovic",
|
||||
@ComponentDescriptor(schematicFactory = CapacitorSchematicFactory.class,
|
||||
name = "Mica Capacitor (Radial)", author = "Branislav Stojkovic",
|
||||
category = "Passive", creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "C",
|
||||
description = "Standard radial mica capacitor", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = SimpleComponentTransformer.class, enableDatasheet = true,
|
||||
|
||||
@@ -32,6 +32,7 @@ import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.schematic.ResistorSchematicFactory;
|
||||
import org.diylc.common.ResistorColorCode;
|
||||
import org.diylc.components.AbstractLeadedComponent;
|
||||
import org.diylc.components.transform.SimpleComponentTransformer;
|
||||
@@ -48,7 +49,8 @@ import org.diylc.core.measures.SizeUnit;
|
||||
|
||||
@AiNote("The two control points are the holes the leads go into and the body is centred between them; 'Length' is the body length, not the hole spacing")
|
||||
@AiNote("If the control points are closer together than 'Length' (0.5in by default) the part is drawn standing, its body over the first point; to keep it lying on a tighter footprint, reduce 'Length'")
|
||||
@ComponentDescriptor(name = "Resistor", author = "Branislav Stojkovic", category = "Passive",
|
||||
@ComponentDescriptor(schematicFactory = ResistorSchematicFactory.class,
|
||||
name = "Resistor", author = "Branislav Stojkovic", category = "Passive",
|
||||
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "R", description = "Resistor layout symbol",
|
||||
zOrder = IDIYComponent.COMPONENT, transformer = SimpleComponentTransformer.class, enableCache = true)
|
||||
public class Resistor extends AbstractLeadedComponent<Resistance> implements IGerberComponentSimple {
|
||||
|
||||
+3
-1
@@ -29,6 +29,7 @@ import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.CapacitorSchematicFactory;
|
||||
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.ObjectCache;
|
||||
@@ -46,7 +47,8 @@ import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
@AiNote("'Diameter' is the body as seen from above; 'Height' is drawn only when 'Folded' lays the part on its side")
|
||||
@ComponentDescriptor(name = "Tantalum Capacitor", author = "Branislav Stojkovic", category = "Passive",
|
||||
@ComponentDescriptor(schematicFactory = CapacitorSchematicFactory.class,
|
||||
name = "Tantalum Capacitor", author = "Branislav Stojkovic", category = "Passive",
|
||||
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "C",
|
||||
description = "Vertically mounted tantalum capacitor", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = SimpleComponentTransformer.class)
|
||||
|
||||
+3
-1
@@ -28,6 +28,7 @@ import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.schematic.InductorSchematicFactory;
|
||||
import org.diylc.components.AbstractLeadedComponent;
|
||||
import org.diylc.components.transform.SimpleComponentTransformer;
|
||||
import org.diylc.core.CreationMethod;
|
||||
@@ -41,7 +42,8 @@ import org.diylc.core.measures.Resistance;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
|
||||
@ComponentDescriptor(name = "Inductor (Toroidal)", author = "Branislav Stojkovic", category = "Passive",
|
||||
@ComponentDescriptor(schematicFactory = InductorSchematicFactory.class,
|
||||
name = "Inductor (Toroidal)", author = "Branislav Stojkovic", category = "Passive",
|
||||
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "L",
|
||||
description = "Ferrite core toroidal inductor mounted vertically", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = SimpleComponentTransformer.class)
|
||||
|
||||
+3
-1
@@ -30,6 +30,7 @@ import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.PotentiometerSchematicFactory;
|
||||
|
||||
import org.diylc.common.Display;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
@@ -50,7 +51,8 @@ import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
@AiNote("The first control point is the anchor; with 'Orientation' DEFAULT the third lug is 0.2in below it (0.4in for FLAT_XLARGE) and the wiper sits halfway between them, offset to the right by 0.2in for FLAT_SMALL and FLAT_XSMALL, 0.4in for FLAT_SMALL2 and FLAT_LARGE, 0.5in for FLAT_XLARGE, 0.1in for VERTICAL_OFFSET, 0.2in for VERTICAL_OFFSET_BIG_GAP and not at all for VERTICAL_INLINE. The other orientations rotate this around the anchor")
|
||||
@ComponentDescriptor(name = "Trimmer Potentiometer", author = "Branislav Stojkovic", category = "Passive",
|
||||
@ComponentDescriptor(schematicFactory = PotentiometerSchematicFactory.class,
|
||||
name = "Trimmer Potentiometer", author = "Branislav Stojkovic", category = "Passive",
|
||||
creationMethod = CreationMethod.SINGLE_CLICK, instanceNamePrefix = "VR",
|
||||
description = "Various types of board mounted trimmer potentiometers", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = TrimmerTransformer.class)
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
/*
|
||||
|
||||
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.Color;
|
||||
import java.awt.Composite;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.awt.StringUtils;
|
||||
import org.diylc.common.Display;
|
||||
import org.diylc.common.HorizontalAlignment;
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.common.VerticalAlignment;
|
||||
import org.diylc.components.AbstractTransparentComponent;
|
||||
import org.diylc.core.ComponentState;
|
||||
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;
|
||||
import org.diylc.core.annotations.KeywordPolicy;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
/**
|
||||
* Generic rectangular IC-style schematic symbol with configurable terminals on each of the four
|
||||
* sides, similar to KiCad's generic symbol. Used as the default fallback representation for physical
|
||||
* components that do not declare a dedicated schematic factory.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
@ComponentDescriptor(name = "Box", author = "DIYLC", category = "Schematic Symbols",
|
||||
instanceNamePrefix = "U", description = "Generic schematic box with configurable terminals",
|
||||
zOrder = IDIYComponent.COMPONENT, bomPolicy = BomPolicy.SHOW_ALL_NAMES,
|
||||
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Schematic")
|
||||
public class SchematicBox extends AbstractTransparentComponent<String> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static Size PIN_SPACING = new Size(0.1d, SizeUnit.in);
|
||||
public static Color BODY_COLOR = Color.white;
|
||||
public static Color BORDER_COLOR = Color.black;
|
||||
public static Color PIN_COLOR = Color.black;
|
||||
|
||||
private String leftNodes = "1,2";
|
||||
private String rightNodes = "3,4";
|
||||
private String topNodes = "";
|
||||
private String bottomNodes = "";
|
||||
|
||||
private String value = "";
|
||||
private Display display = Display.BOTH;
|
||||
private Color bodyColor = BODY_COLOR;
|
||||
private Color borderColor = BORDER_COLOR;
|
||||
|
||||
private Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)};
|
||||
|
||||
transient private List<String> nodeLabels;
|
||||
|
||||
public SchematicBox() {
|
||||
super();
|
||||
updateControlPoints();
|
||||
}
|
||||
|
||||
private static List<String> parseNodes(String csv) {
|
||||
List<String> result = new ArrayList<String>();
|
||||
if (csv == null)
|
||||
return result;
|
||||
for (String token : csv.split(",")) {
|
||||
String trimmed = token.trim();
|
||||
if (!trimmed.isEmpty())
|
||||
result.add(trimmed);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int leftCount() {
|
||||
return parseNodes(leftNodes).size();
|
||||
}
|
||||
|
||||
private int rightCount() {
|
||||
return parseNodes(rightNodes).size();
|
||||
}
|
||||
|
||||
private int topCount() {
|
||||
return parseNodes(topNodes).size();
|
||||
}
|
||||
|
||||
private int bottomCount() {
|
||||
return parseNodes(bottomNodes).size();
|
||||
}
|
||||
|
||||
private double pinSpacing() {
|
||||
return PIN_SPACING.convertToPixels();
|
||||
}
|
||||
|
||||
private double bodyWidth() {
|
||||
double spacing = pinSpacing();
|
||||
int horizontal = Math.max(topCount(), bottomCount());
|
||||
return Math.max((horizontal + 1) * spacing, 4 * spacing);
|
||||
}
|
||||
|
||||
private double bodyHeight() {
|
||||
double spacing = pinSpacing();
|
||||
int vertical = Math.max(leftCount(), rightCount());
|
||||
return Math.max((vertical + 1) * spacing, 2 * spacing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes the pin geometry from the anchor (control point 0) and the configured node counts.
|
||||
* Ordering: left side (top to bottom), right side (top to bottom), top side (left to right),
|
||||
* bottom side (left to right).
|
||||
*
|
||||
* <p>
|
||||
* This is called on construction and whenever the node lists change — <b>not</b> from
|
||||
* {@link #setControlPoint}. During a drag the presenter translates every control point by the
|
||||
* same delta (because {@link #canPointMoveFreely} is {@code false}), which already keeps the pins
|
||||
* on the box edge, so recomputing there would double-apply the delta. When the pin count is
|
||||
* unchanged the existing {@link Point2D} objects are mutated in place so references held by the
|
||||
* drawing/selection code stay valid.
|
||||
* </p>
|
||||
*/
|
||||
private void updateControlPoints() {
|
||||
nodeLabels = null;
|
||||
double spacing = pinSpacing();
|
||||
double leadLength = spacing;
|
||||
double firstPinX = controlPoints[0].getX();
|
||||
double firstPinY = controlPoints[0].getY();
|
||||
|
||||
double bodyX = firstPinX + leadLength;
|
||||
double bodyY = firstPinY - spacing;
|
||||
double bodyW = bodyWidth();
|
||||
double bodyH = bodyHeight();
|
||||
|
||||
List<Point2D> points = new ArrayList<Point2D>();
|
||||
// left
|
||||
for (int i = 0; i < leftCount(); i++) {
|
||||
points.add(new Point2D.Double(firstPinX, bodyY + spacing * (i + 1)));
|
||||
}
|
||||
// right
|
||||
for (int i = 0; i < rightCount(); i++) {
|
||||
points.add(new Point2D.Double(bodyX + bodyW + leadLength, bodyY + spacing * (i + 1)));
|
||||
}
|
||||
// top
|
||||
for (int i = 0; i < topCount(); i++) {
|
||||
points.add(new Point2D.Double(bodyX + spacing * (i + 1), bodyY - leadLength));
|
||||
}
|
||||
// bottom
|
||||
for (int i = 0; i < bottomCount(); i++) {
|
||||
points.add(new Point2D.Double(bodyX + spacing * (i + 1), bodyY + bodyH + leadLength));
|
||||
}
|
||||
if (points.isEmpty()) {
|
||||
points.add(new Point2D.Double(firstPinX, firstPinY));
|
||||
}
|
||||
// keep index 0 exactly where it was
|
||||
points.set(0, new Point2D.Double(firstPinX, firstPinY));
|
||||
|
||||
if (controlPoints.length == points.size()) {
|
||||
for (int i = 0; i < points.size(); i++) {
|
||||
controlPoints[i].setLocation(points.get(i));
|
||||
}
|
||||
} else {
|
||||
controlPoints = points.toArray(new Point2D[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getNodeLabels() {
|
||||
if (nodeLabels == null) {
|
||||
nodeLabels = new ArrayList<String>();
|
||||
nodeLabels.addAll(parseNodes(leftNodes));
|
||||
nodeLabels.addAll(parseNodes(rightNodes));
|
||||
nodeLabels.addAll(parseNodes(topNodes));
|
||||
nodeLabels.addAll(parseNodes(bottomNodes));
|
||||
}
|
||||
return nodeLabels;
|
||||
}
|
||||
|
||||
private Rectangle2D getBodyRectangle() {
|
||||
double spacing = pinSpacing();
|
||||
double leadLength = spacing;
|
||||
double bodyX = controlPoints[0].getX() + leadLength;
|
||||
double bodyY = controlPoints[0].getY() - spacing;
|
||||
return new Rectangle2D.Double(bodyX, bodyY, bodyWidth(), bodyHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
IDrawingObserver drawingObserver) {
|
||||
if (checkPointsClipped(g2d.getClip()))
|
||||
return;
|
||||
|
||||
Composite oldComposite = applyAlpha(g2d, componentState);
|
||||
Rectangle2D body = getBodyRectangle();
|
||||
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
|
||||
g2d.fill(body);
|
||||
g2d.setComposite(oldComposite);
|
||||
|
||||
Color finalBorder =
|
||||
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
|
||||
: borderColor;
|
||||
g2d.setColor(finalBorder);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(body);
|
||||
|
||||
// pins
|
||||
g2d.setColor(componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
|
||||
? SELECTION_COLOR
|
||||
: PIN_COLOR);
|
||||
for (int i = 0; i < controlPoints.length; i++) {
|
||||
Point2D p = controlPoints[i];
|
||||
Point2D edge = nearestEdgePoint(p, body);
|
||||
g2d.drawLine((int) Math.round(p.getX()), (int) Math.round(p.getY()), (int) Math.round(edge.getX()),
|
||||
(int) Math.round(edge.getY()));
|
||||
}
|
||||
|
||||
// pin labels
|
||||
Font smallFont = project.getFont().deriveFont((float) (project.getFont().getSize() * 0.8));
|
||||
g2d.setFont(smallFont);
|
||||
List<String> labels = getNodeLabels();
|
||||
for (int i = 0; i < controlPoints.length && i < labels.size(); i++) {
|
||||
Point2D edge = nearestEdgePoint(controlPoints[i], body);
|
||||
double lx = edge.getX();
|
||||
double ly = edge.getY();
|
||||
// nudge label inward
|
||||
if (Math.abs(edge.getX() - body.getMinX()) < 1)
|
||||
lx += smallFont.getSize();
|
||||
else if (Math.abs(edge.getX() - body.getMaxX()) < 1)
|
||||
lx -= smallFont.getSize();
|
||||
if (Math.abs(edge.getY() - body.getMinY()) < 1)
|
||||
ly += smallFont.getSize() * 0.7;
|
||||
else if (Math.abs(edge.getY() - body.getMaxY()) < 1)
|
||||
ly -= smallFont.getSize() * 0.7;
|
||||
StringUtils.drawCenteredText(g2d, labels.get(i), lx, ly, HorizontalAlignment.CENTER,
|
||||
VerticalAlignment.CENTER);
|
||||
}
|
||||
|
||||
// body label
|
||||
g2d.setColor(componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
|
||||
? LABEL_COLOR_SELECTED
|
||||
: LABEL_COLOR);
|
||||
g2d.setFont(project.getFont());
|
||||
String label = getLabelForDisplay();
|
||||
if (label != null && !label.isEmpty()) {
|
||||
StringUtils.drawCenteredText(g2d, label, body.getCenterX(), body.getCenterY(),
|
||||
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
private String getLabelForDisplay() {
|
||||
switch (display) {
|
||||
case NAME:
|
||||
return getName();
|
||||
case VALUE:
|
||||
return value;
|
||||
case NONE:
|
||||
return "";
|
||||
case BOTH:
|
||||
default:
|
||||
return (getName() == null ? "" : getName()) + (value == null || value.isEmpty() ? "" : " " + value);
|
||||
}
|
||||
}
|
||||
|
||||
private static Point2D nearestEdgePoint(Point2D p, Rectangle2D body) {
|
||||
double x = p.getX();
|
||||
double y = p.getY();
|
||||
if (x <= body.getMinX())
|
||||
return new Point2D.Double(body.getMinX(), clamp(y, body.getMinY(), body.getMaxY()));
|
||||
if (x >= body.getMaxX())
|
||||
return new Point2D.Double(body.getMaxX(), clamp(y, body.getMinY(), body.getMaxY()));
|
||||
if (y <= body.getMinY())
|
||||
return new Point2D.Double(clamp(x, body.getMinX(), body.getMaxX()), body.getMinY());
|
||||
return new Point2D.Double(clamp(x, body.getMinX(), body.getMaxX()), body.getMaxY());
|
||||
}
|
||||
|
||||
private static double clamp(double v, double min, double max) {
|
||||
return Math.max(min, Math.min(max, v));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
int margin = 4 * width / 32;
|
||||
g2d.setColor(BODY_COLOR);
|
||||
g2d.fillRect(margin * 2, margin, width - margin * 4, height - margin * 2);
|
||||
g2d.setColor(BORDER_COLOR);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.drawRect(margin * 2, margin, width - margin * 4, height - margin * 2);
|
||||
g2d.drawLine(0, height / 3, margin * 2, height / 3);
|
||||
g2d.drawLine(0, height * 2 / 3, margin * 2, height * 2 / 3);
|
||||
g2d.drawLine(width - margin * 2, height / 2, width, height / 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
return controlPoints.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
return controlPoints[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setControlPoint(Point2D point, int index) {
|
||||
// Move only the requested point. The presenter drags every control point of the box together
|
||||
// (canPointMoveFreely == false), so the pins stay on the edge without a recompute here.
|
||||
controlPoints[index].setLocation(point);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isControlPointSticky(int index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
|
||||
return VisibilityPolicy.WHEN_SELECTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPointMoveFreely(int pointIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
List<String> labels = getNodeLabels();
|
||||
return index < labels.size() ? labels.get(index) : Integer.toString(index + 1);
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Left Nodes")
|
||||
public String getLeftNodes() {
|
||||
return leftNodes;
|
||||
}
|
||||
|
||||
public void setLeftNodes(String leftNodes) {
|
||||
this.leftNodes = leftNodes;
|
||||
updateControlPoints();
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Right Nodes")
|
||||
public String getRightNodes() {
|
||||
return rightNodes;
|
||||
}
|
||||
|
||||
public void setRightNodes(String rightNodes) {
|
||||
this.rightNodes = rightNodes;
|
||||
updateControlPoints();
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Top Nodes")
|
||||
public String getTopNodes() {
|
||||
return topNodes;
|
||||
}
|
||||
|
||||
public void setTopNodes(String topNodes) {
|
||||
this.topNodes = topNodes;
|
||||
updateControlPoints();
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Bottom Nodes")
|
||||
public String getBottomNodes() {
|
||||
return bottomNodes;
|
||||
}
|
||||
|
||||
public void setBottomNodes(String bottomNodes) {
|
||||
this.bottomNodes = bottomNodes;
|
||||
updateControlPoints();
|
||||
}
|
||||
|
||||
@EditableProperty
|
||||
public Display getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
public void setDisplay(Display display) {
|
||||
this.display = display;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Body")
|
||||
public Color getBodyColor() {
|
||||
return bodyColor;
|
||||
}
|
||||
|
||||
public void setBodyColor(Color bodyColor) {
|
||||
this.bodyColor = bodyColor;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Border")
|
||||
public Color getBorderColor() {
|
||||
return borderColor;
|
||||
}
|
||||
|
||||
public void setBorderColor(Color borderColor) {
|
||||
this.borderColor = borderColor;
|
||||
}
|
||||
|
||||
@EditableProperty
|
||||
@Override
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -26,6 +26,7 @@ import java.awt.Graphics2D;
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.DiodeSchematicFactory;
|
||||
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.components.AbstractLeadedComponent;
|
||||
@@ -43,7 +44,8 @@ import org.diylc.utils.Constants;
|
||||
|
||||
@AiNote("The two control points are the holes the leads go into and the body is centred between them; 'Length' is the body length, not the hole spacing")
|
||||
@AiNote("If the control points are closer together than 'Length' (0.2in by default) the part is drawn standing, its body over the first point, or over the second with 'Reverse (standing)'; to keep it lying on a tighter footprint, reduce 'Length'")
|
||||
@ComponentDescriptor(name = "Diode (glass)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
@ComponentDescriptor(schematicFactory = DiodeSchematicFactory.class,
|
||||
name = "Diode (glass)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "D",
|
||||
description = "Glass diode, like most small signal diodes.", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = SimpleComponentTransformer.class, enableCache = true)
|
||||
|
||||
+3
-1
@@ -26,6 +26,7 @@ import java.awt.Graphics2D;
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.DiodeSchematicFactory;
|
||||
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.components.AbstractLeadedComponent;
|
||||
@@ -43,7 +44,8 @@ import org.diylc.utils.Constants;
|
||||
|
||||
@AiNote("The two control points are the holes the leads go into and the body is centred between them; 'Length' is the body length, not the hole spacing")
|
||||
@AiNote("If the control points are closer together than 'Length' (0.25in by default) the part is drawn standing, its body over the first point; to keep it lying on a tighter footprint, reduce 'Length'")
|
||||
@ComponentDescriptor(name = "Diode (plastic)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
@ComponentDescriptor(schematicFactory = DiodeSchematicFactory.class,
|
||||
name = "Diode (plastic)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "D",
|
||||
description = "Plastic diode, like most rectifier, zener, schottky, etc.", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = SimpleComponentTransformer.class, enableCache = true)
|
||||
|
||||
+3
-1
@@ -34,6 +34,7 @@ import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.TransistorSchematicFactory;
|
||||
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.ObjectCache;
|
||||
@@ -54,7 +55,8 @@ import org.diylc.utils.Constants;
|
||||
|
||||
@AiNote("The first control point is the anchor; with 'Orientation' DEFAULT the third pin is 2 x 'Pin spacing' below it and the middle pin 'Pin spacing' below and 'Pin spacing' to the left, a triangle under the round can. 'Folded' puts the three pins in a straight line down from the anchor and lays the body flat on the right of them. The other orientations rotate this around the anchor")
|
||||
@AiNote("On a 0.1in grid board the default 0.05in 'Pin spacing' puts the middle pin between holes: set 'Pin spacing' to 0.1in to land all three pins on holes")
|
||||
@ComponentDescriptor(name = "Transistor (TO-1)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
@ComponentDescriptor(schematicFactory = TransistorSchematicFactory.class,
|
||||
name = "Transistor (TO-1)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
instanceNamePrefix = "Q", description = "Transistor with small metal body",
|
||||
zOrder = IDIYComponent.COMPONENT, keywordPolicy = KeywordPolicy.SHOW_VALUE, transformer = TO1Transformer.class,
|
||||
enableCache = true)
|
||||
|
||||
+3
-1
@@ -34,6 +34,7 @@ import java.awt.geom.Line2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.TransistorSchematicFactory;
|
||||
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.ObjectCache;
|
||||
@@ -51,7 +52,8 @@ import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
@ComponentDescriptor(name = "Transistor (TO-126)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
@ComponentDescriptor(schematicFactory = TransistorSchematicFactory.class,
|
||||
name = "Transistor (TO-126)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
instanceNamePrefix = "Q", description = "Transistors with a hole for heat sink mounting",
|
||||
zOrder = IDIYComponent.COMPONENT, keywordPolicy = KeywordPolicy.SHOW_VALUE, transformer = TO220Transformer.class,
|
||||
enableCache = true)
|
||||
|
||||
+3
-1
@@ -34,6 +34,7 @@ import java.awt.geom.Line2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.TransistorSchematicFactory;
|
||||
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.ObjectCache;
|
||||
@@ -55,7 +56,8 @@ import org.diylc.utils.Constants;
|
||||
@AiNote("The first control point is the anchor; with 'Orientation' DEFAULT the three pins run straight down from it 0.1in apart, and the other orientations rotate this around the anchor")
|
||||
@AiNote("Seen from above, the metal tab is on the right of the pin row in DEFAULT orientation and the printed front faces left; 'Folded' lays the body flat on the right of the pin row, 'Lead Length' away from it, with the tab and its mounting hole at the far end")
|
||||
@AiNote("'Pinout' values list the pins as a datasheet does, facing the printed front with the legs down, left to right; pick the value that matches the part's datasheet as written")
|
||||
@ComponentDescriptor(name = "Transistor (TO-220)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
@ComponentDescriptor(schematicFactory = TransistorSchematicFactory.class,
|
||||
name = "Transistor (TO-220)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
instanceNamePrefix = "Q", description = "Transistors with metal tab for heat sink mounting",
|
||||
zOrder = IDIYComponent.COMPONENT, keywordPolicy = KeywordPolicy.SHOW_VALUE, transformer = TO220Transformer.class,
|
||||
enableCache = true)
|
||||
|
||||
+3
-1
@@ -35,6 +35,7 @@ import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.TransistorSchematicFactory;
|
||||
|
||||
import org.diylc.awt.TwoCircleTangent;
|
||||
import org.diylc.common.*;
|
||||
@@ -52,7 +53,8 @@ import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
@ComponentDescriptor(name = "Transistor (TO-3)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
@ComponentDescriptor(schematicFactory = TransistorSchematicFactory.class,
|
||||
name = "Transistor (TO-3)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
instanceNamePrefix = "Q", description = "Transistor with large metal body",
|
||||
zOrder = IDIYComponent.COMPONENT, keywordPolicy = KeywordPolicy.SHOW_VALUE, enableCache = true)
|
||||
public class TransistorTO3 extends AbstractLabeledComponent<String> {
|
||||
|
||||
+3
-1
@@ -34,6 +34,7 @@ import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.schematic.TransistorSchematicFactory;
|
||||
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.ObjectCache;
|
||||
@@ -56,7 +57,8 @@ import org.diylc.utils.Constants;
|
||||
@AiNote("Seen from above, the flat face is on the left of the pin row in DEFAULT orientation; 'Folded' lays the body flat on the right of the pin row instead")
|
||||
@AiNote("'Pinout' values list the pins as a datasheet does, facing the flat side with the legs down, left to right; pick the value that matches the part's datasheet as written")
|
||||
@AiNote("On a 0.1in grid board the default 0.05in 'Pin spacing' puts the middle pin between holes: set 'Pin spacing' to 0.1in, or use two or three jumpers acting as flexible leads and connecting TO-92 terminals to the holes on the board")
|
||||
@ComponentDescriptor(name = "Transistor (TO-92)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
@ComponentDescriptor(schematicFactory = TransistorSchematicFactory.class,
|
||||
name = "Transistor (TO-92)", author = "Branislav Stojkovic", category = "Semiconductors",
|
||||
instanceNamePrefix = "Q", description = "Transistor with small plastic or epoxy body",
|
||||
zOrder = IDIYComponent.COMPONENT, keywordPolicy = KeywordPolicy.SHOW_VALUE, transformer = TO92Transformer.class,
|
||||
enableCache = true)
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
|
||||
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.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Base class for the common case of a single-symbol, straight pin-for-pin mapping between a physical
|
||||
* component and its schematic symbol (resistors, capacitors, inductors, diodes, two- and
|
||||
* three-terminal parts, ...). The n-th sticky control point of the physical component is mapped to
|
||||
* the n-th electrical control point of the symbol.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public abstract class AbstractSimpleSchematicFactory implements ISchematicFactory {
|
||||
|
||||
/** @return a fresh, unconfigured schematic symbol instance. */
|
||||
protected abstract IDIYComponent<?> createSymbol();
|
||||
|
||||
/** @return number of electrical (net-carrying) control points to map, in index order. */
|
||||
protected int electricalPinCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchematicSymbolMapping> createSchematicSymbols(IDIYComponent<?> physicalComponent) {
|
||||
IDIYComponent<?> symbol = createSymbol();
|
||||
symbol.setId(UUID.randomUUID());
|
||||
symbol.setName(physicalComponent.getName());
|
||||
copyValue(physicalComponent, symbol);
|
||||
// leaded symbols are born with a one-inch lead span, which leaves far more bare lead than body
|
||||
// and runs into whatever sits in the neighbouring cell; pull them in to the schematic pitch
|
||||
SchematicBuilder.applyPitch(symbol, false, false);
|
||||
|
||||
List<Integer> physicalPins = new ArrayList<Integer>();
|
||||
for (int i = 0; i < physicalComponent.getControlPointCount()
|
||||
&& physicalPins.size() < electricalPinCount(); i++) {
|
||||
if (physicalComponent.isControlPointSticky(i)
|
||||
&& physicalComponent.getControlPointNodeName(i) != null) {
|
||||
physicalPins.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
Map<Integer, Integer> pinMapping = new HashMap<Integer, Integer>();
|
||||
for (int schematicIndex = 0; schematicIndex < physicalPins.size()
|
||||
&& schematicIndex < symbol.getControlPointCount(); schematicIndex++) {
|
||||
pinMapping.put(physicalPins.get(schematicIndex), schematicIndex);
|
||||
}
|
||||
|
||||
List<SchematicSymbolMapping> result = new ArrayList<SchematicSymbolMapping>();
|
||||
result.add(new SchematicSymbolMapping(symbol, pinMapping, null));
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void copyValue(IDIYComponent<?> physical, IDIYComponent<?> symbol) {
|
||||
try {
|
||||
((IDIYComponent<Object>) symbol).setValue(physical.getValue());
|
||||
} catch (Throwable t) {
|
||||
// symbol value type differs from the physical component's; skip
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
|
||||
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 org.diylc.components.passive.CapacitorSymbol;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/** Maps physical capacitors to a {@link CapacitorSymbol}. */
|
||||
public class CapacitorSchematicFactory extends AbstractSimpleSchematicFactory {
|
||||
|
||||
@Override
|
||||
protected IDIYComponent<?> createSymbol() {
|
||||
return new CapacitorSymbol();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
|
||||
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 org.diylc.components.semiconductors.DiodeSymbol;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/** Maps physical diodes to a {@link DiodeSymbol}. */
|
||||
public class DiodeSchematicFactory extends AbstractSimpleSchematicFactory {
|
||||
|
||||
@Override
|
||||
protected IDIYComponent<?> createSymbol() {
|
||||
return new DiodeSymbol();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
|
||||
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.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.diylc.common.Display;
|
||||
import org.diylc.components.schematic.SchematicBox;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Default {@link ISchematicFactory} used for every physical component type that does not declare a
|
||||
* dedicated factory. It inspects the sticky control points of the component, creates a
|
||||
* {@link SchematicBox} with one labeled terminal per node and copies the component's name and value.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class GenericBoxSchematicFactory implements ISchematicFactory {
|
||||
|
||||
@Override
|
||||
public List<SchematicSymbolMapping> createSchematicSymbols(IDIYComponent<?> physicalComponent) {
|
||||
List<Integer> pinIndices = new ArrayList<Integer>();
|
||||
List<String> labels = new ArrayList<String>();
|
||||
for (int i = 0; i < physicalComponent.getControlPointCount(); i++) {
|
||||
if (!physicalComponent.isControlPointSticky(i)) {
|
||||
continue;
|
||||
}
|
||||
String nodeName = physicalComponent.getControlPointNodeName(i);
|
||||
if (nodeName == null) {
|
||||
continue;
|
||||
}
|
||||
pinIndices.add(i);
|
||||
labels.add(nodeName);
|
||||
}
|
||||
if (pinIndices.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
SchematicBox box = new SchematicBox();
|
||||
box.setName(physicalComponent.getName());
|
||||
String value = physicalComponent.getValueForDisplay();
|
||||
box.setValue(value == null ? "" : value);
|
||||
box.setDisplay(Display.BOTH);
|
||||
box.setTopNodes("");
|
||||
box.setBottomNodes("");
|
||||
|
||||
int half = (labels.size() + 1) / 2;
|
||||
List<String> left = labels.subList(0, half);
|
||||
List<String> right = labels.subList(half, labels.size());
|
||||
box.setLeftNodes(String.join(",", left));
|
||||
box.setRightNodes(String.join(",", right));
|
||||
|
||||
// SchematicBox control point order is left (top to bottom) then right (top to bottom), which is
|
||||
// exactly the order we appended the labels in, so the k-th physical pin maps to schematic
|
||||
// control point k.
|
||||
Map<Integer, Integer> pinMapping = new HashMap<Integer, Integer>();
|
||||
for (int k = 0; k < pinIndices.size(); k++) {
|
||||
pinMapping.put(pinIndices.get(k), k);
|
||||
}
|
||||
|
||||
List<SchematicSymbolMapping> result = new ArrayList<SchematicSymbolMapping>();
|
||||
result.add(new SchematicSymbolMapping(box, pinMapping, null));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
|
||||
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 org.diylc.components.passive.InductorSymbol;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/** Maps physical inductors to a {@link InductorSymbol}. */
|
||||
public class InductorSchematicFactory extends AbstractSimpleSchematicFactory {
|
||||
|
||||
@Override
|
||||
protected IDIYComponent<?> createSymbol() {
|
||||
return new InductorSymbol();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
/*
|
||||
|
||||
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.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.netlist.Group;
|
||||
import org.diylc.netlist.Netlist;
|
||||
import org.diylc.netlist.Node;
|
||||
import org.diylc.schematic.SchematicBuilder.SymbolEntry;
|
||||
import org.diylc.schematic.flow.FlowHints;
|
||||
import org.diylc.schematic.flow.FlowRole;
|
||||
import org.diylc.schematic.flow.SignalFlowAnalyzer;
|
||||
|
||||
/**
|
||||
* Arranges schematic symbols in columns that follow the direction the signal travels, so that the
|
||||
* drawing reads from input on the left to output on the right.
|
||||
*
|
||||
* <p>
|
||||
* The circuit becomes a graph of components joined by its nets. Two kinds of net are kept out of that
|
||||
* graph: the ones carrying a declared rail, and the single busiest net, which in practice is ground.
|
||||
* Ground reaches nearly every part of a circuit, so leaving it in makes every component adjacent to
|
||||
* every other and flattens the whole drawing into one column - and most projects never place a ground
|
||||
* symbol, so it cannot be recognised by its label alone.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Where {@link SignalFlowAnalyzer} can tell that one component drives another the edge is directed,
|
||||
* and columns come from the longest path through those edges. Real projects rarely carry that
|
||||
* information - a transistor only reports its legs once someone has set its pin-out - so when
|
||||
* direction is absent the columns come from distance across the graph instead, measured from the far
|
||||
* end of its longest chain. That always spreads a connected circuit out, which is the one thing the
|
||||
* layout must never fail to do.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Every collection here keeps insertion order. The same project has to produce the same drawing every
|
||||
* time, and hash ordering would quietly make the layout depend on object identity.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class LayeredPlacement {
|
||||
|
||||
/** Gap between one column and the next, in pixels. */
|
||||
public static final double COLUMN_GAP = 130d;
|
||||
/** Gap between symbols stacked within a column, in pixels. */
|
||||
public static final double ROW_GAP = 60d;
|
||||
public static final double MARGIN = 120d;
|
||||
/** Grid step the finished positions are snapped to. */
|
||||
public static final double GRID = 10d;
|
||||
|
||||
private static final int ORDERING_SWEEPS = 6;
|
||||
|
||||
private final SignalFlowAnalyzer flowAnalyzer = new SignalFlowAnalyzer();
|
||||
|
||||
/** One placed component: the physical part, the symbols it produced, and where it ended up. */
|
||||
private static class Vertex {
|
||||
|
||||
final UUID id;
|
||||
final IDIYComponent<?> physical;
|
||||
final List<SymbolEntry> entries;
|
||||
final Set<UUID> drives = new LinkedHashSet<UUID>();
|
||||
final Set<UUID> drivenBy = new LinkedHashSet<UUID>();
|
||||
final Set<UUID> neighbours = new LinkedHashSet<UUID>();
|
||||
|
||||
int layer;
|
||||
double barycentre;
|
||||
|
||||
Vertex(IDIYComponent<?> physical, List<SymbolEntry> entries) {
|
||||
this.id = physical.getId();
|
||||
this.physical = physical;
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
boolean isDirected() {
|
||||
return !drives.isEmpty() || !drivenBy.isEmpty();
|
||||
}
|
||||
|
||||
/** @return where the part sits on the board, used to break ties so the schematic echoes it. */
|
||||
double physicalY() {
|
||||
return physical.getControlPointCount() == 0 ? 0 : physical.getControlPoint(0).getY();
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<UUID, Vertex> vertices = new LinkedHashMap<UUID, Vertex>();
|
||||
|
||||
/**
|
||||
* Places every symbol of every eligible component. The symbols must already have been given their
|
||||
* attitude, because the column widths are measured from their current geometry.
|
||||
*/
|
||||
public void place(List<IDIYComponent<?>> eligible,
|
||||
Map<UUID, List<SymbolEntry>> entriesByPhysicalId, Netlist netlist) {
|
||||
buildVertices(eligible, entriesByPhysicalId);
|
||||
if (vertices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
buildEdges(netlist);
|
||||
breakFeedbackLoops();
|
||||
assignLayers();
|
||||
spreadUndirectedVertices();
|
||||
if (distinctLayerCount() < 2) {
|
||||
// nothing in the circuit said which way the signal runs, so fall back to plain distance
|
||||
layerByDistance();
|
||||
}
|
||||
assignCoordinates(orderWithinLayers());
|
||||
}
|
||||
|
||||
private void buildVertices(List<IDIYComponent<?>> eligible,
|
||||
Map<UUID, List<SymbolEntry>> entriesByPhysicalId) {
|
||||
for (IDIYComponent<?> physical : eligible) {
|
||||
List<SymbolEntry> entries = entriesByPhysicalId.get(physical.getId());
|
||||
if (entries != null && !entries.isEmpty()) {
|
||||
vertices.put(physical.getId(), new Vertex(physical, entries));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns each ordinary net into edges.
|
||||
*
|
||||
* <p>
|
||||
* Direction has to carry across the passive parts that sit between active devices: a collector
|
||||
* reaches the next stage's base through a coupling capacitor, so the two are never on one net and a
|
||||
* rule that only looked for an output and an input on the same net would find nothing at all.
|
||||
* Anything driving a net therefore drives every other member of it, and every member drives
|
||||
* whatever takes the net as an input.
|
||||
* </p>
|
||||
*/
|
||||
private void buildEdges(Netlist netlist) {
|
||||
if (netlist == null) {
|
||||
return;
|
||||
}
|
||||
Group backbone = SchematicBuilder.backboneNet(netlist);
|
||||
Map<UUID, FlowHints> hintCache = new LinkedHashMap<UUID, FlowHints>();
|
||||
for (Group group : netlist.getSortedGroups()) {
|
||||
if (group == backbone || SchematicBuilder.railLabelOf(group) != null) {
|
||||
continue;
|
||||
}
|
||||
List<UUID> members = new ArrayList<UUID>();
|
||||
List<UUID> drivers = new ArrayList<UUID>();
|
||||
List<UUID> receivers = new ArrayList<UUID>();
|
||||
for (Node node : group.getSortedNodes()) {
|
||||
UUID id = node.getComponent().getId();
|
||||
if (!vertices.containsKey(id)) {
|
||||
continue;
|
||||
}
|
||||
if (!members.contains(id)) {
|
||||
members.add(id);
|
||||
}
|
||||
FlowRole role = roleOf(node, hintCache);
|
||||
if (role == FlowRole.OUTPUT && !drivers.contains(id)) {
|
||||
drivers.add(id);
|
||||
} else if (role == FlowRole.INPUT && !receivers.contains(id)) {
|
||||
receivers.add(id);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < members.size(); i++) {
|
||||
for (int j = i + 1; j < members.size(); j++) {
|
||||
vertices.get(members.get(i)).neighbours.add(members.get(j));
|
||||
vertices.get(members.get(j)).neighbours.add(members.get(i));
|
||||
}
|
||||
}
|
||||
for (UUID member : members) {
|
||||
for (UUID driver : drivers) {
|
||||
link(driver, member);
|
||||
}
|
||||
for (UUID receiver : receivers) {
|
||||
link(member, receiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void link(UUID from, UUID to) {
|
||||
if (from.equals(to)) {
|
||||
return;
|
||||
}
|
||||
vertices.get(from).drives.add(to);
|
||||
vertices.get(to).drivenBy.add(from);
|
||||
}
|
||||
|
||||
private FlowRole roleOf(Node node, Map<UUID, FlowHints> hintCache) {
|
||||
UUID id = node.getComponent().getId();
|
||||
FlowHints hints;
|
||||
if (hintCache.containsKey(id)) {
|
||||
hints = hintCache.get(id);
|
||||
} else {
|
||||
hints = flowAnalyzer.analyze(node.getComponent());
|
||||
hintCache.put(id, hints);
|
||||
}
|
||||
return hints == null ? FlowRole.NEUTRAL : hints.getRole(node.getPointIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the edges that close a loop, so that the graph can be layered at all.
|
||||
*
|
||||
* <p>
|
||||
* Feedback is not an anomaly here: an op-amp with a feedback network, or a transistor with a
|
||||
* bootstrapped or degenerated bias, produces a cycle in every real circuit of interest. Removing
|
||||
* the edge that closes the loop keeps the forward path intact, which is the one that determines how
|
||||
* the drawing reads.
|
||||
* </p>
|
||||
*/
|
||||
private void breakFeedbackLoops() {
|
||||
Set<UUID> visited = new LinkedHashSet<UUID>();
|
||||
Set<UUID> onPath = new LinkedHashSet<UUID>();
|
||||
for (UUID id : new ArrayList<UUID>(vertices.keySet())) {
|
||||
if (!visited.contains(id)) {
|
||||
removeBackEdges(id, visited, onPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeBackEdges(UUID id, Set<UUID> visited, Set<UUID> onPath) {
|
||||
visited.add(id);
|
||||
onPath.add(id);
|
||||
Vertex vertex = vertices.get(id);
|
||||
for (UUID next : new ArrayList<UUID>(vertex.drives)) {
|
||||
if (onPath.contains(next)) {
|
||||
vertex.drives.remove(next);
|
||||
vertices.get(next).drivenBy.remove(id);
|
||||
} else if (!visited.contains(next)) {
|
||||
removeBackEdges(next, visited, onPath);
|
||||
}
|
||||
}
|
||||
onPath.remove(id);
|
||||
}
|
||||
|
||||
/** Longest path layering over the acyclic graph: a component sits one column after its drivers. */
|
||||
private void assignLayers() {
|
||||
Map<UUID, Integer> remaining = new LinkedHashMap<UUID, Integer>();
|
||||
List<UUID> ready = new ArrayList<UUID>();
|
||||
for (Vertex vertex : vertices.values()) {
|
||||
vertex.layer = 0;
|
||||
remaining.put(vertex.id, vertex.drivenBy.size());
|
||||
if (vertex.drivenBy.isEmpty()) {
|
||||
ready.add(vertex.id);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < ready.size(); i++) {
|
||||
Vertex vertex = vertices.get(ready.get(i));
|
||||
for (UUID next : vertex.drives) {
|
||||
Vertex target = vertices.get(next);
|
||||
target.layer = Math.max(target.layer, vertex.layer + 1);
|
||||
int left = remaining.get(next) - 1;
|
||||
remaining.put(next, left);
|
||||
if (left == 0) {
|
||||
ready.add(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls the parts that no rule could give a direction to - the resistors and capacitors that make
|
||||
* up most of a circuit - towards the components they sit between, so they do not all pile up in the
|
||||
* first column.
|
||||
*/
|
||||
private void spreadUndirectedVertices() {
|
||||
for (int sweep = 0; sweep < ORDERING_SWEEPS; sweep++) {
|
||||
boolean changed = false;
|
||||
for (Vertex vertex : vertices.values()) {
|
||||
if (vertex.isDirected() || vertex.neighbours.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
double sum = 0;
|
||||
int count = 0;
|
||||
for (UUID neighbour : vertex.neighbours) {
|
||||
sum += vertices.get(neighbour).layer;
|
||||
count++;
|
||||
}
|
||||
int target = (int) Math.round(sum / count);
|
||||
if (target != vertex.layer) {
|
||||
vertex.layer = target;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int distinctLayerCount() {
|
||||
Set<Integer> layers = new LinkedHashSet<Integer>();
|
||||
for (Vertex vertex : vertices.values()) {
|
||||
layers.add(vertex.layer);
|
||||
}
|
||||
return layers.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spreads the circuit out by distance when nothing declared a direction.
|
||||
*
|
||||
* <p>
|
||||
* Each connected island is measured from one end of its longest chain - found by walking to the
|
||||
* furthest vertex and then measuring from there - and every component takes its column from how far
|
||||
* along it sits. The result is not electrically meaningful the way a driven chain is, but it does
|
||||
* lay the circuit out along its own longest path, which is far closer to a schematic than a single
|
||||
* stack of parts.
|
||||
* </p>
|
||||
*/
|
||||
private void layerByDistance() {
|
||||
Set<UUID> remaining = new LinkedHashSet<UUID>(vertices.keySet());
|
||||
while (!remaining.isEmpty()) {
|
||||
UUID seed = remaining.iterator().next();
|
||||
Map<UUID, Integer> reach = distancesFrom(seed);
|
||||
UUID far = furthest(reach);
|
||||
Map<UUID, Integer> distances = distancesFrom(far);
|
||||
for (Map.Entry<UUID, Integer> entry : distances.entrySet()) {
|
||||
vertices.get(entry.getKey()).layer = entry.getValue();
|
||||
remaining.remove(entry.getKey());
|
||||
}
|
||||
// a vertex with no connections at all is its own island
|
||||
remaining.remove(seed);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<UUID, Integer> distancesFrom(UUID start) {
|
||||
Map<UUID, Integer> distances = new LinkedHashMap<UUID, Integer>();
|
||||
List<UUID> queue = new ArrayList<UUID>();
|
||||
distances.put(start, 0);
|
||||
queue.add(start);
|
||||
for (int i = 0; i < queue.size(); i++) {
|
||||
UUID current = queue.get(i);
|
||||
int next = distances.get(current) + 1;
|
||||
for (UUID neighbour : vertices.get(current).neighbours) {
|
||||
if (!distances.containsKey(neighbour)) {
|
||||
distances.put(neighbour, next);
|
||||
queue.add(neighbour);
|
||||
}
|
||||
}
|
||||
}
|
||||
return distances;
|
||||
}
|
||||
|
||||
private static UUID furthest(Map<UUID, Integer> distances) {
|
||||
UUID best = null;
|
||||
int bestDistance = -1;
|
||||
for (Map.Entry<UUID, Integer> entry : distances.entrySet()) {
|
||||
if (entry.getValue() > bestDistance) {
|
||||
bestDistance = entry.getValue();
|
||||
best = entry.getKey();
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders each column so that symbols sit near the ones they connect to, which is what keeps the
|
||||
* wires between columns from crossing. Ties fall back to the part's position on the board, so the
|
||||
* schematic keeps a family resemblance to the layout it came from.
|
||||
*/
|
||||
private Map<Integer, List<Vertex>> orderWithinLayers() {
|
||||
Map<Integer, List<Vertex>> byLayer = new LinkedHashMap<Integer, List<Vertex>>();
|
||||
List<Vertex> sorted = new ArrayList<Vertex>(vertices.values());
|
||||
sorted.sort(Comparator.comparingInt((Vertex v) -> v.layer)
|
||||
.thenComparingDouble(Vertex::physicalY));
|
||||
for (Vertex vertex : sorted) {
|
||||
List<Vertex> column = byLayer.get(vertex.layer);
|
||||
if (column == null) {
|
||||
column = new ArrayList<Vertex>();
|
||||
byLayer.put(vertex.layer, column);
|
||||
}
|
||||
column.add(vertex);
|
||||
}
|
||||
|
||||
Map<UUID, Integer> position = new LinkedHashMap<UUID, Integer>();
|
||||
for (List<Vertex> column : byLayer.values()) {
|
||||
for (int i = 0; i < column.size(); i++) {
|
||||
position.put(column.get(i).id, i);
|
||||
}
|
||||
}
|
||||
for (int sweep = 0; sweep < ORDERING_SWEEPS; sweep++) {
|
||||
for (List<Vertex> column : byLayer.values()) {
|
||||
for (Vertex vertex : column) {
|
||||
vertex.barycentre = barycentreOf(vertex, position);
|
||||
}
|
||||
column.sort(Comparator.comparingDouble((Vertex v) -> v.barycentre)
|
||||
.thenComparingDouble(Vertex::physicalY));
|
||||
for (int i = 0; i < column.size(); i++) {
|
||||
position.put(column.get(i).id, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return byLayer;
|
||||
}
|
||||
|
||||
private double barycentreOf(Vertex vertex, Map<UUID, Integer> position) {
|
||||
double sum = 0;
|
||||
int count = 0;
|
||||
for (UUID neighbour : vertex.neighbours) {
|
||||
Integer at = position.get(neighbour);
|
||||
if (at != null) {
|
||||
sum += at;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count == 0 ? position.get(vertex.id) : sum / count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns columns and orders into pixel positions. Each column is as wide as its widest symbol, and a
|
||||
* symbol is placed by its bounding box rather than by a control point, so that a part standing
|
||||
* upright cannot hang out of its cell.
|
||||
*/
|
||||
private void assignCoordinates(Map<Integer, List<Vertex>> byLayer) {
|
||||
List<Integer> layers = new ArrayList<Integer>(byLayer.keySet());
|
||||
Collections.sort(layers);
|
||||
double x = MARGIN;
|
||||
for (Integer layer : layers) {
|
||||
List<Vertex> column = byLayer.get(layer);
|
||||
double columnWidth = 0;
|
||||
for (Vertex vertex : column) {
|
||||
columnWidth = Math.max(columnWidth, boundsOf(vertex.entries).getWidth());
|
||||
}
|
||||
double y = MARGIN;
|
||||
for (Vertex vertex : column) {
|
||||
Rectangle2D bounds = boundsOf(vertex.entries);
|
||||
// centre the symbol in its column so that a narrow part sits under a wide one
|
||||
double offsetX = x + (columnWidth - bounds.getWidth()) / 2;
|
||||
moveBoundsTo(vertex.entries, bounds, snap(offsetX), snap(y));
|
||||
y += bounds.getHeight() + ROW_GAP;
|
||||
}
|
||||
x += columnWidth + COLUMN_GAP;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return the box spanned by every control point of every symbol the component produced. */
|
||||
private static Rectangle2D boundsOf(List<SymbolEntry> entries) {
|
||||
double minX = Double.MAX_VALUE;
|
||||
double minY = Double.MAX_VALUE;
|
||||
double maxX = -Double.MAX_VALUE;
|
||||
double maxY = -Double.MAX_VALUE;
|
||||
for (SymbolEntry entry : entries) {
|
||||
for (int i = 0; i < entry.symbol.getControlPointCount(); i++) {
|
||||
Point2D p = entry.symbol.getControlPoint(i);
|
||||
minX = Math.min(minX, p.getX());
|
||||
minY = Math.min(minY, p.getY());
|
||||
maxX = Math.max(maxX, p.getX());
|
||||
maxY = Math.max(maxY, p.getY());
|
||||
}
|
||||
}
|
||||
if (minX > maxX) {
|
||||
return new Rectangle2D.Double(0, 0, 0, 0);
|
||||
}
|
||||
return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
private static void moveBoundsTo(List<SymbolEntry> entries, Rectangle2D bounds, double x,
|
||||
double y) {
|
||||
double dx = x - bounds.getX();
|
||||
double dy = y - bounds.getY();
|
||||
for (SymbolEntry entry : entries) {
|
||||
translate(entry.symbol, dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves every control point by the same offset. The targets are worked out before anything is
|
||||
* written, because a rigid component such as SchematicBox rebuilds its remaining points when one of
|
||||
* them is set and would otherwise apply the offset twice.
|
||||
*/
|
||||
private static void translate(IDIYComponent<?> component, double dx, double dy) {
|
||||
int count = component.getControlPointCount();
|
||||
Point2D[] targets = new Point2D[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
Point2D p = component.getControlPoint(i);
|
||||
targets[i] = new Point2D.Double(p.getX() + dx, p.getY() + dy);
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
component.setControlPoint(targets[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
private static double snap(double value) {
|
||||
return Math.round(value / GRID) * GRID;
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
|
||||
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 org.diylc.components.passive.PotentiometerSymbol;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Maps physical potentiometers to a {@link PotentiometerSymbol}. The
|
||||
* three package legs are mapped to the symbol legs in index order; if a particular package uses a
|
||||
* different pin-out the user can rotate/relabel the resulting symbol.
|
||||
*/
|
||||
public class PotentiometerSchematicFactory extends AbstractSimpleSchematicFactory {
|
||||
|
||||
@Override
|
||||
protected IDIYComponent<?> createSymbol() {
|
||||
return new PotentiometerSymbol();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int electricalPinCount() {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
|
||||
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 org.diylc.components.passive.ResistorSymbol;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/** Maps physical resistors to a {@link ResistorSymbol}. */
|
||||
public class ResistorSchematicFactory extends AbstractSimpleSchematicFactory {
|
||||
|
||||
@Override
|
||||
protected IDIYComponent<?> createSymbol() {
|
||||
return new ResistorSymbol();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,707 @@
|
||||
/*
|
||||
|
||||
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.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.OrientationHV;
|
||||
import org.diylc.components.AbstractLeadedComponent;
|
||||
import org.diylc.components.connectivity.OrthogonalLine;
|
||||
import org.diylc.components.misc.CommonNode;
|
||||
import org.diylc.components.misc.GroundSymbol;
|
||||
import org.diylc.core.IContinuity;
|
||||
import org.diylc.core.ICommonNode;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.ISwitch;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.SchematicView;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.netlist.Group;
|
||||
import org.diylc.netlist.Netlist;
|
||||
import org.diylc.netlist.NetlistBuilder;
|
||||
import org.diylc.netlist.NetlistException;
|
||||
import org.diylc.netlist.Node;
|
||||
import org.diylc.presenter.ComponentProcessor;
|
||||
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 {@link OrthogonalLine}s.
|
||||
*
|
||||
* <p>
|
||||
* Nets that carry a common node (ground, supply rails) are not wired up at all. Each pin on such a
|
||||
* net gets its own ground or common-node stub drawn right next to it, which is how schematics are
|
||||
* conventionally drawn and keeps the densest net in the circuit from crossing the whole drawing.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class SchematicBuilder {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SchematicBuilder.class);
|
||||
|
||||
/** Grid step, in pixels, used for symbol placement. */
|
||||
public static final double GRID = 90d;
|
||||
/** Horizontal/vertical spacing between grid cells, in pixels. */
|
||||
public static final double CELL_SIZE = 220d;
|
||||
public static final double MARGIN = 120d;
|
||||
|
||||
/**
|
||||
* Pin-to-pin span given to every two-terminal symbol. {@link AbstractLeadedComponent} defaults to
|
||||
* a one-inch span (200px) while the schematic symbol bodies are only 0.05in to 0.3in long, so the
|
||||
* default leaves 70 to 95 pixels of bare lead on each side — more lead than body, running well
|
||||
* into the neighbouring cell. Half an inch clears the longest body with a short stub either side
|
||||
* and lands on the 0.1in schematic grid.
|
||||
*/
|
||||
public static final double SYMBOL_PITCH = new Size(0.5d, SizeUnit.in).convertToPixels();
|
||||
|
||||
/** The label {@link GroundSymbol} reports; every other common-node label is treated as a supply. */
|
||||
private static final String GROUND_LABEL = "GND";
|
||||
|
||||
/** Smallest net that may be taken for the ground backbone. */
|
||||
private static final int MIN_BACKBONE_SIZE = 4;
|
||||
|
||||
private final Map<Class<? extends ISchematicFactory>, ISchematicFactory> factoryCache =
|
||||
new HashMap<Class<? extends ISchematicFactory>, ISchematicFactory>();
|
||||
private final GenericBoxSchematicFactory genericFactory = new GenericBoxSchematicFactory();
|
||||
private final WireRouter router = new WireRouter();
|
||||
|
||||
/**
|
||||
* One schematic symbol produced from a physical component together with the physical->schematic
|
||||
* pin mapping that produced it.
|
||||
*/
|
||||
static class SymbolEntry {
|
||||
final IDIYComponent<?> symbol;
|
||||
final Map<Integer, Integer> pinMapping;
|
||||
|
||||
SymbolEntry(IDIYComponent<?> symbol, Map<Integer, Integer> pinMapping) {
|
||||
this.symbol = symbol;
|
||||
this.pinMapping = pinMapping;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything the wiring pass produces: the routed wires for ordinary signal nets, and the ground
|
||||
* and supply stubs that stand in for the rail nets.
|
||||
*/
|
||||
static class Wiring {
|
||||
|
||||
private final List<OrthogonalLine> wires = new ArrayList<OrthogonalLine>();
|
||||
private final List<IDIYComponent<?>> stubs = new ArrayList<IDIYComponent<?>>();
|
||||
|
||||
List<OrthogonalLine> getWires() {
|
||||
return wires;
|
||||
}
|
||||
|
||||
List<IDIYComponent<?>> getStubs() {
|
||||
return stubs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the schematic and stores it on {@code project.getOrCreateSchematicView()}, replacing any
|
||||
* previous content.
|
||||
*
|
||||
* @param project the layout project
|
||||
* @param continuityAreas continuity areas from the drawing manager (may be empty)
|
||||
*/
|
||||
public void build(Project project, List<ContinuityArea> continuityAreas) {
|
||||
Netlist netlist = extractNetlist(project, continuityAreas);
|
||||
|
||||
List<IDIYComponent<?>> schematicComponents = new ArrayList<IDIYComponent<?>>();
|
||||
Map<UUID, List<UUID>> physicalToSchematicMap = new LinkedHashMap<UUID, List<UUID>>();
|
||||
Map<UUID, List<SymbolEntry>> entriesByPhysicalId = new LinkedHashMap<UUID, List<SymbolEntry>>();
|
||||
|
||||
List<IDIYComponent<?>> eligible = new ArrayList<IDIYComponent<?>>();
|
||||
for (IDIYComponent<?> physical : project.getComponents()) {
|
||||
if (!isEligible(physical)) {
|
||||
continue;
|
||||
}
|
||||
eligible.add(physical);
|
||||
List<SymbolEntry> entries = createSymbols(physical);
|
||||
if (entries.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
List<UUID> symbolIds = new ArrayList<UUID>();
|
||||
for (SymbolEntry entry : entries) {
|
||||
schematicComponents.add(entry.symbol);
|
||||
symbolIds.add(entry.symbol.getId());
|
||||
}
|
||||
physicalToSchematicMap.put(physical.getId(), symbolIds);
|
||||
entriesByPhysicalId.put(physical.getId(), entries);
|
||||
}
|
||||
|
||||
placeSymbols(eligible, entriesByPhysicalId, netlist);
|
||||
|
||||
Wiring wiring = createWires(netlist, entriesByPhysicalId);
|
||||
schematicComponents.addAll(wiring.getStubs());
|
||||
schematicComponents.addAll(wiring.getWires());
|
||||
|
||||
schematicComponents.sort(SCHEMATIC_ORDER);
|
||||
|
||||
SchematicView view = project.getOrCreateSchematicView();
|
||||
view.getComponents().clear();
|
||||
view.getComponents().addAll(schematicComponents);
|
||||
view.setPhysicalToSchematicMap(physicalToSchematicMap);
|
||||
resizeCanvasToFit(view);
|
||||
}
|
||||
|
||||
Netlist extractNetlist(Project project, List<ContinuityArea> continuityAreas) {
|
||||
try {
|
||||
List<Netlist> netlists = NetlistBuilder.extractNetlists(false, project,
|
||||
continuityAreas == null ? new ArrayList<ContinuityArea>() : continuityAreas);
|
||||
if (netlists == null || netlists.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return netlists.get(0);
|
||||
} catch (NetlistException e) {
|
||||
LOG.warn("Could not extract netlist for schematic generation: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ eligibility */
|
||||
|
||||
static boolean isEligible(IDIYComponent<?> component) {
|
||||
if (component instanceof ICommonNode) {
|
||||
// ground and supply nodes are not placed as symbols of their own; every pin on their net gets
|
||||
// a freshly generated stub during wiring instead
|
||||
return false;
|
||||
}
|
||||
if (component instanceof ISwitch) {
|
||||
return true; // switches are drawn as symbols (netlist is built with includeSwitches=false)
|
||||
}
|
||||
if (component instanceof IContinuity) {
|
||||
return false; // wires, traces, solder bridges, board strips
|
||||
}
|
||||
for (int i = 0; i < component.getControlPointCount(); i++) {
|
||||
if (component.isControlPointSticky(i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ symbol creation */
|
||||
|
||||
List<SymbolEntry> createSymbols(IDIYComponent<?> physical) {
|
||||
ISchematicFactory factory = resolveFactory(physical);
|
||||
List<SchematicSymbolMapping> mappings;
|
||||
try {
|
||||
mappings = factory.createSchematicSymbols(physical);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Schematic factory " + factory.getClass().getSimpleName() + " failed for "
|
||||
+ physical.getName() + ", falling back to generic box", e);
|
||||
mappings = genericFactory.createSchematicSymbols(physical);
|
||||
}
|
||||
List<SymbolEntry> entries = new ArrayList<SymbolEntry>();
|
||||
if (mappings != null) {
|
||||
for (SchematicSymbolMapping mapping : mappings) {
|
||||
if (mapping.getSchematicSymbol() == null) {
|
||||
continue;
|
||||
}
|
||||
if (mapping.getSchematicSymbol().getId() == null) {
|
||||
mapping.getSchematicSymbol().setId(UUID.randomUUID());
|
||||
}
|
||||
entries.add(new SymbolEntry(mapping.getSchematicSymbol(), mapping.getPinMapping()));
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ISchematicFactory resolveFactory(IDIYComponent<?> physical) {
|
||||
ComponentType type = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) physical.getClass());
|
||||
Class<? extends ISchematicFactory> factoryClass =
|
||||
type == null ? null : type.getSchematicFactoryClass();
|
||||
if (factoryClass == null) {
|
||||
return genericFactory;
|
||||
}
|
||||
ISchematicFactory cached = factoryCache.get(factoryClass);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
try {
|
||||
ISchematicFactory factory = factoryClass.getDeclaredConstructor().newInstance();
|
||||
factoryCache.put(factoryClass, factory);
|
||||
return factory;
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Could not instantiate schematic factory " + factoryClass.getName()
|
||||
+ ", using generic box", e);
|
||||
return genericFactory;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ placement */
|
||||
|
||||
private void placeSymbols(List<IDIYComponent<?>> eligible,
|
||||
Map<UUID, List<SymbolEntry>> entriesByPhysicalId, Netlist netlist) {
|
||||
Map<UUID, Map<Integer, String>> railPins = railPins(netlist);
|
||||
|
||||
// attitude first: the placement measures each symbol, so it has to see its final geometry
|
||||
for (IDIYComponent<?> physical : eligible) {
|
||||
List<SymbolEntry> entries = entriesByPhysicalId.get(physical.getId());
|
||||
if (entries != null) {
|
||||
orientSymbols(entries, railPins.get(physical.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
new LayeredPlacement().place(eligible, entriesByPhysicalId, netlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the resting attitude of every two-terminal symbol of one component. A part with a leg on
|
||||
* a rail is a shunt element and stands upright with that leg pointing at the rail it belongs to;
|
||||
* everything else passes signal along the drawing and lies down.
|
||||
*
|
||||
* @param rails pin index -> rail label for this component, or null when it touches no rail
|
||||
*/
|
||||
static void orientSymbols(List<SymbolEntry> entries, Map<Integer, String> rails) {
|
||||
for (SymbolEntry entry : entries) {
|
||||
String railLabel = null;
|
||||
int railPin = -1;
|
||||
if (rails != null) {
|
||||
for (Map.Entry<Integer, Integer> mapped : entry.pinMapping.entrySet()) {
|
||||
String label = rails.get(mapped.getKey());
|
||||
if (label != null && mapped.getValue() < 2) {
|
||||
railLabel = label;
|
||||
railPin = mapped.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (railLabel == null) {
|
||||
applyPitch(entry.symbol, false, false);
|
||||
continue;
|
||||
}
|
||||
// a ground symbol is drawn downward from its point, so the grounded leg belongs at the bottom;
|
||||
// supply labels sit above the part the way they are drawn on paper
|
||||
boolean railAtBottom = GROUND_LABEL.equals(railLabel);
|
||||
applyPitch(entry.symbol, true, (railPin == 0) == railAtBottom);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites the two terminals of a leaded symbol so that they sit {@link #SYMBOL_PITCH} apart,
|
||||
* centred on wherever the symbol already is. Rigid multi-pin symbols are left untouched — their
|
||||
* geometry comes from an anchor and a pin spacing, not from the terminals.
|
||||
*
|
||||
* <p>
|
||||
* The midpoint is preserved, which is what keeps the label control point (index 2, positioned
|
||||
* relative to the centre by the constructor) correct without having to move it as well.
|
||||
* </p>
|
||||
*
|
||||
* @param vertical stand the symbol upright rather than lying it down
|
||||
* @param flip put control point 0 at the far end (bottom when upright, right when lying down)
|
||||
*/
|
||||
static void applyPitch(IDIYComponent<?> symbol, boolean vertical, boolean flip) {
|
||||
if (!(symbol instanceof AbstractLeadedComponent) || symbol.getControlPointCount() < 2) {
|
||||
return;
|
||||
}
|
||||
Point2D first = symbol.getControlPoint(0);
|
||||
Point2D second = symbol.getControlPoint(1);
|
||||
double cx = (first.getX() + second.getX()) / 2;
|
||||
double cy = (first.getY() + second.getY()) / 2;
|
||||
double half = SYMBOL_PITCH / 2;
|
||||
Point2D near =
|
||||
vertical ? new Point2D.Double(cx, cy - half) : new Point2D.Double(cx - half, cy);
|
||||
Point2D far = vertical ? new Point2D.Double(cx, cy + half) : new Point2D.Double(cx + half, cy);
|
||||
symbol.setControlPoint(flip ? far : near, 0);
|
||||
symbol.setControlPoint(flip ? near : far, 1);
|
||||
}
|
||||
|
||||
/** Places every symbol of {@code entries} into grid cell {@code slot} of {@code totalSlots}. */
|
||||
static void placeAtGridSlot(List<SymbolEntry> entries, int slot, int totalSlots) {
|
||||
int columns = Math.max(1, (int) Math.ceil(Math.sqrt(Math.max(totalSlots, 1))));
|
||||
int col = slot % columns;
|
||||
int row = slot / columns;
|
||||
double baseX = MARGIN + col * CELL_SIZE;
|
||||
double baseY = MARGIN + row * CELL_SIZE;
|
||||
int sub = 0;
|
||||
for (SymbolEntry entry : entries) {
|
||||
moveAnchorTo(entry.symbol, snap(baseX), snap(baseY + sub * (CELL_SIZE / 2)));
|
||||
sub++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects, per physical component, which of its control points sit on a rail net and under which
|
||||
* label, so that placement can stand shunt parts upright.
|
||||
*/
|
||||
static Map<UUID, Map<Integer, String>> railPins(Netlist netlist) {
|
||||
Map<UUID, Map<Integer, String>> result = new HashMap<UUID, Map<Integer, String>>();
|
||||
if (netlist == null) {
|
||||
return result;
|
||||
}
|
||||
for (Group group : netlist.getGroups()) {
|
||||
String label = railLabelOf(group);
|
||||
if (label == null) {
|
||||
continue;
|
||||
}
|
||||
for (Node node : group.getNodes()) {
|
||||
if (node.getComponent() instanceof ICommonNode) {
|
||||
continue;
|
||||
}
|
||||
result.computeIfAbsent(node.getComponent().getId(), k -> new HashMap<Integer, String>())
|
||||
.put(node.getPointIndex(), label);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link NetlistBuilder} emits a single node per common-node label, so a rail net is recognised by
|
||||
* the one common-node member it carries rather than by its size.
|
||||
*
|
||||
* @return the rail label of the net, or null when it is an ordinary signal net
|
||||
*/
|
||||
static String railLabelOf(Group group) {
|
||||
for (Node node : group.getNodes()) {
|
||||
if (node.getComponent() instanceof ICommonNode) {
|
||||
return ((ICommonNode) node.getComponent()).getCommonNodeLabel();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks out the net that behaves like ground: the busiest one that carries no declared rail.
|
||||
*
|
||||
* <p>
|
||||
* Only about one project in eight places a ground symbol, so in most circuits ground arrives as an
|
||||
* ordinary net that simply happens to touch a large share of the components. Left in the layout
|
||||
* graph it makes every part adjacent to every other and flattens the drawing into a single column;
|
||||
* left in the wiring it is chained from one side of the schematic to the other and crosses
|
||||
* everything in between.
|
||||
* </p>
|
||||
*
|
||||
* @return the backbone net, or null when no net is big enough to be taken for one
|
||||
*/
|
||||
static Group backboneNet(Netlist netlist) {
|
||||
if (netlist == null) {
|
||||
return null;
|
||||
}
|
||||
Group largest = null;
|
||||
int largestSize = 0;
|
||||
for (Group group : netlist.getSortedGroups()) {
|
||||
if (railLabelOf(group) != null) {
|
||||
continue;
|
||||
}
|
||||
int size = group.getNodes().size();
|
||||
if (size > largestSize) {
|
||||
largestSize = size;
|
||||
largest = group;
|
||||
}
|
||||
}
|
||||
return largestSize >= MIN_BACKBONE_SIZE ? largest : null;
|
||||
}
|
||||
|
||||
/** Translates every control point of the component so that control point 0 lands on the target. */
|
||||
static void moveAnchorTo(IDIYComponent<?> component, double targetX, double targetY) {
|
||||
int count = component.getControlPointCount();
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
Point2D anchor = component.getControlPoint(0);
|
||||
double dx = targetX - anchor.getX();
|
||||
double dy = targetY - anchor.getY();
|
||||
// Snapshot the target positions before mutating: some components (e.g. SchematicBox) recompute
|
||||
// all of their control points when control point 0 is set, so reading getControlPoint(i) inside
|
||||
// the loop after that would double-apply the offset.
|
||||
Point2D[] targets = new Point2D[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
Point2D p = component.getControlPoint(i);
|
||||
targets[i] = new Point2D.Double(p.getX() + dx, p.getY() + dy);
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
component.setControlPoint(targets[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
private static double snap(double value) {
|
||||
return Math.round(value / GRID) * GRID;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ wiring */
|
||||
|
||||
Wiring createWires(Netlist netlist, Map<UUID, List<SymbolEntry>> entriesByPhysicalId) {
|
||||
Wiring wiring = new Wiring();
|
||||
if (netlist == null) {
|
||||
return wiring;
|
||||
}
|
||||
|
||||
Map<UUID, Rectangle2D> bodies = symbolBodies(entriesByPhysicalId);
|
||||
// wires already routed become obstacles for the ones that follow, so a dense net does not end up
|
||||
// drawing every connection on top of the last
|
||||
List<Line2D> placed = new ArrayList<Line2D>();
|
||||
Group backbone = backboneNet(netlist);
|
||||
|
||||
for (Group group : netlist.getSortedGroups()) {
|
||||
List<Pin> pins = new ArrayList<Pin>();
|
||||
for (Node node : group.getNodes()) {
|
||||
UUID physicalId = node.getComponent().getId();
|
||||
List<SymbolEntry> entries = entriesByPhysicalId.get(physicalId);
|
||||
if (entries == null) {
|
||||
continue;
|
||||
}
|
||||
for (SymbolEntry entry : entries) {
|
||||
Integer schematicIndex = entry.pinMapping.get(node.getPointIndex());
|
||||
if (schematicIndex == null || schematicIndex >= entry.symbol.getControlPointCount()) {
|
||||
continue;
|
||||
}
|
||||
Point2D location = entry.symbol.getControlPoint(schematicIndex);
|
||||
pins.add(new Pin(entry.symbol, schematicIndex,
|
||||
new Point2D.Double(location.getX(), location.getY())));
|
||||
}
|
||||
}
|
||||
|
||||
String railLabel = railLabelOf(group);
|
||||
if (railLabel == null && group == backbone) {
|
||||
// an unlabelled ground net says the same thing drawn as stubs as it does chained across the
|
||||
// whole drawing, and every stub ties back to the same net, so nothing is claimed that is not
|
||||
// already true of the circuit
|
||||
railLabel = GROUND_LABEL;
|
||||
}
|
||||
if (railLabel != null) {
|
||||
// rails are never routed: a lone stub on each pin says the same thing without dragging the
|
||||
// busiest net in the circuit across the drawing
|
||||
for (Pin pin : pins) {
|
||||
wiring.getStubs().add(createRailStub(railLabel, pin.location));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pins.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
// chain the pins after sorting left-to-right, top-to-bottom
|
||||
pins.sort(Comparator.comparingDouble((Pin p) -> p.location.getX())
|
||||
.thenComparingDouble(p -> p.location.getY()));
|
||||
for (int i = 0; i < pins.size() - 1; i++) {
|
||||
Pin a = pins.get(i);
|
||||
Pin b = pins.get(i + 1);
|
||||
OrthogonalLine wire = createWire(a, b, bodies, placed);
|
||||
wiring.getWires().add(wire);
|
||||
placed.addAll(segmentsOf(wire));
|
||||
}
|
||||
}
|
||||
return wiring;
|
||||
}
|
||||
|
||||
/** Creates the ground or supply symbol that stands in for one pin's connection to a rail. */
|
||||
private static IDIYComponent<?> createRailStub(String label, Point2D location) {
|
||||
IDIYComponent<?> stub;
|
||||
if (GROUND_LABEL.equals(label)) {
|
||||
stub = new GroundSymbol();
|
||||
} else {
|
||||
CommonNode node = new CommonNode();
|
||||
node.setValue(label);
|
||||
stub = node;
|
||||
}
|
||||
stub.setId(UUID.randomUUID());
|
||||
stub.setControlPoint(copyOf(location), 0);
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static class Pin {
|
||||
final IDIYComponent<?> symbol;
|
||||
final int pinIndex;
|
||||
final Point2D location;
|
||||
|
||||
Pin(IDIYComponent<?> symbol, int pinIndex, Point2D location) {
|
||||
this.symbol = symbol;
|
||||
this.pinIndex = pinIndex;
|
||||
this.location = 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 OrthogonalLine createWire(Pin a, Pin b, Map<UUID, Rectangle2D> bodies,
|
||||
List<Line2D> placed) {
|
||||
List<Rectangle2D> obstacles = new ArrayList<Rectangle2D>();
|
||||
for (Map.Entry<UUID, Rectangle2D> entry : bodies.entrySet()) {
|
||||
// the wire starts and ends on these two symbols, so their own boxes are not obstacles
|
||||
if (!entry.getKey().equals(a.symbol.getId()) && !entry.getKey().equals(b.symbol.getId())) {
|
||||
obstacles.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
WireRouter.Route route = router.route(a.location, exitDirection(a.symbol, a.location),
|
||||
b.location, exitDirection(b.symbol, b.location), obstacles, placed);
|
||||
|
||||
OrthogonalLine wire = new OrthogonalLine();
|
||||
wire.setId(UUID.randomUUID());
|
||||
wire.setStartDirection(route.getStartDirection());
|
||||
wire.setControlPoint(copyOf(a.location), 0);
|
||||
wire.setControlPoint(route.getBendPoint(), 1);
|
||||
wire.setControlPoint(copyOf(b.location), 2);
|
||||
return wire;
|
||||
}
|
||||
|
||||
/** @return the boxes spanned by each symbol's control points, keyed by symbol id. */
|
||||
private static Map<UUID, Rectangle2D> symbolBodies(
|
||||
Map<UUID, List<SymbolEntry>> entriesByPhysicalId) {
|
||||
Map<UUID, Rectangle2D> bodies = new LinkedHashMap<UUID, Rectangle2D>();
|
||||
for (List<SymbolEntry> entries : entriesByPhysicalId.values()) {
|
||||
for (SymbolEntry entry : entries) {
|
||||
Rectangle2D bounds = boundsOf(entry.symbol);
|
||||
if (bounds != null) {
|
||||
bodies.put(entry.symbol.getId(), bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bodies;
|
||||
}
|
||||
|
||||
private static Rectangle2D boundsOf(IDIYComponent<?> symbol) {
|
||||
int count = symbol.getControlPointCount();
|
||||
if (count == 0) {
|
||||
return null;
|
||||
}
|
||||
double minX = Double.MAX_VALUE;
|
||||
double minY = Double.MAX_VALUE;
|
||||
double maxX = -Double.MAX_VALUE;
|
||||
double maxY = -Double.MAX_VALUE;
|
||||
for (int i = 0; i < count; i++) {
|
||||
Point2D p = symbol.getControlPoint(i);
|
||||
minX = Math.min(minX, p.getX());
|
||||
minY = Math.min(minY, p.getY());
|
||||
maxX = Math.max(maxX, p.getX());
|
||||
maxY = Math.max(maxY, p.getY());
|
||||
}
|
||||
return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
private static List<Line2D> segmentsOf(OrthogonalLine wire) {
|
||||
List<Point2D> vertices = WireRouter.vertices(wire.getControlPoint(0), wire.getControlPoint(1),
|
||||
wire.getControlPoint(2), wire.getStartDirection());
|
||||
List<Line2D> segments = new ArrayList<Line2D>();
|
||||
for (int i = 1; i < vertices.size(); i++) {
|
||||
segments.add(new Line2D.Double(vertices.get(i - 1), vertices.get(i)));
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Works out which way a wire should leave a pin.
|
||||
*
|
||||
* <p>
|
||||
* A two terminal symbol has an axis of its own and its leads run along it, so a wire has to carry
|
||||
* on in that direction or it will bend the moment it leaves the lead. Anything else - a box, a
|
||||
* transistor - has its pins pointing away from the body, which the centroid finds well enough.
|
||||
* </p>
|
||||
*/
|
||||
static OrientationHV exitDirection(IDIYComponent<?> symbol, Point2D pinLocation) {
|
||||
int n = symbol.getControlPointCount();
|
||||
if (n == 0) {
|
||||
return OrientationHV.HORIZONTAL;
|
||||
}
|
||||
if (symbol instanceof AbstractLeadedComponent && n >= 2) {
|
||||
Point2D first = symbol.getControlPoint(0);
|
||||
Point2D second = symbol.getControlPoint(1);
|
||||
return Math.abs(second.getX() - first.getX()) >= Math.abs(second.getY() - first.getY())
|
||||
? OrientationHV.HORIZONTAL
|
||||
: OrientationHV.VERTICAL;
|
||||
}
|
||||
double cx = 0;
|
||||
double cy = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
cx += symbol.getControlPoint(i).getX();
|
||||
cy += symbol.getControlPoint(i).getY();
|
||||
}
|
||||
cx /= n;
|
||||
cy /= n;
|
||||
double dx = pinLocation.getX() - cx;
|
||||
double dy = pinLocation.getY() - cy;
|
||||
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());
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ misc */
|
||||
|
||||
static void resizeCanvasToFit(SchematicView view) {
|
||||
double maxX = 0;
|
||||
double maxY = 0;
|
||||
for (IDIYComponent<?> component : view.getComponents()) {
|
||||
for (int i = 0; i < component.getControlPointCount(); i++) {
|
||||
Point2D p = component.getControlPoint(i);
|
||||
maxX = Math.max(maxX, p.getX());
|
||||
maxY = Math.max(maxY, p.getY());
|
||||
}
|
||||
}
|
||||
maxX += MARGIN;
|
||||
maxY += MARGIN;
|
||||
if (maxX > view.getWidth().convertToPixels()) {
|
||||
view.setWidth(new Size(maxX, SizeUnit.px));
|
||||
}
|
||||
if (maxY > view.getHeight().convertToPixels()) {
|
||||
view.setHeight(new Size(maxY, SizeUnit.px));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static double zOrderOf(IDIYComponent<?> component) {
|
||||
ComponentType type = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) component.getClass());
|
||||
return type == null ? IDIYComponent.COMPONENT : type.getZOrder();
|
||||
}
|
||||
|
||||
/** Convenience overload used by callers that already have the drawing manager's areas. */
|
||||
public static void generate(Project project, Collection<ContinuityArea> continuityAreas) {
|
||||
new SchematicBuilder().build(project,
|
||||
continuityAreas == null ? new ArrayList<ContinuityArea>()
|
||||
: new ArrayList<ContinuityArea>(continuityAreas));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
|
||||
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.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.diylc.components.connectivity.OrthogonalLine;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.SchematicView;
|
||||
import org.diylc.netlist.Netlist;
|
||||
import org.diylc.presenter.ContinuityArea;
|
||||
import org.diylc.schematic.SchematicBuilder.SymbolEntry;
|
||||
|
||||
/**
|
||||
* Incrementally reconciles an already generated {@link SchematicView} with the current state of the
|
||||
* layout {@link Project}. It is meant to run silently whenever the user switches to the schematic
|
||||
* tab.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Components added to the layout get fresh symbols placed in a free grid cell.</li>
|
||||
* <li>Components removed from the layout lose their symbols (and the wires attached to them).</li>
|
||||
* <li>Symbols that survive keep their position, rotation and mirroring; only name and value are
|
||||
* refreshed.</li>
|
||||
* <li>All wires are regenerated from the current netlist.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class SchematicSynchronizer {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SchematicSynchronizer.class);
|
||||
|
||||
private final SchematicBuilder builder = new SchematicBuilder();
|
||||
|
||||
/**
|
||||
* Synchronizes the schematic view. If the project has no schematic view yet, or it is empty, a
|
||||
* full {@link SchematicBuilder#build} is performed instead.
|
||||
*
|
||||
* <p>
|
||||
* Symbols the user has arranged keep their position for as long as the component behind them is
|
||||
* still in the layout. Improvements to the placement algorithm deliberately do not reach an
|
||||
* already generated schematic: re-laying one out has to be an explicit action, never a side
|
||||
* effect of opening the view.
|
||||
* </p>
|
||||
*/
|
||||
public void synchronize(Project project, List<ContinuityArea> continuityAreas) {
|
||||
SchematicView view = project.getSchematicView();
|
||||
if (view == null || !view.isGenerated()) {
|
||||
builder.build(project, continuityAreas == null ? new ArrayList<ContinuityArea>() : continuityAreas);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<UUID, List<UUID>> map = view.getPhysicalToSchematicMap();
|
||||
Map<UUID, IDIYComponent<?>> symbolsById = new HashMap<UUID, IDIYComponent<?>>();
|
||||
for (IDIYComponent<?> component : view.getComponents()) {
|
||||
if (!(component instanceof OrthogonalLine)) {
|
||||
symbolsById.put(component.getId(), component);
|
||||
}
|
||||
}
|
||||
|
||||
Map<UUID, IDIYComponent<?>> layoutById = new LinkedHashMap<UUID, IDIYComponent<?>>();
|
||||
for (IDIYComponent<?> component : project.getComponents()) {
|
||||
layoutById.put(component.getId(), component);
|
||||
}
|
||||
|
||||
List<IDIYComponent<?>> keptSymbols = new ArrayList<IDIYComponent<?>>();
|
||||
Map<UUID, List<SymbolEntry>> entriesByPhysicalId = new LinkedHashMap<UUID, List<SymbolEntry>>();
|
||||
Map<UUID, List<UUID>> newMap = new LinkedHashMap<UUID, List<UUID>>();
|
||||
|
||||
// the netlist is needed up front so that newly added components are stood upright against their
|
||||
// rails exactly the way a full build would place them
|
||||
Netlist netlist = builder.extractNetlist(project,
|
||||
continuityAreas == null ? new ArrayList<ContinuityArea>() : continuityAreas);
|
||||
Map<UUID, Map<Integer, String>> railPins = SchematicBuilder.railPins(netlist);
|
||||
|
||||
int occupiedSlots = map.size();
|
||||
|
||||
for (Map.Entry<UUID, IDIYComponent<?>> layoutEntry : layoutById.entrySet()) {
|
||||
UUID physicalId = layoutEntry.getKey();
|
||||
IDIYComponent<?> physical = layoutEntry.getValue();
|
||||
if (!SchematicBuilder.isEligible(physical)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<SymbolEntry> freshEntries = builder.createSymbols(physical);
|
||||
if (freshEntries.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<UUID> existingIds = map.get(physicalId);
|
||||
List<SymbolEntry> resolvedEntries = new ArrayList<SymbolEntry>();
|
||||
List<UUID> resolvedIds = new ArrayList<UUID>();
|
||||
|
||||
if (existingIds != null) {
|
||||
// component already present: keep positioned symbols, refresh labels, reuse fresh pin maps
|
||||
for (int i = 0; i < freshEntries.size(); i++) {
|
||||
SymbolEntry fresh = freshEntries.get(i);
|
||||
IDIYComponent<?> existing =
|
||||
i < existingIds.size() ? symbolsById.get(existingIds.get(i)) : null;
|
||||
if (existing != null) {
|
||||
copyLabels(fresh.symbol, existing);
|
||||
resolvedEntries.add(new SymbolEntry(existing, fresh.pinMapping));
|
||||
resolvedIds.add(existing.getId());
|
||||
} else {
|
||||
resolvedEntries.add(fresh);
|
||||
resolvedIds.add(fresh.symbol.getId());
|
||||
}
|
||||
}
|
||||
// place any brand new sub-symbols (e.g. section count grew) that had no counterpart
|
||||
if (resolvedEntries.size() > existingIds.size()) {
|
||||
List<SymbolEntry> added =
|
||||
resolvedEntries.subList(existingIds.size(), resolvedEntries.size());
|
||||
SchematicBuilder.orientSymbols(added, railPins.get(physicalId));
|
||||
SchematicBuilder.placeAtGridSlot(added, occupiedSlots++, occupiedSlots + 1);
|
||||
}
|
||||
} else {
|
||||
// new component
|
||||
resolvedEntries.addAll(freshEntries);
|
||||
for (SymbolEntry e : freshEntries) {
|
||||
resolvedIds.add(e.symbol.getId());
|
||||
}
|
||||
SchematicBuilder.orientSymbols(freshEntries, railPins.get(physicalId));
|
||||
SchematicBuilder.placeAtGridSlot(freshEntries, occupiedSlots++, occupiedSlots + 1);
|
||||
}
|
||||
|
||||
entriesByPhysicalId.put(physicalId, resolvedEntries);
|
||||
newMap.put(physicalId, resolvedIds);
|
||||
for (SymbolEntry e : resolvedEntries) {
|
||||
keptSymbols.add(e.symbol);
|
||||
}
|
||||
}
|
||||
|
||||
// anything left in the old map that we did not resolve corresponds to a deleted component
|
||||
Set<UUID> removed = new HashSet<UUID>(map.keySet());
|
||||
removed.removeAll(newMap.keySet());
|
||||
if (!removed.isEmpty()) {
|
||||
LOG.debug("Removing schematic symbols for " + removed.size() + " deleted component(s)");
|
||||
}
|
||||
|
||||
SchematicBuilder.Wiring wiring = builder.createWires(netlist, entriesByPhysicalId);
|
||||
|
||||
List<IDIYComponent<?>> result = new ArrayList<IDIYComponent<?>>(keptSymbols);
|
||||
result.addAll(wiring.getStubs());
|
||||
result.addAll(wiring.getWires());
|
||||
result.sort(SchematicBuilder.SCHEMATIC_ORDER);
|
||||
|
||||
view.getComponents().clear();
|
||||
view.getComponents().addAll(result);
|
||||
view.setPhysicalToSchematicMap(newMap);
|
||||
SchematicBuilder.resizeCanvasToFit(view);
|
||||
}
|
||||
|
||||
private static void copyLabels(IDIYComponent<?> from, IDIYComponent<?> to) {
|
||||
to.setName(from.getName());
|
||||
try {
|
||||
Object value = from.getValue();
|
||||
@SuppressWarnings("unchecked")
|
||||
IDIYComponent<Object> target = (IDIYComponent<Object>) to;
|
||||
target.setValue(value);
|
||||
} catch (Exception e) {
|
||||
// value types differ between fresh and existing symbol; leave the existing value untouched
|
||||
}
|
||||
}
|
||||
|
||||
public static void synchronize(Project project, Collection<ContinuityArea> continuityAreas,
|
||||
boolean unused) {
|
||||
new SchematicSynchronizer().synchronize(project,
|
||||
continuityAreas == null ? new ArrayList<ContinuityArea>()
|
||||
: new ArrayList<ContinuityArea>(continuityAreas));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
|
||||
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 org.diylc.components.semiconductors.BJTSymbol;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Maps physical transistor packages (TO-92, TO-220, TO-3, TO-126, TO-1) to a {@link BJTSymbol}. The
|
||||
* three package legs are mapped to the symbol legs in index order; if a particular package uses a
|
||||
* different pin-out the user can rotate/relabel the resulting symbol.
|
||||
*/
|
||||
public class TransistorSchematicFactory extends AbstractSimpleSchematicFactory {
|
||||
|
||||
@Override
|
||||
protected IDIYComponent<?> createSymbol() {
|
||||
return new BJTSymbol();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int electricalPinCount() {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
|
||||
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.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.common.OrientationHV;
|
||||
|
||||
/**
|
||||
* Chooses the bend point of a generated schematic wire.
|
||||
*
|
||||
* <p>
|
||||
* A {@code Right Angle Line} derives its whole route from one interior handle plus the axis it leaves
|
||||
* the first pin on, so routing here means picking those two rather than emitting a polyline. The
|
||||
* router proposes a handful of handles - the two L shapes, the mid and quarter points, and a few
|
||||
* positions outside the box spanned by the two pins that let a wire detour - and keeps the one that
|
||||
* scores best.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Scoring is what the previous bend-point rule lacked: it had no idea what else was on the drawing,
|
||||
* so wires ran through symbols and along each other. Here a route is charged for its length, for
|
||||
* every corner, for crossing a wire already placed, for running on top of one, and heavily for
|
||||
* passing through the body of a symbol.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class WireRouter {
|
||||
|
||||
/** Grid the handle is snapped to. */
|
||||
public static final double GRID = 10d;
|
||||
/** How far outside the endpoints a detour candidate is offered. */
|
||||
private static final double DETOUR = 40d;
|
||||
|
||||
private static final double LENGTH_COST = 0.01;
|
||||
private static final double BEND_COST = 6;
|
||||
private static final double CROSSING_COST = 15;
|
||||
private static final double OVERLAP_COST = 0.5;
|
||||
private static final double BODY_COST = 40;
|
||||
private static final double WRONG_AXIS_COST = 8;
|
||||
private static final double EPS = 0.5;
|
||||
|
||||
/** The chosen route: the axis the wire leaves the first pin on, and its single bend point. */
|
||||
public static class Route {
|
||||
|
||||
private final OrientationHV startDirection;
|
||||
private final Point2D bendPoint;
|
||||
|
||||
Route(OrientationHV startDirection, Point2D bendPoint) {
|
||||
this.startDirection = startDirection;
|
||||
this.bendPoint = bendPoint;
|
||||
}
|
||||
|
||||
public OrientationHV getStartDirection() {
|
||||
return startDirection;
|
||||
}
|
||||
|
||||
public Point2D getBendPoint() {
|
||||
return bendPoint;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param start the first pin
|
||||
* @param startAxis the axis the wire ought to leave the first pin on
|
||||
* @param end the second pin
|
||||
* @param endAxis the axis the wire ought to arrive at the second pin on
|
||||
* @param bodies boxes the route should stay out of, the two pins' own symbols excluded
|
||||
* @param placed segments of the wires already routed
|
||||
*/
|
||||
public Route route(Point2D start, OrientationHV startAxis, Point2D end, OrientationHV endAxis,
|
||||
List<Rectangle2D> bodies, List<Line2D> placed) {
|
||||
// pins that already share a row or a column want a straight run, which a handle sitting on the
|
||||
// first pin produces whichever axis the template starts on
|
||||
if (Math.abs(start.getX() - end.getX()) < 1) {
|
||||
return new Route(OrientationHV.VERTICAL, copyOf(start));
|
||||
}
|
||||
if (Math.abs(start.getY() - end.getY()) < 1) {
|
||||
return new Route(OrientationHV.HORIZONTAL, copyOf(start));
|
||||
}
|
||||
|
||||
Route best = null;
|
||||
double bestCost = Double.MAX_VALUE;
|
||||
for (OrientationHV direction : OrientationHV.values()) {
|
||||
for (Point2D handle : candidates(start, end)) {
|
||||
double cost = cost(start, handle, end, direction, startAxis, endAxis, bodies, placed);
|
||||
if (cost < bestCost) {
|
||||
bestCost = cost;
|
||||
best = new Route(direction, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private static List<Point2D> candidates(Point2D start, Point2D end) {
|
||||
double midX = snap((start.getX() + end.getX()) / 2);
|
||||
double midY = snap((start.getY() + end.getY()) / 2);
|
||||
double leftX = snap(Math.min(start.getX(), end.getX()) - DETOUR);
|
||||
double rightX = snap(Math.max(start.getX(), end.getX()) + DETOUR);
|
||||
double topY = snap(Math.min(start.getY(), end.getY()) - DETOUR);
|
||||
double bottomY = snap(Math.max(start.getY(), end.getY()) + DETOUR);
|
||||
|
||||
List<Point2D> result = new ArrayList<Point2D>();
|
||||
// the two L shapes
|
||||
result.add(new Point2D.Double(snap(end.getX()), snap(start.getY())));
|
||||
result.add(new Point2D.Double(snap(start.getX()), snap(end.getY())));
|
||||
// a leg down the middle, which is the shape most schematics use
|
||||
result.add(new Point2D.Double(midX, snap(start.getY())));
|
||||
result.add(new Point2D.Double(midX, snap(end.getY())));
|
||||
result.add(new Point2D.Double(snap(start.getX()), midY));
|
||||
result.add(new Point2D.Double(snap(end.getX()), midY));
|
||||
result.add(new Point2D.Double(midX, midY));
|
||||
// detours around whatever sits between the two pins
|
||||
result.add(new Point2D.Double(leftX, midY));
|
||||
result.add(new Point2D.Double(rightX, midY));
|
||||
result.add(new Point2D.Double(midX, topY));
|
||||
result.add(new Point2D.Double(midX, bottomY));
|
||||
return result;
|
||||
}
|
||||
|
||||
private double cost(Point2D start, Point2D handle, Point2D end, OrientationHV direction,
|
||||
OrientationHV startAxis, OrientationHV endAxis, List<Rectangle2D> bodies,
|
||||
List<Line2D> placed) {
|
||||
List<Point2D> vertices = vertices(start, handle, end, direction);
|
||||
List<Line2D> segments = segments(vertices);
|
||||
if (segments.isEmpty()) {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
double cost = BEND_COST * Math.max(0, vertices.size() - 2);
|
||||
for (Line2D segment : segments) {
|
||||
cost += LENGTH_COST * segment.getP1().distance(segment.getP2());
|
||||
for (Rectangle2D body : bodies) {
|
||||
if (body.intersectsLine(segment)) {
|
||||
cost += BODY_COST;
|
||||
}
|
||||
}
|
||||
for (Line2D other : placed) {
|
||||
if (crosses(segment, other)) {
|
||||
cost += CROSSING_COST;
|
||||
}
|
||||
cost += OVERLAP_COST * overlapLength(segment, other);
|
||||
}
|
||||
}
|
||||
if (axisOf(segments.get(0)) != startAxis) {
|
||||
cost += WRONG_AXIS_COST;
|
||||
}
|
||||
if (axisOf(segments.get(segments.size() - 1)) != endAxis) {
|
||||
cost += WRONG_AXIS_COST;
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a start, a bend point and an end into the vertices of the route, the same way
|
||||
* {@code AbstractOrthogonalComponent} does: every control point is reached by a move along one axis
|
||||
* followed by a move along the other. Zero length steps are dropped as they are produced.
|
||||
*/
|
||||
public static List<Point2D> vertices(Point2D start, Point2D handle, Point2D end,
|
||||
OrientationHV startDirection) {
|
||||
boolean horizontalFirst = startDirection == OrientationHV.HORIZONTAL;
|
||||
List<Point2D> vertices = new ArrayList<Point2D>();
|
||||
add(vertices, start);
|
||||
Point2D previous = start;
|
||||
for (Point2D next : new Point2D[] {handle, end}) {
|
||||
if (horizontalFirst) {
|
||||
add(vertices, new Point2D.Double(next.getX(), previous.getY()));
|
||||
} else {
|
||||
add(vertices, new Point2D.Double(previous.getX(), next.getY()));
|
||||
}
|
||||
add(vertices, next);
|
||||
previous = next;
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
|
||||
private static void add(List<Point2D> vertices, Point2D p) {
|
||||
if (!vertices.isEmpty() && vertices.get(vertices.size() - 1).distance(p) < EPS) {
|
||||
return;
|
||||
}
|
||||
vertices.add(new Point2D.Double(p.getX(), p.getY()));
|
||||
}
|
||||
|
||||
private static List<Line2D> segments(List<Point2D> vertices) {
|
||||
List<Line2D> segments = new ArrayList<Line2D>();
|
||||
for (int i = 1; i < vertices.size(); i++) {
|
||||
segments.add(new Line2D.Double(vertices.get(i - 1), vertices.get(i)));
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
private static OrientationHV axisOf(Line2D segment) {
|
||||
return Math.abs(segment.getX2() - segment.getX1()) >= Math.abs(segment.getY2() - segment.getY1())
|
||||
? OrientationHV.HORIZONTAL
|
||||
: OrientationHV.VERTICAL;
|
||||
}
|
||||
|
||||
/** @return true when the two segments genuinely cross, rather than merely touching end to end. */
|
||||
private static boolean crosses(Line2D a, Line2D b) {
|
||||
if (axisOf(a) == axisOf(b)) {
|
||||
return false;
|
||||
}
|
||||
if (!a.intersectsLine(b)) {
|
||||
return false;
|
||||
}
|
||||
// a shared endpoint is a junction, not a crossing
|
||||
return !(near(a.getP1(), b.getP1()) || near(a.getP1(), b.getP2()) || near(a.getP2(), b.getP1())
|
||||
|| near(a.getP2(), b.getP2()));
|
||||
}
|
||||
|
||||
/** @return the length the two segments run along one another, which reads as a single thick line. */
|
||||
private static double overlapLength(Line2D a, Line2D b) {
|
||||
if (axisOf(a) != axisOf(b)) {
|
||||
return 0;
|
||||
}
|
||||
if (axisOf(a) == OrientationHV.HORIZONTAL) {
|
||||
if (Math.abs(a.getY1() - b.getY1()) > EPS) {
|
||||
return 0;
|
||||
}
|
||||
return span(a.getX1(), a.getX2(), b.getX1(), b.getX2());
|
||||
}
|
||||
if (Math.abs(a.getX1() - b.getX1()) > EPS) {
|
||||
return 0;
|
||||
}
|
||||
return span(a.getY1(), a.getY2(), b.getY1(), b.getY2());
|
||||
}
|
||||
|
||||
private static double span(double a1, double a2, double b1, double b2) {
|
||||
double low = Math.max(Math.min(a1, a2), Math.min(b1, b2));
|
||||
double high = Math.min(Math.max(a1, a2), Math.max(b1, b2));
|
||||
return Math.max(0, high - low);
|
||||
}
|
||||
|
||||
private static boolean near(Point2D a, Point2D b) {
|
||||
return a.distance(b) < EPS;
|
||||
}
|
||||
|
||||
private static Point2D copyOf(Point2D p) {
|
||||
return new Point2D.Double(p.getX(), p.getY());
|
||||
}
|
||||
|
||||
private static double snap(double value) {
|
||||
return Math.round(value / GRID) * GRID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
|
||||
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.flow;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.components.semiconductors.BJTSymbol;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Derives the direction of a drawn transistor symbol from its fixed geometry.
|
||||
*
|
||||
* <p>
|
||||
* A {@code BJTSymbol} names its pins {@code Q1.0}, {@code Q1.1} and so on, so there is nothing to
|
||||
* read. Its three legs are however always built in the same order: control point 0 is the lone pin on
|
||||
* the left, which is the base, and of the two on the right the upper one is the collector.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This rule is deliberately restricted to the transistor symbol rather than to every three legged
|
||||
* symbol, because a potentiometer shares that geometry while being entirely passive and would be
|
||||
* given a direction it does not have.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class BjtSymbolFlowRule implements ISignalFlowRule {
|
||||
|
||||
@Override
|
||||
public FlowHints evaluate(IDIYComponent<?> component, ComponentType type) {
|
||||
if (!(component instanceof BJTSymbol) || component.getControlPointCount() < 3) {
|
||||
return null;
|
||||
}
|
||||
return FlowHints.builder().input(0).output(1).neutral(2).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
|
||||
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.flow;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* What an {@link ISignalFlowRule} worked out about one component: the {@link FlowRole} of each of its
|
||||
* control points. Points the rule said nothing about read back as {@link FlowRole#NEUTRAL}.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class FlowHints {
|
||||
|
||||
private final Map<Integer, FlowRole> roles;
|
||||
|
||||
private FlowHints(Map<Integer, FlowRole> roles) {
|
||||
this.roles = Collections.unmodifiableMap(roles);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public FlowRole getRole(int pointIndex) {
|
||||
FlowRole role = roles.get(pointIndex);
|
||||
return role == null ? FlowRole.NEUTRAL : role;
|
||||
}
|
||||
|
||||
public Map<Integer, FlowRole> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if at least one point is an input or an output. A set of hints that is entirely
|
||||
* neutral says nothing about direction, so the analyzer treats it as no answer at all.
|
||||
*/
|
||||
public boolean hasDirection() {
|
||||
for (FlowRole role : roles.values()) {
|
||||
if (role == FlowRole.INPUT || role == FlowRole.OUTPUT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return roles.toString();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Map<Integer, FlowRole> roles = new HashMap<Integer, FlowRole>();
|
||||
|
||||
public Builder role(int pointIndex, FlowRole role) {
|
||||
roles.put(pointIndex, role);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder input(int pointIndex) {
|
||||
return role(pointIndex, FlowRole.INPUT);
|
||||
}
|
||||
|
||||
public Builder output(int pointIndex) {
|
||||
return role(pointIndex, FlowRole.OUTPUT);
|
||||
}
|
||||
|
||||
public Builder neutral(int pointIndex) {
|
||||
return role(pointIndex, FlowRole.NEUTRAL);
|
||||
}
|
||||
|
||||
public FlowHints build() {
|
||||
return new FlowHints(roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
|
||||
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.flow;
|
||||
|
||||
/**
|
||||
* Direction in which signal travels through one control point of a component. Used to orient the
|
||||
* generated schematic so that it reads from input on the left to output on the right.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public enum FlowRole {
|
||||
|
||||
/** Signal enters the component here: a base, a gate, a control grid. */
|
||||
INPUT,
|
||||
|
||||
/** Signal leaves the component here: a collector, a drain, a plate, a pickup terminal. */
|
||||
OUTPUT,
|
||||
|
||||
/**
|
||||
* The point carries no direction of its own. Emitters, cathodes, heaters, supply and ground legs
|
||||
* and both ends of a passive part all fall here.
|
||||
*/
|
||||
NEUTRAL
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
|
||||
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.flow;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Works out which control points of a component signal enters and leaves by, so that the schematic
|
||||
* generator can lay the circuit out in the direction it actually runs.
|
||||
*
|
||||
* <p>
|
||||
* A rule speaks only about components it recognises and returns {@code null} for everything else,
|
||||
* which lets the next rule try. {@link SignalFlowAnalyzer} holds the rules in priority order, most
|
||||
* specific first, and takes the first answer that carries a direction.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Implementations must be stateless; one instance is created and reused for the life of the
|
||||
* application.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public interface ISignalFlowRule {
|
||||
|
||||
/**
|
||||
* @param component the physical layout component
|
||||
* @param type the component's type, or null when it could not be resolved
|
||||
* @return roles of the component's control points, or null if this rule has nothing to say about
|
||||
* this component
|
||||
*/
|
||||
FlowHints evaluate(IDIYComponent<?> component, ComponentType type);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
|
||||
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.flow;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.netlist.Node;
|
||||
|
||||
/**
|
||||
* Last resort that reads the direction off pin names, for the many modules and sensors that label
|
||||
* their terminals {@code IN+}, {@code OUT-} and the like.
|
||||
*
|
||||
* <p>
|
||||
* It runs after every type aware rule, so a component that one of those recognises is never subject
|
||||
* to a name that happens to look directional. Names that carry no direction, such as {@code VCC},
|
||||
* {@code GND} or the battery terminals of a charger module, are left neutral, and a component whose
|
||||
* pins are merely numbered produces no answer at all.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class NamedPinFlowRule implements ISignalFlowRule {
|
||||
|
||||
@Override
|
||||
public FlowHints evaluate(IDIYComponent<?> component, ComponentType type) {
|
||||
FlowHints.Builder builder = FlowHints.builder();
|
||||
for (int i = 0; i < component.getControlPointCount(); i++) {
|
||||
builder.role(i, roleOf(component.getControlPointNodeName(i)));
|
||||
}
|
||||
FlowHints hints = builder.build();
|
||||
return hints.hasDirection() ? hints : null;
|
||||
}
|
||||
|
||||
private static FlowRole roleOf(String pinName) {
|
||||
String name = Node.sanitizeNodeName(pinName);
|
||||
if (name == null) {
|
||||
return FlowRole.NEUTRAL;
|
||||
}
|
||||
String label = name.trim().toUpperCase();
|
||||
if (label.equals("IN") || label.startsWith("IN+") || label.startsWith("IN-")
|
||||
|| label.equals("INPUT")) {
|
||||
return FlowRole.INPUT;
|
||||
}
|
||||
if (label.equals("OUT") || label.startsWith("OUT+") || label.startsWith("OUT-")
|
||||
|| label.equals("OUTPUT")) {
|
||||
return FlowRole.OUTPUT;
|
||||
}
|
||||
return FlowRole.NEUTRAL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
|
||||
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.flow;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.components.guitar.AbstractGuitarPickup;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Treats a guitar pickup as the origin of the signal, so that everything it reaches is downstream of
|
||||
* it.
|
||||
*
|
||||
* <p>
|
||||
* This is what gives a guitar wiring diagram a direction at all: those circuits are entirely passive,
|
||||
* so no active device is there to say which way the signal runs. Pickups name no pins, so the rule
|
||||
* keys on the type and marks every live terminal as an output.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class PickupFlowRule implements ISignalFlowRule {
|
||||
|
||||
@Override
|
||||
public FlowHints evaluate(IDIYComponent<?> component, ComponentType type) {
|
||||
if (!(component instanceof AbstractGuitarPickup)) {
|
||||
return null;
|
||||
}
|
||||
FlowHints.Builder builder = FlowHints.builder();
|
||||
boolean any = false;
|
||||
for (int i = 0; i < component.getControlPointCount(); i++) {
|
||||
if (component.isControlPointSticky(i)) {
|
||||
builder.output(i);
|
||||
any = true;
|
||||
} else {
|
||||
builder.neutral(i);
|
||||
}
|
||||
}
|
||||
return any ? builder.build() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
|
||||
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.flow;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.presenter.ComponentProcessor;
|
||||
|
||||
/**
|
||||
* Works out which way signal runs through a component by asking each {@link ISignalFlowRule} in turn
|
||||
* and taking the first answer that carries a direction.
|
||||
*
|
||||
* <p>
|
||||
* The rules are held in an explicit list rather than discovered by scanning, because their order is
|
||||
* their priority: the ones that read real per instance data come before the ones that infer from
|
||||
* geometry, and the rule that merely reads pin names comes last. Adding a rule means writing the
|
||||
* class and placing it in this list at the specificity it deserves.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class SignalFlowAnalyzer {
|
||||
|
||||
private static final List<ISignalFlowRule> RULES = Collections.unmodifiableList(Arrays.asList(
|
||||
new TransistorPinoutFlowRule(),
|
||||
new TubeElectrodeFlowRule(),
|
||||
new TubeSymbolFlowRule(),
|
||||
new BjtSymbolFlowRule(),
|
||||
new PickupFlowRule(),
|
||||
new NamedPinFlowRule()));
|
||||
|
||||
/**
|
||||
* @param component the physical layout component
|
||||
* @return the first directional answer any rule had about the component, or null when none of them
|
||||
* recognised it, which is the normal outcome for passive parts
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public FlowHints analyze(IDIYComponent<?> component) {
|
||||
if (component == null) {
|
||||
return null;
|
||||
}
|
||||
ComponentType type = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) component.getClass());
|
||||
for (ISignalFlowRule rule : RULES) {
|
||||
FlowHints hints = rule.evaluate(component, type);
|
||||
if (hints != null && hints.hasDirection()) {
|
||||
return hints;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return the rules in priority order. */
|
||||
public static List<ISignalFlowRule> getRules() {
|
||||
return RULES;
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
|
||||
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.flow;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.components.semiconductors.AbstractTransistorPackage;
|
||||
import org.diylc.components.semiconductors.TransistorPinout;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Reads the direction straight off a transistor's declared pin-out, which already says which leg is
|
||||
* which for every package and every leg ordering the user can pick.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class TransistorPinoutFlowRule implements ISignalFlowRule {
|
||||
|
||||
private static final String REGULATOR = "REGULATOR";
|
||||
|
||||
@Override
|
||||
public FlowHints evaluate(IDIYComponent<?> component, ComponentType type) {
|
||||
if (!(component instanceof AbstractTransistorPackage)) {
|
||||
return null;
|
||||
}
|
||||
TransistorPinout pinout = ((AbstractTransistorPackage) component).getPinout();
|
||||
if (pinout == null) {
|
||||
return null;
|
||||
}
|
||||
// the letters are in control point order, so the n-th letter describes the n-th leg
|
||||
String electrodes = pinout.toPinout();
|
||||
boolean regulator = pinout.name().startsWith(REGULATOR);
|
||||
FlowHints.Builder builder = FlowHints.builder();
|
||||
for (int i = 0; i < electrodes.length(); i++) {
|
||||
builder.role(i, roleOf(electrodes.charAt(i), regulator));
|
||||
}
|
||||
FlowHints hints = builder.build();
|
||||
return hints.hasDirection() ? hints : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base, gate and a regulator's input are where signal arrives; collector, drain and a regulator's
|
||||
* output are where it leaves. Emitters and sources sit on a rail and carry no direction.
|
||||
*
|
||||
* <p>
|
||||
* The {@code regulator} flag is not cosmetic: <code>G</code> is the gate of a JFET or MOSFET but
|
||||
* the ground leg of a regulator, so reading it the same way in both cases would turn every
|
||||
* regulator around.
|
||||
* </p>
|
||||
*/
|
||||
private static FlowRole roleOf(char electrode, boolean regulator) {
|
||||
if (regulator) {
|
||||
switch (electrode) {
|
||||
case 'I':
|
||||
return FlowRole.INPUT;
|
||||
case 'O':
|
||||
return FlowRole.OUTPUT;
|
||||
default:
|
||||
return FlowRole.NEUTRAL;
|
||||
}
|
||||
}
|
||||
switch (electrode) {
|
||||
case 'B':
|
||||
case 'G':
|
||||
return FlowRole.INPUT;
|
||||
case 'C':
|
||||
case 'D':
|
||||
return FlowRole.OUTPUT;
|
||||
default:
|
||||
return FlowRole.NEUTRAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
|
||||
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.flow;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.components.tube.Jumbo4PinTubeSocket;
|
||||
import org.diylc.components.tube.TubeSocket;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Reads the direction off the electrode labels of a tube socket.
|
||||
*
|
||||
* <p>
|
||||
* The labels are the user's own text and default to plain pin numbers on {@link TubeSocket}, so this
|
||||
* rule contributes only when someone has actually named the electrodes. When it finds nothing it
|
||||
* returns null and lets a later rule try, rather than guessing at a pin-out it cannot see.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class TubeElectrodeFlowRule implements ISignalFlowRule {
|
||||
|
||||
@Override
|
||||
public FlowHints evaluate(IDIYComponent<?> component, ComponentType type) {
|
||||
if (!(component instanceof TubeSocket) && !(component instanceof Jumbo4PinTubeSocket)) {
|
||||
return null;
|
||||
}
|
||||
FlowHints.Builder builder = FlowHints.builder();
|
||||
for (int i = 0; i < component.getControlPointCount(); i++) {
|
||||
builder.role(i, roleOf(component.getControlPointNodeName(i)));
|
||||
}
|
||||
FlowHints hints = builder.build();
|
||||
return hints.hasDirection() ? hints : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the control grid steers the tube, so G2 and G3 - the screen and suppressor, which sit on a
|
||||
* supply rail or at the cathode - are deliberately left neutral.
|
||||
*/
|
||||
private static FlowRole roleOf(String electrode) {
|
||||
if (electrode == null) {
|
||||
return FlowRole.NEUTRAL;
|
||||
}
|
||||
String label = electrode.trim().toUpperCase();
|
||||
if (label.equals("G") || label.equals("G1") || label.equals("GRID")) {
|
||||
return FlowRole.INPUT;
|
||||
}
|
||||
if (label.equals("P") || label.equals("A") || label.equals("PLATE") || label.equals("ANODE")) {
|
||||
return FlowRole.OUTPUT;
|
||||
}
|
||||
return FlowRole.NEUTRAL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
|
||||
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.flow;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.components.tube.AbstractTubeSymbol;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Derives the direction of a tube symbol from its fixed geometry.
|
||||
*
|
||||
* <p>
|
||||
* Tube symbols name their pins by number, so there is nothing to read; the electrode order is however
|
||||
* fixed by the drawing. Both {@code TriodeSymbol} and {@code PentodeSymbol} build their control
|
||||
* points with the control grid first - it is the left end of the dashed grid electrode - and the
|
||||
* plate second. Everything after that is a cathode, a screen or suppressor grid, or a heater, none of
|
||||
* which carries signal direction.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class TubeSymbolFlowRule implements ISignalFlowRule {
|
||||
|
||||
@Override
|
||||
public FlowHints evaluate(IDIYComponent<?> component, ComponentType type) {
|
||||
if (!(component instanceof AbstractTubeSymbol) || component.getControlPointCount() < 2) {
|
||||
return null;
|
||||
}
|
||||
return FlowHints.builder().input(0).output(1).build();
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
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.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.components.electromechanical.MiniToggleSwitch;
|
||||
import org.diylc.components.schematic.SchematicBox;
|
||||
import org.diylc.components.semiconductors.ICSymbol;
|
||||
import org.junit.Test;
|
||||
|
||||
public class GenericBoxSchematicFactoryTest {
|
||||
|
||||
@Test
|
||||
public void producesOneBoxWithOnePinPerStickyNode() {
|
||||
ICSymbol physical = new ICSymbol();
|
||||
physical.setName("IC1");
|
||||
physical.setValue("TL072");
|
||||
|
||||
List<SchematicSymbolMapping> mappings =
|
||||
new GenericBoxSchematicFactory().createSchematicSymbols(physical);
|
||||
|
||||
assertEquals(1, mappings.size());
|
||||
SchematicSymbolMapping mapping = mappings.get(0);
|
||||
assertTrue(mapping.getSchematicSymbol() instanceof SchematicBox);
|
||||
assertNotNull(mapping.getSchematicSymbol().getId());
|
||||
|
||||
int stickyNodes = 0;
|
||||
for (int i = 0; i < physical.getControlPointCount(); i++) {
|
||||
if (physical.isControlPointSticky(i) && physical.getControlPointNodeName(i) != null) {
|
||||
stickyNodes++;
|
||||
}
|
||||
}
|
||||
assertEquals(stickyNodes, mapping.getPinMapping().size());
|
||||
assertEquals(stickyNodes, mapping.getSchematicSymbol().getControlPointCount());
|
||||
assertEquals("IC1", mapping.getSchematicSymbol().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void switchesAreEligibleAndFallBackToABox() {
|
||||
MiniToggleSwitch sw = new MiniToggleSwitch();
|
||||
sw.setName("SW1");
|
||||
|
||||
assertTrue(SchematicBuilder.isEligible(sw));
|
||||
|
||||
List<SchematicSymbolMapping> mappings =
|
||||
new GenericBoxSchematicFactory().createSchematicSymbols(sw);
|
||||
assertEquals(1, mappings.size());
|
||||
assertTrue(mappings.get(0).getSchematicSymbol() instanceof SchematicBox);
|
||||
assertTrue(mappings.get(0).getSchematicSymbol().getControlPointCount() >= 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void schematicBoxExposesConfiguredNodeNames() {
|
||||
SchematicBox box = new SchematicBox();
|
||||
box.setLeftNodes("IN+,IN-");
|
||||
box.setRightNodes("OUT");
|
||||
box.setTopNodes("");
|
||||
box.setBottomNodes("");
|
||||
assertEquals(3, box.getControlPointCount());
|
||||
assertEquals("IN+", box.getControlPointNodeName(0));
|
||||
assertEquals("IN-", box.getControlPointNodeName(1));
|
||||
assertEquals("OUT", box.getControlPointNodeName(2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
|
||||
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.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.diylc.components.passive.Resistor;
|
||||
import org.diylc.components.passive.ResistorSymbol;
|
||||
import org.diylc.components.semiconductors.BJTSymbol;
|
||||
import org.diylc.components.semiconductors.TransistorPinout;
|
||||
import org.diylc.components.semiconductors.TransistorTO92;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.netlist.Group;
|
||||
import org.diylc.netlist.Netlist;
|
||||
import org.diylc.netlist.Node;
|
||||
import org.diylc.schematic.SchematicBuilder.SymbolEntry;
|
||||
import org.junit.Test;
|
||||
|
||||
public class LayeredPlacementTest {
|
||||
|
||||
/** BJT_EBC puts the emitter at 0, the base at 1 and the collector at 2. */
|
||||
private static final int BASE = 1;
|
||||
private static final int COLLECTOR = 2;
|
||||
|
||||
@Test
|
||||
public void aDrivenPartLandsInAColumnAfterTheOneDrivingIt() {
|
||||
Circuit circuit = new Circuit();
|
||||
IDIYComponent<?> q1 = circuit.addTransistor("Q1");
|
||||
IDIYComponent<?> q2 = circuit.addTransistor("Q2");
|
||||
circuit.connect(q1, COLLECTOR, q2, BASE);
|
||||
|
||||
circuit.place();
|
||||
|
||||
assertTrue("the driven transistor must sit to the right of the one driving it",
|
||||
circuit.minX(q1) < circuit.minX(q2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aChainOfThreeRunsLeftToRight() {
|
||||
Circuit circuit = new Circuit();
|
||||
IDIYComponent<?> q1 = circuit.addTransistor("Q1");
|
||||
IDIYComponent<?> q2 = circuit.addTransistor("Q2");
|
||||
IDIYComponent<?> q3 = circuit.addTransistor("Q3");
|
||||
circuit.connect(q1, COLLECTOR, q2, BASE);
|
||||
circuit.connect(q2, COLLECTOR, q3, BASE);
|
||||
|
||||
circuit.place();
|
||||
|
||||
assertTrue(circuit.minX(q1) < circuit.minX(q2));
|
||||
assertTrue(circuit.minX(q2) < circuit.minX(q3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Feedback is normal in the circuits this feature exists for, so the layering has to survive a
|
||||
* cycle rather than recurse through it forever.
|
||||
*/
|
||||
@Test
|
||||
public void aFeedbackLoopIsBrokenInsteadOfHanging() {
|
||||
Circuit circuit = new Circuit();
|
||||
IDIYComponent<?> q1 = circuit.addTransistor("Q1");
|
||||
IDIYComponent<?> q2 = circuit.addTransistor("Q2");
|
||||
circuit.connect(q1, COLLECTOR, q2, BASE);
|
||||
circuit.connect(q2, COLLECTOR, q1, BASE);
|
||||
|
||||
circuit.place();
|
||||
|
||||
// the forward edge survives, so the two still land in different columns
|
||||
assertTrue(circuit.minX(q1) != circuit.minX(q2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPassivePartIsPulledInBetweenTheComponentsItConnects() {
|
||||
Circuit circuit = new Circuit();
|
||||
IDIYComponent<?> q1 = circuit.addTransistor("Q1");
|
||||
IDIYComponent<?> q2 = circuit.addTransistor("Q2");
|
||||
IDIYComponent<?> r1 = circuit.addResistor("R1");
|
||||
circuit.connect(q1, COLLECTOR, q2, BASE);
|
||||
circuit.connect(q1, COLLECTOR, r1, 0);
|
||||
circuit.connect(r1, 1, q2, BASE);
|
||||
|
||||
circuit.place();
|
||||
|
||||
double left = Math.min(circuit.minX(q1), circuit.minX(q2));
|
||||
double right = Math.max(circuit.minX(q1), circuit.minX(q2));
|
||||
assertTrue("the resistor should not be pushed outside the parts it sits between",
|
||||
circuit.minX(r1) >= left && circuit.minX(r1) <= right);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void symbolsInTheSameColumnDoNotOverlap() {
|
||||
Circuit circuit = new Circuit();
|
||||
IDIYComponent<?> q1 = circuit.addTransistor("Q1");
|
||||
IDIYComponent<?> a = circuit.addResistor("R1");
|
||||
IDIYComponent<?> b = circuit.addResistor("R2");
|
||||
// both resistors hang off the same pin, so they share a column
|
||||
circuit.connect(q1, COLLECTOR, a, 0);
|
||||
circuit.connect(q1, COLLECTOR, b, 0);
|
||||
|
||||
circuit.place();
|
||||
|
||||
boolean separated = circuit.maxY(a) <= circuit.minY(b) || circuit.maxY(b) <= circuit.minY(a)
|
||||
|| circuit.maxX(a) <= circuit.minX(b) || circuit.maxX(b) <= circuit.minX(a);
|
||||
assertTrue("two symbols were placed on top of each other", separated);
|
||||
}
|
||||
|
||||
/** The regression suite compares rendered output, so the same circuit must place identically. */
|
||||
@Test
|
||||
public void placementIsRepeatable() {
|
||||
List<Point2D> first = placeTwoStageCircuit();
|
||||
List<Point2D> second = placeTwoStageCircuit();
|
||||
|
||||
assertEquals(first.size(), second.size());
|
||||
for (int i = 0; i < first.size(); i++) {
|
||||
assertEquals("point " + i + " x", first.get(i).getX(), second.get(i).getX(), 0.0001);
|
||||
assertEquals("point " + i + " y", first.get(i).getY(), second.get(i).getY(), 0.0001);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Point2D> placeTwoStageCircuit() {
|
||||
Circuit circuit = new Circuit();
|
||||
IDIYComponent<?> q1 = circuit.addTransistor("Q1");
|
||||
IDIYComponent<?> q2 = circuit.addTransistor("Q2");
|
||||
IDIYComponent<?> r1 = circuit.addResistor("R1");
|
||||
IDIYComponent<?> r2 = circuit.addResistor("R2");
|
||||
circuit.connect(q1, COLLECTOR, r1, 0);
|
||||
circuit.connect(r1, 1, q2, BASE);
|
||||
circuit.connect(q2, COLLECTOR, r2, 0);
|
||||
circuit.place();
|
||||
return circuit.allPoints();
|
||||
}
|
||||
|
||||
/** Builds a synthetic layout, its symbols and its netlist, and runs the placement over them. */
|
||||
private static class Circuit {
|
||||
|
||||
private final List<IDIYComponent<?>> eligible = new ArrayList<IDIYComponent<?>>();
|
||||
private final Map<UUID, List<SymbolEntry>> entries = new LinkedHashMap<UUID, List<SymbolEntry>>();
|
||||
private final Netlist netlist = new Netlist(new ArrayList<IDIYComponent<?>>());
|
||||
|
||||
IDIYComponent<?> addTransistor(String name) {
|
||||
TransistorTO92 transistor = new TransistorTO92();
|
||||
transistor.setId(UUID.randomUUID());
|
||||
transistor.setName(name);
|
||||
transistor.setPinout(TransistorPinout.BJT_EBC);
|
||||
BJTSymbol symbol = new BJTSymbol();
|
||||
symbol.setId(UUID.randomUUID());
|
||||
symbol.setName(name);
|
||||
return register(transistor, symbol, 3);
|
||||
}
|
||||
|
||||
IDIYComponent<?> addResistor(String name) {
|
||||
Resistor resistor = new Resistor();
|
||||
resistor.setId(UUID.randomUUID());
|
||||
resistor.setName(name);
|
||||
ResistorSymbol symbol = new ResistorSymbol();
|
||||
symbol.setId(UUID.randomUUID());
|
||||
symbol.setName(name);
|
||||
return register(resistor, symbol, 2);
|
||||
}
|
||||
|
||||
private IDIYComponent<?> register(IDIYComponent<?> physical, IDIYComponent<?> symbol,
|
||||
int pinCount) {
|
||||
Map<Integer, Integer> pinMapping = new HashMap<Integer, Integer>();
|
||||
for (int i = 0; i < pinCount; i++) {
|
||||
pinMapping.put(i, i);
|
||||
}
|
||||
List<SymbolEntry> list = new ArrayList<SymbolEntry>();
|
||||
list.add(new SymbolEntry(symbol, pinMapping));
|
||||
eligible.add(physical);
|
||||
entries.put(physical.getId(), list);
|
||||
return physical;
|
||||
}
|
||||
|
||||
void connect(IDIYComponent<?> a, int pinA, IDIYComponent<?> b, int pinB) {
|
||||
netlist.add(new Group(new Node(a, pinA), new Node(b, pinB)));
|
||||
}
|
||||
|
||||
void place() {
|
||||
new LayeredPlacement().place(eligible, entries, netlist);
|
||||
}
|
||||
|
||||
private IDIYComponent<?> symbolOf(IDIYComponent<?> physical) {
|
||||
return entries.get(physical.getId()).get(0).symbol;
|
||||
}
|
||||
|
||||
double minX(IDIYComponent<?> physical) {
|
||||
return extreme(symbolOf(physical), true, false);
|
||||
}
|
||||
|
||||
double maxX(IDIYComponent<?> physical) {
|
||||
return extreme(symbolOf(physical), true, true);
|
||||
}
|
||||
|
||||
double minY(IDIYComponent<?> physical) {
|
||||
return extreme(symbolOf(physical), false, false);
|
||||
}
|
||||
|
||||
double maxY(IDIYComponent<?> physical) {
|
||||
return extreme(symbolOf(physical), false, true);
|
||||
}
|
||||
|
||||
private static double extreme(IDIYComponent<?> symbol, boolean horizontal, boolean max) {
|
||||
double result = max ? -Double.MAX_VALUE : Double.MAX_VALUE;
|
||||
for (int i = 0; i < symbol.getControlPointCount(); i++) {
|
||||
Point2D p = symbol.getControlPoint(i);
|
||||
double value = horizontal ? p.getX() : p.getY();
|
||||
result = max ? Math.max(result, value) : Math.min(result, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<Point2D> allPoints() {
|
||||
List<Point2D> points = new ArrayList<Point2D>();
|
||||
for (IDIYComponent<?> physical : eligible) {
|
||||
IDIYComponent<?> symbol = symbolOf(physical);
|
||||
for (int i = 0; i < symbol.getControlPointCount(); i++) {
|
||||
Point2D p = symbol.getControlPoint(i);
|
||||
points.add(new Point2D.Double(p.getX(), p.getY()));
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
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 java.awt.geom.Point2D;
|
||||
|
||||
import org.diylc.components.schematic.SchematicBox;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SchematicBoxGeometryTest {
|
||||
|
||||
@Test
|
||||
public void moveAnchorToTranslatesEverySchematicBoxPinExactlyOnce() {
|
||||
SchematicBox box = new SchematicBox();
|
||||
box.setLeftNodes("A,B");
|
||||
box.setRightNodes("C,D");
|
||||
box.setTopNodes("");
|
||||
box.setBottomNodes("");
|
||||
|
||||
int n = box.getControlPointCount();
|
||||
double ax = box.getControlPoint(0).getX();
|
||||
double ay = box.getControlPoint(0).getY();
|
||||
double[] relX = new double[n];
|
||||
double[] relY = new double[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
relX[i] = box.getControlPoint(i).getX() - ax;
|
||||
relY[i] = box.getControlPoint(i).getY() - ay;
|
||||
}
|
||||
|
||||
SchematicBuilder.moveAnchorTo(box, 500, 300);
|
||||
|
||||
assertEquals(500.0, box.getControlPoint(0).getX(), 0.001);
|
||||
assertEquals(300.0, box.getControlPoint(0).getY(), 0.001);
|
||||
for (int i = 0; i < n; i++) {
|
||||
assertEquals("pin " + i + " x", 500 + relX[i], box.getControlPoint(i).getX(), 0.001);
|
||||
assertEquals("pin " + i + " y", 300 + relY[i], box.getControlPoint(i).getY(), 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void draggingSchematicBoxTranslatesEveryPinByTheSameDelta() {
|
||||
// the presenter drags a box by calling setControlPoint(old + delta, i) for every index, in an
|
||||
// arbitrary (HashSet) order; every pin must end up translated by exactly the delta
|
||||
int[][] orders = {{0, 1, 2, 3, 4, 5, 6}, {6, 5, 4, 3, 2, 1, 0}, {3, 0, 5, 1, 6, 2, 4}};
|
||||
for (int[] order : orders) {
|
||||
SchematicBox box = new SchematicBox();
|
||||
box.setLeftNodes("A,B,C");
|
||||
box.setRightNodes("D,E");
|
||||
box.setTopNodes("F");
|
||||
box.setBottomNodes("G");
|
||||
int n = box.getControlPointCount();
|
||||
assertEquals(7, n);
|
||||
|
||||
Point2D[] before = new Point2D[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
before[i] = new Point2D.Double(box.getControlPoint(i).getX(), box.getControlPoint(i).getY());
|
||||
}
|
||||
|
||||
double dx = 137;
|
||||
double dy = -84;
|
||||
for (int idx : order) {
|
||||
Point2D old = box.getControlPoint(idx);
|
||||
box.setControlPoint(new Point2D.Double(old.getX() + dx, old.getY() + dy), idx);
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
assertEquals("pin " + i + " x", before[i].getX() + dx, box.getControlPoint(i).getX(), 0.001);
|
||||
assertEquals("pin " + i + " y", before[i].getY() + dy, box.getControlPoint(i).getY(), 0.001);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
|
||||
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.Graphics2D;
|
||||
import java.awt.geom.Line2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.appframework.miscutils.InMemoryConfigurationManager;
|
||||
import org.diylc.common.DrawOption;
|
||||
import org.diylc.components.connectivity.OrthogonalLine;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.SchematicView;
|
||||
import org.diylc.netlist.Group;
|
||||
import org.diylc.netlist.Netlist;
|
||||
import org.diylc.presenter.ContinuityArea;
|
||||
import org.diylc.presenter.DrawingManager;
|
||||
import org.diylc.schematic.flow.SignalFlowAnalyzer;
|
||||
import org.diylc.serialization.ProjectFileManager;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Measures what the schematic generator actually produces for real projects, rather than for the
|
||||
* synthetic graphs the other tests build.
|
||||
*
|
||||
* <p>
|
||||
* This is a report, not an assertion. For each sample project it renders the layout offscreen to
|
||||
* collect continuity areas - without them most boards have no netlist at all, because pads joined by
|
||||
* copper are only connected through those areas - then generates a schematic and prints how many
|
||||
* columns came out, how large the busiest net is, how many components any flow rule recognised, and
|
||||
* how many wire segments run across a symbol.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* A layout collapsed into a single column shows up here immediately, which is the point: unit tests
|
||||
* over hand built graphs cannot see it. It skips silently when the regression data is absent, and it
|
||||
* never fails the build.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class SchematicLayoutReportTest {
|
||||
|
||||
/** How many projects to report on; the corpus is large and this is a diagnostic. */
|
||||
private static final int SAMPLE_SIZE = 40;
|
||||
/** Nothing decorative is wanted, only enough of a render to populate the component areas. */
|
||||
private static final Set<DrawOption> DRAW_OPTIONS = EnumSet.noneOf(DrawOption.class);
|
||||
private static final int MAX_CANVAS = 4000;
|
||||
|
||||
@Test
|
||||
public void reportLayoutQualityOverRealProjects() {
|
||||
File directory = findSampleDirectory();
|
||||
if (directory == null) {
|
||||
System.out.println("[layout-report] regression data not present, skipping");
|
||||
return;
|
||||
}
|
||||
File[] files = directory.listFiles((dir, name) -> name.endsWith(".diy"));
|
||||
if (files == null || files.length == 0) {
|
||||
System.out.println("[layout-report] no sample projects found, skipping");
|
||||
return;
|
||||
}
|
||||
Arrays.sort(files);
|
||||
ConfigurationManager.getInstance().initialize("diylc-test");
|
||||
|
||||
System.out.println();
|
||||
System.out.printf("%-24s %6s %8s %7s %8s %9s %9s%n", "project", "symb", "columns", "nets",
|
||||
"maxNet", "flowKnown", "crossings");
|
||||
System.out.println("--------------------------------------------------------------------------------");
|
||||
|
||||
int reported = 0;
|
||||
int singleColumn = 0;
|
||||
int totalCrossings = 0;
|
||||
int loadFailures = 0;
|
||||
int generateFailures = 0;
|
||||
for (File file : files) {
|
||||
if (reported >= SAMPLE_SIZE) {
|
||||
break;
|
||||
}
|
||||
Project project = load(file);
|
||||
if (project == null) {
|
||||
loadFailures++;
|
||||
continue;
|
||||
}
|
||||
Report report = analyze(project);
|
||||
if (report == null) {
|
||||
generateFailures++;
|
||||
continue;
|
||||
}
|
||||
reported++;
|
||||
if (report.columns <= 1) {
|
||||
singleColumn++;
|
||||
}
|
||||
totalCrossings += report.crossings;
|
||||
System.out.printf("%-24s %6d %8d %7d %8d %9d %9d%n", trim(file.getName()), report.symbols,
|
||||
report.columns, report.nets, report.largestNet, report.flowKnown, report.crossings);
|
||||
}
|
||||
|
||||
System.out.println("--------------------------------------------------------------------------------");
|
||||
System.out.printf("reported %d single-column %d crossings %d loadFailed %d genFailed %d%n",
|
||||
reported, singleColumn, totalCrossings, loadFailures, generateFailures);
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
private static Project load(File file) {
|
||||
try {
|
||||
Project project = new ProjectFileManager(null)
|
||||
.deserializeProjectFromFile(file.getAbsolutePath(), new ArrayList<String>());
|
||||
return project == null || project.getComponents().isEmpty() ? null : project;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Report analyze(Project project) {
|
||||
List<ContinuityArea> areas;
|
||||
try {
|
||||
areas = continuityAreasOf(project);
|
||||
} catch (Exception e) {
|
||||
System.out.println("[layout-report] render failed: " + e);
|
||||
areas = new ArrayList<ContinuityArea>();
|
||||
}
|
||||
|
||||
SchematicBuilder builder = new SchematicBuilder();
|
||||
try {
|
||||
builder.build(project, areas);
|
||||
} catch (Exception e) {
|
||||
System.out.println("[layout-report] generation failed: " + e);
|
||||
return null;
|
||||
}
|
||||
|
||||
SchematicView view = project.getSchematicView();
|
||||
if (view == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Report report = new Report();
|
||||
List<Rectangle2D> bodies = new ArrayList<Rectangle2D>();
|
||||
Set<Long> columns = new LinkedHashSet<Long>();
|
||||
for (IDIYComponent<?> component : view.getComponents()) {
|
||||
if (component instanceof OrthogonalLine) {
|
||||
continue;
|
||||
}
|
||||
report.symbols++;
|
||||
Rectangle2D bounds = boundsOf(component);
|
||||
if (bounds != null) {
|
||||
bodies.add(bounds);
|
||||
// symbols are centred within their column, so the centre - not the left edge - is what is
|
||||
// shared by everything in the same column
|
||||
columns.add(Math.round(bounds.getCenterX() / 5));
|
||||
}
|
||||
}
|
||||
report.columns = columns.size();
|
||||
|
||||
for (IDIYComponent<?> component : view.getComponents()) {
|
||||
if (!(component instanceof OrthogonalLine) || component.getControlPointCount() < 3) {
|
||||
continue;
|
||||
}
|
||||
OrthogonalLine wire = (OrthogonalLine) component;
|
||||
List<Point2D> vertices = WireRouter.vertices(wire.getControlPoint(0), wire.getControlPoint(1),
|
||||
wire.getControlPoint(2), wire.getStartDirection());
|
||||
for (int i = 1; i < vertices.size(); i++) {
|
||||
Line2D segment = new Line2D.Double(vertices.get(i - 1), vertices.get(i));
|
||||
for (Rectangle2D body : bodies) {
|
||||
// a wire ending on a pin touches that symbol's box legitimately, so only count a segment
|
||||
// that actually passes through
|
||||
if (body.getWidth() > 1 && body.getHeight() > 1 && body.intersectsLine(segment)
|
||||
&& !body.contains(segment.getP1()) && !body.contains(segment.getP2())) {
|
||||
report.crossings++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Netlist netlist = builder.extractNetlist(project, areas);
|
||||
if (netlist != null) {
|
||||
report.nets = netlist.getGroups().size();
|
||||
for (Group group : netlist.getGroups()) {
|
||||
report.largestNet = Math.max(report.largestNet, group.getNodes().size());
|
||||
}
|
||||
}
|
||||
|
||||
SignalFlowAnalyzer analyzer = new SignalFlowAnalyzer();
|
||||
for (IDIYComponent<?> component : project.getComponents()) {
|
||||
if (analyzer.analyze(component) != null) {
|
||||
report.flowKnown++;
|
||||
}
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the layout offscreen purely so that the drawing manager records each component's area,
|
||||
* which is where continuity areas come from. Nothing looks at the image.
|
||||
*/
|
||||
private static List<ContinuityArea> continuityAreasOf(Project project) {
|
||||
int width = clampCanvas(project.getWidth().convertToPixels());
|
||||
int height = clampCanvas(project.getHeight().convertToPixels());
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
try {
|
||||
DrawingManager manager =
|
||||
new DrawingManager(null, InMemoryConfigurationManager.getInstance());
|
||||
manager.drawProject(g2d, project, DRAW_OPTIONS, null, null,
|
||||
new HashSet<IDIYComponent<?>>(), new HashSet<IDIYComponent<?>>(),
|
||||
new HashSet<IDIYComponent<?>>(), null, null, false, null, null, null, null);
|
||||
return manager.getContinuityAreas();
|
||||
} finally {
|
||||
g2d.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static int clampCanvas(double pixels) {
|
||||
return (int) Math.min(MAX_CANVAS, Math.max(100, pixels));
|
||||
}
|
||||
|
||||
private static Rectangle2D boundsOf(IDIYComponent<?> symbol) {
|
||||
int count = symbol.getControlPointCount();
|
||||
if (count == 0) {
|
||||
return null;
|
||||
}
|
||||
double minX = Double.MAX_VALUE;
|
||||
double minY = Double.MAX_VALUE;
|
||||
double maxX = -Double.MAX_VALUE;
|
||||
double maxY = -Double.MAX_VALUE;
|
||||
for (int i = 0; i < count; i++) {
|
||||
Point2D p = symbol.getControlPoint(i);
|
||||
minX = Math.min(minX, p.getX());
|
||||
minY = Math.min(minY, p.getY());
|
||||
maxX = Math.max(maxX, p.getX());
|
||||
maxY = Math.max(maxY, p.getY());
|
||||
}
|
||||
return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
private static File findSampleDirectory() {
|
||||
String[] candidates = {"../../diylc-regression-data/input/cloud/diy",
|
||||
"../diylc-regression-data/input/cloud/diy", "diylc-regression-data/input/cloud/diy"};
|
||||
for (String candidate : candidates) {
|
||||
File directory = new File(candidate);
|
||||
if (directory.isDirectory()) {
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String trim(String name) {
|
||||
return name.length() <= 24 ? name : name.substring(0, 21) + "...";
|
||||
}
|
||||
|
||||
private static class Report {
|
||||
int symbols;
|
||||
int columns;
|
||||
int nets;
|
||||
int largestNet;
|
||||
int flowKnown;
|
||||
int crossings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
|
||||
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.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.diylc.components.misc.CommonNode;
|
||||
import org.diylc.components.misc.GroundSymbol;
|
||||
import org.diylc.components.passive.ResistorSymbol;
|
||||
import org.diylc.components.semiconductors.BJTSymbol;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.netlist.Group;
|
||||
import org.diylc.netlist.Netlist;
|
||||
import org.diylc.netlist.Node;
|
||||
import org.diylc.schematic.SchematicBuilder.SymbolEntry;
|
||||
import org.diylc.schematic.SchematicBuilder.Wiring;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SchematicLayoutTest {
|
||||
|
||||
@Test
|
||||
public void applyPitchPullsTheLeadSpanInToTheSchematicPitch() {
|
||||
ResistorSymbol symbol = new ResistorSymbol();
|
||||
double originalSpan = symbol.getControlPoint(0).distance(symbol.getControlPoint(1));
|
||||
double cx = (symbol.getControlPoint(0).getX() + symbol.getControlPoint(1).getX()) / 2;
|
||||
double cy = (symbol.getControlPoint(0).getY() + symbol.getControlPoint(1).getY()) / 2;
|
||||
|
||||
SchematicBuilder.applyPitch(symbol, false, false);
|
||||
|
||||
assertTrue("the stock one-inch span is what makes the leads overlap the neighbours",
|
||||
originalSpan > SchematicBuilder.SYMBOL_PITCH);
|
||||
assertEquals(SchematicBuilder.SYMBOL_PITCH,
|
||||
symbol.getControlPoint(0).distance(symbol.getControlPoint(1)), 0.001);
|
||||
assertTrue(symbol.getControlPoint(0).getX() < symbol.getControlPoint(1).getX());
|
||||
// the midpoint has to survive, because the label point is positioned relative to it
|
||||
assertEquals(cx, (symbol.getControlPoint(0).getX() + symbol.getControlPoint(1).getX()) / 2,
|
||||
0.001);
|
||||
assertEquals(cy, symbol.getControlPoint(0).getY(), 0.001);
|
||||
assertEquals(cy, symbol.getControlPoint(1).getY(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyPitchStandsShuntPartsUprightAndCanPutPinZeroAtTheBottom() {
|
||||
ResistorSymbol upright = new ResistorSymbol();
|
||||
SchematicBuilder.applyPitch(upright, true, false);
|
||||
assertEquals(upright.getControlPoint(0).getX(), upright.getControlPoint(1).getX(), 0.001);
|
||||
assertTrue("pin 0 sits on top when not flipped",
|
||||
upright.getControlPoint(0).getY() < upright.getControlPoint(1).getY());
|
||||
|
||||
ResistorSymbol flipped = new ResistorSymbol();
|
||||
SchematicBuilder.applyPitch(flipped, true, true);
|
||||
assertEquals(flipped.getControlPoint(0).getX(), flipped.getControlPoint(1).getX(), 0.001);
|
||||
assertTrue("a grounded pin 0 has to end up at the bottom, where the ground symbol is drawn",
|
||||
flipped.getControlPoint(0).getY() > flipped.getControlPoint(1).getY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyPitchLeavesRigidSymbolsAlone() {
|
||||
BJTSymbol bjt = new BJTSymbol();
|
||||
int count = bjt.getControlPointCount();
|
||||
Point2D[] before = new Point2D[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
before[i] = new Point2D.Double(bjt.getControlPoint(i).getX(), bjt.getControlPoint(i).getY());
|
||||
}
|
||||
|
||||
SchematicBuilder.applyPitch(bjt, true, false);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
assertEquals("leg " + i + " x", before[i].getX(), bjt.getControlPoint(i).getX(), 0.001);
|
||||
assertEquals("leg " + i + " y", before[i].getY(), bjt.getControlPoint(i).getY(), 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void railNetsProduceStubsInsteadOfWires() {
|
||||
ResistorSymbol physical = named(new ResistorSymbol(), "R1");
|
||||
GroundSymbol ground = new GroundSymbol();
|
||||
ground.setId(UUID.randomUUID());
|
||||
|
||||
ResistorSymbol symbol = named(new ResistorSymbol(), "R1");
|
||||
Map<UUID, List<SymbolEntry>> entries = entriesFor(physical, symbol);
|
||||
|
||||
Netlist netlist = new Netlist(new ArrayList<IDIYComponent<?>>());
|
||||
netlist.add(new Group(new Node(ground, 0), new Node(physical, 1)));
|
||||
|
||||
Wiring wiring = new SchematicBuilder().createWires(netlist, entries);
|
||||
|
||||
assertTrue("a rail net must never be routed as a wire", wiring.getWires().isEmpty());
|
||||
assertEquals(1, wiring.getStubs().size());
|
||||
IDIYComponent<?> stub = wiring.getStubs().get(0);
|
||||
assertTrue(stub instanceof GroundSymbol);
|
||||
assertEquals(symbol.getControlPoint(1).getX(), stub.getControlPoint(0).getX(), 0.001);
|
||||
assertEquals(symbol.getControlPoint(1).getY(), stub.getControlPoint(0).getY(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supplyRailsProduceALabelledCommonNodeStub() {
|
||||
ResistorSymbol physical = named(new ResistorSymbol(), "R1");
|
||||
CommonNode supply = new CommonNode();
|
||||
supply.setId(UUID.randomUUID());
|
||||
supply.setValue("B+");
|
||||
|
||||
ResistorSymbol symbol = named(new ResistorSymbol(), "R1");
|
||||
Map<UUID, List<SymbolEntry>> entries = entriesFor(physical, symbol);
|
||||
|
||||
Netlist netlist = new Netlist(new ArrayList<IDIYComponent<?>>());
|
||||
netlist.add(new Group(new Node(supply, 0), new Node(physical, 0)));
|
||||
|
||||
Wiring wiring = new SchematicBuilder().createWires(netlist, entries);
|
||||
|
||||
assertTrue(wiring.getWires().isEmpty());
|
||||
assertEquals(1, wiring.getStubs().size());
|
||||
IDIYComponent<?> stub = wiring.getStubs().get(0);
|
||||
assertTrue(stub instanceof CommonNode);
|
||||
assertEquals("B+", ((CommonNode) stub).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinarySignalNetsAreStillWired() {
|
||||
ResistorSymbol physicalA = named(new ResistorSymbol(), "R1");
|
||||
ResistorSymbol physicalB = named(new ResistorSymbol(), "R2");
|
||||
ResistorSymbol symbolA = named(new ResistorSymbol(), "R1");
|
||||
ResistorSymbol symbolB = named(new ResistorSymbol(), "R2");
|
||||
SchematicBuilder.moveAnchorTo(symbolB, 400, 400);
|
||||
|
||||
Map<UUID, List<SymbolEntry>> entries = entriesFor(physicalA, symbolA);
|
||||
entries.putAll(entriesFor(physicalB, symbolB));
|
||||
|
||||
Group group = new Group(new Node(physicalA, 1), new Node(physicalB, 0));
|
||||
assertEquals("the two pins must stay distinct in the group", 2, group.getNodes().size());
|
||||
|
||||
Netlist netlist = new Netlist(new ArrayList<IDIYComponent<?>>());
|
||||
netlist.add(group);
|
||||
|
||||
Wiring wiring = new SchematicBuilder().createWires(netlist, entries);
|
||||
|
||||
assertTrue(wiring.getStubs().isEmpty());
|
||||
assertEquals(1, wiring.getWires().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commonNodesAreNoLongerPlacedAsSymbolsOfTheirOwn() {
|
||||
assertFalse(SchematicBuilder.isEligible(new GroundSymbol()));
|
||||
assertFalse(SchematicBuilder.isEligible(new CommonNode()));
|
||||
}
|
||||
|
||||
private static ResistorSymbol named(ResistorSymbol symbol, String name) {
|
||||
symbol.setId(UUID.randomUUID());
|
||||
symbol.setName(name);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
/** Maps one physical component to a single schematic symbol with a straight pin-for-pin mapping. */
|
||||
private static Map<UUID, List<SymbolEntry>> entriesFor(IDIYComponent<?> physical,
|
||||
IDIYComponent<?> symbol) {
|
||||
Map<Integer, Integer> pinMapping = new HashMap<Integer, Integer>();
|
||||
pinMapping.put(0, 0);
|
||||
pinMapping.put(1, 1);
|
||||
List<SymbolEntry> list = new ArrayList<SymbolEntry>();
|
||||
list.add(new SymbolEntry(symbol, pinMapping));
|
||||
Map<UUID, List<SymbolEntry>> entries = new LinkedHashMap<UUID, List<SymbolEntry>>();
|
||||
entries.put(physical.getId(), list);
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
|
||||
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.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.awt.geom.Line2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.common.OrientationHV;
|
||||
import org.junit.Test;
|
||||
|
||||
public class WireRouterTest {
|
||||
|
||||
private static final List<Rectangle2D> NO_BODIES = new ArrayList<Rectangle2D>();
|
||||
private static final List<Line2D> NO_WIRES = new ArrayList<Line2D>();
|
||||
|
||||
private final WireRouter router = new WireRouter();
|
||||
|
||||
@Test
|
||||
public void pinsOnTheSameRowGetAStraightRun() {
|
||||
List<Point2D> vertices = routeBetween(new Point2D.Double(100, 100),
|
||||
OrientationHV.HORIZONTAL, new Point2D.Double(400, 100), OrientationHV.HORIZONTAL,
|
||||
NO_BODIES, NO_WIRES);
|
||||
|
||||
assertEquals("a straight run needs no corner", 2, vertices.size());
|
||||
assertEquals(100.0, vertices.get(0).getY(), 0.001);
|
||||
assertEquals(100.0, vertices.get(1).getY(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pinsOnTheSameColumnGetAStraightRun() {
|
||||
List<Point2D> vertices = routeBetween(new Point2D.Double(100, 100), OrientationHV.VERTICAL,
|
||||
new Point2D.Double(100, 400), OrientationHV.VERTICAL, NO_BODIES, NO_WIRES);
|
||||
|
||||
assertEquals(2, vertices.size());
|
||||
assertEquals(100.0, vertices.get(0).getX(), 0.001);
|
||||
assertEquals(100.0, vertices.get(1).getX(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theRouteStartsAndEndsOnTheTwoPins() {
|
||||
Point2D start = new Point2D.Double(120, 140);
|
||||
Point2D end = new Point2D.Double(430, 360);
|
||||
|
||||
List<Point2D> vertices = routeBetween(start, OrientationHV.HORIZONTAL, end,
|
||||
OrientationHV.VERTICAL, NO_BODIES, NO_WIRES);
|
||||
|
||||
assertEquals(start.getX(), vertices.get(0).getX(), 0.001);
|
||||
assertEquals(start.getY(), vertices.get(0).getY(), 0.001);
|
||||
assertEquals(end.getX(), vertices.get(vertices.size() - 1).getX(), 0.001);
|
||||
assertEquals(end.getY(), vertices.get(vertices.size() - 1).getY(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everySegmentRunsAlongAnAxis() {
|
||||
List<Point2D> vertices = routeBetween(new Point2D.Double(120, 140), OrientationHV.HORIZONTAL,
|
||||
new Point2D.Double(430, 360), OrientationHV.VERTICAL, NO_BODIES, NO_WIRES);
|
||||
|
||||
for (Line2D segment : segmentsOf(vertices)) {
|
||||
boolean axisAligned = Math.abs(segment.getX1() - segment.getX2()) < 0.001
|
||||
|| Math.abs(segment.getY1() - segment.getY2()) < 0.001;
|
||||
assertTrue("segment " + segment.getP1() + " -> " + segment.getP2() + " is diagonal",
|
||||
axisAligned);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theRequestedExitAxisIsHonouredWhenNothingIsInTheWay() {
|
||||
List<Point2D> vertices = routeBetween(new Point2D.Double(100, 100), OrientationHV.VERTICAL,
|
||||
new Point2D.Double(400, 300), OrientationHV.HORIZONTAL, NO_BODIES, NO_WIRES);
|
||||
|
||||
List<Line2D> segments = segmentsOf(vertices);
|
||||
assertEquals("the wire should leave the pin on the axis it was asked for",
|
||||
OrientationHV.VERTICAL, axisOf(segments.get(0)));
|
||||
assertEquals("and arrive on the axis it was asked for", OrientationHV.HORIZONTAL,
|
||||
axisOf(segments.get(segments.size() - 1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aSymbolBodyBetweenThePinsIsRoutedAround() {
|
||||
Rectangle2D body = new Rectangle2D.Double(180, 180, 140, 140);
|
||||
|
||||
List<Point2D> vertices = routeBetween(new Point2D.Double(100, 100), OrientationHV.HORIZONTAL,
|
||||
new Point2D.Double(400, 400), OrientationHV.HORIZONTAL, Arrays.asList(body), NO_WIRES);
|
||||
|
||||
for (Line2D segment : segmentsOf(vertices)) {
|
||||
assertFalse("the wire runs straight through a symbol", body.intersectsLine(segment));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aWireAlreadyPlacedChangesTheRouteChosen() {
|
||||
Point2D start = new Point2D.Double(100, 100);
|
||||
Point2D end = new Point2D.Double(400, 400);
|
||||
|
||||
List<Point2D> clear = routeBetween(start, OrientationHV.HORIZONTAL, end,
|
||||
OrientationHV.HORIZONTAL, NO_BODIES, NO_WIRES);
|
||||
// lay a wire along the whole of the route the router picked when the drawing was empty
|
||||
List<Line2D> placed = segmentsOf(clear);
|
||||
List<Point2D> crowded = routeBetween(start, OrientationHV.HORIZONTAL, end,
|
||||
OrientationHV.HORIZONTAL, NO_BODIES, placed);
|
||||
|
||||
assertFalse("the router ignored the wire already on the drawing", samePath(clear, crowded));
|
||||
}
|
||||
|
||||
private List<Point2D> routeBetween(Point2D start, OrientationHV startAxis, Point2D end,
|
||||
OrientationHV endAxis, List<Rectangle2D> bodies, List<Line2D> placed) {
|
||||
WireRouter.Route route = router.route(start, startAxis, end, endAxis, bodies, placed);
|
||||
return WireRouter.vertices(start, route.getBendPoint(), end, route.getStartDirection());
|
||||
}
|
||||
|
||||
private static List<Line2D> segmentsOf(List<Point2D> vertices) {
|
||||
List<Line2D> segments = new ArrayList<Line2D>();
|
||||
for (int i = 1; i < vertices.size(); i++) {
|
||||
segments.add(new Line2D.Double(vertices.get(i - 1), vertices.get(i)));
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
private static OrientationHV axisOf(Line2D segment) {
|
||||
return Math.abs(segment.getX2() - segment.getX1()) >= Math.abs(segment.getY2() - segment.getY1())
|
||||
? OrientationHV.HORIZONTAL
|
||||
: OrientationHV.VERTICAL;
|
||||
}
|
||||
|
||||
private static boolean samePath(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.001) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
|
||||
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.flow;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.components.guitar.SingleCoilPickup;
|
||||
import org.diylc.components.modules.LM2596BuckConverter;
|
||||
import org.diylc.components.passive.Resistor;
|
||||
import org.diylc.components.semiconductors.BJTSymbol;
|
||||
import org.diylc.components.semiconductors.TransistorPinout;
|
||||
import org.diylc.components.semiconductors.TransistorTO92;
|
||||
import org.diylc.components.tube.Jumbo4PinTubeSocket;
|
||||
import org.diylc.components.tube.TriodeSymbol;
|
||||
import org.diylc.components.tube.TubeSocket;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SignalFlowAnalyzerTest {
|
||||
|
||||
private static final TransistorPinoutFlowRule PINOUT_RULE = new TransistorPinoutFlowRule();
|
||||
|
||||
@Test
|
||||
public void bjtPinoutMapsBaseToInputAndCollectorToOutput() {
|
||||
FlowHints hints = evaluatePinout(TransistorPinout.BJT_EBC);
|
||||
|
||||
assertNotNull(hints);
|
||||
assertEquals(FlowRole.NEUTRAL, hints.getRole(0)); // emitter
|
||||
assertEquals(FlowRole.INPUT, hints.getRole(1)); // base
|
||||
assertEquals(FlowRole.OUTPUT, hints.getRole(2)); // collector
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pinoutIsReadInTheDeclaredLegOrder() {
|
||||
FlowHints hints = evaluatePinout(TransistorPinout.BJT_CBE);
|
||||
|
||||
assertNotNull(hints);
|
||||
assertEquals(FlowRole.OUTPUT, hints.getRole(0)); // collector
|
||||
assertEquals(FlowRole.INPUT, hints.getRole(1)); // base
|
||||
assertEquals(FlowRole.NEUTRAL, hints.getRole(2)); // emitter
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fetGateIsAnInputAndDrainIsAnOutput() {
|
||||
FlowHints hints = evaluatePinout(TransistorPinout.JFET_GSD);
|
||||
|
||||
assertNotNull(hints);
|
||||
assertEquals(FlowRole.INPUT, hints.getRole(0)); // gate
|
||||
assertEquals(FlowRole.NEUTRAL, hints.getRole(1)); // source
|
||||
assertEquals(FlowRole.OUTPUT, hints.getRole(2)); // drain
|
||||
}
|
||||
|
||||
/**
|
||||
* G is the gate of a FET but the ground leg of a regulator. Reading it the same way in both cases
|
||||
* would turn every regulator around, so this is the case most worth pinning down.
|
||||
*/
|
||||
@Test
|
||||
public void regulatorGroundIsNotTreatedAsAGate() {
|
||||
FlowHints hints = evaluatePinout(TransistorPinout.REGULATOR_IGO);
|
||||
|
||||
assertNotNull(hints);
|
||||
assertEquals(FlowRole.INPUT, hints.getRole(0));
|
||||
assertEquals(FlowRole.NEUTRAL, hints.getRole(1)); // ground, not an input
|
||||
assertEquals(FlowRole.OUTPUT, hints.getRole(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aTransistorWithNoPinoutSaysNothing() {
|
||||
TransistorTO92 transistor = new TransistorTO92();
|
||||
transistor.setPinout(null);
|
||||
|
||||
assertNull(PINOUT_RULE.evaluate(transistor, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tubeSymbolGridIsInputAndPlateIsOutput() {
|
||||
FlowHints hints = new TubeSymbolFlowRule().evaluate(new TriodeSymbol(), null);
|
||||
|
||||
assertNotNull(hints);
|
||||
assertEquals(FlowRole.INPUT, hints.getRole(0)); // control grid
|
||||
assertEquals(FlowRole.OUTPUT, hints.getRole(1)); // plate
|
||||
assertEquals(FlowRole.NEUTRAL, hints.getRole(2)); // cathode
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aTubeSocketWithNumberedPinsSaysNothing() {
|
||||
// the stock electrode labels are just pin numbers, which carry no electrode meaning
|
||||
assertNull(new TubeElectrodeFlowRule().evaluate(new TubeSocket(), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aTubeSocketWithNamedElectrodesGivesDirection() {
|
||||
Jumbo4PinTubeSocket socket = new Jumbo4PinTubeSocket();
|
||||
|
||||
FlowHints hints = new TubeElectrodeFlowRule().evaluate(socket, null);
|
||||
|
||||
assertNotNull(hints);
|
||||
assertTrue(hints.hasDirection());
|
||||
assertRoleOfNamedPin(socket, hints, "G", FlowRole.INPUT);
|
||||
assertRoleOfNamedPin(socket, hints, "P", FlowRole.OUTPUT);
|
||||
assertRoleOfNamedPin(socket, hints, "F", FlowRole.NEUTRAL); // filament
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pickupTerminalsAreOutputsBecauseTheSignalStartsThere() {
|
||||
SingleCoilPickup pickup = new SingleCoilPickup();
|
||||
|
||||
FlowHints hints = new PickupFlowRule().evaluate(pickup, null);
|
||||
|
||||
assertNotNull(hints);
|
||||
assertTrue(hints.hasDirection());
|
||||
for (int i = 0; i < pickup.getControlPointCount(); i++) {
|
||||
FlowRole expected = pickup.isControlPointSticky(i) ? FlowRole.OUTPUT : FlowRole.NEUTRAL;
|
||||
assertEquals("point " + i, expected, hints.getRole(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void namedPinsGiveDirectionOnModulesThatLabelThem() {
|
||||
LM2596BuckConverter module = new LM2596BuckConverter();
|
||||
|
||||
FlowHints hints = new NamedPinFlowRule().evaluate(module, null);
|
||||
|
||||
assertNotNull(hints);
|
||||
assertRoleOfNamedPin(module, hints, "IN+", FlowRole.INPUT);
|
||||
assertRoleOfNamedPin(module, hints, "IN-", FlowRole.INPUT);
|
||||
assertRoleOfNamedPin(module, hints, "OUT+", FlowRole.OUTPUT);
|
||||
assertRoleOfNamedPin(module, hints, "OUT-", FlowRole.OUTPUT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void drawnTransistorSymbolsFallBackToTheirGeometry() {
|
||||
FlowHints hints = new BjtSymbolFlowRule().evaluate(new BJTSymbol(), null);
|
||||
|
||||
assertNotNull(hints);
|
||||
assertEquals(FlowRole.INPUT, hints.getRole(0)); // base, the lone pin on the left
|
||||
assertEquals(FlowRole.OUTPUT, hints.getRole(1)); // collector
|
||||
assertEquals(FlowRole.NEUTRAL, hints.getRole(2)); // emitter
|
||||
}
|
||||
|
||||
@Test
|
||||
public void passivePartsGetNoDirectionAtAll() {
|
||||
assertNull(new SignalFlowAnalyzer().analyze(new Resistor()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rulesThatReadRealDataComeBeforeRulesThatGuess() {
|
||||
List<ISignalFlowRule> rules = SignalFlowAnalyzer.getRules();
|
||||
|
||||
assertTrue("the pin-out rule must outrank the pin-name rule",
|
||||
indexOf(rules, TransistorPinoutFlowRule.class) < indexOf(rules, NamedPinFlowRule.class));
|
||||
assertTrue("named electrodes must outrank bare geometry",
|
||||
indexOf(rules, TubeElectrodeFlowRule.class) < indexOf(rules, TubeSymbolFlowRule.class));
|
||||
}
|
||||
|
||||
private static FlowHints evaluatePinout(TransistorPinout pinout) {
|
||||
TransistorTO92 transistor = new TransistorTO92();
|
||||
transistor.setPinout(pinout);
|
||||
return PINOUT_RULE.evaluate(transistor, null);
|
||||
}
|
||||
|
||||
/** Finds the pins the component reports under {@code pinName} and checks the role each was given. */
|
||||
private static void assertRoleOfNamedPin(IDIYComponent<?> component, FlowHints hints,
|
||||
String pinName, FlowRole expected) {
|
||||
boolean found = false;
|
||||
for (int i = 0; i < component.getControlPointCount(); i++) {
|
||||
if (pinName.equals(component.getControlPointNodeName(i))) {
|
||||
assertEquals("pin " + pinName + " at index " + i, expected, hints.getRole(i));
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
assertTrue("expected a pin named " + pinName, found);
|
||||
}
|
||||
|
||||
private static int indexOf(List<ISignalFlowRule> rules, Class<? extends ISignalFlowRule> type) {
|
||||
for (int i = 0; i < rules.size(); i++) {
|
||||
if (type.isInstance(rules.get(i))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -133,6 +133,7 @@ passive.ToroidalInductor = fixed
|
||||
passive.TransformerCoil = fixed
|
||||
passive.TransformerCore = none
|
||||
passive.TrimmerPotentiometer = fixed
|
||||
schematic.SchematicBox = fixed
|
||||
semiconductors.BJTSymbol = fixed
|
||||
semiconductors.BridgeRectifier = fixed
|
||||
semiconductors.DIL_IC = Display Pin #s, Pins
|
||||
|
||||
@@ -130,6 +130,7 @@ public class MainFrame extends JFrame implements ISwingUI {
|
||||
presenter.installPlugin(() -> new ToolBox(this));
|
||||
presenter.installPlugin(() -> new FileMenuPlugin(this));
|
||||
presenter.installPlugin(() -> new AnalyzeMenuPlugin(this));
|
||||
presenter.installPlugin(() -> new org.diylc.swing.plugins.schematic.SchematicMenuPlugin(this));
|
||||
presenter.installPlugin(() -> new EditMenuPlugin(this));
|
||||
presenter.installPlugin(() -> new ConfigPlugin(this));
|
||||
presenter.installPlugin(() -> new LayersMenuPlugin(this));
|
||||
@@ -146,7 +147,9 @@ public class MainFrame extends JFrame implements ISwingUI {
|
||||
presenter.installPlugin(() -> canvasPlugin);
|
||||
presenter.installPlugin(() -> new ComponentTree(this, canvasPlugin.getCanvasPanel()));
|
||||
presenter.installPlugin(() -> new ExplorerPlugin(this, canvasPlugin.getCanvasPanel()));
|
||||
|
||||
presenter.installPlugin(() -> new org.diylc.swing.plugins.schematic.SchematicTabPlugin(this,
|
||||
configManager, canvasPlugin));
|
||||
|
||||
presenter.installPlugin(() -> new FramePlugin());
|
||||
|
||||
presenter.installPlugin(() -> new AutoSavePlugin(this));
|
||||
|
||||
@@ -351,6 +351,14 @@ public class CanvasPlugin implements IPlugIn{
|
||||
return canvasPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the scroll pane that hosts the layout canvas, so other plugins (e.g. the schematic tab)
|
||||
* can show or hide it when switching views.
|
||||
*/
|
||||
public javax.swing.JComponent getCanvasScrollComponent() {
|
||||
return getScrollPane();
|
||||
}
|
||||
|
||||
private RulerScrollPane getScrollPane() {
|
||||
if (scrollPane == null) {
|
||||
String subdivision = configManager.readString(
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
|
||||
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 java.awt.event.ActionEvent;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.diylc.common.EventType;
|
||||
import org.diylc.common.IPlugIn;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.swing.ISwingUI;
|
||||
|
||||
/**
|
||||
* Adds the "Schematic View..." entry to the Analyze menu. It generates (or incrementally
|
||||
* synchronizes) the schematic representation of the current layout and shows it in a
|
||||
* {@link SchematicViewFrame}.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class SchematicMenuPlugin implements IPlugIn {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SchematicMenuPlugin.class);
|
||||
|
||||
private static final String ANALYZE_TITLE = "Analyze";
|
||||
|
||||
private final ISwingUI swingUI;
|
||||
private IPlugInPort plugInPort;
|
||||
|
||||
public SchematicMenuPlugin(ISwingUI swingUI) {
|
||||
this.swingUI = swingUI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(IPlugInPort plugInPort) {
|
||||
this.plugInPort = plugInPort;
|
||||
swingUI.injectMenuAction(new AbstractAction("Schematic View...") {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
openSchematicView();
|
||||
}
|
||||
}, ANALYZE_TITLE);
|
||||
}
|
||||
|
||||
private void openSchematicView() {
|
||||
try {
|
||||
SchematicViewFrame frame = SchematicViewFrame.getInstance();
|
||||
frame.showFor(plugInPort.getCurrentProject(), plugInPort.getContinuityAreas());
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Could not open schematic view", ex);
|
||||
SwingUtilities.invokeLater(() -> swingUI.showMessage(
|
||||
"Could not generate the schematic view: " + ex.getMessage(), "Schematic View",
|
||||
ISwingUI.ERROR_MESSAGE));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EnumSet<EventType> getSubscribedEventTypes() {
|
||||
return EnumSet.noneOf(EventType.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processMessage(EventType eventType, Object... params) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
|
||||
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 java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.appframework.simplemq.MessageDispatcher;
|
||||
import org.diylc.common.DrawOption;
|
||||
import org.diylc.common.EventType;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.SchematicView;
|
||||
import org.diylc.presenter.ContinuityArea;
|
||||
import org.diylc.presenter.DrawingManager;
|
||||
import org.diylc.schematic.SchematicSynchronizer;
|
||||
|
||||
/**
|
||||
* Read-only rendering surface for the generated {@link SchematicView}. It reuses the regular
|
||||
* {@link DrawingManager} pipeline so schematic symbols are drawn by exactly the same code that draws
|
||||
* layout components.
|
||||
*
|
||||
* <p>
|
||||
* This is the minimal viewer shipped with the first iteration of the feature: it can regenerate the
|
||||
* schematic, pan (via the enclosing scroll pane), zoom and export. Interactive editing (moving,
|
||||
* rotating and mirroring symbols) with a restricted {@code Presenter} is a planned follow-up.
|
||||
* </p>
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class SchematicPanel extends JComponent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SchematicPanel.class);
|
||||
|
||||
private final DrawingManager drawingManager;
|
||||
private Project schematicProject;
|
||||
private double zoom = 1d;
|
||||
|
||||
public SchematicPanel() {
|
||||
this.drawingManager =
|
||||
new DrawingManager(new MessageDispatcher<EventType>(false), ConfigurationManager.getInstance());
|
||||
setDoubleBuffered(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerates (or incrementally syncs) the schematic for the given layout project and shows it.
|
||||
*/
|
||||
public void refresh(Project layoutProject, List<ContinuityArea> continuityAreas) {
|
||||
try {
|
||||
new SchematicSynchronizer().synchronize(layoutProject,
|
||||
continuityAreas == null ? Collections.<ContinuityArea>emptyList() : continuityAreas);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Schematic synchronization failed", e);
|
||||
}
|
||||
this.schematicProject = wrap(layoutProject.getOrCreateSchematicView(), layoutProject);
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
public boolean hasContent() {
|
||||
return schematicProject != null && !schematicProject.getComponents().isEmpty();
|
||||
}
|
||||
|
||||
public double getZoom() {
|
||||
return zoom;
|
||||
}
|
||||
|
||||
public void setZoom(double zoom) {
|
||||
this.zoom = Math.max(0.1d, Math.min(5d, zoom));
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
private static Project wrap(SchematicView view, Project layoutProject) {
|
||||
Project p = new Project();
|
||||
p.setTitle(layoutProject.getTitle() + " — Schematic");
|
||||
p.setAuthor(layoutProject.getAuthor());
|
||||
p.setWidth(view.getWidth());
|
||||
p.setHeight(view.getHeight());
|
||||
p.setGridSpacing(view.getGridSpacing());
|
||||
p.setFont(layoutProject.getFont());
|
||||
p.getComponents().addAll(view.getComponents());
|
||||
return p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
if (schematicProject == null) {
|
||||
return new Dimension(800, 600);
|
||||
}
|
||||
return drawingManager.getCanvasDimensions(schematicProject, zoom, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
if (schematicProject == null) {
|
||||
return;
|
||||
}
|
||||
drawingManager.setZoomLevel(zoom);
|
||||
drawSchematic((Graphics2D) g);
|
||||
}
|
||||
|
||||
private void drawSchematic(Graphics2D g2d) {
|
||||
Set<DrawOption> options = EnumSet.of(DrawOption.ANTIALIASING, DrawOption.ZOOM, DrawOption.GRID,
|
||||
DrawOption.EXTRA_SPACE);
|
||||
Set<IDIYComponent<?>> empty = Collections.emptySet();
|
||||
drawingManager.drawProject(g2d, schematicProject, options, null, null, empty, empty, empty,
|
||||
Arrays.<Point2D>asList(null, null), null, false, 1d, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an image of the whole schematic at the current zoom, for export.
|
||||
*/
|
||||
public BufferedImage renderToImage() {
|
||||
Dimension size = getPreferredSize();
|
||||
BufferedImage image =
|
||||
new BufferedImage(Math.max(1, size.width), Math.max(1, size.height), BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
g2d.setColor(java.awt.Color.white);
|
||||
g2d.fillRect(0, 0, size.width, size.height);
|
||||
drawingManager.setZoomLevel(zoom);
|
||||
drawSchematic(g2d);
|
||||
g2d.dispose();
|
||||
return image;
|
||||
}
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
/*
|
||||
|
||||
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 java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Font;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.ButtonGroup;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JToggleButton;
|
||||
import javax.swing.JToolBar;
|
||||
import javax.swing.SwingConstants;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.UIManager;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.diylc.appframework.miscutils.IConfigurationManager;
|
||||
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.IView;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.SchematicView;
|
||||
import org.diylc.lang.LangUtil;
|
||||
import org.diylc.presenter.ContinuityArea;
|
||||
import org.diylc.presenter.Presenter;
|
||||
import org.diylc.schematic.SchematicSynchronizer;
|
||||
import org.diylc.swing.ISwingUI;
|
||||
import org.diylc.swing.plugins.canvas.CanvasPlugin;
|
||||
import org.diylc.utils.IconLoader;
|
||||
|
||||
/**
|
||||
* Adds an Excel-style tab strip below the canvas with two tabs, <b>Layout</b> and <b>Schematic</b>.
|
||||
*
|
||||
* <p>
|
||||
* The schematic tab reuses the regular canvas UI: its own {@link Presenter} + {@link CanvasPlugin}
|
||||
* (the same {@code RulerScrollPane}, rulers, scroll bars and zoom-scroll) render a wrapper
|
||||
* {@link Project} backed by the layout's {@link SchematicView}. Switching to the Schematic tab
|
||||
* regenerates the schematic automatically ({@link SchematicSynchronizer}) and shows the schematic
|
||||
* canvas; switching to Layout shows the layout canvas again.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* 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
|
||||
*/
|
||||
public class SchematicTabPlugin implements IPlugIn {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SchematicTabPlugin.class);
|
||||
|
||||
private final ISwingUI swingUI;
|
||||
private final IConfigurationManager<?> configManager;
|
||||
private final CanvasPlugin layoutCanvasPlugin;
|
||||
|
||||
private IPlugInPort plugInPort;
|
||||
private Presenter schematicPresenter;
|
||||
private CanvasPlugin schematicCanvasPlugin;
|
||||
private JComponent layoutScroll;
|
||||
private JComponent schematicScroll;
|
||||
private JToggleButton schematicTab;
|
||||
private JButton regenerateButton;
|
||||
private Project schematicProject;
|
||||
private boolean rewiring;
|
||||
|
||||
public SchematicTabPlugin(ISwingUI swingUI, IConfigurationManager<?> configManager,
|
||||
CanvasPlugin layoutCanvasPlugin) {
|
||||
this.swingUI = swingUI;
|
||||
this.configManager = configManager;
|
||||
this.layoutCanvasPlugin = layoutCanvasPlugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(IPlugInPort plugInPort) {
|
||||
this.plugInPort = plugInPort;
|
||||
this.layoutScroll = layoutCanvasPlugin.getCanvasScrollComponent();
|
||||
|
||||
// A second presenter + canvas plugin gives the schematic the exact same rendering, scrolling
|
||||
// and zoom behaviour as the layout canvas.
|
||||
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);
|
||||
|
||||
try {
|
||||
swingUI.injectGUIComponent(buildTabBar(), SwingConstants.CENTER, false, null);
|
||||
} catch (BadPositionException e) {
|
||||
LOG.error("Could not install schematic tab", e);
|
||||
}
|
||||
}
|
||||
|
||||
private JComponent buildTabBar() {
|
||||
JToolBar bar = new JToolBar();
|
||||
bar.setFloatable(false);
|
||||
bar.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, UIManager.getColor("controlShadow")));
|
||||
bar.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
ButtonGroup group = new ButtonGroup();
|
||||
JToggleButton layoutTab = makeTab("Layout", true, e -> showLayout());
|
||||
schematicTab = makeTab("Schematic", false, e -> showSchematic());
|
||||
group.add(layoutTab);
|
||||
group.add(schematicTab);
|
||||
bar.add(layoutTab);
|
||||
bar.add(schematicTab);
|
||||
|
||||
bar.addSeparator();
|
||||
bar.add(buildRegenerateButton());
|
||||
|
||||
Dimension pref = bar.getPreferredSize();
|
||||
bar.setMaximumSize(new Dimension(Integer.MAX_VALUE, pref.height));
|
||||
bar.setMinimumSize(new Dimension(0, pref.height));
|
||||
return bar;
|
||||
}
|
||||
|
||||
private static JToggleButton makeTab(String label, boolean selected, ActionListener listener) {
|
||||
JToggleButton button = new JToggleButton(label, selected);
|
||||
button.setFocusPainted(false);
|
||||
button.setFont(button.getFont().deriveFont(Font.PLAIN));
|
||||
button.addActionListener(listener);
|
||||
return button;
|
||||
}
|
||||
|
||||
private JButton buildRegenerateButton() {
|
||||
regenerateButton =
|
||||
new JButton(LangUtil.translate("Regenerate"), IconLoader.MagicWand.getIcon());
|
||||
regenerateButton.setToolTipText(LangUtil.translate("Lay the schematic out again from scratch"));
|
||||
regenerateButton.setFocusPainted(false);
|
||||
regenerateButton.setFont(regenerateButton.getFont().deriveFont(Font.PLAIN));
|
||||
// the layout tab is the one showing when the bar is built
|
||||
regenerateButton.setEnabled(false);
|
||||
regenerateButton.addActionListener(e -> regenerate());
|
||||
return regenerateButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws the current schematic away and lays it out again from the layout. How the symbols are
|
||||
* arranged is the user's own work and the synchronizer never touches it, so this is the only place
|
||||
* that discards it — which is why it asks first.
|
||||
*/
|
||||
private void regenerate() {
|
||||
int choice = swingUI.showConfirmDialog(
|
||||
LangUtil.translate("Discard the current arrangement and lay the schematic out again?"),
|
||||
LangUtil.translate("Regenerate Schematic"), IView.YES_NO_OPTION, IView.WARNING_MESSAGE);
|
||||
if (choice != IView.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
SchematicView view = plugInPort.getCurrentProject().getSchematicView();
|
||||
if (view != null) {
|
||||
// an empty view is exactly what makes the synchronizer fall back to a full build
|
||||
view.getComponents().clear();
|
||||
view.getPhysicalToSchematicMap().clear();
|
||||
}
|
||||
showSchematic();
|
||||
}
|
||||
|
||||
private void showLayout() {
|
||||
schematicScroll.setVisible(false);
|
||||
layoutScroll.setVisible(true);
|
||||
regenerateButton.setEnabled(false);
|
||||
relayout();
|
||||
}
|
||||
|
||||
private void showSchematic() {
|
||||
Project wrapper;
|
||||
try {
|
||||
wrapper = buildSchematicProject();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Could not build schematic view", e);
|
||||
swingUI.showMessage("Could not build the schematic view: " + e.getMessage(), "Schematic View",
|
||||
ISwingUI.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
this.schematicProject = wrapper;
|
||||
schematicPresenter.loadProject(wrapper, true, null);
|
||||
layoutScroll.setVisible(false);
|
||||
schematicScroll.setVisible(true);
|
||||
regenerateButton.setEnabled(true);
|
||||
relayout();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
schematicCanvasPlugin.scrollToCenterAndShowContents();
|
||||
relayout();
|
||||
});
|
||||
}
|
||||
|
||||
/** Regenerates the schematic from the current layout and wraps it in a renderable project. */
|
||||
private Project buildSchematicProject() {
|
||||
Project layoutProject = plugInPort.getCurrentProject();
|
||||
java.util.List<ContinuityArea> areas = plugInPort.getContinuityAreas();
|
||||
new SchematicSynchronizer().synchronize(layoutProject,
|
||||
areas == null ? new java.util.ArrayList<>() : areas);
|
||||
|
||||
SchematicView view = layoutProject.getOrCreateSchematicView();
|
||||
Project wrapper = new Project();
|
||||
wrapper.setTitle(layoutProject.getTitle() + " — Schematic");
|
||||
wrapper.setAuthor(layoutProject.getAuthor());
|
||||
wrapper.setWidth(view.getWidth());
|
||||
wrapper.setHeight(view.getHeight());
|
||||
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;
|
||||
}
|
||||
|
||||
private void relayout() {
|
||||
JComponent parent = layoutScroll.getParent() instanceof JComponent
|
||||
? (JComponent) layoutScroll.getParent()
|
||||
: null;
|
||||
if (parent != null) {
|
||||
parent.revalidate();
|
||||
parent.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
|
||||
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 java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JToolBar;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.presenter.ContinuityArea;
|
||||
|
||||
/**
|
||||
* Stand-alone window that hosts a {@link SchematicPanel}. Opened from the Analyze menu; there is at
|
||||
* most one instance, reused on subsequent invocations.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class SchematicViewFrame extends JFrame {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SchematicViewFrame.class);
|
||||
|
||||
private static SchematicViewFrame instance;
|
||||
|
||||
private final SchematicPanel panel;
|
||||
private Project layoutProject;
|
||||
|
||||
private SchematicViewFrame() {
|
||||
super("Schematic View");
|
||||
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
|
||||
this.panel = new SchematicPanel();
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(panel);
|
||||
scrollPane.getVerticalScrollBar().setUnitIncrement(16);
|
||||
scrollPane.getHorizontalScrollBar().setUnitIncrement(16);
|
||||
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
getContentPane().add(buildToolBar(), BorderLayout.NORTH);
|
||||
getContentPane().add(scrollPane, BorderLayout.CENTER);
|
||||
setPreferredSize(new Dimension(1000, 720));
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
}
|
||||
|
||||
public static SchematicViewFrame getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new SchematicViewFrame();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private JToolBar buildToolBar() {
|
||||
JToolBar toolBar = new JToolBar();
|
||||
toolBar.setFloatable(false);
|
||||
toolBar.add(action("Refresh", e -> doRefresh()));
|
||||
toolBar.addSeparator();
|
||||
toolBar.add(action("Zoom -", e -> panel.setZoom(panel.getZoom() / 1.25d)));
|
||||
toolBar.add(action("Zoom +", e -> panel.setZoom(panel.getZoom() * 1.25d)));
|
||||
toolBar.add(action("Reset Zoom", e -> panel.setZoom(1d)));
|
||||
toolBar.addSeparator();
|
||||
toolBar.add(action("Export PNG...", e -> doExport()));
|
||||
return toolBar;
|
||||
}
|
||||
|
||||
private static JButton action(String label, java.util.function.Consumer<ActionEvent> handler) {
|
||||
return new JButton(new AbstractAction(label) {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
handler.accept(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Points the window at a layout project and (re)generates the schematic.
|
||||
*/
|
||||
public void showFor(Project layoutProject, List<ContinuityArea> continuityAreas) {
|
||||
this.layoutProject = layoutProject;
|
||||
this.continuityAreas = continuityAreas;
|
||||
panel.refresh(layoutProject, continuityAreas);
|
||||
setVisible(true);
|
||||
toFront();
|
||||
}
|
||||
|
||||
private List<ContinuityArea> continuityAreas;
|
||||
|
||||
private void doRefresh() {
|
||||
if (layoutProject != null) {
|
||||
panel.refresh(layoutProject, continuityAreas);
|
||||
}
|
||||
}
|
||||
|
||||
private void doExport() {
|
||||
if (!panel.hasContent()) {
|
||||
JOptionPane.showMessageDialog(this, "Nothing to export.", "Schematic View",
|
||||
JOptionPane.INFORMATION_MESSAGE);
|
||||
return;
|
||||
}
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setSelectedFile(new File("schematic.png"));
|
||||
if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) {
|
||||
return;
|
||||
}
|
||||
File file = chooser.getSelectedFile();
|
||||
if (!file.getName().toLowerCase().endsWith(".png")) {
|
||||
file = new File(file.getParentFile(), file.getName() + ".png");
|
||||
}
|
||||
try {
|
||||
ImageIO.write(panel.renderToImage(), "png", file);
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Could not export schematic image", ex);
|
||||
JOptionPane.showMessageDialog(this, "Could not export image: " + ex.getMessage(),
|
||||
"Schematic View", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package org.diylc;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
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;
|
||||
import org.diylc.schematic.SchematicSynchronizer;
|
||||
import org.diylc.swing.plugins.schematic.SchematicPanel;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* End-to-end check of the schematic pipeline: instantiate real components through the presenter,
|
||||
* build/sync the schematic and render it through the viewer panel.
|
||||
*/
|
||||
public class SchematicViewIntegrationTest extends TestBase {
|
||||
|
||||
@Test
|
||||
public void buildsSymbolsAndWiresFromLayout() {
|
||||
// two resistors sharing a node at (1200, 1000)
|
||||
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();
|
||||
assertNotNull(view);
|
||||
assertTrue(view.isGenerated());
|
||||
assertEquals(2, view.getPhysicalToSchematicMap().size());
|
||||
|
||||
long symbolCount =
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void synchronizerAddsAndRemovesSymbols() {
|
||||
instantiateTwoClick("Passive", "Resistor", new Point(1000, 1000), new Point(1200, 1000));
|
||||
Project project = presenter.getCurrentProject();
|
||||
|
||||
new SchematicSynchronizer().synchronize(project, presenter.getContinuityAreas());
|
||||
assertEquals(1, project.getSchematicView().getPhysicalToSchematicMap().size());
|
||||
|
||||
instantiateTwoClick("Passive", "Resistor", new Point(1000, 1400), new Point(1200, 1400));
|
||||
new SchematicSynchronizer().synchronize(project, presenter.getContinuityAreas());
|
||||
assertEquals(2, project.getSchematicView().getPhysicalToSchematicMap().size());
|
||||
|
||||
project.getComponents().remove(project.getComponents().size() - 1);
|
||||
new SchematicSynchronizer().synchronize(project, presenter.getContinuityAreas());
|
||||
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));
|
||||
Project project = presenter.getCurrentProject();
|
||||
|
||||
SchematicPanel panel = new SchematicPanel();
|
||||
panel.refresh(project, presenter.getContinuityAreas());
|
||||
|
||||
assertTrue(panel.hasContent());
|
||||
BufferedImage image = panel.renderToImage();
|
||||
assertNotNull(image);
|
||||
assertTrue(image.getWidth() > 0 && image.getHeight() > 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
# Schematic View
|
||||
|
||||
How the Schematic View is built and **why** it looks the way it does. The original feature
|
||||
spec and its running status are in [`../schematic_view_plan.md`](../plans/schematic_view_plan.md).
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
DIYLC projects are *physical layouts* — components on a board with leads, traces and wires.
|
||||
The Schematic View derives a **schematic diagram** from that layout on demand: every real
|
||||
component becomes a schematic symbol, every net becomes an auto-routed wire, and the result is
|
||||
drawn with the same rendering pipeline as the layout.
|
||||
|
||||
It is **derived, not authored**: the user does not draw the schematic. They can rearrange
|
||||
symbols; wires follow. Switching to the Schematic tab (re)generates it.
|
||||
|
||||
## Design principles
|
||||
|
||||
1. **Reuse the rendering pipeline.** Schematic symbols are ordinary `IDIYComponent`s drawn by
|
||||
the same `DrawingManager` that draws layout components. Wires are components too.
|
||||
2. **Per-type mapping, with a fallback.** Each physical component type can name an
|
||||
`ISchematicFactory`; anything without one gets a generic labeled box. Every component gets
|
||||
*some* symbol.
|
||||
3. **The layout is the source of truth.** The schematic is regenerated / reconciled from the
|
||||
layout + its netlist. User edits to the schematic are limited to symbol position.
|
||||
4. **Zero cost when unused.** The `SchematicView` field on `Project` stays `null` until the
|
||||
user first opens the view, so existing `.diy` files are byte-for-byte unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Module & file map
|
||||
|
||||
`diylc-library` depends on `diylc-core`; `diylc-swing` depends on both.
|
||||
|
||||
```
|
||||
diylc-core data model + SPI (no rendering, no Swing)
|
||||
org.diylc.core.SchematicView embedded in Project
|
||||
org.diylc.core.annotations.ComponentDescriptor.schematicFactory()
|
||||
org.diylc.common.ComponentType.schematicFactoryClass
|
||||
org.diylc.schematic.ISchematicFactory the strategy interface
|
||||
org.diylc.schematic.SchematicSymbolMapping factory output: symbol + pin map
|
||||
org.diylc.common.INetlistProcessor.getContinuityAreas() (impl on Presenter)
|
||||
|
||||
diylc-library components + generation logic
|
||||
org.diylc.components.schematic.SchematicBox generic rectangular symbol
|
||||
org.diylc.components.schematic.SchematicWire auto-routed Manhattan wire (IContinuity)
|
||||
org.diylc.schematic.ManhattanRouter routing algorithm (pure)
|
||||
org.diylc.schematic.GenericBoxSchematicFactory default fallback
|
||||
org.diylc.schematic.AbstractSimpleSchematicFactory 1:1 pin-for-pin base
|
||||
org.diylc.schematic.{Resistor,Capacitor,Diode,Transistor,Inductor,Potentiometer}SchematicFactory
|
||||
org.diylc.schematic.SchematicBuilder initial generation
|
||||
org.diylc.schematic.SchematicSynchronizer incremental reconcile
|
||||
|
||||
diylc-swing UI
|
||||
org.diylc.swing.plugins.schematic.SchematicTabPlugin Layout | Schematic tab strip
|
||||
org.diylc.swing.plugins.schematic.SchematicMenuPlugin Analyze > Schematic View...
|
||||
org.diylc.swing.plugins.schematic.SchematicViewFrame detached viewer window
|
||||
org.diylc.swing.plugins.schematic.SchematicPanel read-only renderer for the window
|
||||
org.diylc.swing.plugins.canvas.CanvasPlugin.getCanvasScrollComponent() (new accessor)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data model
|
||||
|
||||
### `SchematicView` (embedded in `Project`)
|
||||
|
||||
```java
|
||||
class SchematicView implements Serializable, Cloneable {
|
||||
List<IDIYComponent<?>> components; // symbols + wires, z-order ascending
|
||||
Map<UUID, List<UUID>> physicalToSchematicMap; // physical component id -> schematic symbol id(s)
|
||||
Size width, height, gridSpacing;
|
||||
}
|
||||
```
|
||||
|
||||
- `Project.getSchematicView()` returns the field (may be `null`).
|
||||
- `Project.getOrCreateSchematicView()` lazily creates it — **calling this makes the schematic
|
||||
persist with the project from now on**.
|
||||
- `Project.clone()` and `Project.equals()` include the schematic view, so schematic edits go
|
||||
through the normal dirty / undo-snapshot machinery.
|
||||
- Serialized via XStream alongside the rest of `Project`. Older DIYLC ignores the unknown
|
||||
field; a `null` field serializes to nothing.
|
||||
|
||||
The `physicalToSchematicMap` is 1:N to support multi-section components (a dual triode → two
|
||||
triode symbols). Pin mappings are **not** stored — they are a pure function of the physical
|
||||
component and are recomputed by re-running the factory whenever needed.
|
||||
|
||||
### `ISchematicFactory` / `SchematicSymbolMapping`
|
||||
|
||||
```java
|
||||
interface ISchematicFactory {
|
||||
List<SchematicSymbolMapping> createSchematicSymbols(IDIYComponent<?> physicalComponent);
|
||||
}
|
||||
|
||||
class SchematicSymbolMapping {
|
||||
IDIYComponent<?> schematicSymbol; // fully configured, only needs positioning
|
||||
Map<Integer, Integer> pinMapping; // physical control-point index -> schematic control-point index
|
||||
String sectionLabel; // optional, e.g. "Section A"
|
||||
}
|
||||
```
|
||||
|
||||
A type declares its factory on the annotation:
|
||||
|
||||
```java
|
||||
@ComponentDescriptor(name = "Resistor", ..., schematicFactory = ResistorSchematicFactory.class)
|
||||
```
|
||||
|
||||
`ComponentProcessor.extractComponentTypeFrom` reads it into
|
||||
`ComponentType.getSchematicFactoryClass()`. The default (`ISchematicFactory.class` itself,
|
||||
used only as a "none" marker) means **no factory declared → use the generic box**.
|
||||
|
||||
---
|
||||
|
||||
## Generation pipeline — `SchematicBuilder.build(project, continuityAreas)`
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Project + ContinuityAreas] --> B["NetlistBuilder.extractNetlists(includeSwitches=false)"]
|
||||
B --> C[single Netlist: Groups of Nodes]
|
||||
A --> D[for each component: isEligible?]
|
||||
D -->|ICommonNode| E[clone through as-is]
|
||||
D -->|ISwitch or has sticky pins| F["resolve factory (declared or GenericBox)"]
|
||||
D -->|IContinuity and not ISwitch, or no sticky pins| G[excluded, silently]
|
||||
F --> H["factory.createSchematicSymbols() -> SymbolEntry{symbol, pinMapping}"]
|
||||
E --> I[record physicalToSchematicMap]
|
||||
H --> I
|
||||
I --> J["placeSymbols: degree-sorted grid (moveAnchorTo)"]
|
||||
J --> K["createWires: per Netlist Group, chain the schematic pins, ManhattanRouter.route each pair"]
|
||||
K --> L[sort by z-order, store into SchematicView, resize canvas to fit]
|
||||
```
|
||||
|
||||
- **Eligibility** (`SchematicBuilder.isEligible`):
|
||||
- `ICommonNode` (ground, common node) → eligible, cloned through unchanged.
|
||||
- `ISwitch` → eligible (drawn as a symbol).
|
||||
- other `IContinuity` (wires, traces, solder bridges, board strips) → excluded.
|
||||
- no sticky control points (labels, images, shapes, boards) → excluded.
|
||||
- Exclusions are silent — no warning.
|
||||
- **Factory resolution** caches one instance per factory class. Factory exceptions fall back
|
||||
to the generic box.
|
||||
- **Placement** sorts components by connection degree (sum over netlist groups of
|
||||
`groupSize - 1`) descending, then drops them into a `ceil(sqrt(n))`-column grid. Multi-symbol
|
||||
mappings stack vertically within the cell. `moveAnchorTo` translates *every* control point so
|
||||
control point 0 lands on the target, snapshotting the targets first (see *Design decisions*).
|
||||
- **Wiring** walks each `Group`, resolves every `Node` (physical component + pin index) to a
|
||||
schematic `(symbol, schematicPinIndex, location)` via the factory's `pinMapping`, sorts the
|
||||
pins left-to-right / top-to-bottom, and creates a `SchematicWire` between consecutive pins
|
||||
(a chain, not a star). Each wire stores `sourceComponentId/sourcePinIndex` and
|
||||
`targetComponentId/targetPinIndex` plus the routed `routePoints`. Already-placed wire
|
||||
segments are fed to the router as obstacles.
|
||||
|
||||
## Incremental sync — `SchematicSynchronizer.synchronize(project, continuityAreas)`
|
||||
|
||||
Runs every time the user switches to the Schematic tab. If the view is empty it does a full
|
||||
`SchematicBuilder.build`. Otherwise:
|
||||
|
||||
- **Kept** components: re-run the factory for fresh symbols + pin maps, but keep the *existing*
|
||||
positioned symbol instance, only refreshing its name/value.
|
||||
- **New** components (id in layout, not in map): place fresh symbols in the next free grid cell.
|
||||
- **Removed** components (id in map, not in layout): drop their symbols.
|
||||
- **Wires** are always rebuilt from scratch from the current netlist — they are cheap.
|
||||
|
||||
## Routing — `ManhattanRouter`
|
||||
|
||||
Pure, no dependencies on the rest of the system. `route(start, end, startDir, endDir,
|
||||
obstacles, grid)` returns axis-aligned way-points from `start` to `end`.
|
||||
|
||||
- Aligned endpoints → a single straight segment.
|
||||
- Otherwise it generates candidate routes: two L shapes, several
|
||||
horizontal-vertical-horizontal routes (vertical middle leg at candidate x positions) and
|
||||
several vertical-horizontal-vertical routes (midpoint / quarter points / grid offsets).
|
||||
- Each candidate is scored: `length*0.01 + bends*6 + crossings*15 + overlapLength*0.5 +
|
||||
directionMismatchPenalty`. `startDir` / `endDir` are the directions the wire should leave
|
||||
each pin — away from the symbol centroid along the dominant axis
|
||||
(`SchematicBuilder.exitDirection`).
|
||||
- Lowest score wins; the result is de-duplicated and collinear middle points are dropped.
|
||||
|
||||
`SchematicBuilder.rerouteWires(components)` re-routes every wire from the *current* symbol pin
|
||||
positions (looked up by component id) — used after the user moves a symbol.
|
||||
|
||||
---
|
||||
|
||||
## UI
|
||||
|
||||
### The tab — `SchematicTabPlugin`
|
||||
|
||||
The tab renders through the **real canvas**, not a bespoke widget.
|
||||
|
||||
- The plugin owns a second `Presenter` and a second `CanvasPlugin`, installed on that
|
||||
presenter. Both `CanvasPlugin`s inject their `RulerScrollPane` into the center `BoxLayout`
|
||||
panel; the plugin shows exactly one at a time.
|
||||
- A `JToolBar` with two `JToggleButton`s (`Layout` / `Schematic`) is injected below them.
|
||||
**No zoom or refresh buttons** — rulers, scroll bars and wheel/gesture zoom come from the
|
||||
`RulerScrollPane`; refresh is automatic on tab switch.
|
||||
- Switching to **Schematic**: `SchematicSynchronizer.synchronize(...)` on the layout project,
|
||||
wrap the resulting `SchematicView` in a throwaway `Project` (shares the same component
|
||||
instances), `loadProject()` it into the schematic presenter, swap the visible scroll pane,
|
||||
`scrollToCenterAndShowContents()`.
|
||||
- The wrapper project **locks the `WIRING` layer** so wires are inert; a listener on the
|
||||
schematic presenter re-routes wires on `PROJECT_MODIFIED` (fires once when a drag-move ends).
|
||||
|
||||
### The detached window — `SchematicMenuPlugin` / `SchematicViewFrame` / `SchematicPanel`
|
||||
|
||||
*Analyze → Schematic View…* opens a separate window that renders the `SchematicView` through
|
||||
a private `DrawingManager` (read-only, no `Presenter`) and adds PNG export. Independent of
|
||||
the tab; the simplest possible viewer.
|
||||
|
||||
---
|
||||
|
||||
## How to add a dedicated symbol for a component type
|
||||
|
||||
1. Write an `ISchematicFactory` in `diylc-library` under `org.diylc.schematic`. For a simple
|
||||
1:1 mapping, extend `AbstractSimpleSchematicFactory` and return `new XxxSymbol()` from
|
||||
`createSymbol()` (override `electricalPinCount()` if not 2).
|
||||
2. Point the physical component at it: add `schematicFactory = XxxSchematicFactory.class` to
|
||||
its `@ComponentDescriptor` and import the class.
|
||||
3. If the symbol's pin order does not match the physical component's control-point order,
|
||||
build the `pinMapping` explicitly instead of using the base class.
|
||||
4. For multi-section parts (dual triode, dual op-amp) return more than one
|
||||
`SchematicSymbolMapping`, each with its own `pinMapping` and a `sectionLabel`.
|
||||
|
||||
No other wiring is needed — `SchematicBuilder` discovers the factory through `ComponentType`.
|
||||
|
||||
---
|
||||
|
||||
## Design decisions & rationale
|
||||
|
||||
### 1. The schematic is a lazy field on `Project`, not a sidecar file
|
||||
|
||||
One file per project is a strong existing expectation, and schematic edits should participate
|
||||
in undo/redo and the modified flag with no bespoke plumbing (the app already snapshots
|
||||
`Project` via `clone()` and diffs via `equals()`). So `SchematicView` is a field on `Project`,
|
||||
serialized inside the `.diy`.
|
||||
|
||||
The field stays `null` until the user first opens the view, so files for users who never touch
|
||||
the feature are unchanged, and old DIYLC ignores the unknown XStream element.
|
||||
|
||||
*Cost:* opening the view calls `getOrCreateSchematicView()`, which flips the project to
|
||||
*modified* even if nothing changed (intentional — the schematic should persist once generated —
|
||||
but surprising). Pin maps are deliberately not serialized; they are recomputed from the
|
||||
physical component to avoid a second source of truth.
|
||||
|
||||
### 2. Per-type `ISchematicFactory`, declared on the annotation, with a generic fallback
|
||||
|
||||
Hundreds of component types; most map trivially, a few (dual triodes, dual op-amps) do not.
|
||||
A strategy interface keeps per-type logic with the type: a type opts in with
|
||||
`@ComponentDescriptor(schematicFactory = ...)`, the factory returns *ready* symbol instances
|
||||
plus a pin map, and `GenericBoxSchematicFactory` covers everything else so nothing is ever
|
||||
un-mapped. `AbstractSimpleSchematicFactory` is a base for the common 1:1 case.
|
||||
|
||||
*Cost:* factories must be stateless (instantiated once, reused) — convention, not enforced.
|
||||
`diylc-core` gains an `org.diylc.schematic` package that `diylc-library` also uses (split
|
||||
package across jars — consistent with `org.diylc.components`, but a smell).
|
||||
|
||||
### 3. `SchematicWire` is a real component; connections are drawn, not implied
|
||||
|
||||
The alternative — computing routes from the netlist at paint time in a custom overlay — was
|
||||
rejected. Keeping wires as stored `IDIYComponent`s means they persist in the `.diy`, are
|
||||
picked up by the existing image/PDF export paths, and can gain user-editable bend points later
|
||||
without a redesign; routing runs once per generation/move instead of every repaint; and there
|
||||
is no second rendering code path.
|
||||
|
||||
`SchematicWire` implements `IContinuity`, stores its two endpoints as
|
||||
`(componentId, pinIndex)` plus the calculated `routePoints`, has `zOrder = WIRING` (symbols
|
||||
are `COMPONENT`), and overrides `clone()` to deep-copy the `routePoints` list.
|
||||
|
||||
*Cost:* a real component is selectable/draggable by default — see decision 5. The router is a
|
||||
heuristic scorer over a fixed candidate set, not a real grid/A\* router; it avoids overlap
|
||||
pairwise but is not globally crossing-aware.
|
||||
|
||||
### 4. The Schematic tab reuses the real canvas via a second `Presenter` + `CanvasPlugin`
|
||||
|
||||
The canvas is inseparable from a `Presenter` (`CanvasPanel.paint()` calls `plugInPort.draw`,
|
||||
`RulerScrollPane` wraps a `ProjectDrawingProvider(plugInPort, ...)`, zoom is
|
||||
`plugInPort.setZoomLevel`). The user wants the schematic to render *identically* — same
|
||||
rulers, scroll bars, wheel/gesture zoom — with no extra controls. So `SchematicTabPlugin`
|
||||
builds a second `Presenter` (`importVariantsAndBlocks = false` to skip the expensive startup)
|
||||
and a second `CanvasPlugin`, and toggles which scroll pane is visible. `IPlugInPort` gained
|
||||
`getContinuityAreas()` so the tab can build the netlist without reaching into `Presenter`
|
||||
internals.
|
||||
|
||||
*Cost:* a second `Presenter` is heavyweight (its own `DrawingManager`, `ProjectFileManager`,
|
||||
`InstantiationManager`, `VariantManager`, `BuildingBlockManager`, config listeners).
|
||||
`loadProject` on it calls the process-wide `DrawingCache.Instance.clear()`. The schematic
|
||||
canvas is a full editing presenter with a separate undo stack; nothing yet blocks symbol
|
||||
add/delete/paste/value-edit. The tab and the *Analyze → Schematic View…* window are two
|
||||
independent renderers of the same data.
|
||||
|
||||
### 5. Wires are made inert by locking the `WIRING` layer; they re-route on symbol move
|
||||
|
||||
The user must never be able to select, drag or detach a wire, but symbols stay movable. There
|
||||
is no `IDIYComponent.isSelectable()` hook; `Presenter.isComponentLocked()` already gates
|
||||
select/drag/control-point-grab off the project's `lockedLayers`. `SchematicWire` is the only
|
||||
thing on `WIRING`, so the schematic's wrapper project does
|
||||
`wrapper.getLockedLayers().add(IDIYComponent.WIRING)` and every wire becomes inert while
|
||||
symbols (on `COMPONENT`) stay editable.
|
||||
|
||||
Locked components render at 0.5 alpha (`DrawOption.LOCKED_ALPHA`), so `SchematicWire.draw()`
|
||||
forces an opaque composite for its own drawing and restores it afterwards.
|
||||
|
||||
Re-routing: `SchematicTabPlugin` installs a tiny `IPlugIn` that listens for
|
||||
`EventType.PROJECT_MODIFIED` (dispatched once when a drag-move ends) and calls
|
||||
`SchematicBuilder.rerouteWires(components)`, which re-runs the router from the current pin
|
||||
positions. A `sameRoute` check and a `rerouting` guard prevent needless repaints and
|
||||
re-entrancy.
|
||||
|
||||
*Cost:* `rerouteWires` recomputes *all* wires on every move, not just the affected ones. The
|
||||
opaque-composite workaround exists only because the wire is on a locked layer.
|
||||
|
||||
### 6. `SchematicBox` is a rigid multi-pin component — it must not recompute in `setControlPoint`
|
||||
|
||||
`SchematicBox` pin positions are a function of an anchor (control point 0) and the per-side
|
||||
node counts. The obvious implementation — recompute all pins whenever the anchor moves, inside
|
||||
`setControlPoint` — is **wrong**, because that is not how `Presenter` moves a rigid component.
|
||||
For a component whose `canPointMoveFreely(i)` is `false`, the drag code adds *every* control
|
||||
point index to the move set and applies the delta one index at a time, in `HashSet` order:
|
||||
|
||||
```java
|
||||
for (Integer index : indices) {
|
||||
Point2D old = c.getControlPoint(index);
|
||||
c.setControlPoint(new Point2D.Double(old.getX() + dx, old.getY() + dy), index);
|
||||
}
|
||||
```
|
||||
|
||||
With a recompute on index 0, pins 1..N get rebuilt relative to the new anchor and then the
|
||||
loop adds the delta *again* — pins moved by 2× the drag, by amounts that depend on iteration
|
||||
order ("control points randomly move off the edge"). The same shape of bug hit
|
||||
`SchematicBuilder.moveAnchorTo`, which read `getControlPoint(i)` inside its loop after setting
|
||||
index 0 — that is why initial generation put every wire on one node until the user nudged the
|
||||
symbol.
|
||||
|
||||
Fix (matching `ICSymbol`, which `SchematicBox` was modelled on):
|
||||
|
||||
- `setControlPoint` moves **only** that point.
|
||||
- `updateControlPoints()` runs only from the constructor and the node-list setters; when the
|
||||
pin count is unchanged it mutates the existing `Point2D` objects in place (references held by
|
||||
drawing/selection stay valid), allocating a new array only when the count changes.
|
||||
- `moveAnchorTo` snapshots every target position *before* mutating any point.
|
||||
|
||||
A uniform translation of a valid box configuration is another valid one, so the pins stay on
|
||||
the edge during a drag without any recompute.
|
||||
|
||||
*Rule of thumb:* a rigid multi-control-point component must never mutate points other than the
|
||||
one passed to `setControlPoint`; geometry recompute belongs in explicit structure-changing
|
||||
setters.
|
||||
|
||||
### 7. Switches are drawn as symbols, not treated as connections
|
||||
|
||||
`ISwitch extends IContinuity`, so a naive "exclude `IContinuity`" filter silently dropped
|
||||
every switch even though the netlist (built with `includeSwitches = false`) still contained
|
||||
its terminals with real connections. `NetlistBuilder` itself special-cases switches with
|
||||
`c instanceof IContinuity && !(c instanceof ISwitch)`; the schematic filter now matches:
|
||||
`isEligible()` returns `true` for `ISwitch` before the `IContinuity` check. With no dedicated
|
||||
factory yet, a switch falls through to `GenericBoxSchematicFactory` (a labeled box).
|
||||
|
||||
*General principle:* anywhere the code says "exclude `IContinuity`", check whether it should
|
||||
special-case `ISwitch`.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Pure-logic pieces have unit tests in `diylc-library/src/test/java/org/diylc/schematic/`:
|
||||
|
||||
- `ManhattanRouterTest` — straight vs. bent routes, axis-alignment, endpoint fidelity.
|
||||
- `GenericBoxSchematicFactoryTest` — pin count / labels, switch eligibility.
|
||||
- `SchematicRerouteTest` — wire endpoints follow a moved symbol; `SchematicBox` drag/placement
|
||||
geometry (every pin translated by exactly the drag delta, in any index order).
|
||||
|
||||
`SchematicViewIntegrationTest` (in `diylc-swing`) exercises build → sync → render end to end
|
||||
through a real `Presenter`. **Note:** the `diylc-swing` test module currently fails to resolve
|
||||
`org.reflections` in some local setups (pre-existing — `PresenterTests` fails the same way),
|
||||
so this test may not run locally; it is expected to run in CI.
|
||||
|
||||
---
|
||||
|
||||
## Known limitations
|
||||
|
||||
- The schematic canvas blocks wire interaction, but not symbol add / delete / paste /
|
||||
value-edit; there is no shared undo with the layout history.
|
||||
- `rerouteWires` recomputes *all* wires on every move; routing is not globally crossing-aware.
|
||||
- Placement is a degree-sorted grid, not signal-flow or true clustering.
|
||||
- Multi-section (1:N) factories for tubes, dual op-amps and multi-pole switches are not done —
|
||||
those fall back to the generic box.
|
||||
- 3-pin factories (`Transistor`, `Potentiometer`) map legs to symbol legs in index order,
|
||||
which may not match every package's pin-out.
|
||||
- No print or PDF/SVG export for the schematic (PNG only, from the detached window).
|
||||
- Schematic symbols do not follow rotation/mirroring cleanly (`SchematicBox` derives its body
|
||||
from an axis-aligned anchor).
|
||||
|
||||
## Gotchas for future work
|
||||
|
||||
- `ISwitch extends IContinuity`. Any "exclude `IContinuity`" check must special-case switches.
|
||||
- `SchematicView` participating in `Project.equals()` means opening the schematic marks the
|
||||
project modified.
|
||||
- A rigid multi-pin component (`SchematicBox`, `ICSymbol`) must **not** recompute its other
|
||||
control points inside `setControlPoint`.
|
||||
- Component source files in this repo have mixed CRLF/LF line endings and ISO-8859-1 encoding.
|
||||
Bulk-editing them with a tool that normalizes newlines produces enormous phantom diffs —
|
||||
preserve the original newline style per file.
|
||||
@@ -212,3 +212,57 @@ 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 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.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
# DIYLC Schematic View — Feature Specification
|
||||
|
||||
> This is the original spec plus a running status section (§11). For how the shipped code is
|
||||
> actually put together and *why*, see [`adr/schematic-view.md`](../adr/001-schematic-view.md).
|
||||
|
||||
## Overview
|
||||
|
||||
Add the ability to generate and display a schematic diagram representation of a physical layout project at any time. The schematic is derived from the existing netlist infrastructure and rendered using DIYLC's existing rendering pipeline. Users can view, rearrange, and export the schematic to verify their circuit at a glance.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Overview
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Layout Side"
|
||||
LP[Layout Project<br/>List<IDIYComponent>]
|
||||
NB[NetlistBuilder]
|
||||
LP --> NB
|
||||
end
|
||||
|
||||
subgraph "Schematic Generation"
|
||||
SB[SchematicBuilder<br/>Initial generation]
|
||||
SS[SchematicSynchronizer<br/>Incremental sync]
|
||||
SF[SchematicFactory<br/>per component type]
|
||||
GBF[GenericBoxFactory<br/>default fallback]
|
||||
NB --> SB
|
||||
NB --> SS
|
||||
SB --> SF
|
||||
SB --> GBF
|
||||
SS --> SF
|
||||
end
|
||||
|
||||
subgraph "Schematic Side"
|
||||
SV["SchematicView<br/>(embedded in Project)"]
|
||||
SP[Schematic Presenter<br/>Restricted mode]
|
||||
DM[DrawingManager<br/>Existing pipeline]
|
||||
SB --> SV
|
||||
SS --> SV
|
||||
SV --> SP
|
||||
SP --> DM
|
||||
end
|
||||
|
||||
subgraph "UI"
|
||||
TAB["Tab Bar<br/>Layout | Schematic"]
|
||||
TAB --> LP
|
||||
TAB --> SP
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Data Model
|
||||
|
||||
### 2.1 SchematicView (new class, embedded in Project)
|
||||
|
||||
Storage: new field on [`Project`](file:///Users/branislavstojkovic/GitHub/diy-layout-creator/diylc/diylc-core/src/main/java/org/diylc/core/Project.java) — `private SchematicView schematicView`.
|
||||
|
||||
Serialized inside the `.diy` file alongside existing project data. **Lazily initialized** — the field remains `null` until the user first opens the Schematic tab, keeping files unchanged for users who don't use the feature.
|
||||
|
||||
```java
|
||||
public class SchematicView implements Serializable {
|
||||
// Schematic components (symbols + wires)
|
||||
private List<IDIYComponent<?>> components;
|
||||
|
||||
// Mapping: physical component UUID → list of schematic symbol UUIDs
|
||||
// (1:N for multi-section components like dual triodes/opamps)
|
||||
private Map<UUID, List<UUID>> physicalToSchematicMap;
|
||||
|
||||
// Canvas dimensions
|
||||
private Size width;
|
||||
private Size height;
|
||||
private Size gridSpacing;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Component Identity
|
||||
|
||||
Physical ↔ Schematic matching uses the physical component's **UUID** (`IDIYComponent.getId()`). The `physicalToSchematicMap` tracks which schematic symbol(s) were spawned from each physical component.
|
||||
|
||||
---
|
||||
|
||||
## 3. Component Mapping: Physical → Schematic
|
||||
|
||||
### 3.1 Factory/Strategy Pattern
|
||||
|
||||
Each physical component type can declare a `SchematicFactory` via a new attribute on [`@ComponentDescriptor`](file:///Users/branislavstojkovic/GitHub/diy-layout-creator/diylc/diylc-core/src/main/java/org/diylc/core/annotations/ComponentDescriptor.java):
|
||||
|
||||
```java
|
||||
@ComponentDescriptor(
|
||||
name = "Tube Socket",
|
||||
category = "Tubes",
|
||||
schematicFactory = TubeSocketSchematicFactory.class, // NEW
|
||||
...
|
||||
)
|
||||
public class TubeSocket extends AbstractAngledComponent<String> { ... }
|
||||
```
|
||||
|
||||
When no `schematicFactory` is specified, a **`GenericBoxSchematicFactory`** is used as the default fallback.
|
||||
|
||||
### 3.2 ISchematicFactory Interface
|
||||
|
||||
```java
|
||||
public interface ISchematicFactory {
|
||||
/**
|
||||
* Creates schematic symbol instances for a given physical component.
|
||||
* Returns fully instantiated, configured IDIYComponent instances
|
||||
* that just need positioning.
|
||||
*
|
||||
* @param physicalComponent the physical layout component
|
||||
* @return list of SchematicSymbolMapping entries (1:1 or 1:N)
|
||||
*/
|
||||
List<SchematicSymbolMapping> createSchematicSymbols(
|
||||
IDIYComponent<?> physicalComponent);
|
||||
}
|
||||
|
||||
public class SchematicSymbolMapping {
|
||||
// The fully configured schematic symbol instance
|
||||
private IDIYComponent<?> schematicSymbol;
|
||||
|
||||
// Pin mapping: physical control point index → schematic control point index
|
||||
private Map<Integer, Integer> pinMapping;
|
||||
|
||||
// Optional section label (e.g., "Section A", "Section B" for dual triodes)
|
||||
private String sectionLabel;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Factory Returns Fully Instantiated Components
|
||||
|
||||
The factory's `createSchematicSymbols()` method returns **ready-to-use `IDIYComponent` instances**. This gives factories full control over:
|
||||
- Which schematic symbol class to use
|
||||
- Property values (name, value, display mode)
|
||||
- For multi-section components: which sections get shared pins (heaters, B+, etc.)
|
||||
|
||||
### 3.4 Default Fallback: GenericBoxSchematicFactory
|
||||
|
||||
For any physical component without a declared factory, the `GenericBoxSchematicFactory`:
|
||||
1. Inspects `getControlPointNodeName(i)` for each sticky control point
|
||||
2. Creates a `SchematicBox` component with those labeled terminals
|
||||
3. Copies the component's name and value
|
||||
|
||||
### 3.5 Exclusion Criteria
|
||||
|
||||
A physical component is **excluded from the schematic** if:
|
||||
- It implements `IContinuity` (wires, traces, solder bridges, board strips) — these are represented as connections, not symbols
|
||||
- It has **no sticky control points** (decorative elements, labels, images, shapes, boards)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Components are silently excluded. No warning is shown to the user.
|
||||
|
||||
### 3.6 Pin Name Matching for 1:N Mapping
|
||||
|
||||
For multi-section components (e.g., dual triode tube sockets), pin names follow a convention:
|
||||
- Physical: `G1, P1, K1, H+, H-, G2, P2, K2`
|
||||
- Schematic TriodeSymbol section 1: `G, P, K, H+, H-`
|
||||
- Schematic TriodeSymbol section 2: `G, P, K`
|
||||
|
||||
The factory code handles:
|
||||
- Stripping numeric suffixes to match section pins
|
||||
- Deciding which section gets shared pins (e.g., heaters on section 1 only)
|
||||
- Full programmatic control — no rigid convention imposed
|
||||
|
||||
---
|
||||
|
||||
## 4. New Components
|
||||
|
||||
### 4.1 SchematicBox (new component)
|
||||
|
||||
A new `IDIYComponent` in `diylc-library`, category "Schematic Symbols".
|
||||
|
||||
Purpose: generic rectangular IC-style schematic symbol with configurable terminals, similar to KiCad's generic symbol.
|
||||
|
||||
Key properties:
|
||||
- **Left nodes**: comma-separated string for pins on the left side (typically inputs)
|
||||
- **Right nodes**: comma-separated string for pins on the right side (typically outputs)
|
||||
- **Top nodes**: comma-separated string for pins on the top side (typically positive power)
|
||||
- **Bottom nodes**: comma-separated string for pins on the bottom side (typically ground/negative power)
|
||||
- **Body label**: component name and/or value
|
||||
- **Display**: NAME / VALUE / BOTH / NONE
|
||||
|
||||
```java
|
||||
@ComponentDescriptor(
|
||||
name = "Box",
|
||||
category = "Schematic Symbols",
|
||||
instanceNamePrefix = "U",
|
||||
description = "Generic schematic box with configurable terminals",
|
||||
zOrder = IDIYComponent.COMPONENT
|
||||
)
|
||||
public class SchematicBox extends AbstractTransparentComponent<String> {
|
||||
private String leftNodes = "1,2";
|
||||
private String rightNodes = "3,4";
|
||||
private String topNodes = "";
|
||||
private String bottomNodes = "";
|
||||
// ... rendering, control points, etc.
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 SchematicWire (new component)
|
||||
|
||||
A new `IDIYComponent` for rendering Manhattan-routed connections in the schematic.
|
||||
|
||||
Key characteristics:
|
||||
- Implements `IContinuity`
|
||||
- Draws horizontal/vertical line segments with automatic 90° bends
|
||||
- Knows its source and destination node (component UUID + pin index)
|
||||
- **Auto-routed**: path is calculated, not user-editable
|
||||
- **Targeted re-routing**: when a connected symbol moves, only its wires recalculate
|
||||
- Route minimizes overlap with other wires (simple collision avoidance). Design an algorithm that detects whether the line should be straight horizontal or vertical or it needs to bend horizontal-vertical-horizontal or vertical-horizontal-vertical.
|
||||
|
||||
```java
|
||||
@ComponentDescriptor(
|
||||
name = "Schematic Wire",
|
||||
category = "Schematic Symbols",
|
||||
description = "Auto-routed Manhattan wire for schematic connections",
|
||||
zOrder = IDIYComponent.WIRING,
|
||||
creationMethod = CreationMethod.POINT_BY_POINT
|
||||
)
|
||||
public class SchematicWire extends AbstractComponent<Void> implements IContinuity {
|
||||
private UUID sourceComponentId;
|
||||
private int sourcePinIndex;
|
||||
private UUID targetComponentId;
|
||||
private int targetPinIndex;
|
||||
private List<Point2D> routePoints; // calculated waypoints
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Schematic Generation & Sync
|
||||
|
||||
### 5.1 SchematicBuilder (initial generation)
|
||||
|
||||
Located in `diylc-core` (e.g., `org.diylc.schematic.SchematicBuilder`).
|
||||
|
||||
**Input**: `Project` (with layout components) + `Netlist` (from `NetlistBuilder.extractNetlists(false, ...)` — switches treated as components, single netlist output)
|
||||
|
||||
**Process**:
|
||||
|
||||
1. **Filter components**: exclude `IContinuity` implementors and components with no sticky points
|
||||
2. **Create symbols**: for each remaining physical component:
|
||||
- Look up its `@ComponentDescriptor.schematicFactory`
|
||||
- If present, instantiate the factory and call `createSchematicSymbols(component)`
|
||||
- If absent, use `GenericBoxSchematicFactory`
|
||||
- Record mappings in `physicalToSchematicMap`
|
||||
3. **Include common nodes**: `GroundSymbol` and `CommonNode` components pass through as-is (they're already schematic-compatible)
|
||||
4. **Initial placement**: grid layout with connection-density awareness
|
||||
- Heavily connected components placed closer together
|
||||
- Simple grid arrangement (left-to-right, top-to-bottom)
|
||||
- No signal-flow inference
|
||||
5. **Create wires**: for each `Group` in the `Netlist`, create `SchematicWire` components connecting the corresponding schematic symbol pins
|
||||
6. **Route wires**: calculate Manhattan-style paths for all wires
|
||||
7. **Copy values**: schematic symbols inherit name and value from their physical counterpart
|
||||
|
||||
### 5.2 SchematicSynchronizer (incremental sync)
|
||||
|
||||
Triggered **automatically when the user switches to the Schematic tab** (silent, no diff shown).
|
||||
|
||||
**Process**:
|
||||
|
||||
1. Rebuild the netlist from the current layout
|
||||
2. Compare `physicalToSchematicMap` against current layout components:
|
||||
- **New components** (UUID in layout but not in map): create schematic symbols via factory, place near related components
|
||||
- **Deleted components** (UUID in map but not in layout): remove schematic symbols and their wires
|
||||
- **Existing components**: update name/value if changed, preserve position
|
||||
3. Rebuild all wires based on the new netlist (wires are cheap to regenerate)
|
||||
4. Re-route wires for moved/new symbols
|
||||
|
||||
### 5.3 Netlist Usage
|
||||
|
||||
Use `NetlistBuilder.extractNetlists(includeSwitches=false, project, continuityAreas)` — this mode:
|
||||
- Treats switches as regular components (not expanded into position combinations)
|
||||
- Returns a single `Netlist`
|
||||
- Already accounts for `IContinuity` connections and conductive areas
|
||||
- Each `Group` in the netlist represents a set of connected `Node`s (component + pin index)
|
||||
|
||||
---
|
||||
|
||||
## 6. UI: Tab-Based View
|
||||
|
||||
### 6.1 Tab Bar
|
||||
|
||||
Add a tab bar above the main canvas with two tabs: **Layout** | **Schematic**
|
||||
|
||||
- Default: Layout tab active
|
||||
- Clicking "Schematic" triggers lazy generation (first time) or sync (subsequent times)
|
||||
- Tab styling consistent with the application's look and feel
|
||||
|
||||
### 6.2 Schematic Presenter (restricted mode)
|
||||
|
||||
Two separate `Presenter` instances:
|
||||
- **Layout Presenter**: existing behavior, full edit capabilities
|
||||
- **Schematic Presenter**: configured restrictively at construction time
|
||||
|
||||
The Schematic Presenter operates on `project.getSchematicView().getComponents()` instead of `project.getComponents()`.
|
||||
|
||||
Use the same Presenter class but configure it differently so it behaves differently.
|
||||
|
||||
**Allowed operations** in schematic mode:
|
||||
| Operation | Allowed? |
|
||||
|---|---|
|
||||
| Move (drag) symbols | ✅ |
|
||||
| Multi-select and move | ✅ |
|
||||
| Rotate symbols | ✅ |
|
||||
| Flip/mirror symbols | ✅ |
|
||||
| Edit display properties | ✅ |
|
||||
| Resize canvas | ✅ |
|
||||
| Export as image (PNG/PDF/SVG) | ✅ |
|
||||
| Print | ✅ |
|
||||
| Add components | ❌ |
|
||||
| Delete components | ❌ |
|
||||
| Paste components | ❌ |
|
||||
| Edit component values | ❌ |
|
||||
| Drag wires | ❌ |
|
||||
|
||||
### 6.3 Wire Re-routing on Move
|
||||
|
||||
When the user moves a symbol:
|
||||
- **During drag**: show rubber-band or simple direct lines (preview)
|
||||
- **On mouse release**: re-route only the wires connected to the moved symbol(s)
|
||||
- Other wires remain unchanged
|
||||
|
||||
### 6.4 Undo/Redo
|
||||
|
||||
**Shared undo/redo stack** with the layout. All schematic operations (moves, rotations, flips) go into the same history as layout operations.
|
||||
|
||||
### 6.5 Rendering
|
||||
|
||||
The schematic uses the **existing `DrawingManager` + `Presenter`** rendering pipeline. The schematic sub-project is a standard `Project`-like structure containing `IDIYComponent` instances — the same rendering code that draws layout components draws schematic symbols.
|
||||
|
||||
---
|
||||
|
||||
## 7. File Format Impact
|
||||
|
||||
### 7.1 Backward Compatibility
|
||||
|
||||
- The `SchematicView` field on `Project` defaults to `null`
|
||||
- Files saved without opening the schematic tab contain no schematic data
|
||||
- Older DIYLC versions will ignore the new field (XStream handles unknown fields gracefully)
|
||||
|
||||
### 7.2 Forward Compatibility
|
||||
|
||||
- Once the schematic is generated and the file is saved, the schematic data is persisted
|
||||
- Subsequent file opens will have the schematic data available immediately
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation Phases
|
||||
|
||||
### Phase 1: Foundation (MVP)
|
||||
1. Add `SchematicView` to `Project`
|
||||
2. Create `ISchematicFactory` interface and `SchematicSymbolMapping`
|
||||
3. Create `SchematicBox` component (generic box with configurable terminals)
|
||||
4. Create `SchematicWire` component (Manhattan-routed wire)
|
||||
5. Add `schematicFactory` attribute to `@ComponentDescriptor`
|
||||
6. Implement `GenericBoxSchematicFactory` (default fallback)
|
||||
7. Implement `SchematicBuilder` (initial generation with grid placement)
|
||||
8. Implement basic Manhattan wire routing
|
||||
9. Add tab UI (Layout | Schematic). It should be at the bottom left, similar to excel sheets
|
||||
10. Create a restricted Schematic Presenter
|
||||
|
||||
### Phase 2: Symbol Coverage
|
||||
11. Implement dedicated factories for common components:
|
||||
- `ResistorSchematicFactory` → `ResistorSymbol`
|
||||
- `CapacitorSchematicFactory` → `CapacitorSymbol` (axial, radial, electrolytic, etc.)
|
||||
- `DiodeSchematicFactory` → `DiodeSymbol` (glass, plastic, zener, Schottky, LED, etc.)
|
||||
- `TransistorSchematicFactory` → `BJTSymbol` (TO-92, TO-220, TO-3, TO-126, TO-1)
|
||||
- `FETSchematicFactory` → `JFETSymbol`, `MOSFETSymbol`
|
||||
- `PotentiometerSchematicFactory` → `PotentiometerSymbol`
|
||||
- `InductorSchematicFactory` → `InductorSymbol`
|
||||
- `TubeSocketSchematicFactory` → `TriodeSymbol`, `PentodeSymbol`, `DuoDiodeSymbol` (1:N)
|
||||
- `DIL_ICSchematicFactory` → `ICSymbol` or `SchematicBox` (1:N for dual opamps)
|
||||
- `SwitchSchematicFactory` → `SwitchLatchingSymbol` or `SchematicBox`
|
||||
|
||||
### Phase 3: Sync & Polish
|
||||
12. Implement `SchematicSynchronizer` (incremental updates)
|
||||
13. Implement smart initial placement (connection-density awareness)
|
||||
14. Implement targeted wire re-routing on symbol move
|
||||
15. Add export (image/PDF) and print for schematic view
|
||||
16. Shared undo/redo integration
|
||||
|
||||
### Phase 4: Future Enhancements (out of scope for now)
|
||||
- Junction dots at T-connections
|
||||
- User-adjustable wire bend points
|
||||
- Net labels (toggle-able)
|
||||
- Side-by-side layout + schematic view
|
||||
- Real-time sync on layout changes
|
||||
- Advanced auto-routing with crossing avoidance
|
||||
|
||||
---
|
||||
|
||||
## 9. Key Design Decisions Summary
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|---|---|---|
|
||||
| Mapping mechanism | `@ComponentDescriptor.schematicFactory` + `ISchematicFactory` | Scalable, per-type control, complex 1:N support |
|
||||
| 1:N handling | Factory returns multiple configured instances | Full control over shared pins (heaters, B+) |
|
||||
| Pin mapping | Factory-controlled, name matching with explicit overrides | Flexible, handles sections and shared pins |
|
||||
| Storage | Embedded in `Project` as `SchematicView` field | Single file, lazy initialization |
|
||||
| Wire rendering | Dedicated `SchematicWire` component, Manhattan routing | Proper schematic appearance |
|
||||
| UI | Tab alongside canvas (Layout \| Schematic) | Clean, familiar paradigm |
|
||||
| Sync | Auto-sync on tab switch, silent | Seamless UX |
|
||||
| Rendering | Existing `DrawingManager` + `Presenter` | Maximum code reuse |
|
||||
| Fallback | `GenericBoxSchematicFactory` for unmapped components | Everything gets a symbol |
|
||||
| Exclusions | `IContinuity` + no sticky points | Clean schematic |
|
||||
| Undo | Shared stack | Unified history |
|
||||
| Net labels | Not shown (MVP) | Reduce clutter |
|
||||
| Wires on move | Re-route connected wires only, on mouse release | Efficient |
|
||||
| Switch handling | Netlist built with `includeSwitches=false`, switches rendered as symbols | Single netlist, clean schematic |
|
||||
| Initial placement | Grid with connection-density awareness | Good enough starting point |
|
||||
|
||||
---
|
||||
|
||||
## 10. Open Questions
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Multi-section component coverage**: How many multi-section ICs need dedicated factories at launch? Just tubes and common opamps (TL072, NE5532, LM358), or broader coverage? This affects Phase 2 scope.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **SchematicBox terminal layout**: Should pins always be distributed left/right (like a DIP IC), or should we also support top/bottom terminal placement for power supply pins?
|
||||
|
||||
> [!NOTE]
|
||||
> **Serialization**: The `SchematicView` will be serialized via XStream alongside the existing `Project` fields. We should verify that all new classes (`SchematicWire`, `SchematicBox`, `SchematicSymbolMapping`) serialize cleanly and don't break older DIYLC versions opening new files.
|
||||
|
||||
---
|
||||
|
||||
## 11. Implementation Status
|
||||
|
||||
### Done
|
||||
|
||||
**diylc-core**
|
||||
- `@ComponentDescriptor.schematicFactory` attribute; surfaced on `ComponentType.getSchematicFactoryClass()` via `ComponentProcessor`.
|
||||
- `ISchematicFactory` + `SchematicSymbolMapping` SPI (`org.diylc.schematic`).
|
||||
- `SchematicView` entity (`org.diylc.core`) — components, `physicalToSchematicMap`, canvas size; lazy field on `Project` (`getSchematicView()` / `getOrCreateSchematicView()`), wired into `Project.clone()` and `equals()`. Stays `null` for projects that never open the view, so existing `.diy` files are byte-for-byte unchanged.
|
||||
- `INetlistProcessor.getContinuityAreas()` (implemented on `Presenter`) so the schematic can be built outside the presenter.
|
||||
|
||||
**diylc-library**
|
||||
- `SchematicBox` component (`org.diylc.components.schematic`) — generic box with configurable left/right/top/bottom comma-separated terminal lists, NAME/VALUE/BOTH/NONE display. `setControlPoint` moves only the requested point (the presenter drags all of them together because `canPointMoveFreely` is false); the pin geometry is recomputed only when the node lists change, and then in place when the pin count is unchanged so held references stay valid.
|
||||
- `SchematicWire` component — auto-routed Manhattan connection; implements `IContinuity`; stores source/target component id + pin index and the calculated `routePoints`; deep-cloned.
|
||||
- `ManhattanRouter` — straight / L / HVH / VHV routing with pin exit-direction preference and overlap/crossing cost scoring against existing segments.
|
||||
- `GenericBoxSchematicFactory` (default fallback) + `SchematicBuilder` (filter, symbol creation, connection-density grid placement, per-net wire chaining, canvas auto-resize) + `SchematicSynchronizer` (add/remove/keep-position incremental sync, full wire rebuild). Switches (`ISwitch`, which extends `IContinuity`) are treated as regular symbols — the generic box fallback until a dedicated factory is added — since the netlist is built with `includeSwitches=false`.
|
||||
- Concrete factories: `Resistor`, `Capacitor`, `Diode`, `Transistor` (→ `BJTSymbol`), `Inductor`, `Potentiometer`, wired onto the common physical component types (resistors, film/ceramic/electrolytic/tantalum/mica caps, plastic/glass diodes, TO-1/92/126/220/3 transistors, radial/toroidal inductors, panel/trimmer/miniature pots).
|
||||
|
||||
**diylc-swing**
|
||||
- `SchematicTabPlugin` adds the Excel-style tab strip (**Layout | Schematic**) below the canvas. It owns a **second `Presenter` + `CanvasPlugin`**, so the schematic renders through the exact same `RulerScrollPane` — rulers, scroll bars, wheel/gesture zoom — as the layout canvas, with no extra controls. Selecting **Schematic** runs `SchematicSynchronizer` automatically and loads the resulting wrapper `Project` into the schematic presenter; selecting **Layout** swaps the visible scroll pane back. `CanvasPlugin.getCanvasScrollComponent()` exposes each scroll pane so the plugin can show/hide it.
|
||||
- The wrapper project **locks the `WIRING` layer**, so `SchematicWire`s cannot be selected, dragged or detached from symbols; symbols (on the `COMPONENT` layer) stay movable. `SchematicWire.draw()` forces an opaque composite so the lock-alpha dimming does not fade the wires.
|
||||
- When the user moves a symbol, `SchematicTabPlugin` listens for `EventType.PROJECT_MODIFIED` on the schematic presenter and calls `SchematicBuilder.rerouteWires()`, which re-computes every wire route from the current symbol pin positions so wires stay attached.
|
||||
- `SchematicMenuPlugin` / `SchematicViewFrame` / `SchematicPanel` — a detached-window viewer (*Analyze → Schematic View…*) that renders the `SchematicView` through a private `DrawingManager` and adds PNG export. Independent of the tab.
|
||||
|
||||
Unit tests: `ManhattanRouterTest`, `GenericBoxSchematicFactoryTest` (library); `SchematicViewIntegrationTest` (swing — end-to-end build/sync/render).
|
||||
|
||||
### Not yet done (follow-ups)
|
||||
|
||||
- Wires are fully inert (locked layer) and re-route automatically after a symbol move, but the schematic canvas is otherwise not **restricted**: there is no explicit block on add / delete / paste / value-edit of symbols, and no shared undo integration with the layout history.
|
||||
- Re-routing currently recomputes every wire on each move; targeted re-routing of only the affected wires, and crossing-aware routing, are follow-ups.
|
||||
- Shared undo/redo integration with the layout history.
|
||||
- Multi-section 1:N factories (`TubeSocket` → triode/pentode/duo-diode, `DIL_IC` → dual op-amps, switches).
|
||||
- Smarter placement (true connection-density clustering) and crossing-aware routing.
|
||||
- Print support and PDF/SVG export for the schematic.
|
||||
- Round-trip XStream serialization verification for the new classes.
|
||||
Reference in New Issue
Block a user