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:
Bane Stojkovic
2026-09-04 15:32:46 +02:00
committed by GitHub
parent 26d6ca48fd
commit 047196499a
40 changed files with 1939 additions and 95 deletions
@@ -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;
@@ -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;
}
}
@@ -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() {
@@ -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>
@@ -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) {
}
}