* composite block support * naming * Added ability to set node name (or names) for some connectivity components * cleanup * Include building blocks in the Recently Used category * Prompt to confirm the overwrite when saving over the existing building blocks; When saving building blocks, present a drop down list with most recently used building blocks * add alpha to composite * Update CompositeComponentTest.java * cleanup * Update CompositeComponent.java * cleanup * Update CompositeComponent.java * <org.diylc.appframework.update.Change> <changeType>IMPROVEMENT</changeType> <description>When saving building blocks, present a drop down list with most recently used building blocks for quick selection</description> </org.diylc.appframework.update.Change> * Diode components now label their terminal nodes as 'A' (anode) and 'K' (cathode) instead of '1' and '2' * Capacitor components now label their terminal nodes as '+' and '-' instead of '1' and '2' when 'Polarized' property is ticked
18 KiB
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
ISwitchwill 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.
@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 sameGraphics2DandIDrawingObserver, passingComponentState.NORMALso children do not each paint their own red highlight. Whenstate != NORMALthe 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.extractComponentTypeFromcallsdrawIconon a default-constructed instance with no children, so this must not touchcomponents.getValue()/setValue()—null/ no-op.getValueForDisplay()inherits theAbstractComponentbehaviour of returning""for a null value, which keeps the BOM row (BomMakerskips 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-copiesPoint2DandPoint2D[]. Thecomponentslist 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 ownclone(), which preserves its id, matchingAbstractComponent.clone()'s contract) and leaves the transient index arrays to rebuild lazily.Building BlockManager.loadBlockis 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
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 throughcreateUniqueNameagainst the project would produce meaningless drift (R1becomingR37) and break netlist labels between two instances of the same block. Then wrap them in aCompositeComponent, setblockName, and give the composite itself a project-unique name viacreateUniqueName.
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:
// 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
.diyfiles are unaffected — they contain no composites. - Existing blocks in config are unaffected. The mode is chosen at placement,
not stored on the block, so
buildingBlockskeeps itsMap<String, List<IDIYComponent<?>>>shape and needs no migration. Default and imported blocks work in both modes immediately. - New
.diyfiles containing composites cannot be opened by older DIYLC versions. Expected and unavoidable given D1. - Children serialize as ordinary nested
diylc.*elements inside the composite'scomponentslist, so every existing component converter applies unchanged.
6. Suggested order of work
CompositeComponentwith the delegating members, the point index map, a correctclone(), anddraw. No UI yet — exercise it from a unit test that builds a composite directly and asserts point counts, node names and rigidity.hiddenInPaletteonComponentDescriptor/ComponentType/getComponentTypes().BlockInstantiationMode, theIBlockProcessoroverload,BuildingBlockManager.loadBlock,Presenter.loadBlock.- UI wiring: four default call sites plus the "Insert as Separate Components" popup items.
getDisplayTypeNameand its three consumers.CompositeComponentTransformer, and rotation/mirroring enablement.- 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;canPointMoveFreelyis false for every index;getSectionNames/getControlPointNodeNameproduceARD1.PH1.3throughNode.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
IContinuitypoint-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-datasuite to confirm nothing in the ordinary drawing/netlist path shifted.