mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
Compare commits
15 Commits
ics
...
building-blocks
| Author | SHA1 | Date | |
|---|---|---|---|
| 033f94f331 | |||
| 8b48f34df0 | |||
| 1b21436ad6 | |||
| 5f02fe15d9 | |||
| 819e802a37 | |||
| be5387cbb0 | |||
| c8ead57679 | |||
| 9ee44125db | |||
| 7069cacd0a | |||
| a44b349705 | |||
| a2bd8524ae | |||
| 825a90bbb4 | |||
| 42c833d706 | |||
| 47250ed612 | |||
| 9f52efafbb |
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ public class Eyelet extends AbstractTransparentComponent<String> {
|
||||
private Color color = COLOR;
|
||||
private Point2D.Double point = new Point2D.Double(0, 0);
|
||||
private String value = "";
|
||||
private String nodeName = "";
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
@@ -173,9 +174,22 @@ public class Eyelet extends AbstractTransparentComponent<String> {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Node Name", defaultable = false)
|
||||
public String getNodeName() {
|
||||
if (nodeName == null) {
|
||||
nodeName = "";
|
||||
}
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
public void setNodeName(String nodeName) {
|
||||
this.nodeName = nodeName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
return null;
|
||||
public String getControlPointNodeName(int index) {
|
||||
String n = getNodeName();
|
||||
return n.trim().isEmpty() ? null : n.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+23
-2
@@ -71,6 +71,7 @@ public class PCBTerminalBlock extends AbstractTransparentComponent<PCBTerminalBl
|
||||
private Size pitch = new Size(0.2d, SizeUnit.in);
|
||||
private Size width = new Size(0.3d, SizeUnit.in);
|
||||
private ScrewPosition screwPosition = ScrewPosition.Offset;
|
||||
private String nodeNames = "";
|
||||
|
||||
public PCBTerminalBlock() {
|
||||
super();
|
||||
@@ -330,10 +331,30 @@ public class PCBTerminalBlock extends AbstractTransparentComponent<PCBTerminalBl
|
||||
}
|
||||
|
||||
|
||||
@EditableProperty(name = "Node Names", defaultable = false)
|
||||
public String getNodeNames() {
|
||||
if (nodeNames == null) {
|
||||
nodeNames = "";
|
||||
}
|
||||
return nodeNames;
|
||||
}
|
||||
|
||||
public void setNodeNames(String nodeNames) {
|
||||
this.nodeNames = nodeNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
// we don't want the block to produce any nodes, it just makes connections
|
||||
return null;
|
||||
// By default the block produces no nodes, it just makes connections. Once the user
|
||||
// assigns explicit node names, each labeled point becomes a named node; unlabeled
|
||||
// points fall back to their ordinal number.
|
||||
String raw = getNodeNames();
|
||||
if (raw.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String[] parts = raw.split(",", -1);
|
||||
String label = index < parts.length ? parts[index].trim() : "";
|
||||
return label.isEmpty() ? Integer.toString(index + 1) : label;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -71,6 +71,7 @@ public class SolderPad extends AbstractTransparentComponent<Void>
|
||||
private Size holeSize = HOLE_SIZE;
|
||||
private PCBLayer layer = PCBLayer._1;
|
||||
private HoleType holeType = HoleType.NTPH;
|
||||
private String nodeName = "";
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
|
||||
@@ -246,9 +247,22 @@ public class SolderPad extends AbstractTransparentComponent<Void>
|
||||
return null;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Node Name", defaultable = false)
|
||||
public String getNodeName() {
|
||||
if (nodeName == null) {
|
||||
nodeName = "";
|
||||
}
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
public void setNodeName(String nodeName) {
|
||||
this.nodeName = nodeName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
return null;
|
||||
String n = getNodeName();
|
||||
return n.trim().isEmpty() ? null : n.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -60,6 +60,7 @@ public class Turret extends AbstractTransparentComponent<String> {
|
||||
private Color color = COLOR;
|
||||
private Point2D.Double point = new Point2D.Double(0, 0);
|
||||
private String value = "";
|
||||
private String nodeName = "";
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
@@ -168,9 +169,22 @@ public class Turret extends AbstractTransparentComponent<String> {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Node Name", defaultable = false)
|
||||
public String getNodeName() {
|
||||
if (nodeName == null) {
|
||||
nodeName = "";
|
||||
}
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
public void setNodeName(String nodeName) {
|
||||
this.nodeName = nodeName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
return null;
|
||||
public String getControlPointNodeName(int index) {
|
||||
String n = getNodeName();
|
||||
return n.trim().isEmpty() ? null : n.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+26
@@ -71,6 +71,7 @@ public class PinHeader extends AbstractTransparentComponent<Void> {
|
||||
private int columnCount = 5;
|
||||
private Size spacing = PIN_SPACING;
|
||||
private Boolean shrouded = false;
|
||||
private String nodeNames = "";
|
||||
|
||||
private Color bodyColor = BODY_COLOR;
|
||||
private Color shroudColor = BODY_COLOR.darker();
|
||||
@@ -342,6 +343,31 @@ public class PinHeader extends AbstractTransparentComponent<Void> {
|
||||
this.shroudColor = shroudColor;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Node Names", defaultable = false)
|
||||
public String getNodeNames() {
|
||||
if (nodeNames == null) {
|
||||
nodeNames = "";
|
||||
}
|
||||
return nodeNames;
|
||||
}
|
||||
|
||||
public void setNodeNames(String nodeNames) {
|
||||
this.nodeNames = nodeNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
// Comma-separated list, one entry per pin in control point order. When the list is
|
||||
// blank, or an individual entry is missing/blank, the pin keeps its ordinal number.
|
||||
String raw = getNodeNames();
|
||||
if (raw.trim().isEmpty()) {
|
||||
return Integer.toString(index + 1);
|
||||
}
|
||||
String[] parts = raw.split(",", -1);
|
||||
String label = index < parts.length ? parts[index].trim() : "";
|
||||
return label.isEmpty() ? Integer.toString(index + 1) : label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPointMoveFreely(int pointIndex) {
|
||||
return false;
|
||||
|
||||
+13
@@ -203,6 +203,19 @@ public class AxialElectrolyticCapacitor extends AbstractLeadedComponent<Capacita
|
||||
this.polarized = polarized;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
if (getPolarized()) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return "+";
|
||||
case 1:
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
return super.getControlPointNodeName(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Shape getBodyShape() {
|
||||
double lengthFinal = getLength().convertToPixels();
|
||||
|
||||
@@ -96,6 +96,19 @@ public class CapacitorSymbol extends AbstractSchematicLeadedSymbol<Capacitance>
|
||||
this.polarized = polarized;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
if (getPolarized()) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return "+";
|
||||
case 1:
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
return super.getControlPointNodeName(index);
|
||||
}
|
||||
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
|
||||
g2d.setColor(LEAD_COLOR);
|
||||
|
||||
+15
-1
@@ -72,6 +72,7 @@ public class RadialElectrolytic extends AbstractRadialComponent<Capacitance> imp
|
||||
private boolean polarized = true;
|
||||
private boolean folded = false;
|
||||
private Size height = HEIGHT;
|
||||
@Deprecated
|
||||
private boolean invert = false;
|
||||
|
||||
public RadialElectrolytic() {
|
||||
@@ -304,7 +305,7 @@ public class RadialElectrolytic extends AbstractRadialComponent<Capacitance> imp
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Invert Polarity")
|
||||
// @EditableProperty(name = "Invert Polarity")
|
||||
public boolean getInvert() {
|
||||
return invert;
|
||||
}
|
||||
@@ -313,6 +314,19 @@ public class RadialElectrolytic extends AbstractRadialComponent<Capacitance> imp
|
||||
this.invert = invert;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
if (getPolarized()) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return getInvert() ? "-" : "+";
|
||||
case 1:
|
||||
return getInvert() ? "+" : "-";
|
||||
}
|
||||
}
|
||||
return super.getControlPointNodeName(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Shape getBodyShape() {
|
||||
double height = (int) getHeight().convertToPixels();
|
||||
|
||||
+12
@@ -85,6 +85,18 @@ public abstract class AbstractDiodeSymbol extends AbstractSchematicLeadedSymbol<
|
||||
@Override
|
||||
protected void decorateComponentBody(Graphics2D g2d, boolean outlineMode) {}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return "A";
|
||||
case 1:
|
||||
return "K";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public Size getLength() {
|
||||
|
||||
+3
@@ -244,6 +244,9 @@ public abstract class AbstractTransistorPackage extends AbstractLabeledComponent
|
||||
public String getControlPointNodeName(int index) {
|
||||
if (index >= 3)
|
||||
return null;
|
||||
if (getPinout() != null) {
|
||||
return getPinout().toPinout().substring(index, index + 1);
|
||||
}
|
||||
return Integer.toString(index + 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,18 @@ public class DiodeGlass extends AbstractLeadedComponent<String> implements IGerb
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return "A";
|
||||
case 1:
|
||||
return "K";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
|
||||
g2d.setColor(LEAD_COLOR_ICON);
|
||||
|
||||
+12
@@ -90,6 +90,18 @@ public class DiodePlastic extends AbstractLeadedComponent<String> implements IGe
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return "A";
|
||||
case 1:
|
||||
return "K";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
|
||||
g2d.setColor(LEAD_COLOR_ICON);
|
||||
|
||||
@@ -67,6 +67,18 @@ public class LED extends AbstractLeadedComponent<String> implements IGerberCompo
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return "A";
|
||||
case 1:
|
||||
return "K";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
|
||||
|
||||
+77
-5
@@ -1,9 +1,18 @@
|
||||
package org.diylc.swing.actions.edit;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.common.IBlockProcessor;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.swing.ActionFactory;
|
||||
import org.diylc.utils.IconLoader;
|
||||
@@ -12,6 +21,9 @@ public class SaveAsBlockAction extends AbstractAction {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** How many recently used building blocks to offer in the name drop-down. */
|
||||
private static final int MAX_RECENT_BLOCKS = 10;
|
||||
|
||||
private IPlugInPort plugInPort;
|
||||
|
||||
public SaveAsBlockAction(IPlugInPort plugInPort) {
|
||||
@@ -24,10 +36,70 @@ public class SaveAsBlockAction extends AbstractAction {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
ActionFactory.LOG.info("Save as building block triggered");
|
||||
String templateName = JOptionPane.showInputDialog(null, "Block name:",
|
||||
"Save as Building Block", JOptionPane.PLAIN_MESSAGE);
|
||||
if (templateName != null && !templateName.trim().isEmpty()) {
|
||||
plugInPort.saveSelectionAsBlock(templateName);
|
||||
|
||||
JComboBox<String> nameBox =
|
||||
new JComboBox<String>(getRecentBlockNames().toArray(new String[0]));
|
||||
nameBox.setEditable(true);
|
||||
// Start empty so an existing block is never overwritten by accident. The drop-down still
|
||||
// offers the most recently used blocks (most recent first), so a user editing a block can
|
||||
// pick its name back with a single click instead of retyping it.
|
||||
nameBox.setSelectedItem("");
|
||||
|
||||
JPanel panel = new JPanel(new BorderLayout(5, 5));
|
||||
panel.add(new JLabel("Block name:"), BorderLayout.NORTH);
|
||||
panel.add(nameBox, BorderLayout.CENTER);
|
||||
|
||||
// Loop so that declining the overwrite prompt returns to the name dialog (with whatever was
|
||||
// typed still in it) rather than cancelling the whole action.
|
||||
while (true) {
|
||||
int result = JOptionPane.showConfirmDialog(null, panel, "Save as Building Block",
|
||||
JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
|
||||
if (result != JOptionPane.OK_OPTION)
|
||||
return;
|
||||
|
||||
// getEditor().getItem() captures in-progress typing as well as a picked item
|
||||
Object selected = nameBox.getEditor().getItem();
|
||||
String blockName = selected == null ? null : selected.toString().trim();
|
||||
if (blockName == null || blockName.isEmpty())
|
||||
return;
|
||||
|
||||
if (blockExists(blockName)) {
|
||||
int overwrite = JOptionPane.showConfirmDialog(null,
|
||||
"A building block named \"" + blockName + "\" already exists. Overwrite it?",
|
||||
"Save as Building Block", JOptionPane.YES_NO_CANCEL_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE);
|
||||
if (overwrite == JOptionPane.NO_OPTION)
|
||||
continue; // back to the name dialog
|
||||
if (overwrite != JOptionPane.YES_OPTION)
|
||||
return; // Cancel or dialog closed
|
||||
}
|
||||
|
||||
plugInPort.saveSelectionAsBlock(blockName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean blockExists(String blockName) {
|
||||
Map<String, ?> blocks = (Map<String, ?>) ConfigurationManager.getInstance()
|
||||
.readObject(IBlockProcessor.BLOCKS_KEY, null);
|
||||
return blocks != null && blocks.containsKey(blockName);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<String> getRecentBlockNames() {
|
||||
List<String> recent = (List<String>) ConfigurationManager.getInstance()
|
||||
.readObject(IPlugInPort.RECENT_COMPONENTS_KEY, null);
|
||||
List<String> blockNames = new ArrayList<String>();
|
||||
if (recent != null) {
|
||||
for (String identifier : recent) {
|
||||
if (identifier.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
|
||||
blockNames.add(identifier.substring(IBlockProcessor.BLOCK_PREFIX.length()));
|
||||
if (blockNames.size() >= MAX_RECENT_BLOCKS)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return blockNames;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,10 @@ import org.apache.log4j.Logger;
|
||||
import org.diylc.appframework.miscutils.IConfigListener;
|
||||
import org.diylc.appframework.miscutils.IConfigurationManager;
|
||||
|
||||
import org.diylc.common.BlockInstantiationMode;
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.DrawOption;
|
||||
import org.diylc.common.IBlockProcessor;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||
import org.diylc.swing.plugins.tree.TreePanel;
|
||||
@@ -223,6 +225,17 @@ public class CanvasPanel extends JComponent implements Autoscroll {
|
||||
(List<String>) configManager.readObject(IPlugInPort.RECENT_COMPONENTS_KEY, null);
|
||||
if (recent != null && !recent.isEmpty()) {
|
||||
String clazz = recent.get(0);
|
||||
if (clazz.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
|
||||
String blockName = clazz.substring(IBlockProcessor.BLOCK_PREFIX.length());
|
||||
try {
|
||||
CanvasPanel.this.plugInPort.loadBlock(blockName, BlockInstantiationMode.COMPOSITE);
|
||||
// hack: fake mouse movement to repaint
|
||||
CanvasPanel.this.plugInPort.mouseMoved(getMousePosition(), false, false, false);
|
||||
} catch (InvalidBlockException ex) {
|
||||
LOG.error("Could not repeat last building block: " + blockName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Map<String, List<ComponentType>> componentTypes =
|
||||
CanvasPanel.this.plugInPort.getComponentTypes();
|
||||
for (Map.Entry<String, List<ComponentType>> entry : componentTypes.entrySet()) {
|
||||
@@ -249,10 +262,10 @@ public class CanvasPanel extends JComponent implements Autoscroll {
|
||||
String typeName = shortcutMap.get("F" + i);
|
||||
if (typeName == null)
|
||||
return;
|
||||
if (typeName.startsWith("block:")) {
|
||||
String blockName = typeName.substring(6);
|
||||
if (typeName.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
|
||||
String blockName = typeName.substring(IBlockProcessor.BLOCK_PREFIX.length());
|
||||
try {
|
||||
plugInPort.loadBlock(blockName);
|
||||
plugInPort.loadBlock(blockName, BlockInstantiationMode.COMPOSITE);
|
||||
} catch (InvalidBlockException e) {
|
||||
LOG.error("Could not find block assigned to shortcut: " + blockName);
|
||||
}
|
||||
|
||||
+2
-1
@@ -53,6 +53,7 @@ import org.diylc.appframework.miscutils.Utils;
|
||||
import org.diylc.swingframework.openide.DropDownButtonFactory;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import org.diylc.common.BlockInstantiationMode;
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||
@@ -316,7 +317,7 @@ public class ComponentButtonFactory {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
try {
|
||||
plugInPort.loadBlock(blockName);
|
||||
plugInPort.loadBlock(blockName, BlockInstantiationMode.COMPOSITE);
|
||||
} catch (InvalidBlockException e1) {
|
||||
LOG.error("Error loading building block", e1);
|
||||
}
|
||||
|
||||
+19
-9
@@ -48,6 +48,7 @@ import org.diylc.appframework.miscutils.IConfigListener;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.Favorite;
|
||||
import org.diylc.common.IBlockProcessor;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.Favorite.FavoriteType;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
@@ -59,6 +60,7 @@ import org.diylc.swing.ISwingUI;
|
||||
import org.diylc.utils.IconLoader;
|
||||
import org.diylc.swing.plugins.tree.TreePanel;
|
||||
import org.diylc.swing.plugins.tree.TreePanel.DeleteBlockAction;
|
||||
import org.diylc.swing.plugins.tree.TreePanel.InsertBlockAsGroupAction;
|
||||
import org.diylc.swing.plugins.tree.TreePanel.SelectAllAction;
|
||||
|
||||
/**
|
||||
@@ -286,16 +288,22 @@ class ComponentTabbedPane extends JTabbedPane {
|
||||
private void refreshRecentComponentsToolbar(List<String> recentComponentClassList) {
|
||||
Container toolbar = getRecentToolbar();
|
||||
toolbar.removeAll();
|
||||
for (String componentClassName : recentComponentClassList) {
|
||||
ComponentType componentType;
|
||||
for (String identifier : recentComponentClassList) {
|
||||
try {
|
||||
componentType = ComponentProcessor.getInstance().extractComponentTypeFrom(
|
||||
(Class<? extends IDIYComponent<?>>) Class.forName(componentClassName));
|
||||
Component button = ComponentButtonFactory.create(plugInPort, componentType,
|
||||
createVariantPopup(componentType, null));
|
||||
toolbar.add(button);
|
||||
if (identifier.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
|
||||
String blockName = identifier.substring(IBlockProcessor.BLOCK_PREFIX.length());
|
||||
Component button = ComponentButtonFactory.createBuildingBlockButton(plugInPort, blockName,
|
||||
createVariantPopup(null, blockName));
|
||||
toolbar.add(button);
|
||||
} else {
|
||||
ComponentType componentType = ComponentProcessor.getInstance().extractComponentTypeFrom(
|
||||
(Class<? extends IDIYComponent<?>>) Class.forName(identifier));
|
||||
Component button = ComponentButtonFactory.create(plugInPort, componentType,
|
||||
createVariantPopup(componentType, null));
|
||||
toolbar.add(button);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Could not create recent component button for " + componentClassName, e);
|
||||
LOG.error("Could not create recent component button for " + identifier, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -335,7 +343,8 @@ class ComponentTabbedPane extends JTabbedPane {
|
||||
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
|
||||
variantPopup.removeAll();
|
||||
|
||||
final String identifier = componentType == null ? "block:" + blockName
|
||||
final String identifier = componentType == null
|
||||
? IBlockProcessor.BLOCK_PREFIX + blockName
|
||||
: componentType.getInstanceClass().getCanonicalName();
|
||||
|
||||
JMenu shortcutSubmenu = new JMenu("Assign Shortcut");
|
||||
@@ -456,6 +465,7 @@ class ComponentTabbedPane extends JTabbedPane {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
variantPopup.add(new InsertBlockAsGroupAction(plugInPort, blockName));
|
||||
variantPopup.add(shortcutSubmenu);
|
||||
variantPopup.add(new DeleteBlockAction(plugInPort, swingUI, blockName));
|
||||
}
|
||||
|
||||
@@ -37,8 +37,10 @@ import org.apache.log4j.Logger;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.appframework.miscutils.IConfigListener;
|
||||
|
||||
import org.diylc.common.BlockInstantiationMode;
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.Favorite;
|
||||
import org.diylc.common.IBlockProcessor;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.Favorite.FavoriteType;
|
||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||
@@ -142,7 +144,7 @@ public class CustomTreeModel implements TreeModel {
|
||||
&& System.currentTimeMillis() - previousActionTime > 100) {
|
||||
previousActionTime = System.currentTimeMillis();
|
||||
try {
|
||||
plugInPort.loadBlock(fav.getName());
|
||||
plugInPort.loadBlock(fav.getName(), BlockInstantiationMode.COMPOSITE);
|
||||
} catch (InvalidBlockException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
@@ -155,8 +157,37 @@ public class CustomTreeModel implements TreeModel {
|
||||
}
|
||||
|
||||
if (RECENTLY_USED.equals(category)) {
|
||||
for (String className : this.recentComponents) {
|
||||
ComponentType type = this.typesByClass.get(className);
|
||||
for (String identifier : this.recentComponents) {
|
||||
if (identifier.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
|
||||
final String blockName = identifier.substring(IBlockProcessor.BLOCK_PREFIX.length());
|
||||
// skip blocks that no longer exist
|
||||
if (!this.blocks.contains(blockName))
|
||||
continue;
|
||||
if (this.searchText != null && !blockName.toLowerCase().contains(this.searchText))
|
||||
continue;
|
||||
visibleTypes.add(new TreeNode(blockName, new MouseAdapter() {
|
||||
|
||||
long previousActionTime = 0;
|
||||
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e == null || SwingUtilities.isLeftMouseButton(e)
|
||||
&& System.currentTimeMillis() - previousActionTime > 100) {
|
||||
previousActionTime = System.currentTimeMillis();
|
||||
try {
|
||||
plugInPort.loadBlock(blockName, BlockInstantiationMode.COMPOSITE);
|
||||
} catch (InvalidBlockException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, true));
|
||||
continue;
|
||||
}
|
||||
final ComponentType type = this.typesByClass.get(identifier);
|
||||
// skip unknown component classes
|
||||
if (type == null)
|
||||
continue;
|
||||
if (this.searchText == null || type.getName().toLowerCase().contains(this.searchText)) {
|
||||
visibleTypes.add(new TreeNode(type, new MouseAdapter() {
|
||||
|
||||
@@ -182,7 +213,7 @@ public class CustomTreeModel implements TreeModel {
|
||||
&& System.currentTimeMillis() - previousActionTime > 100) {
|
||||
previousActionTime = System.currentTimeMillis();
|
||||
try {
|
||||
plugInPort.loadBlock(block);
|
||||
plugInPort.loadBlock(block, BlockInstantiationMode.COMPOSITE);
|
||||
} catch (InvalidBlockException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
@@ -233,10 +264,16 @@ public class CustomTreeModel implements TreeModel {
|
||||
}
|
||||
|
||||
if (RECENTLY_USED.equals(category)) {
|
||||
for (String className : this.recentComponents) {
|
||||
ComponentType type = typesByClass.get(className);
|
||||
if (type.getName().toLowerCase().contains(searchText))
|
||||
return true;
|
||||
for (String identifier : this.recentComponents) {
|
||||
if (identifier.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
|
||||
if (identifier.substring(IBlockProcessor.BLOCK_PREFIX.length()).toLowerCase()
|
||||
.contains(searchText))
|
||||
return true;
|
||||
} else {
|
||||
ComponentType type = typesByClass.get(identifier);
|
||||
if (type != null && type.getName().toLowerCase().contains(searchText))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,10 +60,13 @@ import org.apache.log4j.lf5.viewer.categoryexplorer.TreeModelAdapter;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.appframework.miscutils.IConfigListener;
|
||||
|
||||
import org.diylc.common.BlockInstantiationMode;
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.Favorite;
|
||||
import org.diylc.common.IBlockProcessor;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.Favorite.FavoriteType;
|
||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.IView;
|
||||
import org.diylc.core.Template;
|
||||
@@ -313,7 +316,8 @@ public class TreePanel extends JPanel {
|
||||
TreeNode payload = (TreeNode) selectedNode;
|
||||
final ComponentType componentType = payload.getComponentType();
|
||||
|
||||
final String identifier = componentType == null ? "block:" + payload.toString()
|
||||
final String identifier = componentType == null
|
||||
? IBlockProcessor.BLOCK_PREFIX + payload.toString()
|
||||
: componentType.getInstanceClass().getCanonicalName();
|
||||
|
||||
JMenu shortcutSubmenu = new JMenu("Assign Shortcut");
|
||||
@@ -434,6 +438,7 @@ public class TreePanel extends JPanel {
|
||||
}
|
||||
}
|
||||
} else if (getTreeModel().isLeaf(selectedNode)) {
|
||||
popup.add(new InsertBlockAsGroupAction(plugInPort, payload.toString()));
|
||||
popup.add(shortcutSubmenu);
|
||||
popup.add(new DeleteBlockAction(plugInPort, swingUI, payload.toString()));
|
||||
}
|
||||
@@ -494,7 +499,8 @@ public class TreePanel extends JPanel {
|
||||
|
||||
HashMap<String, String> shortcutMap = (HashMap<String, String>) ConfigurationManager
|
||||
.getInstance().readObject(TreePanel.COMPONENT_SHORTCUT_KEY, null);
|
||||
String identifier = payload.getComponentType() == null ? "block:" + payload.toString()
|
||||
String identifier = payload.getComponentType() == null
|
||||
? IBlockProcessor.BLOCK_PREFIX + payload.toString()
|
||||
: payload.getComponentType().getInstanceClass().getCanonicalName();
|
||||
if (shortcutMap != null && shortcutMap.containsValue(identifier)) {
|
||||
for (String key : shortcutMap.keySet()) {
|
||||
@@ -565,6 +571,37 @@ public class TreePanel extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Left click on a building block node inserts it in composite mode (a single rigid component)
|
||||
* by default - see design decision D10 in {@code docs/plans/composite-building-blocks.md}.
|
||||
* This action offers the one extra click needed to fall back to the original behavior:
|
||||
* every component of the block instantiated separately and auto-grouped.
|
||||
*/
|
||||
public static class InsertBlockAsGroupAction extends AbstractAction {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private IPlugInPort plugInPort;
|
||||
private String blockName;
|
||||
|
||||
public InsertBlockAsGroupAction(IPlugInPort plugInPort, String blockName) {
|
||||
super();
|
||||
this.plugInPort = plugInPort;
|
||||
this.blockName = blockName;
|
||||
putValue(AbstractAction.NAME, "Insert as Grouped Components");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
LOG.info(getValue(AbstractAction.NAME) + " triggered");
|
||||
try {
|
||||
plugInPort.loadBlock(blockName, BlockInstantiationMode.GROUP);
|
||||
} catch (InvalidBlockException e1) {
|
||||
LOG.error("Could not find building block: " + blockName, e1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class DeleteBlockAction extends AbstractAction {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
# Composite Building Blocks
|
||||
|
||||
Instantiating a building block as a single, self-contained component.
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Today a building block is a named list of components stored in local config
|
||||
(`buildingBlocks` key). Placing one clones the components, gives each a fresh
|
||||
name and UUID, and drops them on the canvas as an auto-group. They remain
|
||||
individually selectable, editable and stretchable.
|
||||
|
||||
This document plans a second instantiation mode. In **composite mode** a block
|
||||
becomes one component on the canvas: it has its own name, it aggregates the
|
||||
control points of all its children, it moves and rotates as a rigid unit, and it
|
||||
appears as a single entry in the BOM, the netlist and the AI project
|
||||
description. This makes it possible to build something like an Arduino board out
|
||||
of an existing board component plus pin headers, save it as a block, and from
|
||||
then on treat it as a first-class part that other components can connect to.
|
||||
|
||||
Group mode is kept unchanged. Composite mode becomes the default for a plain
|
||||
click; group mode costs one extra click.
|
||||
|
||||
## 2. Design decisions
|
||||
|
||||
| # | Decision | Choice | Rationale |
|
||||
|---|---|---|---|
|
||||
| D1 | Where child definitions live | Embedded in the instance, serialized into the `.diy` file | Projects stay portable; a file opens correctly on a machine that does not have the block in its local config. Blocks in config remain the *template*, not a live dependency. |
|
||||
| D2 | Which control points are exposed | All of them, delegated straight through to children | No curation UI needed, and `isControlPointSticky` / `getControlPointVisibilityPolicy` come along for free with correct per-child semantics. |
|
||||
| D3 | Terminal naming | `getSectionNames(i)` returns the child's name; `getControlPointNodeName(i)` returns the child's node name | `Node.toString()` already renders `component.getName() + "." + section + "." + nodeName`, so a netlist node reads `ARD1.PH1.3` with no changes to the netlist code. |
|
||||
| D4 | Geometry | Rigid — `canPointMoveFreely(i)` returns `false` for every index | This is already a first-class concept used by ~60 components (`DIL_IC`, `Breadboard`, `TubeSocket`…). No new plumbing. |
|
||||
| D5 | Type identity | One shared `ComponentType`; the instance carries a `blockName` field, and the handful of type-name consumers prefer it | `ComponentProcessor` caches `ComponentType` per Java class, so per-block types would mean changing the cache key of a widely used singleton. The field approach has a much smaller blast radius. |
|
||||
| D6 | Electrical modelling | The composite is **not** `IContinuity` and **not** `ISwitch` | A composite is a footprint that exposes terminals, not a circuit model. Internal point-to-point continuity and internal switching are explicitly out of scope. |
|
||||
| D7 | Internal copper | Left alone — child copper still registers continuity areas | Children draw through the same `G2DWrapper`, so `startTrackingContinuityArea` calls land on the composite naturally. A block built from a board with copper pours still conducts, which matches physical reality. Zero code. |
|
||||
| D8 | Child properties | Opaque — the property editor shows only the composite's own properties | Matches the "single component" mental model. Children are frozen as saved. Because children are serialized anyway (D1), flattened child-property editing can be layered on later with no file-format change. |
|
||||
| D9 | Rotation / mirroring | Supported, by delegating to each child's own `IComponentTransformer` around a shared centre | Reuses existing per-component transformers. If any child's type has no transformer, the composite reports that it cannot rotate. |
|
||||
| D10 | Mode selection | Composite is the global default on click; group mode via the existing drop-down / right-click popup | Most uses want the new behaviour, and it costs no extra click. |
|
||||
| D11 | BOM | One row per composite, keyed by block name | Consistent with D5 and D8. |
|
||||
| D12 | Explode / collapse | Not in this change | The only way to get a composite is to place a block in composite mode; the only way to get loose components is group mode. Keeps the surface area small. |
|
||||
|
||||
### Explicitly out of scope
|
||||
|
||||
- Converting a placed composite back into loose components, or collapsing an
|
||||
arbitrary selection into a composite (D12).
|
||||
- Editing child properties after placement (D8).
|
||||
- Internal switches. A block containing an `ISwitch` will place fine, but the
|
||||
switch is inert: it contributes terminals and drawing, no switching
|
||||
behaviour (D6). See §7.
|
||||
- Pushing block-library edits into already-placed instances (D1).
|
||||
|
||||
## 3. Why this fits the existing architecture
|
||||
|
||||
Three mechanisms already exist and carry most of the weight.
|
||||
|
||||
**Rigidity.** `Presenter` reads `canPointMoveFreely` in exactly three places:
|
||||
`Presenter.java:868` excludes the point from individual dragging,
|
||||
`Presenter.java:1243` forces *all* control points into the drag set when the
|
||||
component is stuck to something, and `Presenter.java:1418` marks the move as
|
||||
rigid in `moveComponents`. Returning `false` everywhere yields "moves as a unit,
|
||||
never stretches" with no new code.
|
||||
|
||||
**Placement.** `Presenter.loadBlock` (`Presenter.java:2847`) already routes
|
||||
through `pasteComponents(new ComponentTransferable(components, blockName), true,
|
||||
true)`. Composite mode simply pastes a one-element list. Note that
|
||||
`InstantiationManager.pasteComponents` and `updateSingleClick` position
|
||||
components by translating *every* control point by the same delta via
|
||||
`setControlPoint(p, i)` — so `setControlPoint` must delegate per-point to the
|
||||
child. Rigidity is enforced by `canPointMoveFreely`, never by `setControlPoint`
|
||||
refusing a write.
|
||||
|
||||
**Node naming.** `Node.getDisplayName()` prefixes the node name with
|
||||
`getSectionNames(pointIndex)[0]` when there is exactly one section, and
|
||||
`Node.toString()` prefixes that with the component name. Returning the child's
|
||||
name as the section gives hierarchical netlist labels for free.
|
||||
|
||||
Three things do *not* come for free and are the substance of the work: type
|
||||
identity (D5), palette visibility (§4.2), and a correct deep `clone()` (§7).
|
||||
|
||||
## 4. Implementation
|
||||
|
||||
### 4.1 New: `CompositeComponent`
|
||||
|
||||
`diylc-core/src/main/java/org/diylc/components/composite/CompositeComponent.java`
|
||||
|
||||
The package must sit under `org.diylc.components` — `ComponentProcessor`
|
||||
restricts its Reflections scan with
|
||||
`filterInputsBy(new FilterBuilder().includePackage("org.diylc.components"))`, and
|
||||
`ProjectFileManager.configure` declares
|
||||
`xStream.aliasPackage("diylc", "org.diylc.components")`, so the class serializes
|
||||
as `diylc.composite.CompositeComponent` with no extra XStream configuration.
|
||||
|
||||
```java
|
||||
@ComponentDescriptor(
|
||||
name = "Building Block",
|
||||
description = "A group of components instantiated as a single rigid component",
|
||||
category = "Building Blocks",
|
||||
instanceNamePrefix = "BLK",
|
||||
author = "Branislav Stojkovic",
|
||||
zOrder = IDIYComponent.COMPONENT,
|
||||
flexibleZOrder = true,
|
||||
bomPolicy = BomPolicy.SHOW_ALL_NAMES,
|
||||
autoEdit = false,
|
||||
enableCache = false,
|
||||
transformer = CompositeComponentTransformer.class,
|
||||
hiddenInPalette = true)
|
||||
public class CompositeComponent extends AbstractComponent<Void> {
|
||||
|
||||
private List<IDIYComponent<?>> components; // embedded children (D1)
|
||||
private String blockName; // type identity (D5)
|
||||
|
||||
private transient int[] childOf; // flat index -> child ordinal
|
||||
private transient int[] pointOf; // flat index -> child point index
|
||||
}
|
||||
```
|
||||
|
||||
The `@ComponentDescriptor` annotation is mandatory, not cosmetic:
|
||||
`extractComponentTypeFrom` returns `null` without it, and `NetlistBuilder:69`
|
||||
and `:401` dereference the result.
|
||||
|
||||
**Point index mapping.** A flat index is built lazily from the children and
|
||||
cached in the two transient arrays. Children never change after construction
|
||||
(D8), so the cache never needs invalidating; it only needs rebuilding after
|
||||
deserialization, which the lazy getter handles.
|
||||
|
||||
**Delegating members**
|
||||
|
||||
| Member | Behaviour |
|
||||
|---|---|
|
||||
| `getControlPointCount()` | Sum over children |
|
||||
| `getControlPoint(i)` | Delegate |
|
||||
| `setControlPoint(p, i)` | Delegate (required for placement — see §3) |
|
||||
| `isControlPointSticky(i)` | Delegate |
|
||||
| `canControlPointOverlap(i)` | Delegate |
|
||||
| `getControlPointVisibilityPolicy(i)` | Delegate |
|
||||
| `getControlPointNodeName(i)` | Child's node name, prefixed with the child's own section name when it has exactly one |
|
||||
| `getSectionNames(i)` | `new String[] { child.getName() }` |
|
||||
| `canPointMoveFreely(i)` | `false` (D4) |
|
||||
| `createdIn(project)` | Forward to every child |
|
||||
|
||||
**Own members**
|
||||
|
||||
- `draw(...)` — forwards to each child in order with the same `Graphics2D` and
|
||||
`IDrawingObserver`, passing `ComponentState.NORMAL` so children do not each
|
||||
paint their own red highlight. When `state != NORMAL` the composite then draws
|
||||
a single dashed outline around the aggregate bounds. (DIYLC has no central
|
||||
selection rendering; components draw their own, so the composite must draw
|
||||
its own.)
|
||||
- `drawIcon(...)` — a generic block glyph. `extractComponentTypeFrom` calls
|
||||
`drawIcon` on a default-constructed instance with no children, so this must
|
||||
not touch `components`.
|
||||
- `getValue()` / `setValue()` — `null` / no-op. `getValueForDisplay()` inherits
|
||||
the `AbstractComponent` behaviour of returning `""` for a null value, which
|
||||
keeps the BOM row (`BomMaker` skips rows with a null value).
|
||||
- `getBlockName()` — `@EditableProperty(name = "Block")`, read-only in practice.
|
||||
- `clone()` — **must be overridden.** `AbstractComponent.clone()` copies fields
|
||||
reflectively and only deep-copies `Point2D` and `Point2D[]`. The `components`
|
||||
list would be shared by reference between clones, so moving one instance would
|
||||
move every copy. The override deep-clones each child (via each child's own
|
||||
`clone()`, which preserves its id, matching `AbstractComponent.clone()`'s
|
||||
contract) and leaves the transient index arrays to rebuild lazily. `Building
|
||||
BlockManager.loadBlock` is a separate path and explicitly assigns fresh ids
|
||||
when it clones source components to place a brand new instance. See §7.
|
||||
|
||||
### 4.2 Hiding the type from the palette
|
||||
|
||||
`ComponentProcessor.getComponentTypes()` groups every annotated class by
|
||||
category and feeds the toolbox tabs and the component tree. A composite is never
|
||||
created from the palette, so it should not appear there — but it still needs a
|
||||
resolvable `ComponentType` for the netlist, BOM and name generation.
|
||||
|
||||
Add `boolean hiddenInPalette() default false` to `ComponentDescriptor`, carry it
|
||||
on `ComponentType`, and skip such types inside `getComponentTypes()`.
|
||||
`extractComponentTypeFrom` is unaffected, so every existing consumer keeps
|
||||
working. This is one new annotation attribute and one `continue`.
|
||||
|
||||
### 4.3 New: `CompositeComponentTransformer`
|
||||
|
||||
`diylc-core/src/main/java/org/diylc/components/composite/CompositeComponentTransformer.java`
|
||||
|
||||
```java
|
||||
canRotate(c) -> every child's ComponentType has a transformer that canRotate(child)
|
||||
canMirror(c) -> same, for canMirror
|
||||
mirroringChangesCircuit() -> true if any child's transformer says so
|
||||
rotate(c, center, dir) -> for each child: childType.getTransformer().rotate(child, center, dir)
|
||||
mirror(c, center, dir) -> likewise
|
||||
```
|
||||
|
||||
Passing the same `center` to every child keeps the assembly geometrically
|
||||
consistent. A child whose type declares no transformer makes the whole composite
|
||||
non-rotatable, which is honest — silently leaving one child un-rotated would be
|
||||
worse.
|
||||
|
||||
### 4.4 Instantiation mode
|
||||
|
||||
New enum `org.diylc.common.BlockInstantiationMode { GROUP, COMPOSITE }`.
|
||||
|
||||
`IBlockProcessor` gains `loadBlock(String blockName, BlockInstantiationMode mode)`.
|
||||
The existing single-argument `loadBlock` is kept and delegates with
|
||||
`COMPOSITE`, so the default is the new behaviour (D10) and no call site is
|
||||
left behind.
|
||||
|
||||
`BuildingBlockManager.loadBlock(blockName, existingComponents, mode)`:
|
||||
|
||||
- `GROUP` — exactly today's behaviour.
|
||||
- `COMPOSITE` — clone the stored components with fresh UUIDs but **keep their
|
||||
saved names unchanged**. Child names are namespaced under the composite's name
|
||||
(D3), so running them through `createUniqueName` against the project would
|
||||
produce meaningless drift (`R1` becoming `R37`) and break netlist labels
|
||||
between two instances of the same block. Then wrap them in a
|
||||
`CompositeComponent`, set `blockName`, and give the composite itself a
|
||||
project-unique name via `createUniqueName`.
|
||||
|
||||
`Presenter.loadBlock` passes the mode through. For composite mode, call
|
||||
`pasteComponents(..., autoGroup = false, ...)` — there is nothing to group.
|
||||
|
||||
### 4.5 Type-name consumers (D5)
|
||||
|
||||
Add a helper so the special case lives in one place:
|
||||
|
||||
```java
|
||||
// ComponentProcessor
|
||||
public static String getDisplayTypeName(IDIYComponent<?> c, ComponentType type) {
|
||||
if (c instanceof CompositeComponent cc && cc.getBlockName() != null)
|
||||
return cc.getBlockName();
|
||||
return type.getName();
|
||||
}
|
||||
```
|
||||
|
||||
Call sites to update:
|
||||
|
||||
| File | Line | Effect |
|
||||
|---|---|---|
|
||||
| `AiProjectBuilder.mapComponent` | `:178` | The AI sees `"Arduino Uno"` instead of `"Building Block"` — the main reason for D5 |
|
||||
| `BomMaker.getBom` | `:80`, `:100-101` | Grouping key and BOM row label become the block name (D11) |
|
||||
| `Presenter.selectMatching` | `:1064` | "select matching" finds blocks by their block name |
|
||||
|
||||
`SpiceSumarizer:94,107` should be audited but likely needs no change — a
|
||||
composite is not a recognised SPICE element and will fall through as it does for
|
||||
any other unmodelled component.
|
||||
|
||||
### 4.6 UI wiring
|
||||
|
||||
Four call sites reach `loadBlock`, all through `IPlugInPort`:
|
||||
|
||||
| File | Line | Change |
|
||||
|---|---|---|
|
||||
| `ComponentButtonFactory.createBuildingBlockButton` | `:319` | Default action → `COMPOSITE` |
|
||||
| `CustomTreeModel` (favorites) | `:145` | Click → `COMPOSITE` |
|
||||
| `CustomTreeModel` (Building Blocks category) | `:185` | Click → `COMPOSITE` |
|
||||
| `CanvasPanel.functionKeyPressed` | `:255` | F-key shortcut → `COMPOSITE` |
|
||||
|
||||
Group mode is reached from the drop-down popup the toolbox button already
|
||||
carries. `ComponentTabbedPane.createVariantPopup(null, blockName)` builds it for
|
||||
blocks today (currently only the "Assign Shortcut" submenu); add an **"Insert as
|
||||
Separate Components"** item at the top. Mirror it in the tree's right-click menu
|
||||
for block nodes.
|
||||
|
||||
## 5. Serialization and compatibility
|
||||
|
||||
- **Existing `.diy` files** are unaffected — they contain no composites.
|
||||
- **Existing blocks in config** are unaffected. The mode is chosen at placement,
|
||||
not stored on the block, so `buildingBlocks` keeps its
|
||||
`Map<String, List<IDIYComponent<?>>>` shape and needs no migration. Default
|
||||
and imported blocks work in both modes immediately.
|
||||
- **New `.diy` files containing composites** cannot be opened by older DIYLC
|
||||
versions. Expected and unavoidable given D1.
|
||||
- Children serialize as ordinary nested `diylc.*` elements inside the
|
||||
composite's `components` list, so every existing component converter applies
|
||||
unchanged.
|
||||
|
||||
## 6. Suggested order of work
|
||||
|
||||
1. `CompositeComponent` with the delegating members, the point index map, a
|
||||
correct `clone()`, and `draw`. No UI yet — exercise it from a unit test that
|
||||
builds a composite directly and asserts point counts, node names and rigidity.
|
||||
2. `hiddenInPalette` on `ComponentDescriptor` / `ComponentType` /
|
||||
`getComponentTypes()`.
|
||||
3. `BlockInstantiationMode`, the `IBlockProcessor` overload,
|
||||
`BuildingBlockManager.loadBlock`, `Presenter.loadBlock`.
|
||||
4. UI wiring: four default call sites plus the "Insert as Separate Components"
|
||||
popup items.
|
||||
5. `getDisplayTypeName` and its three consumers.
|
||||
6. `CompositeComponentTransformer`, and rotation/mirroring enablement.
|
||||
7. Round-trip and regression tests (§8).
|
||||
|
||||
Steps 1–4 are the minimum for a usable feature; 5–6 complete it.
|
||||
|
||||
## 7. Risks
|
||||
|
||||
**`clone()` is the sharp edge.** `AbstractComponent.clone()` is reflective and
|
||||
only deep-copies points. Without an override, two placed copies of the same
|
||||
composite share one child list, and moving one moves both. This will not fail
|
||||
loudly — it will look like a bizarre rendering bug. It needs a dedicated test.
|
||||
|
||||
**Z-order.** A composite occupies a single slot in `project.getComponents()`, so
|
||||
children spanning the board, trace and component layers all render at that one
|
||||
slot. For a board-like block this is correct. It does mean an external trace
|
||||
cannot be sandwiched between a block's board and the parts mounted on it. The
|
||||
descriptor uses `zOrder = COMPONENT` with `flexibleZOrder = true`; deriving the
|
||||
z-order from the lowest child at save time is a possible refinement if this
|
||||
proves annoying in practice.
|
||||
|
||||
**Inert internal switches.** Per D6 the composite is not `ISwitch`, so a block
|
||||
containing a switch places and draws correctly but contributes no switching to
|
||||
the netlist. This is a silent difference from group mode. Worth a log warning
|
||||
when a composite is created from a selection containing an `ISwitch`, and worth
|
||||
mentioning in the release notes.
|
||||
|
||||
**Variants.** `Presenter.getVariantsFor` keys on `ComponentType`, so all
|
||||
composites share one variant pool regardless of block. Applying a variant to a
|
||||
composite would be meaningless. Suppress the variant UI for `CompositeComponent`
|
||||
selections.
|
||||
|
||||
**Naming collisions.** Two instances of the same block deliberately carry
|
||||
identically named children (§4.4). Netlist labels stay unique because they are
|
||||
prefixed with the composite's own name (`ARD1.PH1.3` vs `ARD2.PH1.3`). Any code
|
||||
that assumes component names are globally unique across the project would be
|
||||
affected — none is known, but worth watching.
|
||||
|
||||
## 8. Testing
|
||||
|
||||
- **Unit** — point count equals the sum over children; `getControlPoint(i)`
|
||||
tracks the child after the child moves; `canPointMoveFreely` is false for
|
||||
every index; `getSectionNames`/`getControlPointNodeName` produce
|
||||
`ARD1.PH1.3` through `Node.toString()`.
|
||||
- **Clone isolation** — clone a composite, move the clone, assert the original's
|
||||
points did not move.
|
||||
- **Round trip** — save a project containing a composite, reload it, assert the
|
||||
point count, names and geometry survive, and that the transient index arrays
|
||||
rebuild lazily.
|
||||
- **Both modes** — place the same block in group and composite mode; assert the
|
||||
group yields N components and the composite yields one with the same total
|
||||
point count and the same terminal coordinates.
|
||||
- **Connectivity** — place a composite built from a board with copper, run the
|
||||
netlist, confirm internal copper still forms continuity areas (D7) while no
|
||||
`IContinuity` point-to-point connections are reported (D6).
|
||||
- **BOM** — two instances of one block collapse to a single row with quantity 2,
|
||||
labelled by block name.
|
||||
- **Regression** — run the existing `diylc-regression-data` suite to confirm
|
||||
nothing in the ordinary drawing/netlist path shifted.
|
||||
Reference in New Issue
Block a user