mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-13 10:42:09 +10:00
Building blocks (#1072)
* composite block support * naming * Added ability to set node name (or names) for some connectivity components * cleanup * Include building blocks in the Recently Used category * Prompt to confirm the overwrite when saving over the existing building blocks; When saving building blocks, present a drop down list with most recently used building blocks * add alpha to composite * Update CompositeComponentTest.java * cleanup * Update CompositeComponent.java * cleanup * Update CompositeComponent.java * <org.diylc.appframework.update.Change> <changeType>IMPROVEMENT</changeType> <description>When saving building blocks, present a drop down list with most recently used building blocks for quick selection</description> </org.diylc.appframework.update.Change> * Diode components now label their terminal nodes as 'A' (anode) and 'K' (cathode) instead of '1' and '2' * Capacitor components now label their terminal nodes as '+' and '-' instead of '1' and '2' when 'Polarized' property is ticked
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.common;
|
||||
|
||||
/**
|
||||
* The two ways a saved {@link BuildingBlock} can be placed on the canvas.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public enum BlockInstantiationMode {
|
||||
|
||||
/**
|
||||
* The block's components are cloned as individual, independently selectable components and
|
||||
* grouped together (the original, and only, behavior before composite mode was introduced).
|
||||
*/
|
||||
GROUP,
|
||||
|
||||
/**
|
||||
* The block's components are cloned and wrapped in a single
|
||||
* {@code org.diylc.components.composite.CompositeComponent} that behaves as one rigid
|
||||
* component with its own name, aggregating the control points of all its children.
|
||||
*/
|
||||
COMPOSITE
|
||||
}
|
||||
@@ -56,14 +56,14 @@ public class ComponentType implements Serializable {
|
||||
private boolean enableCache;
|
||||
private List<String[]> datasheet;
|
||||
private int datasheetCreationStepCount;
|
||||
|
||||
public ComponentType() {
|
||||
|
||||
public ComponentType() {
|
||||
}
|
||||
|
||||
public ComponentType(String name, String description, CreationMethod creationMethod, String category,
|
||||
String namePrefix, String author, Icon icon, Class<? extends IDIYComponent<?>> instanceClass, double zOrder,
|
||||
boolean flexibleZOrder, BomPolicy bomPolicy, boolean autoEdit, IComponentTransformer transformer,
|
||||
KeywordPolicy keywordPolicy, String keywordTag, boolean enableCache, List<String[]> datasheet,
|
||||
boolean flexibleZOrder, BomPolicy bomPolicy, boolean autoEdit, IComponentTransformer transformer,
|
||||
KeywordPolicy keywordPolicy, String keywordTag, boolean enableCache, List<String[]> datasheet,
|
||||
int datasheetCreationStepCount) {
|
||||
super();
|
||||
this.name = name;
|
||||
|
||||
@@ -25,13 +25,28 @@ import java.io.IOException;
|
||||
|
||||
public interface IBlockProcessor {
|
||||
|
||||
public static final String BLOCKS_KEY = "buildingBlocks";
|
||||
public static final String BLOCKS_KEY = "buildingBlocks";
|
||||
public static final String DEFAULT_BLOCKS_IMPORTED_KEY = "defaultBlocksImported";
|
||||
|
||||
/**
|
||||
* Prefix that marks a string as a reference to a building block rather than a component class
|
||||
* name. Used wherever components and blocks share a single identifier list, such as keyboard
|
||||
* shortcuts ({@code COMPONENT_SHORTCUT_KEY}) and the recently used list
|
||||
* ({@link org.diylc.common.IPlugInPort#RECENT_COMPONENTS_KEY}). A block identifier looks like
|
||||
* {@code "block:My Block Name"}.
|
||||
*/
|
||||
public static final String BLOCK_PREFIX = "block:";
|
||||
|
||||
void saveSelectionAsBlock(String blockName);
|
||||
|
||||
void loadBlock(String blockName) throws InvalidBlockException;
|
||||
|
||||
/**
|
||||
* Loads the block in the given {@link BlockInstantiationMode}. {@link BlockInstantiationMode#COMPOSITE}
|
||||
* (a single rigid component) is the default mode used by most call sites (see design decision D10 in
|
||||
* {@code docs/plans/composite-building-blocks.md}); {@link BlockInstantiationMode#GROUP} instantiates
|
||||
* the block as a group of loose components.
|
||||
*/
|
||||
void loadBlock(String blockName, BlockInstantiationMode mode) throws InvalidBlockException;
|
||||
|
||||
void deleteBlock(String blockName);
|
||||
|
||||
int importBlocks(String fileName) throws IOException;
|
||||
|
||||
@@ -294,11 +294,6 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSectionNames(int pointIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPointMoveFreely(int pointIndex) {
|
||||
return true;
|
||||
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.components.composite;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Composite;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Stroke;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.diylc.components.AbstractComponent;
|
||||
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.utils.Constants;
|
||||
|
||||
/**
|
||||
* Instantiation of a {@code BuildingBlock} as a single, rigid component rather than a group of
|
||||
* loose ones. Wraps an embedded list of child components and aggregates their control points,
|
||||
* so it can be placed, moved, rotated and wired up like any other component while the netlist,
|
||||
* BOM and AI project description still see its individual terminals (see
|
||||
* {@link #getControlPointNodeName(int)}).
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
@ComponentDescriptor(
|
||||
name = "Building Block",
|
||||
description = "A group of components instantiated as a single rigid component",
|
||||
category = "Building Blocks",
|
||||
author = "Branislav Stojkovic",
|
||||
instanceNamePrefix = "BLK",
|
||||
zOrder = IDIYComponent.COMPONENT,
|
||||
flexibleZOrder = true,
|
||||
bomPolicy = BomPolicy.SHOW_ALL_NAMES,
|
||||
autoEdit = false,
|
||||
enableCache = false,
|
||||
transformer = CompositeComponentTransformer.class,
|
||||
hiddenInPalette = true)
|
||||
public class CompositeComponent extends AbstractTransparentComponent<String> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private List<IDIYComponent<?>> childComponents = new ArrayList<IDIYComponent<?>>();
|
||||
|
||||
// Name of the building block this instance was created from. Drives its display identity in
|
||||
// the BOM, netlist report and AI project description (see ComponentProcessor.getDisplayTypeName).
|
||||
private String blockName;
|
||||
|
||||
// Free-form label the user can edit after placement, independent of blockName. Shown in the BOM.
|
||||
private String value;
|
||||
|
||||
// Flat index mapping a composite control point index to (child ordinal, child point index).
|
||||
// Rebuilt lazily since children never change after construction and the arrays are not
|
||||
// serialized (transient).
|
||||
private transient int[] childOf;
|
||||
private transient int[] pointOf;
|
||||
|
||||
/**
|
||||
* Required no-arg constructor, used reflectively by {@code ComponentProcessor} and XStream.
|
||||
*/
|
||||
public CompositeComponent() {
|
||||
super();
|
||||
}
|
||||
|
||||
private void buildIndexIfNeeded() {
|
||||
if (childOf != null) {
|
||||
return;
|
||||
}
|
||||
int total = 0;
|
||||
for (IDIYComponent<?> c : childComponents) {
|
||||
total += c.getControlPointCount();
|
||||
}
|
||||
int[] newChildOf = new int[total];
|
||||
int[] newPointOf = new int[total];
|
||||
int idx = 0;
|
||||
for (int ci = 0; ci < childComponents.size(); ci++) {
|
||||
IDIYComponent<?> c = childComponents.get(ci);
|
||||
int count = c.getControlPointCount();
|
||||
for (int pi = 0; pi < count; pi++) {
|
||||
newChildOf[idx] = ci;
|
||||
newPointOf[idx] = pi;
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
childOf = newChildOf;
|
||||
pointOf = newPointOf;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the embedded child components, in save order. Never {@code null}.
|
||||
*/
|
||||
public List<IDIYComponent<?>> getChildComponents() {
|
||||
if (childComponents == null) {
|
||||
childComponents = new ArrayList<IDIYComponent<?>>();
|
||||
}
|
||||
return childComponents;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Block", defaultable = false, sortOrder = 1)
|
||||
public String getBlockName() {
|
||||
return blockName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the source block name. Deliberately not named {@code setBlockName} so that
|
||||
* {@code ComponentProcessor}'s getter/setter matching does not turn the "Block" property
|
||||
* editable in the property editor - it is metadata about the instance, not a value the user
|
||||
* should be able to change after placement.
|
||||
*/
|
||||
public void initBlockName(String blockName) {
|
||||
this.blockName = blockName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
buildIndexIfNeeded();
|
||||
return childOf.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
buildIndexIfNeeded();
|
||||
return childComponents.get(childOf[index]).getControlPoint(pointOf[index]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setControlPoint(Point2D point, int index) {
|
||||
buildIndexIfNeeded();
|
||||
childComponents.get(childOf[index]).setControlPoint(point, pointOf[index]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isControlPointSticky(int index) {
|
||||
buildIndexIfNeeded();
|
||||
return childComponents.get(childOf[index]).isControlPointSticky(pointOf[index]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canControlPointOverlap(int index) {
|
||||
buildIndexIfNeeded();
|
||||
return childComponents.get(childOf[index]).canControlPointOverlap(pointOf[index]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
|
||||
buildIndexIfNeeded();
|
||||
return childComponents.get(childOf[index]).getControlPointVisibilityPolicy(pointOf[index]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
buildIndexIfNeeded();
|
||||
return childComponents.get(childOf[index]).getControlPointNodeName(pointOf[index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The composite is always rigid: it moves and rotates as a unit and never stretches. See design
|
||||
* decision D4 in {@code docs/plans/composite-building-blocks.md}.
|
||||
*/
|
||||
@Override
|
||||
public boolean canPointMoveFreely(int pointIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createdIn(Project project) {
|
||||
for (IDIYComponent<?> c : getChildComponents()) {
|
||||
c.createdIn(project);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@EditableProperty
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
IDrawingObserver drawingObserver) {
|
||||
Composite oldComposite = applyAlpha(g2d, componentState);
|
||||
for (IDIYComponent<?> c : getChildComponents()) {
|
||||
// Children always draw in their normal state - the composite draws its own selection
|
||||
// outline below instead of letting every child paint its own highlight.
|
||||
c.draw(g2d, componentState, outlineMode, project, drawingObserver);
|
||||
}
|
||||
g2d.setComposite(oldComposite);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
// Must not touch `components` - ComponentProcessor draws this on a default-constructed,
|
||||
// childless instance to build the palette icon.
|
||||
g2d.setColor(LIGHT_METAL_COLOR);
|
||||
g2d.fillRect(2, 2, width - 4, height - 4);
|
||||
g2d.setColor(METAL_COLOR);
|
||||
g2d.drawRect(2, 2, width - 4, height - 4);
|
||||
int halfW = width / 2;
|
||||
int halfH = height / 2;
|
||||
g2d.drawLine(halfW, 2, halfW, height - 2);
|
||||
g2d.drawLine(2, halfH, width - 2, halfH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep-clones the composite together with all its children, so that two placed instances never
|
||||
* share child objects (that would make moving one instance move the other - see the "clone()
|
||||
* is the sharp edge" risk in {@code docs/plans/composite-building-blocks.md}). Preserves this
|
||||
* component's own id and every child's id, matching {@link AbstractComponent#clone()}'s
|
||||
* contract of copying identity by default - callers that need a genuinely new component
|
||||
* reassign ids themselves, exactly as {@code BuildingBlockManager.loadBlock} does when it
|
||||
* clones each source component individually to place a brand new instance of a block. This also
|
||||
* keeps id-sensitive reflective comparisons (like the default {@code equalsTo} most child
|
||||
* component classes inherit) working correctly between a composite and its own clone, which the
|
||||
* undo dirty-check in {@code Presenter.notifyProjectModifiedIfNeeded} relies on.
|
||||
*/
|
||||
@Override
|
||||
public IDIYComponent<String> clone() throws CloneNotSupportedException {
|
||||
CompositeComponent clone = new CompositeComponent();
|
||||
clone.setId(getId());
|
||||
clone.setName(getName());
|
||||
clone.blockName = this.blockName;
|
||||
clone.value = this.value;
|
||||
// matches AbstractComponent.clone(), which shallow-copies the Percentage reference
|
||||
clone.setAlpha(getAlpha());
|
||||
List<IDIYComponent<?>> clonedComponents = new ArrayList<IDIYComponent<?>>(getChildComponents().size());
|
||||
for (IDIYComponent<?> c : getChildComponents()) {
|
||||
clonedComponents.add(c.clone());
|
||||
}
|
||||
clone.childComponents = clonedComponents;
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden because {@link AbstractComponent#equalsTo(IDIYComponent)} compares the
|
||||
* {@code components} field with {@code List.equals()}, which falls back to child object
|
||||
* identity since no component class overrides {@code Object.equals()} - two structurally
|
||||
* identical composites built from different object graphs (e.g. an original and its clone)
|
||||
* would otherwise never compare equal, defeating the undo dirty-check in
|
||||
* {@code Presenter.notifyProjectModifiedIfNeeded}.
|
||||
*/
|
||||
@Override
|
||||
public boolean equalsTo(IDIYComponent<?> other) {
|
||||
if (!(other instanceof CompositeComponent)) {
|
||||
return false;
|
||||
}
|
||||
CompositeComponent o = (CompositeComponent) other;
|
||||
if (!Objects.equals(getName(), o.getName()) || !Objects.equals(blockName, o.blockName)
|
||||
|| !Objects.equals(value, o.value) || !Objects.equals(getAlpha(), o.getAlpha())) {
|
||||
return false;
|
||||
}
|
||||
List<IDIYComponent<?>> otherComponents = o.getChildComponents();
|
||||
if (getChildComponents().size() != otherComponents.size()) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < childComponents.size(); i++) {
|
||||
if (!childComponents.get(i).equalsTo(otherComponents.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.components.composite;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.IComponentTransformer;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.presenter.ComponentProcessor;
|
||||
|
||||
/**
|
||||
* Rotates and mirrors a {@link CompositeComponent} by delegating to each child's own
|
||||
* {@link IComponentTransformer}, around the same centre for every child so the assembly stays
|
||||
* geometrically consistent. If any child's type cannot rotate (or mirror), the whole composite
|
||||
* reports that it cannot either - silently leaving one child un-rotated would be worse (see
|
||||
* design decision D9 in {@code docs/plans/composite-building-blocks.md}).
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class CompositeComponentTransformer implements IComponentTransformer {
|
||||
|
||||
@Override
|
||||
public boolean canRotate(IDIYComponent<?> component) {
|
||||
CompositeComponent composite = (CompositeComponent) component;
|
||||
if (composite.getChildComponents().isEmpty())
|
||||
return false;
|
||||
for (IDIYComponent<?> child : composite.getChildComponents()) {
|
||||
if (!childTransformer(child).canRotate(child))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canMirror(IDIYComponent<?> component) {
|
||||
CompositeComponent composite = (CompositeComponent) component;
|
||||
if (composite.getChildComponents().isEmpty())
|
||||
return false;
|
||||
for (IDIYComponent<?> child : composite.getChildComponents()) {
|
||||
if (!childTransformer(child).canMirror(child))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mirroringChangesCircuit() {
|
||||
// This transformer instance is shared by every CompositeComponent (ComponentProcessor caches
|
||||
// one instance per class), so it has no access to a specific composite's children here.
|
||||
// Conservatively assume mirroring can change the circuit, so the user is always warned - a
|
||||
// spurious confirmation is far cheaper than silently flipping a board's pinout.
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rotate(IDIYComponent<?> component, Point2D center, int direction) {
|
||||
CompositeComponent composite = (CompositeComponent) component;
|
||||
for (IDIYComponent<?> child : composite.getChildComponents()) {
|
||||
childTransformer(child).rotate(child, center, direction);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mirror(IDIYComponent<?> component, Point2D center, int direction) {
|
||||
CompositeComponent composite = (CompositeComponent) component;
|
||||
for (IDIYComponent<?> child : composite.getChildComponents()) {
|
||||
childTransformer(child).mirror(child, center, direction);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private IComponentTransformer childTransformer(IDIYComponent<?> child) {
|
||||
ComponentType type =
|
||||
ComponentProcessor.getInstance().extractComponentTypeFrom((Class) child.getClass());
|
||||
return type.getTransformer();
|
||||
}
|
||||
}
|
||||
@@ -137,13 +137,6 @@ public interface IDIYComponent<T> extends Serializable, Cloneable {
|
||||
*/
|
||||
String getInternalLinkName(int index1, int index2);
|
||||
|
||||
/**
|
||||
* @param pointIndex
|
||||
* @return name of the internal section containing the specified point, or null if the component does not have internal sections.
|
||||
* One point can be shared between multiple sections, in which case, the result may contain more than one element.
|
||||
*/
|
||||
String[] getSectionNames(int pointIndex);
|
||||
|
||||
/**
|
||||
* @param pointIndex
|
||||
* @return true if the control point can move without affecting other points' position, false otherwise
|
||||
|
||||
@@ -123,9 +123,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;
|
||||
|
||||
/**
|
||||
* @return true if the component type should not be listed in the palette (toolbox and
|
||||
* component tree), while still being resolvable via
|
||||
* {@code ComponentProcessor.extractComponentTypeFrom} for netlist, BOM and name
|
||||
* generation purposes. Used by component types that are only ever created
|
||||
* programmatically, e.g. {@code CompositeComponent}.
|
||||
*/
|
||||
boolean hiddenInPalette() default false;
|
||||
}
|
||||
|
||||
@@ -52,8 +52,7 @@ public class Node implements Comparable<Node> {
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
String[] section = component.getSectionNames(pointIndex);
|
||||
return (section == null || section.length > 1 ? "" : section[0] + ".") + component.getControlPointNodeName(pointIndex);
|
||||
return component.getControlPointNodeName(pointIndex);
|
||||
}
|
||||
|
||||
public Point2D getPoint2D() {
|
||||
|
||||
+2
-1
@@ -175,7 +175,8 @@ public class AiProjectBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
return new AiComponent(component.getName(), componentType.getName(),
|
||||
return new AiComponent(component.getName(),
|
||||
ComponentProcessor.getDisplayTypeName(component, componentType),
|
||||
component.getValue() == null ? null : component.getValue().toString(),
|
||||
fromPos, toPos, pos, terminals.isEmpty() ? null : terminals);
|
||||
}
|
||||
|
||||
@@ -27,11 +27,15 @@ import com.thoughtworks.xstream.XStream;
|
||||
import com.thoughtworks.xstream.io.xml.DomDriver;
|
||||
import com.thoughtworks.xstream.security.AnyTypePermission;
|
||||
|
||||
import org.diylc.common.BlockInstantiationMode;
|
||||
import org.diylc.common.BuildingBlockPackage;
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.IBlockProcessor;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||
import org.diylc.components.composite.CompositeComponent;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.ISwitch;
|
||||
import org.diylc.serialization.ProjectFileManager;
|
||||
|
||||
public class BuildingBlockManager {
|
||||
@@ -119,19 +123,30 @@ public class BuildingBlockManager {
|
||||
return pkg.getBlocks().size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mode {@link BlockInstantiationMode#GROUP} reproduces the original behavior: every
|
||||
* component is cloned individually, with a fresh project-unique name, ready to be
|
||||
* pasted as an auto-grouped set of independent components.
|
||||
* {@link BlockInstantiationMode#COMPOSITE} clones the components and wraps them in a
|
||||
* single {@link CompositeComponent}, returned as the only element of the result list.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<IDIYComponent<?>> loadBlock(String blockName,
|
||||
List<IDIYComponent<?>> existingComponents) throws InvalidBlockException {
|
||||
LOG.debug(String.format("loadBlock(%s)", blockName));
|
||||
List<IDIYComponent<?>> existingComponents, BlockInstantiationMode mode)
|
||||
throws InvalidBlockException {
|
||||
LOG.debug(String.format("loadBlock(%s, %s)", blockName, mode));
|
||||
Map<String, List<IDIYComponent<?>>> blocks = (Map<String, List<IDIYComponent<?>>>) configManager
|
||||
.readObject(IPlugInPort.BLOCKS_KEY, null);
|
||||
if (blocks != null) {
|
||||
Collection<IDIYComponent<?>> components = blocks.get(blockName);
|
||||
if (components == null)
|
||||
throw new InvalidBlockException();
|
||||
// clear potential control point every time!
|
||||
instantiationManager.setPotentialControlPoint(null);
|
||||
// clone components
|
||||
if (blocks == null)
|
||||
throw new InvalidBlockException();
|
||||
Collection<IDIYComponent<?>> components = blocks.get(blockName);
|
||||
if (components == null)
|
||||
throw new InvalidBlockException();
|
||||
// clear potential control point every time!
|
||||
instantiationManager.setPotentialControlPoint(null);
|
||||
|
||||
if (mode == BlockInstantiationMode.GROUP) {
|
||||
// clone components, each getting its own fresh, project-unique name and id
|
||||
List<IDIYComponent<?>> clones = new ArrayList<IDIYComponent<?>>();
|
||||
List<IDIYComponent<?>> testComponents = new ArrayList<IDIYComponent<?>>(existingComponents);
|
||||
for (IDIYComponent<?> c : components)
|
||||
@@ -147,8 +162,42 @@ public class BuildingBlockManager {
|
||||
LOG.error("Could not clone component: " + c);
|
||||
}
|
||||
return clones;
|
||||
} else
|
||||
throw new InvalidBlockException();
|
||||
}
|
||||
|
||||
// COMPOSITE mode: clone components keeping their saved names. Those names are namespaced
|
||||
// under the composite's own name in the netlist (e.g. "ARD1.PH1.3"), so running them through
|
||||
// createUniqueName against the whole project would produce meaningless drift and break
|
||||
// netlist labels between two instances of the same block.
|
||||
List<IDIYComponent<?>> childClones = new ArrayList<IDIYComponent<?>>();
|
||||
for (IDIYComponent<?> c : components) {
|
||||
try {
|
||||
IDIYComponent<?> clone = c.clone();
|
||||
clone.setId(UUID.randomUUID());
|
||||
childClones.add(clone);
|
||||
} catch (CloneNotSupportedException e) {
|
||||
LOG.error("Could not clone component: " + c);
|
||||
}
|
||||
// CompositeComponent does not implement ISwitch (see design decision D6): a switch placed
|
||||
// inside a composite still draws and exposes its terminals, but contributes no switching
|
||||
// behavior to the netlist. Flag it so this silent difference from group mode isn't a
|
||||
// surprise.
|
||||
if (c instanceof ISwitch) {
|
||||
LOG.warn(String.format(
|
||||
"Building block \"%s\" contains a switch (%s). Its switching behavior will not be "
|
||||
+ "reflected in the netlist when instantiated as a single component.",
|
||||
blockName, c.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
CompositeComponent composite = new CompositeComponent();
|
||||
composite.getChildComponents().addAll(childClones);
|
||||
composite.initBlockName(blockName);
|
||||
composite.setId(UUID.randomUUID());
|
||||
ComponentType compositeType =
|
||||
ComponentProcessor.getInstance().extractComponentTypeFrom(CompositeComponent.class);
|
||||
composite.setName(instantiationManager.createUniqueName(compositeType, existingComponents));
|
||||
|
||||
return Collections.singletonList((IDIYComponent<?>) composite);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -197,5 +246,11 @@ public class BuildingBlockManager {
|
||||
blocks.remove(blockName);
|
||||
configManager.writeValue(IBlockProcessor.BLOCKS_KEY, blocks);
|
||||
}
|
||||
|
||||
// drop the block from the recently used list too, so it doesn't linger as a dead entry
|
||||
List<String> recent =
|
||||
(List<String>) configManager.readObject(IPlugInPort.RECENT_COMPONENTS_KEY, null);
|
||||
if (recent != null && recent.remove(IBlockProcessor.BLOCK_PREFIX + blockName))
|
||||
configManager.writeValue(IPlugInPort.RECENT_COMPONENTS_KEY, recent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.apache.log4j.Logger;
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.IComponentTransformer;
|
||||
import org.diylc.common.PropertyWrapper;
|
||||
import org.diylc.components.composite.CompositeComponent;
|
||||
import org.diylc.core.CreationMethod;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
@@ -113,7 +114,7 @@ public class ComponentProcessor {
|
||||
String keywordTag;
|
||||
List<String[]> datasheet = null;
|
||||
int datasheetCreationStepCount = 0;
|
||||
|
||||
|
||||
if (clazz.isAnnotationPresent(ComponentDescriptor.class)) {
|
||||
ComponentDescriptor annotation = clazz.getAnnotation(ComponentDescriptor.class);
|
||||
name = annotation.name();
|
||||
@@ -159,7 +160,7 @@ 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);
|
||||
componentTypeMap.put(clazz.getName(), componentType);
|
||||
return componentType;
|
||||
@@ -273,6 +274,11 @@ public class ComponentProcessor {
|
||||
// just to store in the cache
|
||||
ComponentProcessor.getInstance().extractProperties(clazz);
|
||||
|
||||
// types created only programmatically (e.g. CompositeComponent) don't belong in the
|
||||
// palette, but still need to go through extractProperties() above so the cache is warm
|
||||
if (clazz.getAnnotation(ComponentDescriptor.class).hiddenInPalette())
|
||||
continue;
|
||||
|
||||
List<ComponentType> nestedList;
|
||||
if (componentTypes.containsKey(componentType.getCategory())) {
|
||||
nestedList = componentTypes.get(componentType.getCategory());
|
||||
@@ -422,6 +428,23 @@ public class ComponentProcessor {
|
||||
return transformer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the name that should represent the component's type to a human or to the AI, e.g. in
|
||||
* the BOM, the netlist report or the AI project description. For a
|
||||
* {@link CompositeComponent} this is the source building block's name (e.g.
|
||||
* "Arduino Uno") rather than the generic "Building Block" type name, since every
|
||||
* composite shares one {@link ComponentType} (see design decision D5 in
|
||||
* {@code docs/plans/composite-building-blocks.md}).
|
||||
*/
|
||||
public static String getDisplayTypeName(IDIYComponent<?> component, ComponentType type) {
|
||||
if (component instanceof CompositeComponent) {
|
||||
String blockName = ((CompositeComponent) component).getBlockName();
|
||||
if (blockName != null && !blockName.isEmpty())
|
||||
return blockName;
|
||||
}
|
||||
return type.getName();
|
||||
}
|
||||
|
||||
public static boolean hasStickyPoint(IDIYComponent<?> c) {
|
||||
for (int i = 0; i < c.getControlPointCount(); i++)
|
||||
if (c.isControlPointSticky(0))
|
||||
|
||||
@@ -272,6 +272,31 @@ public class InstantiationManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes an identifier to the front of the recently used list, removing any earlier occurrence
|
||||
* and trimming the list to {@link #MAX_RECENT_COMPONENTS}. If the identifier is already at the
|
||||
* front nothing is written. The identifier is either a fully qualified component class name or a
|
||||
* building block reference of the form {@code "block:<blockName>"} (see
|
||||
* {@link IBlockProcessor#BLOCK_PREFIX}).
|
||||
*
|
||||
* @param identifier component class name or {@code "block:<blockName>"}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void addToRecentComponents(String identifier) {
|
||||
List<String> recent = (List<String>) ConfigurationManager.getInstance()
|
||||
.readObject(IPlugInPort.RECENT_COMPONENTS_KEY, new ArrayList<String>());
|
||||
if (!recent.isEmpty() && recent.get(0).equals(identifier))
|
||||
return;
|
||||
// Remove if it's already somewhere in the list.
|
||||
recent.remove(identifier);
|
||||
// Add to the front of the list.
|
||||
recent.add(0, identifier);
|
||||
// Trim the list if necessary.
|
||||
while (recent.size() > MAX_RECENT_COMPONENTS)
|
||||
recent.remove(recent.size() - 1);
|
||||
ConfigurationManager.getInstance().writeValue(IPlugInPort.RECENT_COMPONENTS_KEY, recent);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<IDIYComponent<?>> instantiateComponent(ComponentType componentType, Point2D point,
|
||||
Project currentProject) throws InstantiationException, IllegalAccessException, NoSuchMethodException,
|
||||
@@ -311,21 +336,7 @@ public class InstantiationManager {
|
||||
}
|
||||
|
||||
// Write to recent components
|
||||
List<String> recentComponentTypes =
|
||||
(List<String>) ConfigurationManager.getInstance().readObject(IPlugInPort.RECENT_COMPONENTS_KEY,
|
||||
new ArrayList<ComponentType>());
|
||||
String className = componentType.getInstanceClass().getName();
|
||||
if (recentComponentTypes.size() == 0 || !recentComponentTypes.get(0).equals(className)) {
|
||||
// Remove if it's already somewhere in the list.
|
||||
recentComponentTypes.remove(className);
|
||||
// Add to the end of the list.
|
||||
recentComponentTypes.add(0, className);
|
||||
// Trim the list if necessary.
|
||||
if (recentComponentTypes.size() > MAX_RECENT_COMPONENTS) {
|
||||
recentComponentTypes.remove(recentComponentTypes.size() - 1);
|
||||
}
|
||||
ConfigurationManager.getInstance().writeValue(IPlugInPort.RECENT_COMPONENTS_KEY, recentComponentTypes);
|
||||
}
|
||||
addToRecentComponents(componentType.getInstanceClass().getName());
|
||||
|
||||
List<IDIYComponent<?>> list = new ArrayList<IDIYComponent<?>>();
|
||||
list.add(component);
|
||||
|
||||
@@ -1060,9 +1060,9 @@ public class Presenter implements IPlugInPort {
|
||||
String regex = ".*" + criteria.toLowerCase() + ".*";
|
||||
for (IDIYComponent<?> c : currentProject.getComponents()) {
|
||||
ComponentType type = ComponentProcessor.getInstance().extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) c.getClass());
|
||||
if ((c.getName() != null && c.getName().toLowerCase().matches(regex)) ||
|
||||
if ((c.getName() != null && c.getName().toLowerCase().matches(regex)) ||
|
||||
(c.getValueForDisplay() != null && c.getValueForDisplay().toLowerCase().matches(regex)) ||
|
||||
(type != null && type.getName().toLowerCase().matches(regex)))
|
||||
(type != null && ComponentProcessor.getDisplayTypeName(c, type).toLowerCase().matches(regex)))
|
||||
matching.add(c);
|
||||
}
|
||||
updateSelection(matching);
|
||||
@@ -2844,9 +2844,14 @@ public class Presenter implements IPlugInPort {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadBlock(String blockName) throws InvalidBlockException {
|
||||
List<IDIYComponent<?>> components = buildingBlockManager.loadBlock(blockName, currentProject.getComponents());
|
||||
pasteComponents(new ComponentTransferable(components, blockName), true, true);
|
||||
public void loadBlock(String blockName, BlockInstantiationMode mode) throws InvalidBlockException {
|
||||
List<IDIYComponent<?>> components =
|
||||
buildingBlockManager.loadBlock(blockName, currentProject.getComponents(), mode);
|
||||
// a composite is a single, already-assembled component - nothing to auto-group
|
||||
boolean autoGroup = mode == BlockInstantiationMode.GROUP;
|
||||
pasteComponents(new ComponentTransferable(components, blockName), autoGroup, true);
|
||||
// record the block in the recently used list, alongside component classes
|
||||
InstantiationManager.addToRecentComponents(IBlockProcessor.BLOCK_PREFIX + blockName);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -81,6 +81,7 @@ public class BomMaker {
|
||||
(Class<? extends IDIYComponent<?>>) component.getClass());
|
||||
if (type.getBomPolicy() == BomPolicy.NEVER_SHOW)
|
||||
continue;
|
||||
String typeName = ComponentProcessor.getDisplayTypeName(component, type);
|
||||
String name = component.getName();
|
||||
String value;
|
||||
try {
|
||||
@@ -89,7 +90,7 @@ public class BomMaker {
|
||||
value = "<undefined>";
|
||||
}
|
||||
if ((name != null) && (value != null)) {
|
||||
String key = type.getName() + "|" + value;
|
||||
String key = typeName + "|" + value;
|
||||
if (entryMap.containsKey(key)) {
|
||||
BomEntry entry = entryMap.get(key);
|
||||
entry.setQuantity(entry.getQuantity() + 1);
|
||||
@@ -98,7 +99,7 @@ public class BomMaker {
|
||||
}
|
||||
} else {
|
||||
entryMap.put(key,
|
||||
new BomEntry(type.getName(), type.getBomPolicy() == BomPolicy.SHOW_ALL_NAMES ? name : type.getName(),
|
||||
new BomEntry(typeName, type.getBomPolicy() == BomPolicy.SHOW_ALL_NAMES ? name : typeName,
|
||||
value, 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +369,38 @@
|
||||
<releaseDate>2026-08-22 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Building blocks instantiate by default as a single component, inheriting control points and terminal nodes from all individual components but acting as a single component; still possible to instantiate individual components using 'Insert as Grouped Components' action from the context menu over a building block</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added ability to set node name (or names) for some connectivity components (e.g. Solder Pad, Eyelet, PCB Terminal Block, etc); when combined into building blocks, these names will become node names of the composite component stamped from the building block</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Include building blocks in the Recently Used category</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>When saving building blocks, present a drop down list with most recently used building blocks for quick selection</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Prompt to confirm the overwrite when saving over the existing building blocks</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>When transistor pinout is selected for TO-1, TO-92, TO-126, TO-220 transistor, the corresponding electrode name will be used as a node name in programmatical and AI analysis</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Diode components now label their terminal nodes as 'A' (anode) and 'K' (cathode) instead of '1' and '2'</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Capacitor components now label their terminal nodes as '+' and '-' instead of '1' and '2' when 'Polarized' property is ticked</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Percentage values such as transparency Alpha, image Scale, etc, can have decimal values and not just integers</description>
|
||||
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.components.composite;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.diylc.common.Percentage;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.netlist.Node;
|
||||
import org.diylc.serialization.ProjectFileManager;
|
||||
import org.diylc.testcomponents.TwoPointTestComponent;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Exercises the delegation, rigidity, cloning and equality behavior described in
|
||||
* {@code docs/plans/composite-building-blocks.md}.
|
||||
*/
|
||||
public class CompositeComponentTest {
|
||||
|
||||
private CompositeComponent twoChildComposite() {
|
||||
CompositeComponent composite = new CompositeComponent();
|
||||
composite.setName("BLK1");
|
||||
composite.initBlockName("Test Block");
|
||||
composite.getChildComponents().add(new TwoPointTestComponent("R1",
|
||||
new Point2D.Double(0, 0), new Point2D.Double(10, 0)));
|
||||
composite.getChildComponents().add(new TwoPointTestComponent("R2",
|
||||
new Point2D.Double(20, 0), new Point2D.Double(30, 0)));
|
||||
return composite;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controlPointCountIsSumOverChildren() {
|
||||
CompositeComponent composite = twoChildComposite();
|
||||
assertEquals(4, composite.getControlPointCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controlPointsMapToTheRightChildAndLocalIndex() {
|
||||
CompositeComponent composite = twoChildComposite();
|
||||
|
||||
assertEquals(new Point2D.Double(0, 0), composite.getControlPoint(0));
|
||||
assertEquals(new Point2D.Double(10, 0), composite.getControlPoint(1));
|
||||
assertEquals(new Point2D.Double(20, 0), composite.getControlPoint(2));
|
||||
assertEquals(new Point2D.Double(30, 0), composite.getControlPoint(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setControlPointDelegatesToTheOwningChild() {
|
||||
CompositeComponent composite = twoChildComposite();
|
||||
|
||||
composite.setControlPoint(new Point2D.Double(99, 99), 2);
|
||||
|
||||
assertEquals(new Point2D.Double(99, 99), composite.getControlPoint(2));
|
||||
// the sibling child's points must be untouched
|
||||
assertEquals(new Point2D.Double(30, 0), composite.getControlPoint(3));
|
||||
TwoPointTestComponent r2 = (TwoPointTestComponent) composite.getChildComponents().get(1);
|
||||
assertEquals(new Point2D.Double(99, 99), r2.getControlPoint(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stickinessIsDelegatedPerChildPoint() {
|
||||
CompositeComponent composite = twoChildComposite();
|
||||
((TwoPointTestComponent) composite.getChildComponents().get(0)).setSticky(1, false);
|
||||
|
||||
assertTrue(composite.isControlPointSticky(0));
|
||||
assertFalse(composite.isControlPointSticky(1));
|
||||
assertTrue(composite.isControlPointSticky(2));
|
||||
assertTrue(composite.isControlPointSticky(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isAlwaysRigid() {
|
||||
CompositeComponent composite = twoChildComposite();
|
||||
for (int i = 0; i < composite.getControlPointCount(); i++)
|
||||
assertFalse("point " + i + " should not move freely", composite.canPointMoveFreely(i));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nodeNamingProducesHierarchicalNetlistLabels() {
|
||||
// Design decision D3: Node.toString() should read "<composite>.<child>.<local point name>"
|
||||
CompositeComponent composite = twoChildComposite();
|
||||
|
||||
Node r1FirstPoint = new Node(composite, 0);
|
||||
Node r2FirstPoint = new Node(composite, 2);
|
||||
Node r2SecondPoint = new Node(composite, 3);
|
||||
|
||||
assertEquals("BLK1.1", r1FirstPoint.toString());
|
||||
assertEquals("BLK1.1", r2FirstPoint.toString());
|
||||
// local point index must reset per child, not keep growing with the flat composite index
|
||||
assertEquals("BLK1.2", r2SecondPoint.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cloneDeepCopiesChildrenSoInstancesAreIndependent() throws Exception {
|
||||
CompositeComponent original = twoChildComposite();
|
||||
|
||||
IDIYComponent<?> clone = original.clone();
|
||||
|
||||
((CompositeComponent) clone).setControlPoint(new Point2D.Double(-1, -1), 0);
|
||||
|
||||
assertEquals(new Point2D.Double(0, 0), original.getControlPoint(0));
|
||||
assertEquals(new Point2D.Double(-1, -1), clone.getControlPoint(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cloneKeepsBothItsOwnAndChildrenIds() throws Exception {
|
||||
// Matches AbstractComponent.clone()'s contract of preserving identity by default - a caller
|
||||
// that needs a genuinely new component (e.g. BuildingBlockManager placing a fresh instance)
|
||||
// reassigns ids itself, exactly as it does for any other component's clone().
|
||||
CompositeComponent original = twoChildComposite();
|
||||
UUID originalChildId = original.getChildComponents().get(0).getId();
|
||||
|
||||
CompositeComponent clone = (CompositeComponent) original.clone();
|
||||
|
||||
assertEquals(original.getId(), clone.getId());
|
||||
assertEquals(originalChildId, clone.getChildComponents().get(0).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsToComparesStructureNotObjectIdentity() throws Exception {
|
||||
CompositeComponent original = twoChildComposite();
|
||||
CompositeComponent clone = (CompositeComponent) original.clone();
|
||||
|
||||
// structurally identical, distinct child objects - must still compare equal
|
||||
assertTrue(original.equalsTo(clone));
|
||||
|
||||
clone.setControlPoint(new Point2D.Double(-1, -1), 0);
|
||||
assertFalse(original.equalsTo(clone));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsToRejectsDifferentBlockName() {
|
||||
CompositeComponent a = twoChildComposite();
|
||||
CompositeComponent b = twoChildComposite();
|
||||
b.initBlockName("Different Block");
|
||||
|
||||
assertFalse(a.equalsTo(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultsToFullyOpaque() {
|
||||
assertEquals(new Percentage(100), twoChildComposite().getAlpha());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cloneCopiesAlpha() throws Exception {
|
||||
CompositeComponent original = twoChildComposite();
|
||||
original.setAlpha(new Percentage(40));
|
||||
|
||||
CompositeComponent clone = (CompositeComponent) original.clone();
|
||||
|
||||
assertEquals(new Percentage(40), clone.getAlpha());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsToRejectsDifferentAlpha() throws Exception {
|
||||
CompositeComponent original = twoChildComposite();
|
||||
CompositeComponent clone = (CompositeComponent) original.clone();
|
||||
assertTrue(original.equalsTo(clone));
|
||||
|
||||
clone.setAlpha(new Percentage(50));
|
||||
assertFalse(original.equalsTo(clone));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void survivesAnXStreamRoundTripAndRebuildsItsIndexLazily() {
|
||||
CompositeComponent original = twoChildComposite();
|
||||
|
||||
String xml = ProjectFileManager.xStreamSerializer.toXML(original);
|
||||
CompositeComponent reloaded = (CompositeComponent) ProjectFileManager.xStreamSerializer.fromXML(xml);
|
||||
|
||||
// the transient child-index arrays are never serialized - this call exercises the lazy
|
||||
// rebuild in buildIndexIfNeeded() on an instance XStream constructed without going through
|
||||
// the no-arg constructor
|
||||
assertEquals(original.getControlPointCount(), reloaded.getControlPointCount());
|
||||
assertEquals(original.getName(), reloaded.getName());
|
||||
assertEquals(original.getBlockName(), reloaded.getBlockName());
|
||||
for (int i = 0; i < original.getControlPointCount(); i++)
|
||||
assertEquals(original.getControlPoint(i), reloaded.getControlPoint(i));
|
||||
assertEquals("BLK1.2", new Node(reloaded, 3).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void drawIconDoesNotTouchChildren() {
|
||||
// ComponentProcessor draws the palette icon on a default-constructed, childless instance
|
||||
CompositeComponent blank = new CompositeComponent();
|
||||
BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
|
||||
blank.drawIcon(g2d, 32, 32);
|
||||
|
||||
g2d.dispose();
|
||||
// no exception means components list (empty) was never dereferenced as if populated
|
||||
assertNotNull(blank.getChildComponents());
|
||||
assertTrue(blank.getChildComponents().isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.diylc.appframework.miscutils.IConfigurationManager;
|
||||
import org.diylc.common.BlockInstantiationMode;
|
||||
import org.diylc.common.IBlockProcessor;
|
||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.components.composite.CompositeComponent;
|
||||
import org.diylc.core.ISwitch;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.testcomponents.SwitchTestComponent;
|
||||
import org.diylc.testcomponents.TwoPointTestComponent;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Exercises {@link BuildingBlockManager#loadBlock} in both {@link BlockInstantiationMode}s (see
|
||||
* design decision D10 and section 4.4 of {@code docs/plans/composite-building-blocks.md}).
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public class BuildingBlockManagerTest {
|
||||
|
||||
private static final String BLOCK_NAME = "TestBlock";
|
||||
|
||||
private IConfigurationManager<?> configManager;
|
||||
private BuildingBlockManager manager;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
List<IDIYComponent<?>> storedComponents = Arrays.asList(
|
||||
new TwoPointTestComponent("R1", new Point2D.Double(0, 0), new Point2D.Double(10, 0)),
|
||||
new TwoPointTestComponent("R2", new Point2D.Double(20, 0), new Point2D.Double(30, 0)));
|
||||
Map<String, List<IDIYComponent<?>>> blocksMap = new HashMap<String, List<IDIYComponent<?>>>();
|
||||
blocksMap.put(BLOCK_NAME, storedComponents);
|
||||
|
||||
configManager = mock(IConfigurationManager.class);
|
||||
when(configManager.readObject(eq(IPlugInPort.BLOCKS_KEY), org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(blocksMap);
|
||||
|
||||
manager = new BuildingBlockManager(configManager, null, new InstantiationManager());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void groupModeClonesEachComponentWithAFreshNameAndId() throws InvalidBlockException {
|
||||
List<IDIYComponent<?>> result =
|
||||
manager.loadBlock(BLOCK_NAME, new ArrayList<IDIYComponent<?>>(), BlockInstantiationMode.GROUP);
|
||||
|
||||
assertEquals(2, result.size());
|
||||
// group mode always assigns fresh, project-unique names (pre-existing behavior, unchanged)
|
||||
assertEquals("TP1", result.get(0).getName());
|
||||
assertEquals("TP2", result.get(1).getName());
|
||||
assertNotEquals(result.get(0).getId(), result.get(1).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compositeModeWrapsChildrenInASingleComponent() throws InvalidBlockException {
|
||||
List<IDIYComponent<?>> result =
|
||||
manager.loadBlock(BLOCK_NAME, new ArrayList<IDIYComponent<?>>(), BlockInstantiationMode.COMPOSITE);
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertTrue(result.get(0) instanceof CompositeComponent);
|
||||
CompositeComponent composite = (CompositeComponent) result.get(0);
|
||||
|
||||
assertEquals(BLOCK_NAME, composite.getBlockName());
|
||||
assertEquals("BLK1", composite.getName());
|
||||
assertEquals(4, composite.getControlPointCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compositeModePreservesChildNamesUnlikeGroupMode() throws InvalidBlockException {
|
||||
List<IDIYComponent<?>> result =
|
||||
manager.loadBlock(BLOCK_NAME, new ArrayList<IDIYComponent<?>>(), BlockInstantiationMode.COMPOSITE);
|
||||
CompositeComponent composite = (CompositeComponent) result.get(0);
|
||||
|
||||
// children are namespaced under the composite's own name in the netlist, so their saved
|
||||
// names must survive untouched
|
||||
assertEquals("R1", composite.getChildComponents().get(0).getName());
|
||||
assertEquals("R2", composite.getChildComponents().get(1).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compositeModeGivesEachInstanceItsOwnNameButChildrenStayIdenticallyNamed()
|
||||
throws InvalidBlockException {
|
||||
List<IDIYComponent<?>> existing = new ArrayList<IDIYComponent<?>>();
|
||||
CompositeComponent first = (CompositeComponent) manager
|
||||
.loadBlock(BLOCK_NAME, existing, BlockInstantiationMode.COMPOSITE).get(0);
|
||||
existing.add(first);
|
||||
|
||||
CompositeComponent second = (CompositeComponent) manager
|
||||
.loadBlock(BLOCK_NAME, existing, BlockInstantiationMode.COMPOSITE).get(0);
|
||||
|
||||
assertNotEquals(first.getName(), second.getName());
|
||||
assertEquals(first.getChildComponents().get(0).getName(), second.getChildComponents().get(0).getName());
|
||||
// but the underlying child components must be distinct objects with distinct ids
|
||||
assertNotEquals(first.getChildComponents().get(0).getId(), second.getChildComponents().get(0).getId());
|
||||
}
|
||||
|
||||
@Test(expected = InvalidBlockException.class)
|
||||
public void groupModeThrowsOnUnknownBlock() throws InvalidBlockException {
|
||||
manager.loadBlock("NoSuchBlock", new ArrayList<IDIYComponent<?>>(), BlockInstantiationMode.GROUP);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidBlockException.class)
|
||||
public void compositeModeThrowsOnUnknownBlock() throws InvalidBlockException {
|
||||
manager.loadBlock("NoSuchBlock", new ArrayList<IDIYComponent<?>>(), BlockInstantiationMode.COMPOSITE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteBlockAlsoStripsItFromTheRecentlyUsedList() {
|
||||
List<String> recent = new ArrayList<String>(Arrays.asList(
|
||||
IBlockProcessor.BLOCK_PREFIX + BLOCK_NAME, "org.diylc.components.SomeComponent"));
|
||||
when(configManager.readObject(eq(IPlugInPort.RECENT_COMPONENTS_KEY),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(recent);
|
||||
|
||||
manager.deleteBlock(BLOCK_NAME);
|
||||
|
||||
assertFalse(recent.contains(IBlockProcessor.BLOCK_PREFIX + BLOCK_NAME));
|
||||
assertTrue(recent.contains("org.diylc.components.SomeComponent"));
|
||||
verify(configManager).writeValue(eq(IPlugInPort.RECENT_COMPONENTS_KEY), eq(recent));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compositeModeToleratesAnInternalSwitchWithoutFailing() throws InvalidBlockException {
|
||||
// Design decision D6: the composite is not ISwitch, so a block containing a switch must
|
||||
// still place successfully - it just won't switch (see BuildingBlockManager's log warning).
|
||||
List<IDIYComponent<?>> storedComponents = Arrays.<IDIYComponent<?>>asList(new SwitchTestComponent("SW1"));
|
||||
Map<String, List<IDIYComponent<?>>> blocksMap = new HashMap<String, List<IDIYComponent<?>>>();
|
||||
blocksMap.put("SwitchBlock", storedComponents);
|
||||
when(configManager.readObject(eq(IPlugInPort.BLOCKS_KEY), org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(blocksMap);
|
||||
|
||||
List<IDIYComponent<?>> result = manager.loadBlock("SwitchBlock", new ArrayList<IDIYComponent<?>>(),
|
||||
BlockInstantiationMode.COMPOSITE);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertTrue(((CompositeComponent) result.get(0)).getChildComponents().get(0) instanceof ISwitch);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.testcomponents;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import org.diylc.core.ISwitch;
|
||||
|
||||
/**
|
||||
* Smallest possible {@link ISwitch} implementation, used to verify that a building block
|
||||
* containing a switch can still be instantiated as a {@code CompositeComponent} (design decision
|
||||
* D6 in {@code docs/plans/composite-building-blocks.md} - the composite itself is not
|
||||
* {@code ISwitch}, so the switch places and draws but contributes no switching behavior).
|
||||
* <p>
|
||||
* Deliberately a public top-level class, not a nested one: {@link
|
||||
* org.diylc.components.AbstractComponent#clone()} instantiates the no-arg constructor via plain
|
||||
* reflection (no {@code setAccessible}), which fails across packages unless both the class and
|
||||
* the constructor are public.
|
||||
*/
|
||||
public class SwitchTestComponent extends TwoPointTestComponent implements ISwitch {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public SwitchTestComponent() {
|
||||
this("SW");
|
||||
}
|
||||
|
||||
public SwitchTestComponent(String name) {
|
||||
super(name, new Point2D.Double(0, 0), new Point2D.Double(10, 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPositionCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPositionName(int position) {
|
||||
return position == 0 ? "ON" : "OFF";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean arePointsConnected(int index1, int index2, int position) {
|
||||
return position == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.testcomponents;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import org.diylc.components.AbstractComponent;
|
||||
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.ComponentDescriptor;
|
||||
|
||||
/**
|
||||
* Minimal two-point component used to exercise {@code CompositeComponent} and
|
||||
* {@code BuildingBlockManager} in tests: two control points, individually configurable
|
||||
* stickiness, node naming left at the {@link AbstractComponent} default (the 1-based point
|
||||
* index), so a composite's per-child node name delegation can be verified precisely.
|
||||
*/
|
||||
@ComponentDescriptor(name = "Two Point Test", category = "Test", author = "Test",
|
||||
description = "test", instanceNamePrefix = "TP", zOrder = IDIYComponent.COMPONENT)
|
||||
public class TwoPointTestComponent extends AbstractComponent<Void> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Point2D p0;
|
||||
private Point2D p1;
|
||||
private boolean sticky0 = true;
|
||||
private boolean sticky1 = true;
|
||||
|
||||
public TwoPointTestComponent() {
|
||||
this("", new Point2D.Double(0, 0), new Point2D.Double(10, 0));
|
||||
}
|
||||
|
||||
public TwoPointTestComponent(String name, Point2D p0, Point2D p1) {
|
||||
setName(name);
|
||||
this.p0 = p0;
|
||||
this.p1 = p1;
|
||||
}
|
||||
|
||||
public void setSticky(int index, boolean sticky) {
|
||||
if (index == 0)
|
||||
sticky0 = sticky;
|
||||
else
|
||||
sticky1 = sticky;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
return index == 0 ? p0 : p1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setControlPoint(Point2D point, int index) {
|
||||
if (index == 0)
|
||||
p0 = point;
|
||||
else
|
||||
p1 = point;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isControlPointSticky(int index) {
|
||||
return index == 0 ? sticky0 : sticky1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
|
||||
return VisibilityPolicy.ALWAYS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void getValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Void value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
IDrawingObserver drawingObserver) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user