diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/ComparatorFactory.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/ComparatorFactory.java index 8c79b51f..bea419cf 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/ComparatorFactory.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/ComparatorFactory.java @@ -118,16 +118,10 @@ public class ComparatorFactory { if (componentZOrderComparator == null) { componentZOrderComparator = new Comparator>() { - @SuppressWarnings("unchecked") @Override public int compare(IDIYComponent o1, IDIYComponent o2) { - ComponentType type1 = - ComponentProcessor.getInstance().extractComponentTypeFrom( - (Class>) o1.getClass()); - ComponentType type2 = - ComponentProcessor.getInstance().extractComponentTypeFrom( - (Class>) o2.getClass()); - return Double.valueOf(type1.getZOrder()).compareTo(type2.getZOrder()); + return Double.compare(ComponentProcessor.getInstance().getZOrder(o1), + ComponentProcessor.getInstance().getZOrder(o2)); } }; } diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentProcessor.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentProcessor.java index 921c196f..040acf69 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentProcessor.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentProcessor.java @@ -409,6 +409,31 @@ public class ComponentProcessor { return type.getName(); } + /** + * @return the z-order (layer) of the component. A {@link CompositeComponent} sits on the lowest + * layer among its children rather than the one its class declares: it is drawn as a + * single unit, so it takes one place in the stack, and that place has to be under + * whatever is later put on top of it. A block of chassis panels therefore stays on the + * chassis layer, and a block built from a board, pads and silkscreen text on the board + * layer. + */ + @SuppressWarnings("unchecked") + public double getZOrder(IDIYComponent component) { + if (component instanceof CompositeComponent) { + double zOrder = Double.MAX_VALUE; + for (IDIYComponent child : ((CompositeComponent) component).getChildComponents()) { + zOrder = Math.min(zOrder, getZOrder(child)); + } + if (zOrder < Double.MAX_VALUE) { + return zOrder; + } + } + ComponentType type = + extractComponentTypeFrom((Class>) component.getClass()); + // internal types without a descriptor do not take part in layering + return type == null ? Double.MAX_VALUE : type.getZOrder(); + } + public static boolean hasStickyPoint(IDIYComponent c) { for (int i = 0; i < c.getControlPointCount(); i++) if (c.isControlPointSticky(0)) diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/GroupAndLockIndex.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/GroupAndLockIndex.java index 217d2483..18c0aa78 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/GroupAndLockIndex.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/GroupAndLockIndex.java @@ -94,7 +94,8 @@ public class GroupAndLockIndex { if (componentType == null) { return false; } - return project.getLockedLayers().contains((int) Math.round(componentType.getZOrder())) + return project.getLockedLayers() + .contains((int) Math.round(ComponentProcessor.getInstance().getZOrder(component))) || project.getLockedComponents().contains(component); } diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java index addb1079..cd8f97f0 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java @@ -494,7 +494,8 @@ public class Presenter implements IPlugInPort { // for internal types if (componentType == null) return true; - return !currentProject.getHiddenLayers().contains((int) Math.round(componentType.getZOrder())); + return !currentProject.getHiddenLayers() + .contains((int) Math.round(ComponentProcessor.getInstance().getZOrder(component))); } @Override @@ -1076,10 +1077,9 @@ public class Presenter implements IPlugInPort { Iterator> i = newSelection.iterator(); while (i.hasNext()) { IDIYComponent c = i.next(); - ComponentType type = - ComponentProcessor.getInstance().extractComponentTypeFrom((Class>) c.getClass()); - if ((int) type.getZOrder() != layer) + if ((int) ComponentProcessor.getInstance().getZOrder(c) != layer) { i.remove(); + } } } updateSelection(newSelection); @@ -2229,6 +2229,7 @@ public class Presenter implements IPlugInPort { ComponentType componentType = ComponentProcessor.getInstance().extractComponentTypeFrom( (Class>) component.getClass()); + long layer = Math.round(ComponentProcessor.getInstance().getZOrder(component)); int index = currentProject.getComponents().indexOf(component); if (index < 0) { LOG.warn("Component not found in the project: " + component.getName()); @@ -2236,11 +2237,8 @@ public class Presenter implements IPlugInPort { while (index > 0) { IDIYComponent componentBefore = currentProject.getComponents().get(index - 1); if (!selectedComponents.contains(componentBefore)) { - ComponentType componentBeforeType = - ComponentProcessor.getInstance().extractComponentTypeFrom( - (Class>) componentBefore.getClass()); if (!componentType.isFlexibleZOrder() - && Math.round(componentBeforeType.getZOrder()) < Math.round(componentType.getZOrder()) + && Math.round(ComponentProcessor.getInstance().getZOrder(componentBefore)) < layer && forceConfirmation != IView.YES_OPTION && (forceConfirmation = this.view @@ -2278,6 +2276,7 @@ public class Presenter implements IPlugInPort { ComponentType componentType = ComponentProcessor.getInstance().extractComponentTypeFrom( (Class>) component.getClass()); + long layer = Math.round(ComponentProcessor.getInstance().getZOrder(component)); int index = currentProject.getComponents().indexOf(component); if (index < 0) { LOG.warn("Component not found in the project: " + component.getName()); @@ -2285,11 +2284,8 @@ public class Presenter implements IPlugInPort { while (index < currentProject.getComponents().size() - 1) { IDIYComponent componentAfter = currentProject.getComponents().get(index + 1); if (!selectedComponents.contains(componentAfter)) { - ComponentType componentAfterType = - ComponentProcessor.getInstance().extractComponentTypeFrom( - (Class>) componentAfter.getClass()); if (!componentType.isFlexibleZOrder() - && Math.round(componentAfterType.getZOrder()) > Math.round(componentType.getZOrder()) + && Math.round(ComponentProcessor.getInstance().getZOrder(componentAfter)) > layer && forceConfirmation != IView.YES_OPTION && (forceConfirmation = this.view @@ -2684,13 +2680,9 @@ public class Presenter implements IPlugInPort { @SuppressWarnings("unchecked") private void addComponent(IDIYComponent component, boolean allowAutoCreate) { int index = currentProject.getComponents().size(); - while (index > 0 - && ComponentProcessor.getInstance() - .extractComponentTypeFrom((Class>) component.getClass()).getZOrder() < ComponentProcessor - .getInstance() - .extractComponentTypeFrom( - (Class>) currentProject.getComponents().get(index - 1).getClass()) - .getZOrder()) { + double zOrder = ComponentProcessor.getInstance().getZOrder(component); + while (index > 0 && zOrder < ComponentProcessor.getInstance() + .getZOrder(currentProject.getComponents().get(index - 1))) { index--; } if (index < currentProject.getComponents().size()) { @@ -2724,12 +2716,9 @@ public class Presenter implements IPlugInPort { int maxIndex = 0; for (IDIYComponent component : components) { int index = currentProject.getComponents().size(); - while (index > maxIndex && ComponentProcessor.getInstance() - .extractComponentTypeFrom((Class>) component.getClass()) - .getZOrder() < ComponentProcessor.getInstance() - .extractComponentTypeFrom((Class>) currentProject - .getComponents().get(index - 1).getClass()) - .getZOrder()) { + double zOrder = ComponentProcessor.getInstance().getZOrder(component); + while (index > maxIndex && zOrder < ComponentProcessor.getInstance() + .getZOrder(currentProject.getComponents().get(index - 1))) { index--; } @@ -3132,12 +3121,9 @@ public class Presenter implements IPlugInPort { return GroupAndLockIndex.isLocked(currentProject, component); } - @SuppressWarnings("unchecked") private boolean isComponentLayerLocked(IDIYComponent component) { - ComponentType componentType = - ComponentProcessor.getInstance().extractComponentTypeFrom( - (Class>) component.getClass()); - return currentProject.getLockedLayers().contains((int) Math.round(componentType.getZOrder())); + return currentProject.getLockedLayers() + .contains((int) Math.round(ComponentProcessor.getInstance().getZOrder(component))); } /** diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml index a40c622c..57473799 100644 --- a/diylc/diylc-core/src/main/resources/update.xml +++ b/diylc/diylc-core/src/main/resources/update.xml @@ -517,6 +517,10 @@ IMPROVEMENT Re-modeled factory variants of Resistor component type as datasheet in order to make them available to the AI; this will produce more accurate AI-driven edits + + IMPROVEMENT + Derive the z-order of a building blocks placed as a single component based on its child components + NEW_FEATURE Added datasheets to 'DIP IC', 'SIP IC' and 'Multiwatt IC' - pick a part by its number from the component's submenu to set its pin count, spacing and pin names at once; the AI Assistant can place these parts by part number too diff --git a/diylc/diylc-core/src/test/java/org/diylc/components/composite/CompositeComponentTest.java b/diylc/diylc-core/src/test/java/org/diylc/components/composite/CompositeComponentTest.java index bd5f549f..76842d78 100644 --- a/diylc/diylc-core/src/test/java/org/diylc/components/composite/CompositeComponentTest.java +++ b/diylc/diylc-core/src/test/java/org/diylc/components/composite/CompositeComponentTest.java @@ -35,7 +35,12 @@ import java.util.UUID; import org.diylc.common.Percentage; import org.diylc.core.IDIYComponent; +import org.diylc.core.Project; +import org.diylc.core.annotations.ComponentDescriptor; import org.diylc.netlist.Node; +import org.diylc.presenter.ComparatorFactory; +import org.diylc.presenter.ComponentProcessor; +import org.diylc.presenter.GroupAndLockIndex; import org.diylc.serialization.ProjectFileManager; import org.diylc.testcomponents.OnePointTestComponent; import org.diylc.testcomponents.TwoPointTestComponent; @@ -326,4 +331,77 @@ public class CompositeComponentTest { assertNotNull(blank.getChildComponents()); assertTrue(blank.getChildComponents().isEmpty()); } + + @Test + public void layerOfAChassisOnlyCompositeIsTheChassisLayer() { + CompositeComponent composite = customBoard(new ChassisTestComponent(), + new ChassisTestComponent()); + + assertEquals(IDIYComponent.CHASSIS, ComponentProcessor.getInstance().getZOrder(composite), 0); + } + + @Test + public void layerOfAMixedCompositeIsItsLowestChildLayer() { + CompositeComponent composite = customBoard(new TextTestComponent(), twoPoint("R1"), + new BoardTestComponent()); + + assertEquals(IDIYComponent.BOARD, ComponentProcessor.getInstance().getZOrder(composite), 0); + } + + @Test + public void layerOfANestedCompositeLooksThroughTheInnerComposite() { + CompositeComponent inner = customBoard(new ChassisTestComponent()); + CompositeComponent outer = customBoard(twoPoint("R1"), inner); + + assertEquals(IDIYComponent.CHASSIS, ComponentProcessor.getInstance().getZOrder(outer), 0); + } + + @Test + public void layerOfAChildlessCompositeIsTheDeclaredOne() { + assertEquals(IDIYComponent.COMPONENT, + ComponentProcessor.getInstance().getZOrder(new CompositeComponent()), 0); + } + + @Test + public void lockingTheChildLayerLocksTheComposite() { + Project project = new Project(); + CompositeComponent chassis = customBoard(new ChassisTestComponent()); + project.getComponents().add(chassis); + + project.getLockedLayers().add(IDIYComponent.COMPONENT); + assertFalse(GroupAndLockIndex.isLocked(project, chassis)); + + project.getLockedLayers().add(IDIYComponent.CHASSIS); + assertTrue(GroupAndLockIndex.isLocked(project, chassis)); + } + + @Test + public void compositeSortsByItsChildLayer() { + CompositeComponent chassis = customBoard(new ChassisTestComponent()); + BoardTestComponent board = new BoardTestComponent(); + + assertTrue(ComparatorFactory.getInstance().getComponentZOrderComparator().compare(chassis, + board) < 0); + } + + @ComponentDescriptor(name = "Chassis Test", category = "Test", author = "Test", + description = "test", instanceNamePrefix = "CT", zOrder = IDIYComponent.CHASSIS) + public static class ChassisTestComponent extends TwoPointTestComponent { + + private static final long serialVersionUID = 1L; + } + + @ComponentDescriptor(name = "Board Test", category = "Test", author = "Test", + description = "test", instanceNamePrefix = "BT", zOrder = IDIYComponent.BOARD) + public static class BoardTestComponent extends TwoPointTestComponent { + + private static final long serialVersionUID = 1L; + } + + @ComponentDescriptor(name = "Text Test", category = "Test", author = "Test", + description = "test", instanceNamePrefix = "TT", zOrder = IDIYComponent.TEXT) + public static class TextTestComponent extends TwoPointTestComponent { + + private static final long serialVersionUID = 1L; + } } diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/explorer/ExplorerPane.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/explorer/ExplorerPane.java index e08d4e98..0c8c4966 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/explorer/ExplorerPane.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/explorer/ExplorerPane.java @@ -37,11 +37,11 @@ import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; -import org.diylc.common.ComponentType; import org.diylc.common.IPlugInPort; import org.diylc.core.ComponentGroup; import org.diylc.core.IDIYComponent; import org.diylc.lang.LangUtil; +import org.diylc.presenter.ComponentProcessor; import org.diylc.swing.ISwingUI; import org.diylc.swing.gui.ComponentPopupMenu; import org.diylc.swing.gui.DragDropList; @@ -257,7 +257,7 @@ public class ExplorerPane extends JPanel { 0, false, false) .getPreferredSize().height); componentList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - componentList.setCellRenderer(new ComponentListCellRenderer(plugInPort.getComponentTypes())); + componentList.setCellRenderer(new ComponentListCellRenderer()); selectionListener = new ListSelectionListener() { @Override @@ -586,13 +586,6 @@ public class ExplorerPane extends JPanel { protected DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer(); - private Map componentTypes; - - public ComponentListCellRenderer(Map> componentTypeMap) { - this.componentTypes = componentTypeMap.values().stream().flatMap(x -> x.stream()) - .collect(Collectors.toMap(x -> x.getInstanceClass().getCanonicalName(), x -> x)); - } - @Override public Component getListCellRendererComponent(JList list, ComponentGroupItem value, int index, boolean isSelected, boolean cellHasFocus) { @@ -605,14 +598,15 @@ public class ExplorerPane extends JPanel { if (groupItem.getComponentCount() == 1) { // Render as individual component (ungrouped) IDIYComponent component = groupItem.getGroup().iterator().next(); - ComponentType componentType = componentTypes.get(component.getClass().getCanonicalName()); - - if (componentType == null) { + double zOrder = ComponentProcessor.getInstance().getZOrder(component); + + // internal types without a descriptor have no layer + if (zOrder == Double.MAX_VALUE) { renderer.setText(component.toString()); return renderer; } - int layerId = (int) Math.round(componentType.getZOrder()); + int layerId = (int) Math.round(zOrder); String valueForDisplay = component.getValueForDisplay(); if (isSelected) { @@ -637,9 +631,9 @@ public class ExplorerPane extends JPanel { // Calculate maximum layer from components in the group int maxLayerId = 0; for (IDIYComponent component : groupItem.getGroup()) { - ComponentType componentType = componentTypes.get(component.getClass().getCanonicalName()); - if (componentType != null) { - int layerId = (int) Math.round(componentType.getZOrder()); + double zOrder = ComponentProcessor.getInstance().getZOrder(component); + if (zOrder != Double.MAX_VALUE) { + int layerId = (int) Math.round(zOrder); if (layerId > maxLayerId) { maxLayerId = layerId; } diff --git a/diylc/diylc-swing/src/test/java/org/diylc/PresenterTests.java b/diylc/diylc-swing/src/test/java/org/diylc/PresenterTests.java index 4c6e63f8..21b9ea30 100644 --- a/diylc/diylc-swing/src/test/java/org/diylc/PresenterTests.java +++ b/diylc/diylc-swing/src/test/java/org/diylc/PresenterTests.java @@ -1,6 +1,7 @@ package org.diylc; import java.awt.Point; +import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import java.io.IOException; import java.util.ArrayList; @@ -14,10 +15,13 @@ import org.junit.Test; import org.diylc.appframework.miscutils.ConfigurationManager; import org.diylc.appframework.miscutils.InMemoryConfigurationManager; +import org.diylc.clipboard.ComponentTransferable; import org.diylc.common.ComponentType; import org.diylc.common.EventType; import org.diylc.common.IPlugIn; import org.diylc.common.IPlugInPort; +import org.diylc.components.chassis.ChassisPanel; +import org.diylc.components.composite.CompositeComponent; import org.diylc.core.IDIYComponent; import org.diylc.core.Template; import org.diylc.presenter.Presenter; @@ -45,6 +49,24 @@ public class PresenterTests extends TestBase { Assert.assertEquals(1, presenter.getCurrentProject().getComponents().size()); } + @Test + public void testChassisCompositeIsPlacedUnderExistingComponents() { + instantiateTwoClick("Passive", "Resistor", new Point(1000, 1000), new Point(1200, 1000)); + + CompositeComponent chassis = new CompositeComponent(); + chassis.initBlockName("Chassis"); + chassis.getChildComponents().add(new ChassisPanel()); + chassis.getChildComponents().add(new ChassisPanel()); + List> clipboard = new ArrayList>(); + clipboard.add(chassis); + presenter.pasteComponents(new ComponentTransferable(clipboard, "Chassis"), false, true); + presenter.mouseClicked(new Point(500, 500), MouseEvent.BUTTON1, false, false, false, 1); + + List> components = presenter.getCurrentProject().getComponents(); + Assert.assertEquals(2, components.size()); + Assert.assertTrue(components.get(0) instanceof CompositeComponent); + } + @Test public void testNodeNameHover() { final String[] receivedTooltip = new String[1]; diff --git a/diylc/docs/plans/chassis-enclosures.md b/diylc/docs/plans/chassis-enclosures.md new file mode 100644 index 00000000..24171f19 --- /dev/null +++ b/diylc/docs/plans/chassis-enclosures.md @@ -0,0 +1,296 @@ +# Chassis and Enclosures + +Covering the Hammond pedal and amplifier chassis lineup with a `ChassisPanel` datasheet, and +shipping the common pedal enclosures as exploded five-panel factory building blocks. + +## 1. Current state + +- `ChassisPanel` (`diylc-library`, `org.diylc.components.chassis`) is a two-point + `AbstractShape` on the `CHASSIS` layer with an `Edge Radius`. One instance is one side of a + chassis; its `@AiNote` already fixes the exploded-view convention (top in the centre, front + against its upper edge, back against its lower edge, left and right against the side edges). +- Four factory variants in `variants.xml` (`125B`, `A`, `B`, `BB`) store nothing but corner points + and a 5 mm radius. Their sizes are not Hammond's (the `125B` variant is 122.3 x 67.1 mm, the + 1590N1 drawing says 121.2 x 65.5). +- Factory building blocks (`import-defaults/blocks.xml`, three entries) are imported once, behind + the boolean `DEFAULT_BLOCKS_IMPORTED_KEY`; blocks added later never reach existing users. +- Composites take the lowest layer of their children (`ComponentProcessor.getZOrder`), so a + composite of chassis panels sits on the Chassis layer, under parts placed before it, and + follows that layer's hide/lock. Done; see §3.1. + +## 2. Design decisions + +| # | Decision | Rationale | +|---|---|---| +| D1 | Chassis sizes move to a datasheet, `datasheets/ChassisPanel.datasheet`, two creation steps (Group > Model) | Same mechanism as `DIL_IC` and `Resistor`; the AI can name a model; one row serves every side of the enclosure (D3). | +| D2 | Row format `Group,Model,Length,Width,Height,Radius`. Sizes in mm unless a unit follows (`8 in`), parsed like `Resistor.parseDatasheetSize` | Pedal drawings are metric, Hammond chassis are nominal inches; writing each in its native unit keeps the file checkable against the drawings. | +| D3 | `ChassisPanel` gets a `Value` property holding the model, and a new `Side` (`TOP`, `FRONT`, `BACK`, `LEFT`, `RIGHT`). `applyModel` sets `Value` to the model and sizes the panel from the row according to its side: top = Length x Width, front/back = Length x Height, left/right = Width x Height, keeping the first control point. Changing `Side` on a panel that has a model resizes it | One class, no per-side types. `Value` is backed by `AbstractShape.value`, a `String` that every shape already serializes and nothing reads (always `""`), so no new field is needed for it; since `AbstractShape.getValue()` is typed `Void`, the getter is `getModel()` with `@EditableProperty(name = "Value")`. `side` is new and reads `null` from old files: `getSide()` defaults to `TOP`. An empty value means "free-form panel" exactly as today. | +| D4 | Top panel is drawn landscape, Length along X | Matches the four existing variants and the amp chassis front-panel orientation; pedal builders rotate (the transformer already supports it, and a composite rotates as a unit). | +| D5 | Length/Width are Hammond's outside dimensions; Height is the drillable wall: box height without the lid for die-cast boxes, outside height for 1441/1444 | The lid of a 1590 is the pedal's bottom, which the exploded view leaves out. | +| D6 | Die-cast taper is ignored: the top panel uses the outer size at the lid opening and side panels are rectangles | Hammond does not publish the floor size; the walls slope at most 2 deg, so the floor is 0.7-1.9 mm smaller per side. Recorded in §5 as unsourced. | +| D7 | The four variants are withdrawn as `deletedOn` stubs | The mechanism from the Resistor and DIP migrations (`Template.deletedOn`, `DeletedVariantsTest`). | +| D8 | Factory blocks get variant-style import tracking: each factory block has `createdOn`/`deletedOn`, the import date is stored under a new key | Mirrors `VariantManager`; lets new factory blocks reach existing users. | +| D9 | Exploded factory blocks only for the common enclosures: pedals 1590A, 1590B, 125B, 1590BB, 1590XX, 1590DD; amps 1441-10, 1441-18, 1441-28, 1441-32 (§3.4) | The block toolbar is a flat list of buttons; the full lineup stays in the datasheet. | +| D10 | A composite's z-order is the **minimum** of its children's, computed per instance | A composite occupies one slot in the stack and is drawn as a unit, so it should sit under what is placed on it afterwards. For a chassis block min = max = `CHASSIS`. For a maker board built from a board, pads and silkscreen text, max would be `TEXT` and the board would cover every part placed later; min is `BOARD`, and locking the Board layer locks the whole board. | +| D11 | Die-cast boxes come from the 1590 and 1550 series only, natural finish only, plus generic sizes such as 125B under `Others`. HWCHAS walnut chassis and rack cases are out of this change | Painted and black versions share the natural part's dimensions, and the shaped 1590 STOMP boxes (TRPB, TRPC, STPC) are sold painted only. The others are listed in §6. | +| D12 | BOM: one line per enclosure, valued by model. `getValueForDisplay()` returns the model for a `TOP` panel and `null` for any other side; `bomPolicy` stays `SHOW_ONLY_TYPE_NAME` | `BomMaker` keys rows by type + value and skips a `null` value, so an exploded 1590BB reads `Chassis Panel / 1590BB / 1` instead of five panels. Free-form and old-file panels are `TOP` with an empty value and list as they do today. An exploded block placed as a composite lists under its block name, so the block's `CompositeComponent` carries the model as its value too (§3.4). | + +## 3. Implementation + +Step 4 depends on 2 and 3. + +### 3.1 Composite z-order (core) - done + +- `ComponentProcessor.getZOrder(IDIYComponent)`: for a `CompositeComponent`, the minimum over + its children, recursing into nested composites; a childless composite and any other component + use the type's z-order. Not cached: a type lookup is a map hit and composites are few. +- Every class-level layer read goes through it: `Presenter` insertion (`addComponent`, + `addComponents`), hidden and locked layers, `selectAll(layer)`, send to back / bring to front; + `GroupAndLockIndex.isLocked`; `ComparatorFactory.getComponentZOrderComparator`; the `[L#]` tag + in `ExplorerPane`, which now shows composites too. +- Paint order is the project list order, so existing files render the same; only hide/lock and + future insertion change. `flexibleZOrder = true` stays. +- Tests: `CompositeComponentTest` (chassis-only, mixed, nested and childless composites; layer + lock; comparator) and `PresenterTests.testChassisCompositeIsPlacedUnderExistingComponents`. + +### 3.2 `ChassisPanel` datasheet (library) + +- `enableDatasheet = true, datasheetCreationStepCount = 2, datasheetModelColumn = 1`, + `implements IDatasheetSupport`, `ChassisPanel(String[] model)` constructor. +- `getModel()` / `setModel()` over the inherited `value` field as `Value`, and a new serialized + `side` whose getter tolerates `null` (D3). Override `getValueForDisplay()` for the BOM (D12). +- `@AiNote`: a datasheet model is a Hammond part number such as 1590BB or 1444-32 and is stored as + 'Value'; it sets the panel size for the panel's `Side`, so set `Side` before or together with + the model. Only the top panel carries the enclosure into the BOM. "125B" is a generic size + sold by several makers (about 122 x 66 x 40 mm with the lid, often with thinner walls) and has + its own row under `Others`; its Hammond equivalent is 1590N1. +- `ChassisPanel.datasheet` from §4. Rows are listed per group in the order given there. +- `variants.xml`: `125B`, `A`, `B`, `BB` become `deletedOn` stubs; extend `DeletedVariantsTest`. +- `ChassisPanelDatasheetTest`, modelled on `ResistorDatasheetTest`: every row parses; each side + gets the expected size; changing `Side` resizes; a panel from an old file (empty value, null + side) keeps its geometry; the BOM lists one line per top panel, valued by model, and none for + side panels. +- Localization: `Side`, `Top`, `Front`, `Back`, `Left`, `Right` in + every `lang/` file. Fix the description typo "withouth". +- Regenerate `catalog_full.json` as the Resistor change did. + +### 3.3 Factory block import tracking (core) + +- New `org.diylc.core.FactoryBlock` (`name`, `createdOn`, `deletedOn`, `components`), the shape of + `Template`. `blocks.xml` becomes a list of these; the user's `BLOCKS_KEY` map is unchanged. +- `BuildingBlockManager.importDefaultBlocks` mirrors `VariantManager`: import a block when its + `createdOn` is after the stored `DEFAULT_BLOCKS_IMPORT_DATE_KEY` and the user has no block of + that name; remove it when `deletedOn` is after the import date. +- Migration: the three existing blocks keep `createdOn = null` and are imported only when the + legacy `DEFAULT_BLOCKS_IMPORTED_KEY` is false (a fresh install), so users who deleted them do not + get them back. +- Test alongside the variant import tests: fresh install, upgrade from the boolean flag, new block + after an import, deleted block, user block with a clashing name. + +### 3.4 Exploded blocks + +- Ten blocks named `Hammond (Exploded)`, or ` (Exploded)` for a generic size + (`125B (Exploded)`): the six pedal enclosures from D9 and four amp chassis, one per size class. + The amp picks are the steel 1441 sizes amp builders and kit + suppliers stock (Tube Town, an amp-parts shop, carries 1441-10, -18, -28 and -32); steel over + the 1444 because builders warn that thin chassis sag under heavy transformers. + +| Class | Typical build | Block | W x D x H | Why this size | +|---|---|---|---|---| +| Small | Tweed Champ (5F1) | 1441-10 | 8 x 4 x 2 in | Closest Hammond to the original 5F1 chassis (8-3/4 x 4-1/8 x 2-5/8 in). | +| Medium | 18 W Marshall (TMB) | 1441-18 | 13.5 x 5 x 2 in | The Hammond size suggested for 18 W builds; dedicated 18 W chassis are similarly long and narrow (17 x 6-1/2 x 2-1/2 in), which fits the in-line tube row. | +| Larger | 50 W Plexi / JTM45 | 1441-28 | 16 x 8 x 3 in | 16 x 8 x 2-3 in is the size quoted for 50 W Marshall builds; 3 in clears the transformers. | +| Large | 100 W modern (SLO-style) | 1441-32 | 17 x 10 x 3 in | Largest common format; room for two channels, four power tubes and large iron. | + +- No source names a Hammond for the SLO specifically (the real head is 25 in wide, wider than + any Hammond chassis); 1441-32 is a size judgement. +- Each block is five `ChassisPanel`s with `Value` (model) and `Side` set; the block's composite + value is the model as well, so the BOM line reads `Hammond 1590BB (Exploded) / 1590BB`. With the top at (0,0)-(L,W): front (0,-H)-(L,0), back (0,W)-(L,W+H), left (-H,0)-(0,W), + right (L,0)-(L+H,W). Edge radius on the top panel only. +- Generate the XML once from the datasheet rather than by hand, and add a test that every panel of + every factory chassis block matches its datasheet row and touches its neighbours edge to edge, + so the two cannot drift. + +### 3.5 Verify + +`mvn -q test` (spans core and library), then offer the regression suite: §3.1 changes hide/lock +behaviour of existing composites and §3.2 touches serialization. + +## 4. Datasheet rows + +Sources: Hammond per-part drawings, `https://www.hammfg.com/files/parts/pdf/.pdf`; series +tables on hammfg.com (1590, 1590-STOMP, 1441, 1444). Die-cast sizes are drawing outside dimensions +at the lid opening; heights are the box without the 4 mm lid. Chassis sizes are Hammond's nominal +outside W x D x H. Radius 5 on die-cast boxes is carried over from the old variants, **unverified** +(§5); sheet-metal chassis are drawn square. + +Die-cast boxes are grouped by series only (1590, 1550), in Hammond's catalogue order, and only +natural-finish (unpainted) part numbers are rows. Chassis are grouped by family, then height. The +1590 group has 37 rows; the toolbox already splits a datasheet submenu longer than +`MAX_MENU_ITEMS` (30) into ranged submenus, so it needs no further split. + +``` +Diecast (1590),1590LLB,50.5,50.5,21,5 +Diecast (1590),1590LB,50.55,50.6,27,5 +Diecast (1590),1590H,53,38,27,5 +Diecast (1590),1590Y,92,92,38,5 +Diecast (1590),1590A,92.6,38.5,27,5 +Diecast (1590),1590G,100,50,21.5,5 +Diecast (1590),1590G2,100,50,27,5 +Diecast (1590),1590S,110.5,82.4,40,5 +Diecast (1590),1590B,112.4,60.5,27,5 +Diecast (1590),1590B2,112.4,60.5,33.5,5 +Diecast (1590),1590BS,112,60.5,38,5 +Diecast (1590),1590B3,116,77,33.5,5 +Diecast (1590),1590BB,119.5,94,30,5 +Diecast (1590),1590BB2,119.5,94,33.75,5 +Diecast (1590),1590BBS,119.5,94,38.1,5 +Diecast (1590),1590Q,120,120,30,5 +Diecast (1590),1590U,120,120,55,5 +Diecast (1590),1590V,120,120,90,5 +Diecast (1590),1590C,119.5,94,52.5,5 +Diecast (1590),1590T,120,80,55,5 +Diecast (1590),1590CE,120,100,61,5 +Diecast (1590),1590N1,121.2,65.5,35.8,5 +Diecast (1590),1590KK,125,125,53,5 +Diecast (1590),1590K,125,125,75,5 +Diecast (1590),1590J,145,95,45.1,5 +Diecast (1590),1590XX,145.2,121.2,35.2,5 +Diecast (1590),1590X,145.2,121.2,52.3,5 +Diecast (1590),1590P1,153,83.1,46.4,5 +Diecast (1590),1590D,187.75,119.5,52,5 +Diecast (1590),1590DD,188,120,33,5 +Diecast (1590),1590E,188,120,78,5 +Diecast (1590),1590F,188,188,63,5 +Diecast (1590),1590R1,192,112,57,5 +Diecast (1590),1590DE,200,120,60,5 +Diecast (1590),1590EE,200,120,81,5 +Diecast (1590),1590BX2,254,70,31,5 +Diecast (1590),1590BX,254,70,46,5 +Diecast (1550),1550Q,60,55,26,5 +Diecast (1550),1550P,80,55,21,5 +Diecast (1550),1550A,89,35,26,5 +Diecast (1550),1550B,114.5,64,26,5 +Diecast (1550),1550D,115,64,51,5 +Diecast (1550),1550C,115,90,51,5 +Diecast (1550),1550M,120,100,31,5 +Diecast (1550),1550K,140,102,73,5 +Diecast (1550),1550L,165,127,73,5 +Diecast (1550),1550E,171,121,51,5 +Diecast (1550),1550F,171,121,101,5 +Diecast (1550),1550G,222,146,51,5 +Diecast (1550),1550H,222,146,101,5 +Diecast (1550),1550N,250,250,97,5 +Diecast (1550),1550J,275,175,63,5 +Others,125B,122,66,36,5 +Chassis - Steel (1441),1441-9,8 in,4 in,1 in,0 +Chassis - Steel (1441),1441-15,10 in,6 in,1 in,0 +Chassis - Steel (1441),1441-6,4 in,4 in,2 in,0 +Chassis - Steel (1441),1441-8,6 in,4 in,2 in,0 +Chassis - Steel (1441),1441-12,7 in,5 in,2 in,0 +Chassis - Steel (1441),1441-10,8 in,4 in,2 in,0 +Chassis - Steel (1441),1441-14,9 in,5 in,2 in,0 +Chassis - Steel (1441),1441-16,10 in,6 in,2 in,0 +Chassis - Steel (1441),1441-22,12 in,8 in,2 in,0 +Chassis - Steel (1441),1441-29,12 in,10 in,2 in,0 +Chassis - Steel (1441),1441-18,13.5 in,5 in,2 in,0 +Chassis - Steel (1441),1441-26,16 in,8 in,2 in,0 +Chassis - Steel (1441),1441-30,17 in,10 in,2 in,0 +Chassis - Steel (1441),1441-34,17 in,12 in,2 in,0 +Chassis - Steel (1441),1441-24,12 in,8 in,3 in,0 +Chassis - Steel (1441),1441-28,16 in,8 in,3 in,0 +Chassis - Steel (1441),1441-20,17 in,4 in,3 in,0 +Chassis - Steel (1441),1441-32,17 in,10 in,3 in,0 +Chassis - Steel (1441),1441-36,17 in,12 in,3 in,0 +Chassis - Steel (1441),1441-38,17 in,14 in,3 in,0 +Chassis - Steel (1441),1441-33,17 in,10 in,4 in,0 +Chassis - Steel (1441),1441-40,17 in,14 in,4 in,0 +Chassis - Steel (1441),1441-42,17 in,14 in,5 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-45351,4.5 in,3.5 in,1 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-9,8 in,4 in,1 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-15,10 in,6 in,1 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-6415,6 in,4 in,1.5 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-84515,8 in,4.5 in,1.5 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-6,4 in,4 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-542,5 in,4 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-8,6 in,4 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-12,7 in,5 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-772,7 in,7 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-10,8 in,4 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-862,8 in,6 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-14,9 in,5 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-972,9 in,7 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-9552,9.5 in,5 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-16,10 in,6 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-1172,11 in,7 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-22,12 in,8 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-29,12 in,10 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-1372,13 in,7 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-18,13.5 in,5 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-26,16 in,8 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-30,17 in,10 in,2 in,0 +Chassis - Aluminium (1444) 1-2 in,1444-17132,17 in,13 in,2 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-95525,9.5 in,5 in,2.5 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-10825,10 in,8 in,2.5 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-12825,12 in,8 in,2.5 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-643,6 in,4 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-753,7 in,5 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-9553,9.5 in,5 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-1053,10 in,5 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-1273,12 in,7 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-24,12 in,8 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-12103,12 in,10 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-12123,12 in,12 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-1463,14 in,6 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-14103,14 in,10 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-1573,15 in,7 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-1593,15 in,9 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-28,16 in,8 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-20,17 in,4 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-1763,17 in,6 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-1773,17 in,7 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-1783,17 in,8 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-32,17 in,10 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-17123,17 in,12 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-17133,17 in,13 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-17143,17 in,14 in,3 in,0 +Chassis - Aluminium (1444) 2.5-3 in,1444-17173,17 in,17 in,3 in,0 +Chassis - Aluminium (1444) 4-6 in,1444-33,17 in,10 in,4 in,0 +Chassis - Aluminium (1444) 4-6 in,1444-17134,17 in,13 in,4 in,0 +Chassis - Aluminium (1444) 4-6 in,1444-17154,17 in,15 in,4 in,0 +Chassis - Aluminium (1444) 4-6 in,1444-17174,17 in,17 in,4 in,0 +Chassis - Aluminium (1444) 4-6 in,1444-17105,17 in,10 in,5 in,0 +Chassis - Aluminium (1444) 4-6 in,1444-17135,17 in,13 in,5 in,0 +Chassis - Aluminium (1444) 4-6 in,1444-17156,17 in,15 in,6 in,0 +``` + +Group sizes: 37 (1590), 15 (1550), 1 (Others); 23 / 24 / 25 / 7 chassis (132 rows). `Others` +holds generic sizes sold by several makers under one name, currently only 125B (122 x 66 x 40 mm +with the backplate, per a maker's listing). The 1590 rows already in the research were read from the per-part drawings; 1590H, U, +V, CE, KK, K, E, F, R1, DE, EE, BX2 and BX, and all 1550 rows except 1550B, come from Hammond's +series tables (whole mm, lid included). +Finish and option suffixes are not rows: `BK3` (black 1441), `BK` (black 1550), colour codes on +1590 (`RD`, `CB`, ...), `W` prefix (watertight; IP66/IP67 gaskets on the 1550), `F`/`FL` +(flanged), `CWW` (walnut sides on sizes 12/16/22/24/32/33, which shorten the usable front, back and +top by about 22 mm). + +## 5. Unsourced or conflicting figures + +| Item | Status | +|---|---| +| Die-cast corner radius (top view) and top-to-side edge fillet | Not published; 5 mm is the old variants' figure. Measure from Hammond STEP models or a box. | +| Die-cast floor size (pedal top face) | Not published; between the outer size and outer minus 2 x (box height x tan 2 deg). | +| 1590DD box height | Drawing gives 37.00 both overall and box; 33 assumes the usual 4 mm lid. | +| 1590D box height | ~52, same assumption. | +| Box heights of the table-only 1590 rows (H, U, V, CE, KK, K, E, F, R1, DE, EE, BX2, BX) | Table height minus the 4 mm lid; length and width are the table's rounded figures. Read the drawings to firm them up. | +| 125B box height | 40 mm with the backplate minus an assumed 4 mm backplate (the 1590 lid); makers do not state the plate thickness. | +| 1550 box heights (all but 1550B) | Series table height (lid included) minus 4 mm, the overall-to-box difference on the 1550B drawing (30.10 / 26.00). | +| 1590BS / 1590B2 height | Web table 38 for both; drawings 42.00 overall / 38.00 box and 37.50 overall. Drawings used. | +| 1444-10/-12/-14 height | Drawings say 49.78 mm; catalogue and inner height fit 50.80 (2 in). 2 in used. | +| 1441-42 sheet | Drawing 0.057", product page 18 ga. Irrelevant to the outline. | + +## 6. Later + +- HWCHAS walnut-frame chassis (HWCHAS1310 / 1710; aluminium top 298.45 x 222.25 / 400.05 x + 222.25 mm on a 19.05 mm walnut frame): a different construction from the folded sheet metal. +- Hammond rack cases (RM, RMCS) and the 1458 instrument enclosure.