diff --git a/diylc/catalog_full.json b/diylc/catalog_full.json index 467051d2..a30e54e4 100644 --- a/diylc/catalog_full.json +++ b/diylc/catalog_full.json @@ -19662,6 +19662,8 @@ "namePrefix" : "V", "description" : "Sub-miniature (pencil) vacuum tube", "creationMethod" : "SINGLE_CLICK", + "notes" : [ "The tube is drawn as seen from above, from the component side of the board. Leads are numbered as in the data book, lead 1 first. In the circular arrangement standing upright, the first control point is the body centre and the leads follow it counter-clockwise from the gap in the circle - the mirror of the data book's bottom view; there the top lead, if any, is the centre point. In-line and folded tubes have lead 1 at the first control point, and the top lead is the last control point", "'Pin Names' is a comma-separated list of lead names in lead order with the top lead last: P plate, G triode grid, G1/G2/G3 grids, K cathode, IS internal shield, F or H plus the lead number for filament or heater ends; sections of a multi-section tube are numbered first (1P, 2G); leads joined inside end in _1, _2; NC and IC leads are never connected. 'Pin Labels' draws the names (default), the lead numbers or nothing", "A datasheet model is a tube type such as 6021 or 6N16B and sets 'Pin Arrangement', 'Top Lead', 'Lead Count', 'Diameter', 'Thickness', 'Length', 'Pin Names' and 'Value'; 'Diameter' is the broad side of a flat bulb, the width seen when the tube lies folded. Set 'Value' to the same tube type together with the model" ], + "datasheetModels" : [ "6D6A", "6N16B", "6N17B", "6N21B", "6021", "6111", "6112", "5896", "1Zh18B", "1Zh24B", "1Zh29B", "5886", "6Zh1B", "6Zh2B", "5636", "5678", "5840", "5899", "6205", "6206", "1P24B", "6P30B", "5672", "5902", "6088", "6S6B", "6S7B", "6S31B", "6S32B", "5703", "5718", "5719", "5744", "5977", "6814" ], "properties" : [ { "name" : "Alpha", "type" : "Percentage", @@ -19736,6 +19738,20 @@ "type" : "PinArrangement", "possibleValues" : [ "Inline", "Circular" ], "defaultValue" : "Circular" + }, { + "name" : "Pin Labels", + "type" : "PinLabels", + "possibleValues" : [ "NONE", "NAMES", "NUMBERS" ], + "defaultValue" : "NAMES" + }, { + "name" : "Pin Names", + "type" : "String" + }, { + "name" : "Thickness", + "type" : "Size", + "format" : "(value)(unit)", + "possibleUnits" : [ "px", "mm", "cm", "m", "in", "ft", "yd" ], + "defaultValue" : "0.4in" }, { "name" : "Top Lead", "type" : "Boolean", @@ -19745,11 +19761,12 @@ "type" : "String" } ], "pinout" : { - "drivers" : [ "Folded", "Lead Count", "Pin Arrangement" ], + "drivers" : [ "Folded", "Lead Count", "Pin Arrangement", "Top Lead" ], "variants" : [ { "when" : { "Folded" : "false", - "Pin Arrangement" : "Inline" + "Pin Arrangement" : "Inline", + "Top Lead" : "false" }, "pinCountExpression" : "Lead Count", "labels" : "sequential", @@ -19762,7 +19779,100 @@ }, { "when" : { "Folded" : "false", - "Pin Arrangement" : "Circular" + "Pin Arrangement" : "Inline", + "Top Lead" : "true" + }, + "pinCountExpression" : "Lead Count + 1", + "labels" : "sequential", + "pinCountSamples" : { + "_3" : 4, + "_5" : 6, + "_7" : 8, + "_9" : 10 + } + }, { + "when" : { + "Folded" : "false", + "Pin Arrangement" : "Circular", + "Top Lead" : "false" + }, + "pinCountExpression" : "Lead Count + 1", + "labels" : "custom", + "pinCountSamples" : { + "_3" : 4, + "_5" : 6, + "_7" : 8, + "_9" : 10 + }, + "example" : { + "when" : { + "Lead Count" : "_3" + }, + "pins" : [ { + "i" : 0, + "label" : null + }, { + "i" : 1, + "label" : "1" + }, { + "i" : 2, + "label" : "2" + }, { + "i" : 3, + "label" : "3" + } ] + } + }, { + "when" : { + "Folded" : "false", + "Pin Arrangement" : "Circular", + "Top Lead" : "true" + }, + "pinCountExpression" : "Lead Count + 1", + "labels" : "custom", + "pinCountSamples" : { + "_3" : 4, + "_5" : 6, + "_7" : 8, + "_9" : 10 + }, + "example" : { + "when" : { + "Lead Count" : "_3" + }, + "pins" : [ { + "i" : 0, + "label" : "4" + }, { + "i" : 1, + "label" : "1" + }, { + "i" : 2, + "label" : "2" + }, { + "i" : 3, + "label" : "3" + } ] + } + }, { + "when" : { + "Folded" : "true", + "Pin Arrangement" : "Inline", + "Top Lead" : "false" + }, + "pinCountExpression" : "Lead Count", + "labels" : "sequential", + "pinCountSamples" : { + "_3" : 3, + "_5" : 5, + "_7" : 7, + "_9" : 9 + } + }, { + "when" : { + "Folded" : "true", + "Pin Arrangement" : "Inline", + "Top Lead" : "true" }, "pinCountExpression" : "Lead Count + 1", "labels" : "sequential", @@ -19775,7 +19885,8 @@ }, { "when" : { "Folded" : "true", - "Pin Arrangement" : "Inline" + "Pin Arrangement" : "Circular", + "Top Lead" : "false" }, "pinCountExpression" : "Lead Count", "labels" : "sequential", @@ -19788,15 +19899,16 @@ }, { "when" : { "Folded" : "true", - "Pin Arrangement" : "Circular" + "Pin Arrangement" : "Circular", + "Top Lead" : "true" }, - "pinCountExpression" : "Lead Count", + "pinCountExpression" : "Lead Count + 1", "labels" : "sequential", "pinCountSamples" : { - "_3" : 3, - "_5" : 5, - "_7" : 7, - "_9" : 9 + "_3" : 4, + "_5" : 6, + "_7" : 8, + "_9" : 10 } } ] } diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml index d42e3043..ae74193e 100644 --- a/diylc/diylc-core/src/main/resources/update.xml +++ b/diylc/diylc-core/src/main/resources/update.xml @@ -533,6 +533,10 @@ NEW_FEATURE Added datasheets to 'Diode (plastic)' and 'Diode (glass)' - pick a part by its number from the component's submenu to draw it at its real package size and colours - more than 300 rectifiers, Schottky, signal, germanium and zener diodes, including the Japanese clippers of classic overdrive pedals; the AI Assistant can place these parts by part number too + + NEW_FEATURE + Added 'Pin Names', 'Pin Labels' and 'Thickness' to 'Sub-Mini Tube' to name each lead and draw flat bulbs, and a datasheet with popular Western and Soviet sub-miniature tubes such as 6021, 6111, 5840, 5672, 6N16B and 6N17B that sets leads, bulb size and lead names at once + NEW_FEATURE 'Chassis Panel' is now 'Enclosure' and can draw an exploded view of the whole box; set 'Height' and choose which 'Sides' to unfold @@ -593,6 +597,10 @@ BUG_FIX 'Tube Socket' with a Noval, 7-pin or Duodecar base on a chassis mount did not rotate its mounting notches with the pins when 'Angle' was changed + + BUG_FIX + 'Sub-Mini Tube' with circular leads numbered the body centre as lead 1 in netlists and AI analysis, so every real lead was reported one number too high + https://github.com/bancika/diy-layout-creator/releases diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/tube/SubminiTube.java b/diylc/diylc-library/src/main/java/org/diylc/components/tube/SubminiTube.java index 63933680..56f68f06 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/tube/SubminiTube.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/tube/SubminiTube.java @@ -30,20 +30,29 @@ import java.awt.geom.Ellipse2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; +import java.util.Arrays; import org.diylc.appframework.miscutils.ConfigurationManager; +import org.diylc.awt.StringUtils; import org.diylc.common.Display; +import org.diylc.common.HorizontalAlignment; import org.diylc.common.IPlugInPort; import org.diylc.common.ObjectCache; import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; import org.diylc.components.AbstractLabeledComponent; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.semiconductors.ICPinNames; +import org.diylc.components.semiconductors.PinLabels; import org.diylc.components.transform.TO220Transformer; import org.diylc.core.ComponentState; +import org.diylc.core.IDatasheetSupport; import org.diylc.core.IDIYComponent; import org.diylc.core.IDrawingObserver; import org.diylc.core.Project; import org.diylc.core.Theme; import org.diylc.core.VisibilityPolicy; +import org.diylc.core.annotations.AiNote; import org.diylc.core.annotations.ComponentDescriptor; import org.diylc.core.annotations.EditableProperty; import org.diylc.core.annotations.KeywordPolicy; @@ -51,11 +60,15 @@ import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; +@AiNote("The tube is drawn as seen from above, from the component side of the board. Leads are numbered as in the data book, lead 1 first. In the circular arrangement standing upright, the first control point is the body centre and the leads follow it counter-clockwise from the gap in the circle - the mirror of the data book's bottom view; there the top lead, if any, is the centre point. In-line and folded tubes have lead 1 at the first control point, and the top lead is the last control point") +@AiNote("'Pin Names' is a comma-separated list of lead names in lead order with the top lead last: P plate, G triode grid, G1/G2/G3 grids, K cathode, IS internal shield, F or H plus the lead number for filament or heater ends; sections of a multi-section tube are numbered first (1P, 2G); leads joined inside end in _1, _2; NC and IC leads are never connected. 'Pin Labels' draws the names (default), the lead numbers or nothing") +@AiNote("A datasheet model is a tube type such as 6021 or 6N16B and sets 'Pin Arrangement', 'Top Lead', 'Lead Count', 'Diameter', 'Thickness', 'Length', 'Pin Names' and 'Value'; 'Diameter' is the broad side of a flat bulb, the width seen when the tube lies folded. Set 'Value' to the same tube type together with the model") @ComponentDescriptor(name = "Sub-Mini Tube", author = "Branislav Stojkovic", category = "Tubes", instanceNamePrefix = "V", description = "Sub-miniature (pencil) vacuum tube", zOrder = IDIYComponent.COMPONENT, keywordPolicy = KeywordPolicy.SHOW_VALUE, - transformer = TO220Transformer.class) -public class SubminiTube extends AbstractLabeledComponent { + transformer = TO220Transformer.class, enableDatasheet = true, datasheetCreationStepCount = 2, + datasheetModelColumn = 1) +public class SubminiTube extends AbstractLabeledComponent implements IDatasheetSupport { private static final long serialVersionUID = 1L; @@ -86,16 +99,41 @@ public class SubminiTube extends AbstractLabeledComponent { private PinArrangement leadArrangement = PinArrangement.Circular; private boolean topLead = false; private Size diameter = DIAMETER; + private Size thickness = null; private Size length = LENGTH; private PinCount pinCount = PinCount._8; private Size leadSpacing = LEAD_SPACING; private Color labelColor = LABEL_COLOR; + private String pinNames = null; + private PinLabels pinLabels = null; + transient private String[] parsedPinNames; public SubminiTube() { super(); updateControlPoints(); } + public SubminiTube(String[] model) { + this(); + + applyModel(model); + } + + // columns: group, tube type, pin arrangement, top lead (yes/no), bulb width, bulb thickness, + // bulb length, then one name per lead in lead order with the top lead last + @Override + public void applyModel(String[] model) { + boolean topLead = "yes".equalsIgnoreCase(model[3]); + setValue(model[1]); + setPinArrangement(PinArrangement.valueOf(model[2])); + setTopLead(topLead); + setDiameter(Size.parseSize(model[4])); + setThickness(Size.parseSize(model[5])); + setLength(Size.parseSize(model[6])); + setPinCount(PinCount.valueOf("_" + (model.length - 7 - (topLead ? 1 : 0)))); + setPinNames(String.join(",", Arrays.copyOfRange(model, 7, model.length))); + } + @EditableProperty public String getValue() { return value; @@ -226,17 +264,18 @@ public class SubminiTube extends AbstractLabeledComponent { Point2D firstPoint = controlPoints[0]; double angleIncrement = Math.PI * 2 / (pinCount + 1); - double initialAngleOffset = angleIncrement; double r = (pinCount + 1) * leadSpacing / (2 * Math.PI); controlPoints = new Point2D[pinCount + 1]; - double theta = initialAngleOffset + getOrientation().toRadians(); + // seen from above the leads run counter-clockwise from the gap, the mirror of the data book's + // bottom view; with y pointing down that is a decreasing angle + double theta = getOrientation().toRadians() - angleIncrement; controlPoints[0] = firstPoint; for (int i = 0; i < pinCount; i++) { controlPoints[i + 1] = new Point2D.Double((int) (firstPoint.getX() + Math.cos(theta) * r), (int) (firstPoint.getY() + Math.sin(theta) * r)); - theta += angleIncrement; + theta -= angleIncrement; } } } @@ -290,9 +329,16 @@ public class SubminiTube extends AbstractLabeledComponent { default: throw new RuntimeException("Unexpected orientation: " + orientation); } - } else { + } else if (getPinArrangement() == PinArrangement.Circular) { body[0] = new Area(new Ellipse2D.Double(centerX - diameter / 2, centerY - diameter / 2, diameter, diameter)); + } else { + // a flat bulb stands with its broad side along the lead row + double thickness = getThickness().convertToPixels(); + boolean rowAlongY = orientation == Orientation.DEFAULT || orientation == Orientation._180; + double w = rowAlongY ? thickness : diameter; + double h = rowAlongY ? diameter : thickness; + body[0] = new Area(new Ellipse2D.Double(centerX - w / 2, centerY - h / 2, w, h)); } } return body; @@ -473,6 +519,67 @@ public class SubminiTube extends AbstractLabeledComponent { } g2d.drawString(label, (int)(x + dx), (int)(y + dy)); + + if (getPinLabels() != PinLabels.NONE) { + drawLeadLabels(g2d, mainArea.getBounds2D()); + } + } + + // In a row the labels sit square to it on the side away from the body; on a standing circular + // tube they point outwards from the centre, past the edge of the bulb. The top lead leaves the far + // end of the bulb, away from the row, and is left unlabelled. + private void drawLeadLabels(Graphics2D g2d, Rectangle2D bodyBounds) { + g2d.setFont(ICPinNames.PIN_NAME_FONT); + double pinRadius = LEAD_THICKNESS.convertToPixels() / 2; + boolean circular = getPinArrangement() == PinArrangement.Circular && !folded; + int rowCount = controlPoints.length - (getTopLead() && !circular ? 1 : 0); + double alongX = 0; + double alongY = 0; + double awayX = 0; + double awayY = 0; + if (!circular && rowCount > 1) { + double rowDistance = controlPoints[0].distance(controlPoints[1]); + alongX = (controlPoints[1].getX() - controlPoints[0].getX()) / rowDistance; + alongY = (controlPoints[1].getY() - controlPoints[0].getY()) / rowDistance; + awayX = alongY; + awayY = -alongX; + double rowCenterX = (controlPoints[0].getX() + controlPoints[rowCount - 1].getX()) / 2; + double rowCenterY = (controlPoints[0].getY() + controlPoints[rowCount - 1].getY()) / 2; + if ((bodyBounds.getCenterX() - rowCenterX) * awayX + + (bodyBounds.getCenterY() - rowCenterY) * awayY > 1) { + awayX = -awayX; + awayY = -awayY; + } + } + for (int i = 0; i < controlPoints.length; i++) { + int lead = getLeadNumber(i); + if (lead == 0 || (circular && i == 0)) { + continue; + } + String label; + if (getPinLabels() == PinLabels.NUMBERS) { + label = Integer.toString(lead); + } else { + String name = ICPinNames.get(getParsedPinNames(), lead); + if (name == null) { + continue; + } + label = AbstractMakerBoard.getDisplayPinLabel(name); + } + Point2D point = controlPoints[i]; + if (circular) { + double dx = point.getX() - controlPoints[0].getX(); + double dy = point.getY() - controlPoints[0].getY(); + double r = Math.sqrt(dx * dx + dy * dy); + double reach = Math.max(r + pinRadius, bodyBounds.getWidth() / 2) + ICPinNames.PIN_NAME_GAP + + g2d.getFontMetrics().stringWidth(label) / 2d; + StringUtils.drawCenteredText(g2d, label, controlPoints[0].getX() + dx / r * reach, + controlPoints[0].getY() + dy / r * reach, HorizontalAlignment.CENTER, + VerticalAlignment.CENTER); + } else if (i < rowCount) { + ICPinNames.drawOutside(g2d, label, point, awayX, awayY, bodyBounds, pinRadius); + } + } } @Override @@ -581,6 +688,19 @@ public class SubminiTube extends AbstractLabeledComponent { public void setDiameter(Size diameter) { this.diameter = diameter; updateControlPoints(); + body = null; + } + + // the narrow side of a flat bulb; a round bulb, and every file saved before the property + // existed, has none and is as thick as it is wide + @EditableProperty + public Size getThickness() { + return thickness == null ? getDiameter() : thickness; + } + + public void setThickness(Size thickness) { + this.thickness = thickness; + body = null; } @EditableProperty @@ -590,6 +710,7 @@ public class SubminiTube extends AbstractLabeledComponent { public void setLength(Size length) { this.length = length; + body = null; } @EditableProperty(name = "Lead Count") @@ -630,6 +751,57 @@ public class SubminiTube extends AbstractLabeledComponent { return false; } + @Override + public String getControlPointNodeName(int index) { + int lead = getLeadNumber(index); + if (lead == 0) { + return null; + } + String name = ICPinNames.get(getParsedPinNames(), lead); + return name == null ? Integer.toString(lead) : name; + } + + // the data book's lead number of a control point, 0 for the centre of a standing circular tube + // without a top lead; the top lead is numbered after the last regular lead + private int getLeadNumber(int index) { + if (getPinArrangement() == PinArrangement.Circular && !folded) { + if (index == 0) { + return getTopLead() ? getPinCount().getValue() + 1 : 0; + } + return index; + } + return index + 1; + } + + private String[] getParsedPinNames() { + if (parsedPinNames == null) { + parsedPinNames = ICPinNames.parse(getPinNames()); + } + return parsedPinNames; + } + + @EditableProperty(name = "Pin Names") + public String getPinNames() { + return pinNames; + } + + public void setPinNames(String pinNames) { + this.pinNames = pinNames; + this.parsedPinNames = null; + } + + @EditableProperty(name = "Pin Labels") + public PinLabels getPinLabels() { + if (pinLabels == null) { + pinLabels = PinLabels.NAMES; + } + return pinLabels; + } + + public void setPinLabels(PinLabels pinLabels) { + this.pinLabels = pinLabels; + } + public static enum PinArrangement { Inline("In-line"), Circular("Circular"); diff --git a/diylc/diylc-library/src/main/resources/datasheets/SubminiTube.datasheet b/diylc/diylc-library/src/main/resources/datasheets/SubminiTube.datasheet new file mode 100644 index 00000000..c480669a --- /dev/null +++ b/diylc/diylc-library/src/main/resources/datasheets/SubminiTube.datasheet @@ -0,0 +1,35 @@ +Diode,6D6A,Inline,no,7.2 mm,7.2 mm,36 mm,P,H2,H3,K +Dual Triode,6N16B,Circular,no,10.2 mm,10.2 mm,36 mm,2P,1K,1G,H4,1P,2K,2G,H8 +Dual Triode,6N17B,Circular,no,10.2 mm,10.2 mm,36 mm,2P,1K,1G,H4,1P,2K,2G,H8 +Dual Triode,6N21B,Circular,no,10.2 mm,10.2 mm,40 mm,1K,IS,1G,1P,H5,2K,NC,2G,2P,H10 +Dual Triode,6021,Circular,no,0.4 in,0.4 in,1.375 in,2P,2G,H3,2K,1K,H6,1G,1P +Dual Triode,6111,Circular,no,0.4 in,0.4 in,1.375 in,2P,2G,H3,2K,1K,H6,1G,1P +Dual Triode,6112,Circular,no,0.4 in,0.4 in,1.375 in,2P,2G,H3,2K,1K,H6,1G,1P +Duo-Diode,5896,Circular,no,0.4 in,0.4 in,1.375 in,2P,2K,H3,IS,1P,H6,1K,NC +Pentode,1Zh18B,Circular,yes,8.5 mm,8.5 mm,42 mm,F1,F2,NC,NC,G2,G1,P +Pentode,1Zh24B,Circular,yes,8.5 mm,8.5 mm,42 mm,F1,F2,IS,G3,G2,G1,P +Pentode,1Zh29B,Circular,yes,8.5 mm,8.5 mm,40 mm,HCT,G3,G2,F4,IS,F6,G1,P +Pentode,5886,Inline,no,0.4 in,0.285 in,1.5 in,P,G2,F3,F4,NC,NC,G1 +Pentode (RF/IF),6Zh1B,Inline,no,10.2 mm,10.2 mm,36 mm,P,G2,H3,H4,G3,K,G1 +Pentode (RF/IF),6Zh2B,Inline,no,10.2 mm,10.2 mm,38 mm,P,G3,G2,H4,H5,K,G1 +Pentode (RF/IF),5636,Circular,no,0.4 in,0.4 in,1.375 in,G1,K_1,H3,G3,P,H6,G2,K_2 +Pentode (RF/IF),5678,Inline,no,0.4 in,0.285 in,1.5 in,P,G2,F3,G1,F5 +Pentode (RF/IF),5840,Circular,no,0.4 in,0.4 in,1.375 in,G1,K_1,H3,K_2,P,H6,G2,K_3 +Pentode (RF/IF),5899,Circular,no,0.4 in,0.4 in,1.375 in,G1,K_1,H3,K_2,P,H6,G2,K_3 +Pentode (RF/IF),6205,Circular,no,0.4 in,0.4 in,1.375 in,G1,K_1,H3,G3,P,H6,G2,K_2 +Pentode (RF/IF),6206,Circular,no,0.4 in,0.4 in,1.375 in,G1,K_1,H3,K_2,P,H6,G2,K_3 +Power Pentode,1P24B,Circular,yes,10.5 mm,10.5 mm,45 mm,HCT,F2,G2,IS,NC,F6,G3,NC,G1,P +Power Pentode,6P30B,Circular,no,10.8 mm,10.8 mm,48 mm,K,H2,G2,P,NC,H6,G1,G3 +Power Pentode,5672,Inline,no,0.385 in,0.285 in,1.5 in,P,G2,F3,G1,F5 +Power Pentode,5902,Circular,no,0.4 in,0.4 in,1.75 in,G1,K_1,H3,K_2,P,H6,G2,K_3 +Power Pentode,6088,Inline,no,0.385 in,0.285 in,1.5 in,P,G2,F3,G1,F5 +Triode,6S6B,Inline,no,10.2 mm,10.2 mm,36 mm,P,H2,H3,G,K +Triode,6S7B,Inline,no,10.2 mm,10.2 mm,36 mm,P,H2,H3,G,K +Triode,6S31B,Circular,no,10.2 mm,10.2 mm,39 mm,K,H2,NC,NC,P,NC,H7,G,NC,NC +Triode,6S32B,Circular,no,10.2 mm,10.2 mm,30 mm,K,NC,G,NC,H5,NC,P,NC,NC,H10 +Triode,5703,Inline,no,0.4 in,0.4 in,1.5 in,P,NC,H3,H4,G,K +Triode,5718,Circular,no,0.4 in,0.4 in,1.375 in,G,NC,H3,NC,K,H6,NC,P +Triode,5719,Circular,no,0.4 in,0.4 in,1.375 in,G,NC,H3,NC,K,H6,NC,P +Triode,5744,Inline,no,0.4 in,0.4 in,1.5 in,P,H2,H3,G,K +Triode,5977,Circular,no,0.4 in,0.4 in,1.375 in,G,NC,H3,NC,K,H6,NC,P +Triode,6814,Circular,no,0.4 in,0.4 in,1.375 in,G,NC,H3,NC,K,H6,NC,P diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/tube/SubminiTubeDatasheetTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/tube/SubminiTubeDatasheetTest.java new file mode 100644 index 00000000..986f84d9 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/tube/SubminiTubeDatasheetTest.java @@ -0,0 +1,87 @@ +/* + + 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 . + +*/ +package org.diylc.components.tube; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.diylc.netlist.Node; +import org.diylc.presenter.DatasheetService; +import org.junit.Assert; +import org.junit.Test; + +public class SubminiTubeDatasheetTest { + + private static final int FIRST_LEAD_COLUMN = 7; + + @Test + public void everyRowApplies() { + for (String[] row : datasheet()) { + SubminiTube tube = new SubminiTube(row); + String context = String.join(",", row); + Assert.assertEquals(context, row[1], tube.getValue()); + Assert.assertTrue(context, row[3].equals("yes") || row[3].equals("no")); + int leads = row.length - FIRST_LEAD_COLUMN - (tube.getTopLead() ? 1 : 0); + Assert.assertEquals(context, leads, tube.getPinCount().getValue()); + } + } + + @Test + public void tubesAreUnique() { + Set tubes = new HashSet(); + for (String[] row : datasheet()) { + Assert.assertTrue("repeated tube " + row[1], tubes.add(row[1].toUpperCase())); + } + } + + /** Two leads share a net only when both say so with a numbered suffix, like K_1 and K_2. */ + @Test + public void leadsShareANetOnlyThroughTheSuffix() { + for (String[] row : datasheet()) { + for (int i = FIRST_LEAD_COLUMN; i < row.length; i++) { + for (int j = i + 1; j < row.length; j++) { + String rail = Node.railName(row[i]); + if (rail != null && rail.equals(Node.railName(row[j]))) { + Assert.assertTrue(row[1] + " leads " + row[i] + " and " + row[j], + row[i].matches(".*_\\d+") && row[j].matches(".*_\\d+")); + } + } + } + } + } + + @Test + public void tubeIsFoundByTypeNotByGroup() { + DatasheetService service = DatasheetService.getInstance(); + String[] first = datasheet().get(0); + Assert.assertEquals(1, service.findSeries(SubminiTube.class, first[1]).size()); + Assert.assertTrue(service.findSeries(SubminiTube.class, first[0]).isEmpty()); + } + + private static List datasheet() { + List datasheet = DatasheetService.getInstance().loadDatasheet(SubminiTube.class); + Assert.assertNotNull(datasheet); + Assert.assertFalse(datasheet.isEmpty()); + return datasheet; + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/tube/SubminiTubeTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/tube/SubminiTubeTest.java new file mode 100644 index 00000000..fad17b44 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/tube/SubminiTubeTest.java @@ -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 . + +*/ +package org.diylc.components.tube; + +import org.diylc.common.Orientation; +import org.diylc.components.tube.SubminiTube.PinArrangement; +import org.diylc.components.tube.SubminiTube.PinCount; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.junit.Assert; +import org.junit.Test; + +public class SubminiTubeTest { + + /** The centre of a standing circular tube is not a lead; lead 1 is the first on the circle. */ + @Test + public void circularCentreIsNotALead() { + SubminiTube tube = new SubminiTube(); + tube.setPinArrangement(PinArrangement.Circular); + tube.setPinCount(PinCount._8); + Assert.assertNull(tube.getControlPointNodeName(0)); + Assert.assertEquals("1", tube.getControlPointNodeName(1)); + Assert.assertEquals("8", tube.getControlPointNodeName(8)); + } + + /** Standing upright, the top lead of a circular tube rises from the centre. */ + @Test + public void circularTopLeadIsTheCentre() { + SubminiTube tube = new SubminiTube(); + tube.setPinArrangement(PinArrangement.Circular); + tube.setPinCount(PinCount._5); + tube.setTopLead(true); + tube.setPinNames("A,B,C,D,E,TOP"); + Assert.assertEquals("TOP", tube.getControlPointNodeName(0)); + Assert.assertEquals("A", tube.getControlPointNodeName(1)); + } + + @Test + public void inlineLeadsStartAtTheFirstPointAndEndWithTheTopLead() { + SubminiTube tube = new SubminiTube(); + tube.setPinArrangement(PinArrangement.Inline); + tube.setPinCount(PinCount._3); + tube.setTopLead(true); + tube.setPinNames("K,G,,P"); + Assert.assertEquals(4, tube.getControlPointCount()); + Assert.assertEquals("K", tube.getControlPointNodeName(0)); + // a blank name falls back to the lead number + Assert.assertEquals("3", tube.getControlPointNodeName(2)); + Assert.assertEquals("P", tube.getControlPointNodeName(3)); + } + + @Test + public void modelSetsTheTubeAndLeavesTheMounting() { + SubminiTube tube = new SubminiTube(); + tube.setOrientation(Orientation._90); + tube.setFolded(true); + tube.applyModel(new String[] {"Dual Triode", "TEST", "Inline", "yes", "10 mm", "7 mm", "40 mm", + "1P", "1G", "1K", "F4", "F5", "2K", "2G", "2P", "IS"}); + + Assert.assertEquals("TEST", tube.getValue()); + Assert.assertEquals(PinArrangement.Inline, tube.getPinArrangement()); + Assert.assertTrue(tube.getTopLead()); + Assert.assertEquals(PinCount._8, tube.getPinCount()); + Assert.assertEquals(new Size(10d, SizeUnit.mm), tube.getDiameter()); + Assert.assertEquals(new Size(7d, SizeUnit.mm), tube.getThickness()); + Assert.assertEquals(new Size(40d, SizeUnit.mm), tube.getLength()); + Assert.assertEquals(Orientation._90, tube.getOrientation()); + Assert.assertTrue(tube.getFolded()); + Assert.assertEquals("IS", tube.getControlPointNodeName(8)); + } + + /** Files saved before the property existed draw a round bulb, as they always did. */ + @Test + public void bulbWithoutThicknessIsRound() { + SubminiTube tube = new SubminiTube(); + Assert.assertEquals(tube.getDiameter(), tube.getThickness()); + } + + /** + * Seen from above, leads run counter-clockwise from the gap, which faces right by default: lead 1 + * is just above the gap and the last lead just below it. + */ + @Test + public void circularLeadsRunCounterClockwiseFromTheGap() { + SubminiTube tube = new SubminiTube(); + tube.setPinArrangement(PinArrangement.Circular); + tube.setPinCount(PinCount._8); + double centreY = tube.getControlPoint(0).getY(); + double centreX = tube.getControlPoint(0).getX(); + Assert.assertTrue(tube.getControlPoint(1).getY() < centreY); + Assert.assertTrue(tube.getControlPoint(1).getX() > centreX); + Assert.assertTrue(tube.getControlPoint(8).getY() > centreY); + Assert.assertTrue(tube.getControlPoint(8).getX() > centreX); + } +} diff --git a/diylc/diylc-library/src/test/resources/pinout-drivers.txt b/diylc/diylc-library/src/test/resources/pinout-drivers.txt index 05587892..ab21051d 100644 --- a/diylc/diylc-library/src/test/resources/pinout-drivers.txt +++ b/diylc/diylc-library/src/test/resources/pinout-drivers.txt @@ -165,6 +165,6 @@ tube.DiodeSymbol = Directly heated, Heaters tube.DuoDiodeSymbol = Directly heated, Heaters tube.Jumbo4PinTubeSocket = fixed tube.PentodeSymbol = Heaters, Suppressor grid -tube.SubminiTube = Folded, Lead Count, Pin Arrangement +tube.SubminiTube = Folded, Lead Count, Pin Arrangement, Top Lead tube.TriodeSymbol = Directly heated, Heaters tube.TubeSocket = Base diff --git a/diylc/docs/plans/submini-tube-datasheets.md b/diylc/docs/plans/submini-tube-datasheets.md new file mode 100644 index 00000000..74bea977 --- /dev/null +++ b/diylc/docs/plans/submini-tube-datasheets.md @@ -0,0 +1,124 @@ +# Sub-Mini Tube Datasheets + +Naming the leads of a `SubminiTube` and applying a known tube's leads and body from a datasheet. + +## 1. Goal + +Follows [Tube Socket Datasheets](tube-socket-datasheets.md). Sub-miniature ("pencil") tubes are +soldered in by their wire leads, so the tube itself carries what a socket carries for a noval: which +lead is the plate, which the grid. `SubminiTube` draws the body and leads (in-line flat press or +circular button, 3 to 10 leads, optional top lead) but cannot name a lead, so the netlist, tooltips +and the AI see only numbers. A datasheet of the popular types - Western (6021, 5840, 5672, 6111...) +and Soviet (6N16B, 6N17B, 6S6B, 6Zh1B...) - sets the lead names, lead count, arrangement and body +size in one step. + +### What is wrong today + +`SubminiTube` does not override `getControlPointNodeName`, so the inherited `index + 1` applies. In +the circular arrangement control point 0 is the body centre, not a lead: the netlist calls the centre +"1" and the real leads "2" ... "N+1". No sample project contains a sub-mini tube, so fixing it +changes no reference output. + +## 2. Design decisions + +| # | Decision | Choice | Rationale | +|---|---|---|---| +| S1 | Lead names | New `String pinNames` field, comma-separated in lead-number order, shown as "Pin Names"; `null` means unnamed. Node name = the name if present and not blank, otherwise the lead number | The shape used by `DIL_IC`, `SIL_IC` and `TubeSocket`. Old files deserialize with `null` and keep numbers. | +| S2 | Lead numbering | Lead 1 is the first lead control point: control point 0 on in-line and folded tubes, control point 1 on circular ones (0 is the centre and gets no node name). A top lead is the last lead | Fixes ยง1. Matches how data books number flat-press leads across the row and button leads around the circle. | +| S3 | Naming style | As on tube sockets (tube plan V5-V8): `P`, `G`, `G1`/`G2`/`G3`, `K`, `IS`, section prefixes `1P`, `2G`; filament ends `F` + lead number on directly heated battery tubes, heater ends `H` + lead number; `_n` only for leads joined inside; `NC` for an empty lead position, `IC` for "do not use" | One vocabulary across all tubes. | +| S4 | What a lead shows | New `pinLabels` field with the shared `PinLabels` enum, shown as "Pin Labels": None, Names (default), Numbers. Names are drawn beside each lead end, away from the body, with `ICPinNames.drawOutside` and its 6 px font; unnamed leads draw nothing under Names | Same option as the ICs and tube sockets. Old files have no names, so under the default they draw exactly what they drew before. | +| S5 | Datasheet row | `group, tube, arrangement, top lead, width, thickness, length, name1, ..., nameN`, e.g. `Dual Triode,6021,Inline,no,0.4 in,0.3 in,1.5 in,...`. `arrangement` is a `PinArrangement` constant, `top lead` is `yes`/`no`, sizes are the maximum body dimensions from the data book; the lead count is the number of names, minus one when there is a top lead | Trailing names as in every pin-naming datasheet. | +| S6 | `applyModel` | Sets value, pin arrangement, top lead, pin count, diameter, thickness, length and pin names. Leaves orientation, folded, lead length, lead spacing (the board hole pitch, not the wire pitch) and colours alone | The datasheet says what the tube is; how it is bent onto the board is the user's choice. | +| S7 | Model identity | `enableDatasheet = true`, `datasheetCreationStepCount = 2`, `datasheetModelColumn = 1` | As for all other datasheets. | +| S9 | Bulb dimensions | Three sizes per tube: "Diameter" is the broad side of the cross-section - the width seen when the tube lies folded on the board - a new "Thickness" the narrow side, and "Length" the glass length excluding leads; all maximums from the data book outline. A standing in-line tube draws an oval, broad side along the lead row; circular tubes stay round. `thickness` is `null` in old files and reads as the diameter | Flat T-2 bulbs are oval, so one diameter cannot draw both the folded view (broad face down) and the standing top view. A new field keeps old files round, as drawn before. "Thickness" is already translated. | +| S10 | View | The tube is drawn as seen from above, from the component side: circular leads run counter-clockwise from the gap, the mirror of the data book's bottom view. In-line and folded tubes are unaffected - lead 1 is the first control point and nothing marks a side. `TubeSocket` stays a bottom view | A sub-mini tube is soldered onto a board and laid out from the component side; a socket is wired from under the chassis. Circular leads used to run clockwise; files saved before keep their stored lead positions until the part is edited (none in the sample corpus). | +| S11 | Rod pentodes | 1Zh18B, 1Zh24B, 1Zh29B and 1P24B are listed as circular tubes with the anode as the top lead. Their real centre lead is drawn on the circle with the others | Popular in battery-tube builds. Names, netlist and AI are exact; only the drawn position of the centre lead is approximate. | +| S8 | Lead counts | The `PinCount` enum (3-10) covers the types; a tube outside it is left out rather than the enum widened | Keeps the change to new fields only. | + +### Explicitly out of scope + +- Nuvistors (6S51N, 7586, 13CW4): a different metal-can body; a separate component if wanted. +- Acorn tubes. +- Translating group names. + +## 3. Changes + +### diylc-library + +- `SubminiTube`: fields `pinNames` and `pinLabels` with their properties; `getControlPointNodeName` + per S1/S2; lead-name drawing (S4); `implements IDatasheetSupport`, descriptor attributes, + `(String[] model)` constructor, `applyModel` (S6); `@AiNote`s on lead numbering, names and the + model. +- `datasheets/SubminiTube.datasheet`: rows per S5, each from a data book (RCA, Raytheon, Sylvania, + GE, Tung-Sol, Soviet reference books) or two independent sources. + +### Generated + +- `catalog_full.json`, and `pinout-drivers.txt` if "Pin Names" becomes a pinout driver. + +## 4. Datasheet inventory + +`SubminiTube.datasheet`, 35 tubes: + +| Group | Western | Soviet | +|---|---|---| +| Dual Triode | 6021, 6111, 6112 | 6N16B, 6N17B, 6N21B | +| Triode | 5703, 5718, 5719, 5744, 5977, 6814 | 6S6B, 6S7B, 6S31B, 6S32B | +| Pentode / Pentode (RF/IF) | 5886 / 5636, 5678, 5840, 5899, 6205, 6206 | 1Zh18B, 1Zh24B, 1Zh29B (rod, top anode) / 6Zh1B, 6Zh2B | +| Power Pentode | 5672, 5902, 6088 | 6P30B, 1P24B (rod, top anode) | +| Duo-Diode / Diode | 5896 | 6D6A | + +Sourcing notes: + +- Western rows read from Tung-Sol, RCA, Raytheon and Sylvania sheets (frank.pocnet.net); Soviet rows + from the Soviet export catalogue sheets on the same site, tec.org.ru redraws of the reference books + and the Tubeclinic sub-miniature summary. +- Circular button tubes have 8 or 10 lead positions on a circle with one gap, numbered clockwise in + the data book's bottom view from the lead after the gap; `SubminiTube` already spaces N leads on + N + 1 positions with the gap between the last lead and lead 1. In-line tubes carry a colour dot or + key mark by lead 1. +- Positions the data book numbers but where no lead exists, or the lead is cut as a key, are `NC` + (5703 lead 2, 5886 leads 5-6, 6N21B lead 7, the cut leads of 6S31B/6S32B). +- Bulbs: Western T-3 round 0.400 in, T-2x3 flat 0.385-0.400 x 0.285 in (5672, 5678, 6088, 5886); + every Soviet "B" bulb is round, 7.2-10.8 mm. Lengths are maximum glass length with the tip; the + exhaust tip is always at the far end from the leads. +- Electrodes tied inside to a cathode or filament end are folded into that lead's name (5840 g3 on + the cathode leads, 5672 g3 on a filament end). +- Weakest rows: 5886 and 6814 (one data book each), 6S6B, 6S32B, 6D6A and 6P30B (one source each). +- 6206 follows the Sylvania sheet (basing 8DL: K with G3 on leads 2, 4 and 8, as the 5899), although + its text mentions an external suppressor connection and Tung-Sol gives 8DC (G3 on lead 4). 6Zh1B + follows a Soviet reference-book page (flat press, 7 leads in a row, mark by lead 1, 10.2 x 36 mm); + a round-press version is also sold under that name. 6Zh2B follows the 6Zh2B-V reference drawing + (flat press, 7 leads, 10.2 x 38 mm). +- 6814 follows the Sylvania sheet alone (basing 8DK, the 5718's leads); the second source found, + rtellason.com, refuses automated downloads. +- Left out: 6P30B-R and 6N16B-VR (variants + with other pinouts, single sources), 1Zh17B (lead table only assumed to match 1Zh24B). +- Rod pentodes name the filament centre tap of 1Zh29B and 1P24B `HCT`; the leads cut short as a key + on 1Zh18B are `NC`. + +## 5. Tests + +- `SubminiTubeTest`: node names per arrangement (circular centre unnamed, lead 1 first), names + mapped through, top lead last, `applyModel` sets the S6 properties and leaves the rest. +- `SubminiTubeDatasheetTest`: every row parses and applies; name count fits `PinCount`; tubes are + unique; no two names share a net unless both carry `_n`; `findSeries` finds a tube, not a group. + +## 6. Verification + +- `mvn -q test` over the whole reactor. +- Render in-line, circular and folded tubes with a top lead in all orientations, Pin Labels in each + mode; check names sit by their leads. +- Regression suite optional: no sample project contains a sub-mini tube. + +## 7. Phases + +1. **Plan**: this document. +2. **Code**: done - S1-S4, S6-S7, `SubminiTubeTest` (old files round, circular centre unnamed, circular top lead at + the centre, in-line top lead last, model sets and leaves). Lead names are drawn beside each row + lead away from the body, radially past the bulb on a standing circular tube; the top lead is left + unlabelled. `setDiameter`/`setLength` now reset the cached body, which they did not before. + `SubminiTubeDatasheetTest` fails until the datasheet exists. +3. **Library**: done - 31 tubes, rendered in-line, circular, folded and rod in several orientations + and label modes; whole reactor passes; `catalog_full.json` regenerated; `pinout-drivers.txt` + updated (Top Lead now drives lead names); release notes in 6.10.