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

This commit is contained in:
Bane Stojkovic
2026-10-31 20:21:04 +01:00
parent 5d92812d3e
commit 15277452f4
12 changed files with 690 additions and 1512 deletions
@@ -37,6 +37,7 @@ import org.diylc.components.AbstractLeadedComponent;
import org.diylc.components.transform.SimpleComponentTransformer;
import org.diylc.core.CreationMethod;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDatasheetSupport;
import org.diylc.core.annotations.AiNote;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
@@ -48,10 +49,13 @@ import org.diylc.core.measures.SizeUnit;
@AiNote("The two control points are the holes the leads go into and the body is centred between them; 'Length' is the body length, not the hole spacing")
@AiNote("If the control points are closer together than 'Length' (0.5in by default) the part is drawn standing, its body over the first point; to keep it lying on a tighter footprint, reduce 'Length'")
@AiNote("A datasheet model sets the body size, shape and colors of a resistor family at one power rating but never the resistance; set 'Power Rating' together with the model to pick the matching size")
@ComponentDescriptor(name = "Resistor", author = "Branislav Stojkovic", category = "Passive",
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "R", description = "Resistor layout symbol",
zOrder = IDIYComponent.COMPONENT, transformer = SimpleComponentTransformer.class, enableCache = true)
public class Resistor extends AbstractLeadedComponent<Resistance> implements IGerberComponentSimple {
zOrder = IDIYComponent.COMPONENT, transformer = SimpleComponentTransformer.class, enableCache = true,
enableDatasheet = true, datasheetCreationStepCount = 2)
public class Resistor extends AbstractLeadedComponent<Resistance>
implements IGerberComponentSimple, IDatasheetSupport {
private static final long serialVersionUID = 1L;
@@ -75,6 +79,41 @@ public class Resistor extends AbstractLeadedComponent<Resistance> implements IGe
this.borderColor = BORDER_COLOR;
}
public Resistor(String[] model) {
this();
applyModel(model);
}
// columns: family, power, body length, body width, shape, color code, body color, border color;
// the resistance is not part of a row because the body size depends only on family and power
@Override
public void applyModel(String[] model) {
setPowerNew(org.diylc.core.measures.Power.parseCapacitance(model[1]));
setLength(parseDatasheetSize(model[2]));
setWidth(parseDatasheetSize(model[3]));
setShape(parseDatasheetEnum(ResistorShape.values(), model[4]));
setColorCode(parseDatasheetEnum(ResistorColorCode.values(), model[5]));
setBodyColor(Color.decode(model[6]));
setBorderColor(Color.decode(model[7]));
}
// sizes are in mm unless a unit follows the number, as some families are specified in inches
private static Size parseDatasheetSize(String value) {
String[] parts = value.split(" ");
SizeUnit unit = parts.length > 1 ? SizeUnit.valueOf(parts[1]) : SizeUnit.mm;
return new Size(Double.parseDouble(parts[0]), unit);
}
private static <E extends Enum<E>> E parseDatasheetEnum(E[] values, String name) {
for (E value : values) {
if (value.toString().equalsIgnoreCase(name)) {
return value;
}
}
throw new IllegalArgumentException("Unknown datasheet value: " + name);
}
@Override
protected boolean supportsStandingMode() {
return true;
@@ -0,0 +1,22 @@
Carbon Comp,0.25 W,0.3 in,0.1 in,Tubular,5 band,#8A5E20,#735832
Carbon Comp,0.5 W,0.41 in,0.15 in,Tubular,5 band,#8A5E20,#735832
Carbon Film,0.25 W,7,2.5,Standard,5 band,#E0C68B,#A59268
Carbon Film,0.5 W,9,3.2,Standard,5 band,#E0C68B,#A59268
Metal Film,0.25 W,7,2.5,Standard,5 band,#82CFFD,#5B90B1
Metal Film,0.5 W,9,3.2,Standard,5 band,#82CFFD,#5B90B1
Metal Oxide,1 W,9,3,Standard,5 band,#D2D7DB,#B2B7BB
Metal Oxide,2 W,12,4,Standard,5 band,#D2D7DB,#B2B7BB
Metal Oxide,3 W,16,6,Standard,5 band,#D2D7DB,#B2B7BB
Ceramic,2 W,18,7,Tubular,None,#F6F5F5,#CCCCCC
Ceramic,3 W,22,8,Tubular,None,#F6F5F5,#CCCCCC
Ceramic,5 W,22,9.5,Tubular,None,#F6F5F5,#CCCCCC
Ceramic,7 W,35,9.5,Tubular,None,#F6F5F5,#CCCCCC
Ceramic,10 W,48,9.5,Tubular,None,#F6F5F5,#CCCCCC
Ceramic,15 W,48,13,Tubular,None,#F6F5F5,#CCCCCC
Ceramic,20 W,63,13,Tubular,None,#F6F5F5,#CCCCCC
Ceramic,30 W,75,19,Tubular,None,#F6F5F5,#CCCCCC
Ceramic,50 W,90,19,Tubular,None,#F6F5F5,#CCCCCC
Ceramic,80 W,109,19,Tubular,None,#F6F5F5,#CCCCCC
Ceramic,100 W,135,22,Tubular,None,#F6F5F5,#CCCCCC
Dale CMF55,0.5 W,6.22,2.29,Tubular Beveled,5 band,#D37D5B,#A04C2B
Dale CMF60,1 W,8.75,3.53,Tubular Beveled,5 band,#D37D5B,#A04C2B
@@ -0,0 +1,196 @@
/*
DIY Layout Creator (DIYLC).
Copyright (c) 2009-2025 held jointly by the individual authors.
This file is part of DIYLC.
DIYLC is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
DIYLC is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.components;
import java.awt.geom.Point2D;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.appframework.miscutils.IConfigListener;
import org.diylc.appframework.miscutils.IConfigurationManager;
import org.diylc.common.IPlugInPort;
import org.diylc.core.Template;
import org.diylc.presenter.VariantManager;
import org.diylc.serialization.ProjectFileManager;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Checks that a factory variant marked {@code deletedOn} in {@code import-defaults/variants.xml} is
* taken out of the user's configuration only when the last import predates the deletion, and is
* never imported.
*/
public class DeletedVariantsTest {
private static final String RESISTOR = "org.diylc.components.passive.Resistor";
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
@SuppressWarnings("unchecked")
private static Map<String, List<Template>> loadVariants() {
InputStream in = DeletedVariantsTest.class.getClassLoader()
.getResourceAsStream("import-defaults/variants.xml");
Assert.assertNotNull("variants.xml is not on the classpath", in);
return (Map<String, List<Template>>) ProjectFileManager.xStreamSerializer.fromXML(in);
}
private static Template deletedResistor() {
return loadVariants().get(RESISTOR).stream().filter(t -> t.getDeletedOn() != null)
.findFirst().get();
}
private static InMemoryConfigurationManager configWith(List<Template> userResistors,
LocalDateTime importDate) {
Map<String, List<Template>> variantMap = new HashMap<String, List<Template>>();
variantMap.put(RESISTOR, userResistors);
InMemoryConfigurationManager config = new InMemoryConfigurationManager();
config.writeValue(IPlugInPort.TEMPLATES_KEY, variantMap);
config.writeValue(IPlugInPort.DEFAULT_TEMPLATES_IMPORT_DATE_KEY, importDate);
return config;
}
@SuppressWarnings("unchecked")
private static List<Template> importInto(InMemoryConfigurationManager config) {
new VariantManager(config, null).importDefaultVariants();
return ((Map<String, List<Template>>) config.readObject(IPlugInPort.TEMPLATES_KEY, null))
.get(RESISTOR);
}
private static Template userVariant(String name) {
List<Point2D> points = new ArrayList<Point2D>();
points.add(new Point2D.Double(0, 0));
return new Template(name, new HashMap<String, Object>(), points);
}
@Test
public void deletedVariantsCarryOnlyTheirName() {
for (Map.Entry<String, List<Template>> entry : loadVariants().entrySet()) {
for (Template variant : entry.getValue()) {
if (variant.getDeletedOn() != null) {
String where = entry.getKey() + " / " + variant.getName();
Assert.assertNotNull(where, variant.getName());
Assert.assertNull(where, variant.getValues());
Assert.assertNull(where, variant.getPoints());
}
}
}
}
@Test
public void deletedWhenTheLastImportWasEarlier() {
Template deleted = deletedResistor();
Template own = userVariant("My Resistor");
List<Template> userResistors = new ArrayList<Template>();
userResistors.add(userVariant(deleted.getName()));
userResistors.add(own);
List<Template> remaining =
importInto(configWith(userResistors, deleted.getDeletedOn().minusDays(1)));
Assert.assertEquals(1, remaining.size());
Assert.assertSame(own, remaining.get(0));
}
@Test
public void recreatedByTheUserAfterTheDeletionIsKept() {
Template deleted = deletedResistor();
List<Template> userResistors = new ArrayList<Template>();
userResistors.add(userVariant(deleted.getName()));
List<Template> remaining =
importInto(configWith(userResistors, deleted.getDeletedOn().plusDays(1)));
Assert.assertEquals(1, remaining.size());
Assert.assertEquals(deleted.getName(), remaining.get(0).getName());
}
@Test
public void neverImported() {
List<Template> remaining =
importInto(configWith(new ArrayList<Template>(), LocalDateTime.MIN));
Assert.assertTrue(remaining.stream().noneMatch(t -> t.getDeletedOn() != null));
}
private static class InMemoryConfigurationManager implements IConfigurationManager<Object> {
private final Map<String, Object> values = new HashMap<String, Object>();
@Override
public void addConfigListener(String key, IConfigListener listener) {}
@Override
public boolean readBoolean(String key, boolean defaultValue) {
return (Boolean) values.getOrDefault(key, defaultValue);
}
@Override
public String readString(String key, String defaultValue) {
return (String) values.getOrDefault(key, defaultValue);
}
@Override
public int readInt(String key, int defaultValue) {
return (Integer) values.getOrDefault(key, defaultValue);
}
@Override
public float readFloat(String key, float defaultValue) {
return (Float) values.getOrDefault(key, defaultValue);
}
@Override
public double readDouble(String key, double defaultValue) {
return (Double) values.getOrDefault(key, defaultValue);
}
@Override
public Object readObject(String key, Object defaultValue) {
return values.getOrDefault(key, defaultValue);
}
@Override
public void writeValue(String key, Object value) {
values.put(key, value);
}
@Override
public Object getSerializer() {
return null;
}
@Override
public void initialize(String appName) {}
}
}
@@ -93,6 +93,9 @@ public class FactoryVariantsTest {
for (Template variant : entry.getValue()) {
String where = type.getSimpleName() + " / " + variant.getName();
if (variant.getDeletedOn() != null) {
continue;
}
if (variant.getValues() == null || variant.getValues().isEmpty()) {
problems.add(where + ": sets nothing");
continue;
@@ -0,0 +1,88 @@
/*
DIY Layout Creator (DIYLC).
Copyright (c) 2009-2025 held jointly by the individual authors.
This file is part of DIYLC.
DIYLC is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
DIYLC is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.components.passive;
import java.awt.Color;
import java.util.List;
import org.diylc.common.ResistorColorCode;
import org.diylc.components.passive.Resistor.ResistorShape;
import org.diylc.core.measures.Power;
import org.diylc.core.measures.PowerUnit;
import org.diylc.core.measures.Resistance;
import org.diylc.core.measures.ResistanceUnit;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.presenter.DatasheetService;
import org.junit.Assert;
import org.junit.Test;
public class ResistorDatasheetTest {
@Test
public void everyRowApplies() {
List<String[]> datasheet = DatasheetService.getInstance().loadDatasheet(Resistor.class);
Assert.assertNotNull(datasheet);
Assert.assertFalse(datasheet.isEmpty());
for (String[] row : datasheet) {
Resistor resistor = new Resistor(row);
Assert.assertNotNull(String.join(",", row), resistor.getPowerNew());
Assert.assertNull("a row must not set the resistance", resistor.getValue());
}
}
@Test
public void appliesAllColumns() {
Resistor resistor = new Resistor(DatasheetService.getInstance().findSeries(Resistor.class,
"Ceramic").stream().filter(row -> row[1].equals("10 W")).findFirst().get());
Assert.assertEquals(new Power(10d, PowerUnit.W), resistor.getPowerNew());
Assert.assertEquals(new Size(48d, SizeUnit.mm), resistor.getLength());
Assert.assertEquals(new Size(9.5d, SizeUnit.mm), resistor.getWidth());
Assert.assertEquals(ResistorShape.Tubular, resistor.getShape());
Assert.assertEquals(ResistorColorCode.NONE, resistor.getColorCode());
Assert.assertEquals(Color.decode("#F6F5F5"), resistor.getBodyColor());
}
@Test
public void sizeKeepsItsUnit() {
Resistor resistor = new Resistor(new String[] {"Carbon Comp", "0.5 W", "0.41 in", "0.15 in",
"Tubular", "5 band", "#8A5E20", "#735832"});
Assert.assertEquals(new Size(0.41d, SizeUnit.in), resistor.getLength());
}
@Test
public void modelFollowsPowerRatingAndKeepsValue() {
Resistor resistor = new Resistor();
resistor.setValue(new Resistance(10d, ResistanceUnit.K));
resistor.setPowerNew(new Power(0.5d, PowerUnit.W));
DatasheetService service = DatasheetService.getInstance();
String[] model =
service.findModel(resistor, service.findSeries(Resistor.class, "carbon comp"));
Assert.assertNotNull(model);
Assert.assertEquals("0.5 W", model[1]);
resistor.applyModel(model);
Assert.assertEquals(new Resistance(10d, ResistanceUnit.K), resistor.getValue());
Assert.assertEquals(new Size(0.41d, SizeUnit.in), resistor.getLength());
}
}