From b9740338ce936bb58464819040c32903fda3b08b Mon Sep 17 00:00:00 2001 From: Bane Stojkovic Date: Thu, 3 Sep 2026 09:38:44 +0200 Subject: [PATCH] Add schematic view: generation, symbols, factories and read-only viewer Implements the foundation of the Schematic View feature described in docs/schematic_view_plan.md (Phases 1-3, core/library complete; Swing integration is a read-only viewer with editable tabs still to come). diylc-core - @ComponentDescriptor.schematicFactory attribute, exposed on ComponentType.getSchematicFactoryClass() via ComponentProcessor. - ISchematicFactory / SchematicSymbolMapping SPI (org.diylc.schematic). - SchematicView entity plus a lazy field on Project (getSchematicView / getOrCreateSchematicView), wired into Project.clone()/equals(). The field stays null until the view is first opened, so existing .diy files are unchanged. - INetlistProcessor.getContinuityAreas() (implemented on Presenter). diylc-library - SchematicBox and SchematicWire components (org.diylc.components.schematic). - ManhattanRouter: straight / L / HVH / VHV routing with pin exit-direction preference and overlap/crossing cost scoring. - GenericBoxSchematicFactory fallback, SchematicBuilder (filter, symbol creation, connection-density grid placement, per-net wire chaining) and SchematicSynchronizer (incremental add/remove/keep-position sync). - Concrete factories for resistors, capacitors, diodes, transistors, inductors and potentiometers, wired onto the common physical types. diylc-swing - SchematicMenuPlugin adds "Analyze > Schematic View..." which syncs the schematic and opens SchematicViewFrame. - SchematicViewFrame / SchematicPanel render the SchematicView through the existing DrawingManager pipeline, with zoom, refresh and PNG export. Tests: ManhattanRouterTest, GenericBoxSchematicFactoryTest. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FWD3QJxbr7RuEHxyFEYeEY --- .../java/org/diylc/common/ComponentType.java | 16 +- .../org/diylc/common/INetlistProcessor.java | 10 +- .../src/main/java/org/diylc/core/Project.java | 35 +- .../java/org/diylc/core/SchematicView.java | 183 +++++++ .../core/annotations/ComponentDescriptor.java | 14 +- .../diylc/presenter/ComponentProcessor.java | 7 +- .../java/org/diylc/presenter/Presenter.java | 7 +- .../diylc/schematic/ISchematicFactory.java | 56 +++ .../schematic/SchematicSymbolMapping.java | 103 ++++ .../passive/AxialElectrolyticCapacitor.java | 4 +- .../passive/AxialFilmCapacitor.java | 4 +- .../passive/MiniaturePotentiometer.java | 4 +- .../passive/PotentiometerPanel.java | 4 +- .../passive/RadialCeramicDiskCapacitor.java | 4 +- .../passive/RadialElectrolytic.java | 4 +- .../passive/RadialFilmCapacitor.java | 4 +- .../components/passive/RadialInductor.java | 4 +- .../passive/RadialMicaCapacitor.java | 4 +- .../diylc/components/passive/Resistor.java | 4 +- .../components/passive/TantalumCapacitor.java | 4 +- .../components/passive/ToroidalInductor.java | 4 +- .../passive/TrimmerPotentiometer.java | 4 +- .../components/schematic/SchematicBox.java | 419 ++++++++++++++++ .../components/schematic/SchematicWire.java | 225 +++++++++ .../components/semiconductors/DiodeGlass.java | 4 +- .../semiconductors/DiodePlastic.java | 4 +- .../semiconductors/TransistorTO1.java | 4 +- .../semiconductors/TransistorTO126.java | 4 +- .../semiconductors/TransistorTO220.java | 4 +- .../semiconductors/TransistorTO3.java | 4 +- .../semiconductors/TransistorTO92.java | 4 +- .../AbstractSimpleSchematicFactory.java | 85 ++++ .../schematic/CapacitorSchematicFactory.java | 34 ++ .../schematic/DiodeSchematicFactory.java | 34 ++ .../schematic/GenericBoxSchematicFactory.java | 88 ++++ .../schematic/InductorSchematicFactory.java | 34 ++ .../org/diylc/schematic/ManhattanRouter.java | 235 +++++++++ .../PotentiometerSchematicFactory.java | 43 ++ .../schematic/ResistorSchematicFactory.java | 34 ++ .../org/diylc/schematic/SchematicBuilder.java | 425 ++++++++++++++++ .../schematic/SchematicSynchronizer.java | 186 +++++++ .../schematic/TransistorSchematicFactory.java | 43 ++ .../GenericBoxSchematicFactoryTest.java | 71 +++ .../diylc/schematic/ManhattanRouterTest.java | 64 +++ .../java/org/diylc/swing/gui/MainFrame.java | 1 + .../schematic/SchematicMenuPlugin.java | 90 ++++ .../plugins/schematic/SchematicPanel.java | 160 ++++++ .../plugins/schematic/SchematicViewFrame.java | 150 ++++++ diylc/docs/schematic_view_plan.md | 455 ++++++++++++++++++ 49 files changed, 3360 insertions(+), 27 deletions(-) create mode 100644 diylc/diylc-core/src/main/java/org/diylc/core/SchematicView.java create mode 100644 diylc/diylc-core/src/main/java/org/diylc/schematic/ISchematicFactory.java create mode 100644 diylc/diylc-core/src/main/java/org/diylc/schematic/SchematicSymbolMapping.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/schematic/SchematicBox.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/schematic/SchematicWire.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/schematic/AbstractSimpleSchematicFactory.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/schematic/CapacitorSchematicFactory.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/schematic/DiodeSchematicFactory.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/schematic/GenericBoxSchematicFactory.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/schematic/InductorSchematicFactory.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/schematic/ManhattanRouter.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/schematic/PotentiometerSchematicFactory.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/schematic/ResistorSchematicFactory.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/schematic/SchematicBuilder.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/schematic/SchematicSynchronizer.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/schematic/TransistorSchematicFactory.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/schematic/GenericBoxSchematicFactoryTest.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/schematic/ManhattanRouterTest.java create mode 100644 diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/schematic/SchematicMenuPlugin.java create mode 100644 diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/schematic/SchematicPanel.java create mode 100644 diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/schematic/SchematicViewFrame.java create mode 100644 diylc/docs/schematic_view_plan.md diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/ComponentType.java b/diylc/diylc-core/src/main/java/org/diylc/common/ComponentType.java index 52f9c931..131192bf 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/common/ComponentType.java +++ b/diylc/diylc-core/src/main/java/org/diylc/common/ComponentType.java @@ -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,7 +57,8 @@ public class ComponentType implements Serializable { private boolean enableCache; private List datasheet; private int datasheetCreationStepCount; - + private Class 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 getSchematicFactoryClass() { + return schematicFactoryClass; + } + + public void setSchematicFactoryClass(Class schematicFactoryClass) { + this.schematicFactoryClass = schematicFactoryClass; + } + @Override public String toString() { return name; diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/INetlistProcessor.java b/diylc/diylc-core/src/main/java/org/diylc/common/INetlistProcessor.java index 38944be2..923ffff4 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/common/INetlistProcessor.java +++ b/diylc/diylc-core/src/main/java/org/diylc/common/INetlistProcessor.java @@ -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 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 getContinuityAreas(); + + /** * Finds all available {@link INetlistAnalyzer} implementations. * diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/Project.java b/diylc/diylc-core/src/main/java/org/diylc/core/Project.java index 90c4da51..d77c0cc7 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/Project.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/Project.java @@ -68,6 +68,7 @@ public class Project implements Serializable, Cloneable { private Set> lockedComponents; private Set hiddenLayers; private Font font = DEFAULT_FONT; + private SchematicView schematicView; public Project() { components = new ArrayList>(); @@ -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> cloneMap = new HashMap, IDIYComponent>(); diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/SchematicView.java b/diylc/diylc-core/src/main/java/org/diylc/core/SchematicView.java new file mode 100644 index 00000000..b8dea48f --- /dev/null +++ b/diylc/diylc-core/src/main/java/org/diylc/core/SchematicView.java @@ -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 . + +*/ +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> 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> physicalToSchematicMap; + + private Size width; + private Size height; + private Size gridSpacing; + + public SchematicView() { + this.components = new ArrayList>(); + this.physicalToSchematicMap = new LinkedHashMap>(); + this.width = DEFAULT_WIDTH; + this.height = DEFAULT_HEIGHT; + this.gridSpacing = DEFAULT_GRID_SPACING; + } + + public List> getComponents() { + if (components == null) { + components = new ArrayList>(); + } + return components; + } + + public void setComponents(List> components) { + this.components = components; + } + + public Map> getPhysicalToSchematicMap() { + if (physicalToSchematicMap == null) { + physicalToSchematicMap = new LinkedHashMap>(); + } + return physicalToSchematicMap; + } + + public void setPhysicalToSchematicMap(Map> 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> entry : this.getPhysicalToSchematicMap().entrySet()) { + clone.getPhysicalToSchematicMap().put(entry.getKey(), new ArrayList(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>(getPhysicalToSchematicMap()) + .equals(new HashMap>(other.getPhysicalToSchematicMap())); + } +} diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/annotations/ComponentDescriptor.java b/diylc/diylc-core/src/main/java/org/diylc/core/annotations/ComponentDescriptor.java index f305f7dc..dd5550a5 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/annotations/ComponentDescriptor.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/annotations/ComponentDescriptor.java @@ -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 @@ -123,9 +124,18 @@ public @interface ComponentDescriptor { boolean enableDatasheet() default false; /** - * If datasheet is enabled, this property determines how many columns define a component, e.g. in how many steps we can instantiate a component - * + * If datasheet is enabled, this property determines how many columns define a component, e.g. in how many steps we can instantiate a component + * * @return */ 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 schematicFactory() default ISchematicFactory.class; } diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentProcessor.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentProcessor.java index e967ed40..3ee377f1 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentProcessor.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentProcessor.java @@ -159,8 +159,13 @@ public class ComponentProcessor { } ComponentType componentType = new ComponentType(name, description, creationMethod, category, namePrefix, author, icon, clazz, zOrder, - flexibleZOrder, bomPolicy, autoEdit, transformer, keywordPolicy, keywordTag, cacheDrawing, datasheet, + flexibleZOrder, bomPolicy, autoEdit, transformer, keywordPolicy, keywordTag, cacheDrawing, datasheet, datasheetCreationStepCount); + Class 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; } diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java index f24adfe4..f4fb9580 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java @@ -2883,10 +2883,15 @@ public class Presenter implements IPlugInPort { @Override public List extractNetlists(boolean includeSwitches) throws NetlistException { List continuityAreas = drawingManager.getContinuityAreas(); - + return NetlistBuilder.extractNetlists(includeSwitches, currentProject, continuityAreas); } + @Override + public List getContinuityAreas() { + return drawingManager.getContinuityAreas(); + } + @Override public List getNetlistAnalyzers() { Set> classes; diff --git a/diylc/diylc-core/src/main/java/org/diylc/schematic/ISchematicFactory.java b/diylc/diylc-core/src/main/java/org/diylc/schematic/ISchematicFactory.java new file mode 100644 index 00000000..5da9b62b --- /dev/null +++ b/diylc/diylc-core/src/main/java/org/diylc/schematic/ISchematicFactory.java @@ -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 . + +*/ +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. + * + *

+ * 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. + *

+ * + *

+ * Implementations must have a public no-argument constructor. They are instantiated once and reused, + * so they must be stateless. + *

+ * + * @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 createSchematicSymbols(IDIYComponent physicalComponent); +} diff --git a/diylc/diylc-core/src/main/java/org/diylc/schematic/SchematicSymbolMapping.java b/diylc/diylc-core/src/main/java/org/diylc/schematic/SchematicSymbolMapping.java new file mode 100644 index 00000000..5132a78f --- /dev/null +++ b/diylc/diylc-core/src/main/java/org/diylc/schematic/SchematicSymbolMapping.java @@ -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 . + +*/ +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 pinMapping; + private String sectionLabel; + + public SchematicSymbolMapping() { + this.pinMapping = new HashMap(); + } + + public SchematicSymbolMapping(IDIYComponent schematicSymbol) { + this(); + this.schematicSymbol = schematicSymbol; + } + + public SchematicSymbolMapping(IDIYComponent schematicSymbol, Map pinMapping, + String sectionLabel) { + this.schematicSymbol = schematicSymbol; + this.pinMapping = pinMapping == null ? new HashMap() : 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 getPinMapping() { + if (pinMapping == null) { + pinMapping = new HashMap(); + } + return pinMapping; + } + + public void setPinMapping(Map 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; + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/AxialElectrolyticCapacitor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/AxialElectrolyticCapacitor.java index 2c62d5c1..b782b3c1 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/AxialElectrolyticCapacitor.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/AxialElectrolyticCapacitor.java @@ -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; @@ -45,7 +46,8 @@ import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; -@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, diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/AxialFilmCapacitor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/AxialFilmCapacitor.java index 854218c7..35a78f06 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/AxialFilmCapacitor.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/AxialFilmCapacitor.java @@ -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; @@ -31,7 +32,8 @@ import org.diylc.core.IDatasheetSupport; import org.diylc.core.annotations.ComponentDescriptor; import org.diylc.core.annotations.EditableProperty; -@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, diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/MiniaturePotentiometer.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/MiniaturePotentiometer.java index 04de3bfa..517018f8 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/MiniaturePotentiometer.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/MiniaturePotentiometer.java @@ -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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/PotentiometerPanel.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/PotentiometerPanel.java index 8cdb1850..9b567ad8 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/PotentiometerPanel.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/PotentiometerPanel.java @@ -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.PotentiometerSchematicFactory; import org.diylc.awt.StringUtils; import org.diylc.common.HorizontalAlignment; @@ -55,7 +56,8 @@ import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; -@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 = "Panel mount potentiometer with solder lugs", zOrder = IDIYComponent.COMPONENT, transformer = PotentiometerTransformer.class, enableCache = true) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialCeramicDiskCapacitor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialCeramicDiskCapacitor.java index cce1c1f6..6bf7c8d4 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialCeramicDiskCapacitor.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialCeramicDiskCapacitor.java @@ -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.ComponentDescriptor; @@ -36,7 +37,8 @@ import org.diylc.core.measures.Capacitance; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; -@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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialElectrolytic.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialElectrolytic.java index 4a4f8ab8..396b363a 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialElectrolytic.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialElectrolytic.java @@ -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.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; -@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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialFilmCapacitor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialFilmCapacitor.java index 5fb36976..11878f0d 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialFilmCapacitor.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialFilmCapacitor.java @@ -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; @@ -35,7 +36,8 @@ import org.diylc.core.annotations.EditableProperty; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; -@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, diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialInductor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialInductor.java index 952eb4d0..a8ed36d7 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialInductor.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialInductor.java @@ -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; @@ -44,7 +45,8 @@ import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; -@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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialMicaCapacitor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialMicaCapacitor.java index 31229b86..e837031e 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialMicaCapacitor.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/RadialMicaCapacitor.java @@ -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, diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/Resistor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/Resistor.java index c6c9d2aa..7d6e3db3 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/Resistor.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/Resistor.java @@ -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; @@ -45,7 +46,8 @@ import org.diylc.core.measures.Resistance; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; -@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 implements IGerberComponentSimple { diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/TantalumCapacitor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/TantalumCapacitor.java index 1e188c19..ee8f42e1 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/TantalumCapacitor.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/TantalumCapacitor.java @@ -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; @@ -44,7 +45,8 @@ import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; -@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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/ToroidalInductor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/ToroidalInductor.java index 5573401f..643b7a0a 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/ToroidalInductor.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/ToroidalInductor.java @@ -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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/TrimmerPotentiometer.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/TrimmerPotentiometer.java index cf26800f..3cd33ac1 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/TrimmerPotentiometer.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/TrimmerPotentiometer.java @@ -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; @@ -48,7 +49,8 @@ import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; -@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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/schematic/SchematicBox.java b/diylc/diylc-library/src/main/java/org/diylc/components/schematic/SchematicBox.java new file mode 100644 index 00000000..120635f9 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/schematic/SchematicBox.java @@ -0,0 +1,419 @@ +/* + + 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 . + +*/ +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 { + + 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 nodeLabels; + + public SchematicBox() { + super(); + updateControlPoints(); + } + + private static List parseNodes(String csv) { + List result = new ArrayList(); + 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); + } + + /** + * Rebuilds the control point array so that index 0 keeps its location and every other point is + * placed relative to it. Ordering: left side (top to bottom), right side (top to bottom), top side + * (left to right), bottom side (left to right). + */ + private void updateControlPoints() { + nodeLabels = null; + double spacing = pinSpacing(); + double leadLength = spacing; + Point2D anchor = controlPoints[0]; + double firstPinX = anchor.getX(); + double firstPinY = anchor.getY(); + + double bodyX = firstPinX + leadLength; + double bodyY = firstPinY - spacing; + double bodyW = bodyWidth(); + double bodyH = bodyHeight(); + + List points = new ArrayList(); + // 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)); + controlPoints = points.toArray(new Point2D[0]); + } + + private List getNodeLabels() { + if (nodeLabels == null) { + nodeLabels = new ArrayList(); + 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 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) { + controlPoints[index].setLocation(point); + if (index == 0) { + updateControlPoints(); + } + } + + @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 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; + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/schematic/SchematicWire.java b/diylc/diylc-library/src/main/java/org/diylc/components/schematic/SchematicWire.java new file mode 100644 index 00000000..12bd9a6e --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/schematic/SchematicWire.java @@ -0,0 +1,225 @@ +/* + + 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 . + +*/ +package org.diylc.components.schematic; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.diylc.common.ObjectCache; +import org.diylc.components.AbstractComponent; +import org.diylc.core.ComponentState; +import org.diylc.core.IContinuity; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.VisibilityPolicy; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; + +/** + * Auto-routed Manhattan wire that renders a single connection in the schematic view. The endpoints + * stick to the pins of the schematic symbols they connect; the intermediate way-points are + * calculated by the router and are not meant to be edited by the user. + * + * @author Branislav Stojkovic + */ +@ComponentDescriptor(name = "Schematic Wire", author = "DIYLC", category = "Schematic Symbols", + instanceNamePrefix = "W", description = "Auto-routed Manhattan wire for schematic connections", + zOrder = IDIYComponent.WIRING, bomPolicy = BomPolicy.NEVER_SHOW, autoEdit = false) +public class SchematicWire extends AbstractComponent implements IContinuity { + + private static final long serialVersionUID = 1L; + + public static Color COLOR = Color.black; + + private UUID sourceComponentId; + private int sourcePinIndex; + private UUID targetComponentId; + private int targetPinIndex; + + private List routePoints; + private Color color = COLOR; + private int thickness = 1; + + public SchematicWire() { + super(); + routePoints = new ArrayList(); + routePoints.add(new Point2D.Double(0, 0)); + routePoints.add(new Point2D.Double(0, 0)); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) + return; + List points = getRoutePoints(); + if (points.size() < 2) + return; + g2d.setColor( + componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR + : color); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(thickness)); + GeneralPath path = new GeneralPath(); + path.moveTo(points.get(0).getX(), points.get(0).getY()); + for (int i = 1; i < points.size(); i++) { + path.lineTo(points.get(i).getX(), points.get(i).getY()); + } + g2d.draw(path); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.drawLine(2, height / 3, width / 2, height / 3); + g2d.drawLine(width / 2, height / 3, width / 2, height * 2 / 3); + g2d.drawLine(width / 2, height * 2 / 3, width - 2, height * 2 / 3); + } + + public List getRoutePoints() { + if (routePoints == null) + routePoints = new ArrayList(); + return routePoints; + } + + public void setRoutePoints(List routePoints) { + this.routePoints = routePoints; + } + + @Override + public int getControlPointCount() { + return getRoutePoints().size(); + } + + @Override + public Point2D getControlPoint(int index) { + return getRoutePoints().get(index); + } + + @Override + public void setControlPoint(Point2D point, int index) { + getRoutePoints().get(index).setLocation(point); + } + + @Override + public boolean isControlPointSticky(int index) { + // only the two endpoints attach to symbol pins + return index == 0 || index == getRoutePoints().size() - 1; + } + + @Override + public VisibilityPolicy getControlPointVisibilityPolicy(int index) { + return VisibilityPolicy.NEVER; + } + + @Override + public boolean canPointMoveFreely(int pointIndex) { + return false; + } + + @Override + public String getControlPointNodeName(int index) { + // wires are continuity, not graph nodes + return null; + } + + @Override + public boolean arePointsConnected(int index1, int index2) { + return true; + } + + public UUID getSourceComponentId() { + return sourceComponentId; + } + + public void setSourceComponentId(UUID sourceComponentId) { + this.sourceComponentId = sourceComponentId; + } + + public int getSourcePinIndex() { + return sourcePinIndex; + } + + public void setSourcePinIndex(int sourcePinIndex) { + this.sourcePinIndex = sourcePinIndex; + } + + public UUID getTargetComponentId() { + return targetComponentId; + } + + public void setTargetComponentId(UUID targetComponentId) { + this.targetComponentId = targetComponentId; + } + + public int getTargetPinIndex() { + return targetPinIndex; + } + + public void setTargetPinIndex(int targetPinIndex) { + this.targetPinIndex = targetPinIndex; + } + + @EditableProperty + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } + + @EditableProperty + public int getThickness() { + return thickness; + } + + public void setThickness(int thickness) { + this.thickness = thickness; + } + + @Override + public Void getValue() { + return null; + } + + @Override + public void setValue(Void value) {} + + @Override + public IDIYComponent clone() throws CloneNotSupportedException { + SchematicWire clone = (SchematicWire) super.clone(); + List pointsCopy = new ArrayList(); + for (Point2D p : getRoutePoints()) { + pointsCopy.add(new Point2D.Double(p.getX(), p.getY())); + } + clone.routePoints = pointsCopy; + return clone; + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodeGlass.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodeGlass.java index 82bcf1b8..87b1c4ea 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodeGlass.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodeGlass.java @@ -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; @@ -40,7 +41,8 @@ import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; -@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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodePlastic.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodePlastic.java index 7a552db0..6d003528 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodePlastic.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodePlastic.java @@ -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; @@ -40,7 +41,8 @@ import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; -@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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO1.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO1.java index 4d9631ea..d032d1b2 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO1.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO1.java @@ -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; @@ -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-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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO126.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO126.java index 1a4e275b..81b81716 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO126.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO126.java @@ -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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO220.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO220.java index a6f9345c..18bafb50 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO220.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO220.java @@ -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-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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO3.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO3.java index 3d4deba0..c244dbbb 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO3.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO3.java @@ -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 { diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO92.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO92.java index c126a6b6..4e209c4c 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO92.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/TransistorTO92.java @@ -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; @@ -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-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) diff --git a/diylc/diylc-library/src/main/java/org/diylc/schematic/AbstractSimpleSchematicFactory.java b/diylc/diylc-library/src/main/java/org/diylc/schematic/AbstractSimpleSchematicFactory.java new file mode 100644 index 00000000..22ef28fe --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/schematic/AbstractSimpleSchematicFactory.java @@ -0,0 +1,85 @@ +/* + + 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 . + +*/ +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 createSchematicSymbols(IDIYComponent physicalComponent) { + IDIYComponent symbol = createSymbol(); + symbol.setId(UUID.randomUUID()); + symbol.setName(physicalComponent.getName()); + copyValue(physicalComponent, symbol); + + List physicalPins = new ArrayList(); + for (int i = 0; i < physicalComponent.getControlPointCount() + && physicalPins.size() < electricalPinCount(); i++) { + if (physicalComponent.isControlPointSticky(i) + && physicalComponent.getControlPointNodeName(i) != null) { + physicalPins.add(i); + } + } + + Map pinMapping = new HashMap(); + for (int schematicIndex = 0; schematicIndex < physicalPins.size() + && schematicIndex < symbol.getControlPointCount(); schematicIndex++) { + pinMapping.put(physicalPins.get(schematicIndex), schematicIndex); + } + + List result = new ArrayList(); + result.add(new SchematicSymbolMapping(symbol, pinMapping, null)); + return result; + } + + @SuppressWarnings("unchecked") + protected void copyValue(IDIYComponent physical, IDIYComponent symbol) { + try { + ((IDIYComponent) symbol).setValue(physical.getValue()); + } catch (Throwable t) { + // symbol value type differs from the physical component's; skip + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/schematic/CapacitorSchematicFactory.java b/diylc/diylc-library/src/main/java/org/diylc/schematic/CapacitorSchematicFactory.java new file mode 100644 index 00000000..5cfa8d42 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/schematic/CapacitorSchematicFactory.java @@ -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 . + +*/ +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(); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/schematic/DiodeSchematicFactory.java b/diylc/diylc-library/src/main/java/org/diylc/schematic/DiodeSchematicFactory.java new file mode 100644 index 00000000..d8604876 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/schematic/DiodeSchematicFactory.java @@ -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 . + +*/ +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(); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/schematic/GenericBoxSchematicFactory.java b/diylc/diylc-library/src/main/java/org/diylc/schematic/GenericBoxSchematicFactory.java new file mode 100644 index 00000000..8c107b5a --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/schematic/GenericBoxSchematicFactory.java @@ -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 . + +*/ +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 createSchematicSymbols(IDIYComponent physicalComponent) { + List pinIndices = new ArrayList(); + List labels = new ArrayList(); + 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 left = labels.subList(0, half); + List 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 pinMapping = new HashMap(); + for (int k = 0; k < pinIndices.size(); k++) { + pinMapping.put(pinIndices.get(k), k); + } + + List result = new ArrayList(); + result.add(new SchematicSymbolMapping(box, pinMapping, null)); + return result; + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/schematic/InductorSchematicFactory.java b/diylc/diylc-library/src/main/java/org/diylc/schematic/InductorSchematicFactory.java new file mode 100644 index 00000000..57b9b222 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/schematic/InductorSchematicFactory.java @@ -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 . + +*/ +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(); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/schematic/ManhattanRouter.java b/diylc/diylc-library/src/main/java/org/diylc/schematic/ManhattanRouter.java new file mode 100644 index 00000000..701a9bc8 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/schematic/ManhattanRouter.java @@ -0,0 +1,235 @@ +/* + + 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 . + +*/ +package org.diylc.schematic; + +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Computes Manhattan-style (axis-aligned) routes between two schematic pins. The router decides + * whether a straight horizontal or vertical segment is enough, or whether the connection needs a + * single bend (L shape) or a double bend (horizontal-vertical-horizontal or + * vertical-horizontal-vertical). Among the candidate routes it picks the one that overlaps and + * crosses existing wires the least. + * + * @author Branislav Stojkovic + */ +public class ManhattanRouter { + + /** Coordinates closer than this are considered aligned. */ + public static final double EPS = 1.5; + + public enum Direction { + LEFT, RIGHT, UP, DOWN, NONE + } + + private ManhattanRouter() {} + + public static List route(Point2D start, Point2D end) { + return route(start, end, Direction.NONE, Direction.NONE, Collections.emptyList(), 0); + } + + /** + * @param start start pin location + * @param end end pin location + * @param startDir direction the wire should leave the start pin, or {@link Direction#NONE} + * @param endDir direction the wire should leave the end pin, or {@link Direction#NONE} + * @param obstacles existing wire segments to avoid overlapping/crossing + * @param grid grid step used to generate alternative bend positions; 0 disables it + * @return ordered list of way-points, first equal to {@code start}, last equal to {@code end} + */ + public static List route(Point2D start, Point2D end, Direction startDir, Direction endDir, + Collection obstacles, double grid) { + double dx = end.getX() - start.getX(); + double dy = end.getY() - start.getY(); + + if (Math.abs(dx) < EPS && Math.abs(dy) < EPS) { + return list(start, end); + } + if (Math.abs(dy) < EPS) { + return list(start, new Point2D.Double(end.getX(), start.getY())); + } + if (Math.abs(dx) < EPS) { + return list(start, new Point2D.Double(start.getX(), end.getY())); + } + + List> candidates = new ArrayList>(); + + // L shapes + candidates.add(list(start, new Point2D.Double(end.getX(), start.getY()), end)); + candidates.add(list(start, new Point2D.Double(start.getX(), end.getY()), end)); + + // HVH with a vertical middle leg at x = bx + for (double bx : bendCoordinates(start.getX(), end.getX(), grid)) { + candidates.add(list(start, new Point2D.Double(bx, start.getY()), new Point2D.Double(bx, end.getY()), end)); + } + // VHV with a horizontal middle leg at y = by + for (double by : bendCoordinates(start.getY(), end.getY(), grid)) { + candidates.add(list(start, new Point2D.Double(start.getX(), by), new Point2D.Double(end.getX(), by), end)); + } + + List best = null; + double bestCost = Double.MAX_VALUE; + for (List candidate : candidates) { + double cost = cost(candidate, startDir, endDir, obstacles); + if (cost < bestCost) { + bestCost = cost; + best = candidate; + } + } + return best == null ? list(start, new Point2D.Double(end.getX(), start.getY()), end) : dedupe(best); + } + + private static List bendCoordinates(double a, double b, double grid) { + List result = new ArrayList(); + result.add((a + b) / 2); + result.add((a * 3 + b) / 4); + result.add((a + b * 3) / 4); + if (grid > 0) { + double mid = (a + b) / 2; + for (int k = 1; k <= 3; k++) { + result.add(mid + k * grid); + result.add(mid - k * grid); + } + } + return result; + } + + private static double cost(List path, Direction startDir, Direction endDir, + Collection obstacles) { + double length = 0; + int bends = Math.max(0, path.size() - 2); + double crossings = 0; + double overlap = 0; + for (int i = 0; i < path.size() - 1; i++) { + Point2D p1 = path.get(i); + Point2D p2 = path.get(i + 1); + length += p1.distance(p2); + Line2D seg = new Line2D.Double(p1, p2); + for (Line2D obs : obstacles) { + if (segmentsOverlap(seg, obs)) { + overlap += overlapLength(seg, obs); + } else if (seg.intersectsLine(obs)) { + crossings++; + } + } + } + double dirPenalty = 0; + if (startDir != Direction.NONE && path.size() >= 2 + && !matchesDirection(path.get(0), path.get(1), startDir)) { + dirPenalty += 40; + } + if (endDir != Direction.NONE && path.size() >= 2 + && !matchesDirection(path.get(path.size() - 1), path.get(path.size() - 2), endDir)) { + dirPenalty += 40; + } + return length * 0.01 + bends * 6 + crossings * 15 + overlap * 0.5 + dirPenalty; + } + + private static boolean matchesDirection(Point2D from, Point2D to, Direction dir) { + double dx = to.getX() - from.getX(); + double dy = to.getY() - from.getY(); + switch (dir) { + case LEFT: + return dx < -EPS && Math.abs(dy) < EPS; + case RIGHT: + return dx > EPS && Math.abs(dy) < EPS; + case UP: + return dy < -EPS && Math.abs(dx) < EPS; + case DOWN: + return dy > EPS && Math.abs(dx) < EPS; + default: + return true; + } + } + + private static boolean isHorizontal(Line2D l) { + return Math.abs(l.getY1() - l.getY2()) < EPS; + } + + private static boolean isVertical(Line2D l) { + return Math.abs(l.getX1() - l.getX2()) < EPS; + } + + private static boolean segmentsOverlap(Line2D a, Line2D b) { + if (isHorizontal(a) && isHorizontal(b) && Math.abs(a.getY1() - b.getY1()) < EPS) { + return rangesOverlap(a.getX1(), a.getX2(), b.getX1(), b.getX2()); + } + if (isVertical(a) && isVertical(b) && Math.abs(a.getX1() - b.getX1()) < EPS) { + return rangesOverlap(a.getY1(), a.getY2(), b.getY1(), b.getY2()); + } + return false; + } + + private static double overlapLength(Line2D a, Line2D b) { + if (isHorizontal(a)) { + return rangeOverlap(a.getX1(), a.getX2(), b.getX1(), b.getX2()); + } + return rangeOverlap(a.getY1(), a.getY2(), b.getY1(), b.getY2()); + } + + private static boolean rangesOverlap(double a1, double a2, double b1, double b2) { + return rangeOverlap(a1, a2, b1, b2) > EPS; + } + + private static double rangeOverlap(double a1, double a2, double b1, double b2) { + double lo = Math.max(Math.min(a1, a2), Math.min(b1, b2)); + double hi = Math.min(Math.max(a1, a2), Math.max(b1, b2)); + return Math.max(0, hi - lo); + } + + private static List list(Point2D... points) { + List result = new ArrayList(); + for (Point2D p : points) { + result.add(new Point2D.Double(p.getX(), p.getY())); + } + return result; + } + + private static List dedupe(List points) { + List result = new ArrayList(); + for (Point2D p : points) { + if (result.isEmpty() || result.get(result.size() - 1).distance(p) > EPS) { + result.add(p); + } + } + // drop collinear middle points + int i = 1; + while (i < result.size() - 1) { + Point2D a = result.get(i - 1); + Point2D b = result.get(i); + Point2D c = result.get(i + 1); + boolean collinearH = Math.abs(a.getY() - b.getY()) < EPS && Math.abs(b.getY() - c.getY()) < EPS; + boolean collinearV = Math.abs(a.getX() - b.getX()) < EPS && Math.abs(b.getX() - c.getX()) < EPS; + if (collinearH || collinearV) { + result.remove(i); + } else { + i++; + } + } + return result; + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/schematic/PotentiometerSchematicFactory.java b/diylc/diylc-library/src/main/java/org/diylc/schematic/PotentiometerSchematicFactory.java new file mode 100644 index 00000000..9fd7a24a --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/schematic/PotentiometerSchematicFactory.java @@ -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 . + +*/ +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; + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/schematic/ResistorSchematicFactory.java b/diylc/diylc-library/src/main/java/org/diylc/schematic/ResistorSchematicFactory.java new file mode 100644 index 00000000..87f25528 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/schematic/ResistorSchematicFactory.java @@ -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 . + +*/ +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(); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/schematic/SchematicBuilder.java b/diylc/diylc-library/src/main/java/org/diylc/schematic/SchematicBuilder.java new file mode 100644 index 00000000..91f5eae9 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/schematic/SchematicBuilder.java @@ -0,0 +1,425 @@ +/* + + 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 . + +*/ +package org.diylc.schematic; + +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.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.components.schematic.SchematicWire; +import org.diylc.core.IContinuity; +import org.diylc.core.ICommonNode; +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.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 auto-routed {@link SchematicWire}s. + * + * @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; + + private final Map, ISchematicFactory> factoryCache = + new HashMap, ISchematicFactory>(); + private final GenericBoxSchematicFactory genericFactory = new GenericBoxSchematicFactory(); + + /** + * 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 pinMapping; + + SymbolEntry(IDIYComponent symbol, Map pinMapping) { + this.symbol = symbol; + this.pinMapping = pinMapping; + } + } + + /** + * 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 continuityAreas) { + Netlist netlist = extractNetlist(project, continuityAreas); + + List> schematicComponents = new ArrayList>(); + Map> physicalToSchematicMap = new LinkedHashMap>(); + Map> entriesByPhysicalId = new LinkedHashMap>(); + + List> eligible = new ArrayList>(); + for (IDIYComponent physical : project.getComponents()) { + if (!isEligible(physical)) { + continue; + } + eligible.add(physical); + List entries = createSymbols(physical); + if (entries.isEmpty()) { + continue; + } + List symbolIds = new ArrayList(); + 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); + + List wires = createWires(netlist, entriesByPhysicalId); + schematicComponents.addAll(wires); + + schematicComponents.sort(Comparator.comparingDouble(SchematicBuilder::zOrderOf)); + + SchematicView view = project.getOrCreateSchematicView(); + view.getComponents().clear(); + view.getComponents().addAll(schematicComponents); + view.setPhysicalToSchematicMap(physicalToSchematicMap); + resizeCanvasToFit(view); + } + + Netlist extractNetlist(Project project, List continuityAreas) { + try { + List netlists = NetlistBuilder.extractNetlists(false, project, + continuityAreas == null ? new ArrayList() : 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) { + return true; // ground / common node symbols pass through + } + 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 */ + + @SuppressWarnings("unchecked") + List createSymbols(IDIYComponent physical) { + if (physical instanceof ICommonNode) { + try { + IDIYComponent clone = physical.clone(); + clone.setId(UUID.randomUUID()); + Map identity = new HashMap(); + for (int i = 0; i < physical.getControlPointCount(); i++) { + identity.put(i, i); + } + List list = new ArrayList(); + list.add(new SymbolEntry(clone, identity)); + return list; + } catch (CloneNotSupportedException e) { + LOG.warn("Could not clone common-node component " + physical.getName(), e); + return new ArrayList(); + } + } + + ISchematicFactory factory = resolveFactory(physical); + List 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 entries = new ArrayList(); + 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>) physical.getClass()); + Class 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> eligible, + Map> entriesByPhysicalId, Netlist netlist) { + Map degree = connectionDegree(netlist); + + List> ordered = new ArrayList>(eligible); + ordered.sort(Comparator.comparingInt((IDIYComponent c) -> -degree.getOrDefault(c.getId(), 0))); + + int count = 0; + for (IDIYComponent physical : ordered) { + List entries = entriesByPhysicalId.get(physical.getId()); + if (entries == null) { + continue; + } + placeAtGridSlot(entries, count, ordered.size()); + count++; + } + } + + /** Places every symbol of {@code entries} into grid cell {@code slot} of {@code totalSlots}. */ + static void placeAtGridSlot(List 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++; + } + } + + private static Map connectionDegree(Netlist netlist) { + Map degree = new HashMap(); + if (netlist == null) { + return degree; + } + for (Group group : netlist.getGroups()) { + for (Node node : group.getNodes()) { + UUID id = node.getComponent().getId(); + degree.merge(id, group.getNodes().size() - 1, Integer::sum); + } + } + return degree; + } + + /** 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) { + if (component.getControlPointCount() == 0) { + return; + } + Point2D anchor = component.getControlPoint(0); + double dx = targetX - anchor.getX(); + double dy = targetY - anchor.getY(); + for (int i = 0; i < component.getControlPointCount(); i++) { + Point2D p = component.getControlPoint(i); + component.setControlPoint(new Point2D.Double(p.getX() + dx, p.getY() + dy), i); + } + } + + private static double snap(double value) { + return Math.round(value / GRID) * GRID; + } + + /* ------------------------------------------------------------------ wiring */ + + List createWires(Netlist netlist, + Map> entriesByPhysicalId) { + List wires = new ArrayList(); + if (netlist == null) { + return wires; + } + List obstacles = new ArrayList(); + + for (Group group : netlist.getSortedGroups()) { + List pins = new ArrayList(); + for (Node node : group.getNodes()) { + UUID physicalId = node.getComponent().getId(); + List 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()))); + } + } + 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); + SchematicWire wire = new SchematicWire(); + wire.setId(UUID.randomUUID()); + wire.setSourceComponentId(a.symbol.getId()); + wire.setSourcePinIndex(a.pinIndex); + wire.setTargetComponentId(b.symbol.getId()); + wire.setTargetPinIndex(b.pinIndex); + List route = ManhattanRouter.route(a.location, b.location, exitDirection(a), + exitDirection(b), obstacles, GRID); + wire.setRoutePoints(route); + for (int s = 0; s < route.size() - 1; s++) { + obstacles.add(new Line2D.Double(route.get(s), route.get(s + 1))); + } + wires.add(wire); + } + } + return wires; + } + + 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; + } + } + + private static ManhattanRouter.Direction exitDirection(Pin pin) { + double cx = 0; + double cy = 0; + int n = pin.symbol.getControlPointCount(); + if (n == 0) { + return ManhattanRouter.Direction.NONE; + } + for (int i = 0; i < n; i++) { + cx += pin.symbol.getControlPoint(i).getX(); + cy += pin.symbol.getControlPoint(i).getY(); + } + cx /= n; + cy /= n; + double dx = pin.location.getX() - cx; + double dy = pin.location.getY() - cy; + if (Math.abs(dx) < 1 && Math.abs(dy) < 1) { + return ManhattanRouter.Direction.NONE; + } + if (Math.abs(dx) >= Math.abs(dy)) { + return dx < 0 ? ManhattanRouter.Direction.LEFT : ManhattanRouter.Direction.RIGHT; + } + return dy < 0 ? ManhattanRouter.Direction.UP : ManhattanRouter.Direction.DOWN; + } + + /* ------------------------------------------------------------------ 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 org.diylc.core.measures.Size(maxX, org.diylc.core.measures.SizeUnit.px)); + } + if (maxY > view.getHeight().convertToPixels()) { + view.setHeight(new org.diylc.core.measures.Size(maxY, org.diylc.core.measures.SizeUnit.px)); + } + } + + static double zOrderOf(IDIYComponent component) { + ComponentType type = ComponentProcessor.getInstance() + .extractComponentTypeFrom((Class>) 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 continuityAreas) { + new SchematicBuilder().build(project, + continuityAreas == null ? new ArrayList() + : new ArrayList(continuityAreas)); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/schematic/SchematicSynchronizer.java b/diylc/diylc-library/src/main/java/org/diylc/schematic/SchematicSynchronizer.java new file mode 100644 index 00000000..1de8e23a --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/schematic/SchematicSynchronizer.java @@ -0,0 +1,186 @@ +/* + + 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 . + +*/ +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.schematic.SchematicWire; +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. + * + *
    + *
  • Components added to the layout get fresh symbols placed in a free grid cell.
  • + *
  • Components removed from the layout lose their symbols (and the wires attached to them).
  • + *
  • Symbols that survive keep their position, rotation and mirroring; only name and value are + * refreshed.
  • + *
  • All wires are regenerated from the current netlist.
  • + *
+ * + * @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. + */ + public void synchronize(Project project, List continuityAreas) { + SchematicView view = project.getSchematicView(); + if (view == null || !view.isGenerated()) { + builder.build(project, continuityAreas == null ? new ArrayList() : continuityAreas); + return; + } + + Map> map = view.getPhysicalToSchematicMap(); + Map> symbolsById = new HashMap>(); + for (IDIYComponent component : view.getComponents()) { + if (!(component instanceof SchematicWire)) { + symbolsById.put(component.getId(), component); + } + } + + Map> layoutById = new LinkedHashMap>(); + for (IDIYComponent component : project.getComponents()) { + layoutById.put(component.getId(), component); + } + + List> keptSymbols = new ArrayList>(); + Map> entriesByPhysicalId = new LinkedHashMap>(); + Map> newMap = new LinkedHashMap>(); + + int occupiedSlots = map.size(); + + for (Map.Entry> layoutEntry : layoutById.entrySet()) { + UUID physicalId = layoutEntry.getKey(); + IDIYComponent physical = layoutEntry.getValue(); + if (!SchematicBuilder.isEligible(physical)) { + continue; + } + + List freshEntries = builder.createSymbols(physical); + if (freshEntries.isEmpty()) { + continue; + } + + List existingIds = map.get(physicalId); + List resolvedEntries = new ArrayList(); + List resolvedIds = new ArrayList(); + + 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()) { + SchematicBuilder.placeAtGridSlot( + resolvedEntries.subList(existingIds.size(), resolvedEntries.size()), occupiedSlots++, + occupiedSlots + 1); + } + } else { + // new component + resolvedEntries.addAll(freshEntries); + for (SymbolEntry e : freshEntries) { + resolvedIds.add(e.symbol.getId()); + } + 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 removed = new HashSet(map.keySet()); + removed.removeAll(newMap.keySet()); + if (!removed.isEmpty()) { + LOG.debug("Removing schematic symbols for " + removed.size() + " deleted component(s)"); + } + + Netlist netlist = builder.extractNetlist(project, + continuityAreas == null ? new ArrayList() : continuityAreas); + List wires = builder.createWires(netlist, entriesByPhysicalId); + + List> result = new ArrayList>(keptSymbols); + result.addAll(wires); + result.sort(java.util.Comparator.comparingDouble(SchematicBuilder::zOrderOf)); + + 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 target = (IDIYComponent) 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 continuityAreas, + boolean unused) { + new SchematicSynchronizer().synchronize(project, + continuityAreas == null ? new ArrayList() + : new ArrayList(continuityAreas)); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/schematic/TransistorSchematicFactory.java b/diylc/diylc-library/src/main/java/org/diylc/schematic/TransistorSchematicFactory.java new file mode 100644 index 00000000..eddb2b34 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/schematic/TransistorSchematicFactory.java @@ -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 . + +*/ +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; + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/schematic/GenericBoxSchematicFactoryTest.java b/diylc/diylc-library/src/test/java/org/diylc/schematic/GenericBoxSchematicFactoryTest.java new file mode 100644 index 00000000..8ec53ba9 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/schematic/GenericBoxSchematicFactoryTest.java @@ -0,0 +1,71 @@ +/* + 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 . +*/ +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.semiconductors.ICSymbol; +import org.diylc.components.schematic.SchematicBox; +import org.junit.Test; + +public class GenericBoxSchematicFactoryTest { + + @Test + public void producesOneBoxWithOnePinPerStickyNode() { + ICSymbol physical = new ICSymbol(); + physical.setName("IC1"); + physical.setValue("TL072"); + + List 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 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)); + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/schematic/ManhattanRouterTest.java b/diylc/diylc-library/src/test/java/org/diylc/schematic/ManhattanRouterTest.java new file mode 100644 index 00000000..22aa9b07 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/schematic/ManhattanRouterTest.java @@ -0,0 +1,64 @@ +/* + 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 . +*/ +package org.diylc.schematic; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.awt.geom.Point2D; +import java.util.List; + +import org.junit.Test; + +public class ManhattanRouterTest { + + @Test + public void straightHorizontalWhenAligned() { + List route = + ManhattanRouter.route(new Point2D.Double(0, 10), new Point2D.Double(100, 10)); + assertEquals(2, route.size()); + assertEquals(10d, route.get(1).getY(), 0.001); + } + + @Test + public void straightVerticalWhenAligned() { + List route = + ManhattanRouter.route(new Point2D.Double(20, 0), new Point2D.Double(20, 80)); + assertEquals(2, route.size()); + assertEquals(20d, route.get(1).getX(), 0.001); + } + + @Test + public void bendsForDiagonalOffset() { + List route = + ManhattanRouter.route(new Point2D.Double(0, 0), new Point2D.Double(100, 60)); + assertTrue("expected at least one bend", route.size() >= 3); + // every segment must be axis-aligned + for (int i = 0; i < route.size() - 1; i++) { + Point2D a = route.get(i); + Point2D b = route.get(i + 1); + boolean axisAligned = Math.abs(a.getX() - b.getX()) < ManhattanRouter.EPS + || Math.abs(a.getY() - b.getY()) < ManhattanRouter.EPS; + assertTrue("segment " + i + " is not axis-aligned", axisAligned); + } + assertEquals(0d, route.get(0).distance(new Point2D.Double(0, 0)), 0.001); + assertEquals(0d, route.get(route.size() - 1).distance(new Point2D.Double(100, 60)), 0.001); + } +} diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/gui/MainFrame.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/gui/MainFrame.java index 3c272918..f35edd02 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/gui/MainFrame.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/gui/MainFrame.java @@ -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)); diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/schematic/SchematicMenuPlugin.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/schematic/SchematicMenuPlugin.java new file mode 100644 index 00000000..f81af544 --- /dev/null +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/schematic/SchematicMenuPlugin.java @@ -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 . + +*/ +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 getSubscribedEventTypes() { + return EnumSet.noneOf(EventType.class); + } + + @Override + public void processMessage(EventType eventType, Object... params) { + // no-op + } +} diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/schematic/SchematicPanel.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/schematic/SchematicPanel.java new file mode 100644 index 00000000..5aa3be28 --- /dev/null +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/schematic/SchematicPanel.java @@ -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 . + +*/ +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. + * + *

+ * 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. + *

+ * + * @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(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 continuityAreas) { + try { + new SchematicSynchronizer().synchronize(layoutProject, + continuityAreas == null ? Collections.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 options = EnumSet.of(DrawOption.ANTIALIASING, DrawOption.ZOOM, DrawOption.GRID, + DrawOption.EXTRA_SPACE); + Set> empty = Collections.emptySet(); + drawingManager.drawProject(g2d, schematicProject, options, null, null, empty, empty, empty, + Arrays.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; + } +} diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/schematic/SchematicViewFrame.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/schematic/SchematicViewFrame.java new file mode 100644 index 00000000..12d9a438 --- /dev/null +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/schematic/SchematicViewFrame.java @@ -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 . + +*/ +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 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 continuityAreas) { + this.layoutProject = layoutProject; + this.continuityAreas = continuityAreas; + panel.refresh(layoutProject, continuityAreas); + setVisible(true); + toFront(); + } + + private List 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); + } + } +} diff --git a/diylc/docs/schematic_view_plan.md b/diylc/docs/schematic_view_plan.md new file mode 100644 index 00000000..a13a8efe --- /dev/null +++ b/diylc/docs/schematic_view_plan.md @@ -0,0 +1,455 @@ +# DIYLC Schematic View — Feature Specification + +## 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
List<IDIYComponent>] + NB[NetlistBuilder] + LP --> NB + end + + subgraph "Schematic Generation" + SB[SchematicBuilder
Initial generation] + SS[SchematicSynchronizer
Incremental sync] + SF[SchematicFactory
per component type] + GBF[GenericBoxFactory
default fallback] + NB --> SB + NB --> SS + SB --> SF + SB --> GBF + SS --> SF + end + + subgraph "Schematic Side" + SV["SchematicView
(embedded in Project)"] + SP[Schematic Presenter
Restricted mode] + DM[DrawingManager
Existing pipeline] + SB --> SV + SS --> SV + SV --> SP + SP --> DM + end + + subgraph "UI" + TAB["Tab Bar
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> components; + + // Mapping: physical component UUID → list of schematic symbol UUIDs + // (1:N for multi-section components like dual triodes/opamps) + private Map> 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 { ... } +``` + +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 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 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 { + 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 implements IContinuity { + private UUID sourceComponentId; + private int sourcePinIndex; + private UUID targetComponentId; + private int targetPinIndex; + private List 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. +- `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). +- 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** +- `SchematicMenuPlugin` adds *Analyze → Schematic View…*, which runs `SchematicSynchronizer` on the current project and opens `SchematicViewFrame`. +- `SchematicViewFrame` / `SchematicPanel` — read-only viewer that renders the generated `SchematicView` through the existing `DrawingManager` pipeline, with zoom, refresh and PNG export. + +Unit tests: `ManhattanRouterTest`, `GenericBoxSchematicFactoryTest`. + +### Not yet done (follow-ups) + +- Bottom-left Excel-style tab bar and an editable, **restricted second `Presenter`** for the schematic (drag / multi-select / rotate / flip on symbols, drag-to-move re-routing on mouse release). The current viewer is read-only. +- 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.