mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-13 02:32:09 +10:00
Compare commits
15 Commits
| 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 boolean enableCache;
|
||||||
private List<String[]> datasheet;
|
private List<String[]> datasheet;
|
||||||
private int datasheetCreationStepCount;
|
private int datasheetCreationStepCount;
|
||||||
|
|
||||||
public ComponentType() {
|
public ComponentType() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComponentType(String name, String description, CreationMethod creationMethod, String category,
|
public ComponentType(String name, String description, CreationMethod creationMethod, String category,
|
||||||
String namePrefix, String author, Icon icon, Class<? extends IDIYComponent<?>> instanceClass, double zOrder,
|
String namePrefix, String author, Icon icon, Class<? extends IDIYComponent<?>> instanceClass, double zOrder,
|
||||||
boolean flexibleZOrder, BomPolicy bomPolicy, boolean autoEdit, IComponentTransformer transformer,
|
boolean flexibleZOrder, BomPolicy bomPolicy, boolean autoEdit, IComponentTransformer transformer,
|
||||||
KeywordPolicy keywordPolicy, String keywordTag, boolean enableCache, List<String[]> datasheet,
|
KeywordPolicy keywordPolicy, String keywordTag, boolean enableCache, List<String[]> datasheet,
|
||||||
int datasheetCreationStepCount) {
|
int datasheetCreationStepCount) {
|
||||||
super();
|
super();
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
|||||||
@@ -25,13 +25,28 @@ import java.io.IOException;
|
|||||||
|
|
||||||
public interface IBlockProcessor {
|
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";
|
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 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);
|
void deleteBlock(String blockName);
|
||||||
|
|
||||||
int importBlocks(String fileName) throws IOException;
|
int importBlocks(String fileName) throws IOException;
|
||||||
|
|||||||
@@ -294,11 +294,6 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getSectionNames(int pointIndex) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canPointMoveFreely(int pointIndex) {
|
public boolean canPointMoveFreely(int pointIndex) {
|
||||||
return true;
|
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);
|
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
|
* @param pointIndex
|
||||||
* @return true if the control point can move without affecting other points' position, false otherwise
|
* @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;
|
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
|
* @return
|
||||||
*/
|
*/
|
||||||
int datasheetCreationStepCount() default 0;
|
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() {
|
public String getDisplayName() {
|
||||||
String[] section = component.getSectionNames(pointIndex);
|
return component.getControlPointNodeName(pointIndex);
|
||||||
return (section == null || section.length > 1 ? "" : section[0] + ".") + component.getControlPointNodeName(pointIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Point2D getPoint2D() {
|
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(),
|
component.getValue() == null ? null : component.getValue().toString(),
|
||||||
fromPos, toPos, pos, terminals.isEmpty() ? null : terminals);
|
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.io.xml.DomDriver;
|
||||||
import com.thoughtworks.xstream.security.AnyTypePermission;
|
import com.thoughtworks.xstream.security.AnyTypePermission;
|
||||||
|
|
||||||
|
import org.diylc.common.BlockInstantiationMode;
|
||||||
import org.diylc.common.BuildingBlockPackage;
|
import org.diylc.common.BuildingBlockPackage;
|
||||||
|
import org.diylc.common.ComponentType;
|
||||||
import org.diylc.common.IBlockProcessor;
|
import org.diylc.common.IBlockProcessor;
|
||||||
import org.diylc.common.IPlugInPort;
|
import org.diylc.common.IPlugInPort;
|
||||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||||
|
import org.diylc.components.composite.CompositeComponent;
|
||||||
import org.diylc.core.IDIYComponent;
|
import org.diylc.core.IDIYComponent;
|
||||||
|
import org.diylc.core.ISwitch;
|
||||||
import org.diylc.serialization.ProjectFileManager;
|
import org.diylc.serialization.ProjectFileManager;
|
||||||
|
|
||||||
public class BuildingBlockManager {
|
public class BuildingBlockManager {
|
||||||
@@ -119,19 +123,30 @@ public class BuildingBlockManager {
|
|||||||
return pkg.getBlocks().size();
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
public List<IDIYComponent<?>> loadBlock(String blockName,
|
public List<IDIYComponent<?>> loadBlock(String blockName,
|
||||||
List<IDIYComponent<?>> existingComponents) throws InvalidBlockException {
|
List<IDIYComponent<?>> existingComponents, BlockInstantiationMode mode)
|
||||||
LOG.debug(String.format("loadBlock(%s)", blockName));
|
throws InvalidBlockException {
|
||||||
|
LOG.debug(String.format("loadBlock(%s, %s)", blockName, mode));
|
||||||
Map<String, List<IDIYComponent<?>>> blocks = (Map<String, List<IDIYComponent<?>>>) configManager
|
Map<String, List<IDIYComponent<?>>> blocks = (Map<String, List<IDIYComponent<?>>>) configManager
|
||||||
.readObject(IPlugInPort.BLOCKS_KEY, null);
|
.readObject(IPlugInPort.BLOCKS_KEY, null);
|
||||||
if (blocks != null) {
|
if (blocks == null)
|
||||||
Collection<IDIYComponent<?>> components = blocks.get(blockName);
|
throw new InvalidBlockException();
|
||||||
if (components == null)
|
Collection<IDIYComponent<?>> components = blocks.get(blockName);
|
||||||
throw new InvalidBlockException();
|
if (components == null)
|
||||||
// clear potential control point every time!
|
throw new InvalidBlockException();
|
||||||
instantiationManager.setPotentialControlPoint(null);
|
// clear potential control point every time!
|
||||||
// clone components
|
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<?>> clones = new ArrayList<IDIYComponent<?>>();
|
||||||
List<IDIYComponent<?>> testComponents = new ArrayList<IDIYComponent<?>>(existingComponents);
|
List<IDIYComponent<?>> testComponents = new ArrayList<IDIYComponent<?>>(existingComponents);
|
||||||
for (IDIYComponent<?> c : components)
|
for (IDIYComponent<?> c : components)
|
||||||
@@ -147,8 +162,42 @@ public class BuildingBlockManager {
|
|||||||
LOG.error("Could not clone component: " + c);
|
LOG.error("Could not clone component: " + c);
|
||||||
}
|
}
|
||||||
return clones;
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
@@ -197,5 +246,11 @@ public class BuildingBlockManager {
|
|||||||
blocks.remove(blockName);
|
blocks.remove(blockName);
|
||||||
configManager.writeValue(IBlockProcessor.BLOCKS_KEY, blocks);
|
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.ComponentType;
|
||||||
import org.diylc.common.IComponentTransformer;
|
import org.diylc.common.IComponentTransformer;
|
||||||
import org.diylc.common.PropertyWrapper;
|
import org.diylc.common.PropertyWrapper;
|
||||||
|
import org.diylc.components.composite.CompositeComponent;
|
||||||
import org.diylc.core.CreationMethod;
|
import org.diylc.core.CreationMethod;
|
||||||
import org.diylc.core.IDIYComponent;
|
import org.diylc.core.IDIYComponent;
|
||||||
|
|
||||||
@@ -113,7 +114,7 @@ public class ComponentProcessor {
|
|||||||
String keywordTag;
|
String keywordTag;
|
||||||
List<String[]> datasheet = null;
|
List<String[]> datasheet = null;
|
||||||
int datasheetCreationStepCount = 0;
|
int datasheetCreationStepCount = 0;
|
||||||
|
|
||||||
if (clazz.isAnnotationPresent(ComponentDescriptor.class)) {
|
if (clazz.isAnnotationPresent(ComponentDescriptor.class)) {
|
||||||
ComponentDescriptor annotation = clazz.getAnnotation(ComponentDescriptor.class);
|
ComponentDescriptor annotation = clazz.getAnnotation(ComponentDescriptor.class);
|
||||||
name = annotation.name();
|
name = annotation.name();
|
||||||
@@ -159,7 +160,7 @@ public class ComponentProcessor {
|
|||||||
}
|
}
|
||||||
ComponentType componentType =
|
ComponentType componentType =
|
||||||
new ComponentType(name, description, creationMethod, category, namePrefix, author, icon, clazz, zOrder,
|
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);
|
datasheetCreationStepCount);
|
||||||
componentTypeMap.put(clazz.getName(), componentType);
|
componentTypeMap.put(clazz.getName(), componentType);
|
||||||
return componentType;
|
return componentType;
|
||||||
@@ -273,6 +274,11 @@ public class ComponentProcessor {
|
|||||||
// just to store in the cache
|
// just to store in the cache
|
||||||
ComponentProcessor.getInstance().extractProperties(clazz);
|
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;
|
List<ComponentType> nestedList;
|
||||||
if (componentTypes.containsKey(componentType.getCategory())) {
|
if (componentTypes.containsKey(componentType.getCategory())) {
|
||||||
nestedList = componentTypes.get(componentType.getCategory());
|
nestedList = componentTypes.get(componentType.getCategory());
|
||||||
@@ -422,6 +428,23 @@ public class ComponentProcessor {
|
|||||||
return transformer;
|
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) {
|
public static boolean hasStickyPoint(IDIYComponent<?> c) {
|
||||||
for (int i = 0; i < c.getControlPointCount(); i++)
|
for (int i = 0; i < c.getControlPointCount(); i++)
|
||||||
if (c.isControlPointSticky(0))
|
if (c.isControlPointSticky(0))
|
||||||
|
|||||||
@@ -272,6 +272,31 @@ public class InstantiationManager {
|
|||||||
return true;
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
private List<IDIYComponent<?>> instantiateComponent(ComponentType componentType, Point2D point,
|
private List<IDIYComponent<?>> instantiateComponent(ComponentType componentType, Point2D point,
|
||||||
Project currentProject) throws InstantiationException, IllegalAccessException, NoSuchMethodException,
|
Project currentProject) throws InstantiationException, IllegalAccessException, NoSuchMethodException,
|
||||||
@@ -311,21 +336,7 @@ public class InstantiationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write to recent components
|
// Write to recent components
|
||||||
List<String> recentComponentTypes =
|
addToRecentComponents(componentType.getInstanceClass().getName());
|
||||||
(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<IDIYComponent<?>> list = new ArrayList<IDIYComponent<?>>();
|
List<IDIYComponent<?>> list = new ArrayList<IDIYComponent<?>>();
|
||||||
list.add(component);
|
list.add(component);
|
||||||
|
|||||||
@@ -1060,9 +1060,9 @@ public class Presenter implements IPlugInPort {
|
|||||||
String regex = ".*" + criteria.toLowerCase() + ".*";
|
String regex = ".*" + criteria.toLowerCase() + ".*";
|
||||||
for (IDIYComponent<?> c : currentProject.getComponents()) {
|
for (IDIYComponent<?> c : currentProject.getComponents()) {
|
||||||
ComponentType type = ComponentProcessor.getInstance().extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) c.getClass());
|
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)) ||
|
(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);
|
matching.add(c);
|
||||||
}
|
}
|
||||||
updateSelection(matching);
|
updateSelection(matching);
|
||||||
@@ -2844,9 +2844,14 @@ public class Presenter implements IPlugInPort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadBlock(String blockName) throws InvalidBlockException {
|
public void loadBlock(String blockName, BlockInstantiationMode mode) throws InvalidBlockException {
|
||||||
List<IDIYComponent<?>> components = buildingBlockManager.loadBlock(blockName, currentProject.getComponents());
|
List<IDIYComponent<?>> components =
|
||||||
pasteComponents(new ComponentTransferable(components, blockName), true, true);
|
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
|
@Override
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ public class BomMaker {
|
|||||||
(Class<? extends IDIYComponent<?>>) component.getClass());
|
(Class<? extends IDIYComponent<?>>) component.getClass());
|
||||||
if (type.getBomPolicy() == BomPolicy.NEVER_SHOW)
|
if (type.getBomPolicy() == BomPolicy.NEVER_SHOW)
|
||||||
continue;
|
continue;
|
||||||
|
String typeName = ComponentProcessor.getDisplayTypeName(component, type);
|
||||||
String name = component.getName();
|
String name = component.getName();
|
||||||
String value;
|
String value;
|
||||||
try {
|
try {
|
||||||
@@ -89,7 +90,7 @@ public class BomMaker {
|
|||||||
value = "<undefined>";
|
value = "<undefined>";
|
||||||
}
|
}
|
||||||
if ((name != null) && (value != null)) {
|
if ((name != null) && (value != null)) {
|
||||||
String key = type.getName() + "|" + value;
|
String key = typeName + "|" + value;
|
||||||
if (entryMap.containsKey(key)) {
|
if (entryMap.containsKey(key)) {
|
||||||
BomEntry entry = entryMap.get(key);
|
BomEntry entry = entryMap.get(key);
|
||||||
entry.setQuantity(entry.getQuantity() + 1);
|
entry.setQuantity(entry.getQuantity() + 1);
|
||||||
@@ -98,7 +99,7 @@ public class BomMaker {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
entryMap.put(key,
|
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));
|
value, 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -369,6 +369,38 @@
|
|||||||
<releaseDate>2026-08-22 00:00:00.000 CET</releaseDate>
|
<releaseDate>2026-08-22 00:00:00.000 CET</releaseDate>
|
||||||
<name></name>
|
<name></name>
|
||||||
<changes>
|
<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>
|
<org.diylc.appframework.update.Change>
|
||||||
<changeType>IMPROVEMENT</changeType>
|
<changeType>IMPROVEMENT</changeType>
|
||||||
<description>Percentage values such as transparency Alpha, image Scale, etc, can have decimal values and not just integers</description>
|
<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 Color color = COLOR;
|
||||||
private Point2D.Double point = new Point2D.Double(0, 0);
|
private Point2D.Double point = new Point2D.Double(0, 0);
|
||||||
private String value = "";
|
private String value = "";
|
||||||
|
private String nodeName = "";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||||
@@ -173,9 +174,22 @@ public class Eyelet extends AbstractTransparentComponent<String> {
|
|||||||
this.value = value;
|
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
|
@Override
|
||||||
public String getControlPointNodeName(int index) {
|
public String getControlPointNodeName(int index) {
|
||||||
return null;
|
String n = getNodeName();
|
||||||
|
return n.trim().isEmpty() ? null : n.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+23
-2
@@ -71,6 +71,7 @@ public class PCBTerminalBlock extends AbstractTransparentComponent<PCBTerminalBl
|
|||||||
private Size pitch = new Size(0.2d, SizeUnit.in);
|
private Size pitch = new Size(0.2d, SizeUnit.in);
|
||||||
private Size width = new Size(0.3d, SizeUnit.in);
|
private Size width = new Size(0.3d, SizeUnit.in);
|
||||||
private ScrewPosition screwPosition = ScrewPosition.Offset;
|
private ScrewPosition screwPosition = ScrewPosition.Offset;
|
||||||
|
private String nodeNames = "";
|
||||||
|
|
||||||
public PCBTerminalBlock() {
|
public PCBTerminalBlock() {
|
||||||
super();
|
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
|
@Override
|
||||||
public String getControlPointNodeName(int index) {
|
public String getControlPointNodeName(int index) {
|
||||||
// we don't want the block to produce any nodes, it just makes connections
|
// By default the block produces no nodes, it just makes connections. Once the user
|
||||||
return null;
|
// 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
|
@Override
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ public class SolderPad extends AbstractTransparentComponent<Void>
|
|||||||
private Size holeSize = HOLE_SIZE;
|
private Size holeSize = HOLE_SIZE;
|
||||||
private PCBLayer layer = PCBLayer._1;
|
private PCBLayer layer = PCBLayer._1;
|
||||||
private HoleType holeType = HoleType.NTPH;
|
private HoleType holeType = HoleType.NTPH;
|
||||||
|
private String nodeName = "";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
|
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
|
||||||
@@ -246,9 +247,22 @@ public class SolderPad extends AbstractTransparentComponent<Void>
|
|||||||
return null;
|
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
|
@Override
|
||||||
public String getControlPointNodeName(int index) {
|
public String getControlPointNodeName(int index) {
|
||||||
return null;
|
String n = getNodeName();
|
||||||
|
return n.trim().isEmpty() ? null : n.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ public class Turret extends AbstractTransparentComponent<String> {
|
|||||||
private Color color = COLOR;
|
private Color color = COLOR;
|
||||||
private Point2D.Double point = new Point2D.Double(0, 0);
|
private Point2D.Double point = new Point2D.Double(0, 0);
|
||||||
private String value = "";
|
private String value = "";
|
||||||
|
private String nodeName = "";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||||
@@ -168,9 +169,22 @@ public class Turret extends AbstractTransparentComponent<String> {
|
|||||||
this.value = value;
|
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
|
@Override
|
||||||
public String getControlPointNodeName(int index) {
|
public String getControlPointNodeName(int index) {
|
||||||
return null;
|
String n = getNodeName();
|
||||||
|
return n.trim().isEmpty() ? null : n.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+26
@@ -71,6 +71,7 @@ public class PinHeader extends AbstractTransparentComponent<Void> {
|
|||||||
private int columnCount = 5;
|
private int columnCount = 5;
|
||||||
private Size spacing = PIN_SPACING;
|
private Size spacing = PIN_SPACING;
|
||||||
private Boolean shrouded = false;
|
private Boolean shrouded = false;
|
||||||
|
private String nodeNames = "";
|
||||||
|
|
||||||
private Color bodyColor = BODY_COLOR;
|
private Color bodyColor = BODY_COLOR;
|
||||||
private Color shroudColor = BODY_COLOR.darker();
|
private Color shroudColor = BODY_COLOR.darker();
|
||||||
@@ -342,6 +343,31 @@ public class PinHeader extends AbstractTransparentComponent<Void> {
|
|||||||
this.shroudColor = shroudColor;
|
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
|
@Override
|
||||||
public boolean canPointMoveFreely(int pointIndex) {
|
public boolean canPointMoveFreely(int pointIndex) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+13
@@ -203,6 +203,19 @@ public class AxialElectrolyticCapacitor extends AbstractLeadedComponent<Capacita
|
|||||||
this.polarized = polarized;
|
this.polarized = polarized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getControlPointNodeName(int index) {
|
||||||
|
if (getPolarized()) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
return "+";
|
||||||
|
case 1:
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.getControlPointNodeName(index);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Shape getBodyShape() {
|
protected Shape getBodyShape() {
|
||||||
double lengthFinal = getLength().convertToPixels();
|
double lengthFinal = getLength().convertToPixels();
|
||||||
|
|||||||
@@ -96,6 +96,19 @@ public class CapacitorSymbol extends AbstractSchematicLeadedSymbol<Capacitance>
|
|||||||
this.polarized = polarized;
|
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) {
|
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||||
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
|
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
|
||||||
g2d.setColor(LEAD_COLOR);
|
g2d.setColor(LEAD_COLOR);
|
||||||
|
|||||||
+15
-1
@@ -72,6 +72,7 @@ public class RadialElectrolytic extends AbstractRadialComponent<Capacitance> imp
|
|||||||
private boolean polarized = true;
|
private boolean polarized = true;
|
||||||
private boolean folded = false;
|
private boolean folded = false;
|
||||||
private Size height = HEIGHT;
|
private Size height = HEIGHT;
|
||||||
|
@Deprecated
|
||||||
private boolean invert = false;
|
private boolean invert = false;
|
||||||
|
|
||||||
public RadialElectrolytic() {
|
public RadialElectrolytic() {
|
||||||
@@ -304,7 +305,7 @@ public class RadialElectrolytic extends AbstractRadialComponent<Capacitance> imp
|
|||||||
this.height = height;
|
this.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
@EditableProperty(name = "Invert Polarity")
|
// @EditableProperty(name = "Invert Polarity")
|
||||||
public boolean getInvert() {
|
public boolean getInvert() {
|
||||||
return invert;
|
return invert;
|
||||||
}
|
}
|
||||||
@@ -313,6 +314,19 @@ public class RadialElectrolytic extends AbstractRadialComponent<Capacitance> imp
|
|||||||
this.invert = invert;
|
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
|
@Override
|
||||||
protected Shape getBodyShape() {
|
protected Shape getBodyShape() {
|
||||||
double height = (int) getHeight().convertToPixels();
|
double height = (int) getHeight().convertToPixels();
|
||||||
|
|||||||
+12
@@ -85,6 +85,18 @@ public abstract class AbstractDiodeSymbol extends AbstractSchematicLeadedSymbol<
|
|||||||
@Override
|
@Override
|
||||||
protected void decorateComponentBody(Graphics2D g2d, boolean outlineMode) {}
|
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
|
@Deprecated
|
||||||
@Override
|
@Override
|
||||||
public Size getLength() {
|
public Size getLength() {
|
||||||
|
|||||||
+3
@@ -244,6 +244,9 @@ public abstract class AbstractTransistorPackage extends AbstractLabeledComponent
|
|||||||
public String getControlPointNodeName(int index) {
|
public String getControlPointNodeName(int index) {
|
||||||
if (index >= 3)
|
if (index >= 3)
|
||||||
return null;
|
return null;
|
||||||
|
if (getPinout() != null) {
|
||||||
|
return getPinout().toPinout().substring(index, index + 1);
|
||||||
|
}
|
||||||
return Integer.toString(index + 1);
|
return Integer.toString(index + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,18 @@ public class DiodeGlass extends AbstractLeadedComponent<String> implements IGerb
|
|||||||
this.value = value;
|
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) {
|
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||||
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
|
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
|
||||||
g2d.setColor(LEAD_COLOR_ICON);
|
g2d.setColor(LEAD_COLOR_ICON);
|
||||||
|
|||||||
+12
@@ -90,6 +90,18 @@ public class DiodePlastic extends AbstractLeadedComponent<String> implements IGe
|
|||||||
this.value = value;
|
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) {
|
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||||
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
|
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
|
||||||
g2d.setColor(LEAD_COLOR_ICON);
|
g2d.setColor(LEAD_COLOR_ICON);
|
||||||
|
|||||||
@@ -67,6 +67,18 @@ public class LED extends AbstractLeadedComponent<String> implements IGerberCompo
|
|||||||
this.value = value;
|
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) {
|
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||||
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
|
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
|
||||||
|
|||||||
+77
-5
@@ -1,9 +1,18 @@
|
|||||||
package org.diylc.swing.actions.edit;
|
package org.diylc.swing.actions.edit;
|
||||||
|
|
||||||
|
import java.awt.BorderLayout;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import javax.swing.AbstractAction;
|
import javax.swing.AbstractAction;
|
||||||
|
import javax.swing.JComboBox;
|
||||||
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JOptionPane;
|
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.common.IPlugInPort;
|
||||||
import org.diylc.swing.ActionFactory;
|
import org.diylc.swing.ActionFactory;
|
||||||
import org.diylc.utils.IconLoader;
|
import org.diylc.utils.IconLoader;
|
||||||
@@ -12,6 +21,9 @@ public class SaveAsBlockAction extends AbstractAction {
|
|||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
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;
|
private IPlugInPort plugInPort;
|
||||||
|
|
||||||
public SaveAsBlockAction(IPlugInPort plugInPort) {
|
public SaveAsBlockAction(IPlugInPort plugInPort) {
|
||||||
@@ -24,10 +36,70 @@ public class SaveAsBlockAction extends AbstractAction {
|
|||||||
@Override
|
@Override
|
||||||
public void actionPerformed(ActionEvent e) {
|
public void actionPerformed(ActionEvent e) {
|
||||||
ActionFactory.LOG.info("Save as building block triggered");
|
ActionFactory.LOG.info("Save as building block triggered");
|
||||||
String templateName = JOptionPane.showInputDialog(null, "Block name:",
|
|
||||||
"Save as Building Block", JOptionPane.PLAIN_MESSAGE);
|
JComboBox<String> nameBox =
|
||||||
if (templateName != null && !templateName.trim().isEmpty()) {
|
new JComboBox<String>(getRecentBlockNames().toArray(new String[0]));
|
||||||
plugInPort.saveSelectionAsBlock(templateName);
|
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.IConfigListener;
|
||||||
import org.diylc.appframework.miscutils.IConfigurationManager;
|
import org.diylc.appframework.miscutils.IConfigurationManager;
|
||||||
|
|
||||||
|
import org.diylc.common.BlockInstantiationMode;
|
||||||
import org.diylc.common.ComponentType;
|
import org.diylc.common.ComponentType;
|
||||||
import org.diylc.common.DrawOption;
|
import org.diylc.common.DrawOption;
|
||||||
|
import org.diylc.common.IBlockProcessor;
|
||||||
import org.diylc.common.IPlugInPort;
|
import org.diylc.common.IPlugInPort;
|
||||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||||
import org.diylc.swing.plugins.tree.TreePanel;
|
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);
|
(List<String>) configManager.readObject(IPlugInPort.RECENT_COMPONENTS_KEY, null);
|
||||||
if (recent != null && !recent.isEmpty()) {
|
if (recent != null && !recent.isEmpty()) {
|
||||||
String clazz = recent.get(0);
|
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 =
|
Map<String, List<ComponentType>> componentTypes =
|
||||||
CanvasPanel.this.plugInPort.getComponentTypes();
|
CanvasPanel.this.plugInPort.getComponentTypes();
|
||||||
for (Map.Entry<String, List<ComponentType>> entry : componentTypes.entrySet()) {
|
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);
|
String typeName = shortcutMap.get("F" + i);
|
||||||
if (typeName == null)
|
if (typeName == null)
|
||||||
return;
|
return;
|
||||||
if (typeName.startsWith("block:")) {
|
if (typeName.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
|
||||||
String blockName = typeName.substring(6);
|
String blockName = typeName.substring(IBlockProcessor.BLOCK_PREFIX.length());
|
||||||
try {
|
try {
|
||||||
plugInPort.loadBlock(blockName);
|
plugInPort.loadBlock(blockName, BlockInstantiationMode.COMPOSITE);
|
||||||
} catch (InvalidBlockException e) {
|
} catch (InvalidBlockException e) {
|
||||||
LOG.error("Could not find block assigned to shortcut: " + blockName);
|
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 org.diylc.swingframework.openide.DropDownButtonFactory;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
|
import org.diylc.common.BlockInstantiationMode;
|
||||||
import org.diylc.common.ComponentType;
|
import org.diylc.common.ComponentType;
|
||||||
import org.diylc.common.IPlugInPort;
|
import org.diylc.common.IPlugInPort;
|
||||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||||
@@ -316,7 +317,7 @@ public class ComponentButtonFactory {
|
|||||||
@Override
|
@Override
|
||||||
public void actionPerformed(ActionEvent e) {
|
public void actionPerformed(ActionEvent e) {
|
||||||
try {
|
try {
|
||||||
plugInPort.loadBlock(blockName);
|
plugInPort.loadBlock(blockName, BlockInstantiationMode.COMPOSITE);
|
||||||
} catch (InvalidBlockException e1) {
|
} catch (InvalidBlockException e1) {
|
||||||
LOG.error("Error loading building block", 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.ComponentType;
|
||||||
import org.diylc.common.Favorite;
|
import org.diylc.common.Favorite;
|
||||||
|
import org.diylc.common.IBlockProcessor;
|
||||||
import org.diylc.common.IPlugInPort;
|
import org.diylc.common.IPlugInPort;
|
||||||
import org.diylc.common.Favorite.FavoriteType;
|
import org.diylc.common.Favorite.FavoriteType;
|
||||||
import org.diylc.core.IDIYComponent;
|
import org.diylc.core.IDIYComponent;
|
||||||
@@ -59,6 +60,7 @@ import org.diylc.swing.ISwingUI;
|
|||||||
import org.diylc.utils.IconLoader;
|
import org.diylc.utils.IconLoader;
|
||||||
import org.diylc.swing.plugins.tree.TreePanel;
|
import org.diylc.swing.plugins.tree.TreePanel;
|
||||||
import org.diylc.swing.plugins.tree.TreePanel.DeleteBlockAction;
|
import org.diylc.swing.plugins.tree.TreePanel.DeleteBlockAction;
|
||||||
|
import org.diylc.swing.plugins.tree.TreePanel.InsertBlockAsGroupAction;
|
||||||
import org.diylc.swing.plugins.tree.TreePanel.SelectAllAction;
|
import org.diylc.swing.plugins.tree.TreePanel.SelectAllAction;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -286,16 +288,22 @@ class ComponentTabbedPane extends JTabbedPane {
|
|||||||
private void refreshRecentComponentsToolbar(List<String> recentComponentClassList) {
|
private void refreshRecentComponentsToolbar(List<String> recentComponentClassList) {
|
||||||
Container toolbar = getRecentToolbar();
|
Container toolbar = getRecentToolbar();
|
||||||
toolbar.removeAll();
|
toolbar.removeAll();
|
||||||
for (String componentClassName : recentComponentClassList) {
|
for (String identifier : recentComponentClassList) {
|
||||||
ComponentType componentType;
|
|
||||||
try {
|
try {
|
||||||
componentType = ComponentProcessor.getInstance().extractComponentTypeFrom(
|
if (identifier.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
|
||||||
(Class<? extends IDIYComponent<?>>) Class.forName(componentClassName));
|
String blockName = identifier.substring(IBlockProcessor.BLOCK_PREFIX.length());
|
||||||
Component button = ComponentButtonFactory.create(plugInPort, componentType,
|
Component button = ComponentButtonFactory.createBuildingBlockButton(plugInPort, blockName,
|
||||||
createVariantPopup(componentType, null));
|
createVariantPopup(null, blockName));
|
||||||
toolbar.add(button);
|
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) {
|
} 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) {
|
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
|
||||||
variantPopup.removeAll();
|
variantPopup.removeAll();
|
||||||
|
|
||||||
final String identifier = componentType == null ? "block:" + blockName
|
final String identifier = componentType == null
|
||||||
|
? IBlockProcessor.BLOCK_PREFIX + blockName
|
||||||
: componentType.getInstanceClass().getCanonicalName();
|
: componentType.getInstanceClass().getCanonicalName();
|
||||||
|
|
||||||
JMenu shortcutSubmenu = new JMenu("Assign Shortcut");
|
JMenu shortcutSubmenu = new JMenu("Assign Shortcut");
|
||||||
@@ -456,6 +465,7 @@ class ComponentTabbedPane extends JTabbedPane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
variantPopup.add(new InsertBlockAsGroupAction(plugInPort, blockName));
|
||||||
variantPopup.add(shortcutSubmenu);
|
variantPopup.add(shortcutSubmenu);
|
||||||
variantPopup.add(new DeleteBlockAction(plugInPort, swingUI, blockName));
|
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.ConfigurationManager;
|
||||||
import org.diylc.appframework.miscutils.IConfigListener;
|
import org.diylc.appframework.miscutils.IConfigListener;
|
||||||
|
|
||||||
|
import org.diylc.common.BlockInstantiationMode;
|
||||||
import org.diylc.common.ComponentType;
|
import org.diylc.common.ComponentType;
|
||||||
import org.diylc.common.Favorite;
|
import org.diylc.common.Favorite;
|
||||||
|
import org.diylc.common.IBlockProcessor;
|
||||||
import org.diylc.common.IPlugInPort;
|
import org.diylc.common.IPlugInPort;
|
||||||
import org.diylc.common.Favorite.FavoriteType;
|
import org.diylc.common.Favorite.FavoriteType;
|
||||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||||
@@ -142,7 +144,7 @@ public class CustomTreeModel implements TreeModel {
|
|||||||
&& System.currentTimeMillis() - previousActionTime > 100) {
|
&& System.currentTimeMillis() - previousActionTime > 100) {
|
||||||
previousActionTime = System.currentTimeMillis();
|
previousActionTime = System.currentTimeMillis();
|
||||||
try {
|
try {
|
||||||
plugInPort.loadBlock(fav.getName());
|
plugInPort.loadBlock(fav.getName(), BlockInstantiationMode.COMPOSITE);
|
||||||
} catch (InvalidBlockException e1) {
|
} catch (InvalidBlockException e1) {
|
||||||
e1.printStackTrace();
|
e1.printStackTrace();
|
||||||
}
|
}
|
||||||
@@ -155,8 +157,37 @@ public class CustomTreeModel implements TreeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (RECENTLY_USED.equals(category)) {
|
if (RECENTLY_USED.equals(category)) {
|
||||||
for (String className : this.recentComponents) {
|
for (String identifier : this.recentComponents) {
|
||||||
ComponentType type = this.typesByClass.get(className);
|
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)) {
|
if (this.searchText == null || type.getName().toLowerCase().contains(this.searchText)) {
|
||||||
visibleTypes.add(new TreeNode(type, new MouseAdapter() {
|
visibleTypes.add(new TreeNode(type, new MouseAdapter() {
|
||||||
|
|
||||||
@@ -182,7 +213,7 @@ public class CustomTreeModel implements TreeModel {
|
|||||||
&& System.currentTimeMillis() - previousActionTime > 100) {
|
&& System.currentTimeMillis() - previousActionTime > 100) {
|
||||||
previousActionTime = System.currentTimeMillis();
|
previousActionTime = System.currentTimeMillis();
|
||||||
try {
|
try {
|
||||||
plugInPort.loadBlock(block);
|
plugInPort.loadBlock(block, BlockInstantiationMode.COMPOSITE);
|
||||||
} catch (InvalidBlockException e1) {
|
} catch (InvalidBlockException e1) {
|
||||||
e1.printStackTrace();
|
e1.printStackTrace();
|
||||||
}
|
}
|
||||||
@@ -233,10 +264,16 @@ public class CustomTreeModel implements TreeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (RECENTLY_USED.equals(category)) {
|
if (RECENTLY_USED.equals(category)) {
|
||||||
for (String className : this.recentComponents) {
|
for (String identifier : this.recentComponents) {
|
||||||
ComponentType type = typesByClass.get(className);
|
if (identifier.startsWith(IBlockProcessor.BLOCK_PREFIX)) {
|
||||||
if (type.getName().toLowerCase().contains(searchText))
|
if (identifier.substring(IBlockProcessor.BLOCK_PREFIX.length()).toLowerCase()
|
||||||
return true;
|
.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.ConfigurationManager;
|
||||||
import org.diylc.appframework.miscutils.IConfigListener;
|
import org.diylc.appframework.miscutils.IConfigListener;
|
||||||
|
|
||||||
|
import org.diylc.common.BlockInstantiationMode;
|
||||||
import org.diylc.common.ComponentType;
|
import org.diylc.common.ComponentType;
|
||||||
import org.diylc.common.Favorite;
|
import org.diylc.common.Favorite;
|
||||||
|
import org.diylc.common.IBlockProcessor;
|
||||||
import org.diylc.common.IPlugInPort;
|
import org.diylc.common.IPlugInPort;
|
||||||
import org.diylc.common.Favorite.FavoriteType;
|
import org.diylc.common.Favorite.FavoriteType;
|
||||||
|
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||||
import org.diylc.core.IDIYComponent;
|
import org.diylc.core.IDIYComponent;
|
||||||
import org.diylc.core.IView;
|
import org.diylc.core.IView;
|
||||||
import org.diylc.core.Template;
|
import org.diylc.core.Template;
|
||||||
@@ -313,7 +316,8 @@ public class TreePanel extends JPanel {
|
|||||||
TreeNode payload = (TreeNode) selectedNode;
|
TreeNode payload = (TreeNode) selectedNode;
|
||||||
final ComponentType componentType = payload.getComponentType();
|
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();
|
: componentType.getInstanceClass().getCanonicalName();
|
||||||
|
|
||||||
JMenu shortcutSubmenu = new JMenu("Assign Shortcut");
|
JMenu shortcutSubmenu = new JMenu("Assign Shortcut");
|
||||||
@@ -434,6 +438,7 @@ public class TreePanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (getTreeModel().isLeaf(selectedNode)) {
|
} else if (getTreeModel().isLeaf(selectedNode)) {
|
||||||
|
popup.add(new InsertBlockAsGroupAction(plugInPort, payload.toString()));
|
||||||
popup.add(shortcutSubmenu);
|
popup.add(shortcutSubmenu);
|
||||||
popup.add(new DeleteBlockAction(plugInPort, swingUI, payload.toString()));
|
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
|
HashMap<String, String> shortcutMap = (HashMap<String, String>) ConfigurationManager
|
||||||
.getInstance().readObject(TreePanel.COMPONENT_SHORTCUT_KEY, null);
|
.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();
|
: payload.getComponentType().getInstanceClass().getCanonicalName();
|
||||||
if (shortcutMap != null && shortcutMap.containsValue(identifier)) {
|
if (shortcutMap != null && shortcutMap.containsValue(identifier)) {
|
||||||
for (String key : shortcutMap.keySet()) {
|
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 {
|
public static class DeleteBlockAction extends AbstractAction {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
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