* Update update.xml

* Update update.xml

* warning on unknown op

* Update AiEditScriptEditorTest.java

* 'AI Circuit Analyzer' offers to log in instead of just showing the error

* Update update.xml

* include bounds, selection to ai project

* Update update.xml
This commit is contained in:
Bane Stojkovic
2026-10-30 09:50:20 +01:00
committed by GitHub
parent 518ff921d3
commit bc64cdd0be
24 changed files with 394 additions and 80 deletions
@@ -0,0 +1,31 @@
/*
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.plugins.chatbot.model;
import java.math.BigDecimal;
/**
* The rectangle a component occupies on the canvas, in grid units like {@link AiPoint}.
*/
public record AiBounds(BigDecimal gridX, BigDecimal gridY, BigDecimal gridWidth,
BigDecimal gridHeight) {
}
@@ -26,7 +26,9 @@ import java.util.Map;
/**
* @param properties current values of the editable properties
* @param bounds where the component is drawn, the only position given for components without
* terminals, such as boards and labels
*/
public record AiComponent(String id, String type, String value, Map<String, String> properties,
List<AiTerminal> terminals) {
AiBounds bounds, List<AiTerminal> terminals) {
}
@@ -4,8 +4,14 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @param selection ids of the components the user has selected, which a request such as "change
* this resistor" refers to
* @param components in z-order, the last one is drawn on top
*/
public record AiProject(Map<String, String> metadata,
List<String> tags,
List<String> selection,
List<AiComponent> components,
Map<String, Set<String>> nets,
List<AiSwitch> switches) {
@@ -3,5 +3,9 @@ package org.diylc.plugins.chatbot.model;
import java.util.List;
import java.util.Set;
/**
* @param connectedTerminalGroups terminals the switch ties together in this position, each group
* holding every terminal of one connection
*/
public record AiSwitchPosition(String name, List<Set<Integer>> connectedTerminalGroups) {
}
@@ -130,8 +130,15 @@ public class AiEditScriptEditor implements IProjectEditor {
}
private void apply(AiEditOperation op, IAiEditHandler handler, AiEditContext context) {
// an action this version does not know is skipped, as it always has been
// the service may be newer than this build and propose actions it does not know yet; those
// are skipped so that the rest of the script still applies
if (handler == null) {
if (op.getAction() != null) {
warnings.add("Operation '" + op.getAction() + "'"
+ (op.getComponentName() != null ? " on " + op.getComponentName() : "")
+ " is not supported by this version of DIYLC and was skipped."
+ " Please upgrade to the latest version.");
}
return;
}
try {
@@ -23,10 +23,12 @@ package org.diylc.plugins.chatbot.service;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.log4j.Logger;
@@ -37,6 +39,7 @@ import org.diylc.core.IContinuity;
import org.diylc.core.IDIYComponent;
import org.diylc.core.ISwitch;
import org.diylc.core.Project;
import org.diylc.core.measures.AbstractMeasure;
import org.diylc.netlist.Group;
import org.diylc.netlist.Netlist;
import org.diylc.netlist.NetlistBuilder;
@@ -52,7 +55,14 @@ public class AiProjectBuilder {
public static final String ID_SEPARATOR = "#";
private static final int MIN_ID_SUFFIX_LENGTH = 4;
public static AiProject build(Project project, List<ContinuityArea> continuityAreas) {
/**
* @param drawnBounds the outline each component was last drawn with, or null for one that has not
* been drawn
* @param selection components the user has selected
*/
public static AiProject build(Project project, List<ContinuityArea> continuityAreas,
Function<IDIYComponent<?>, Rectangle2D> drawnBounds,
Collection<IDIYComponent<?>> selection) {
Map<String, String> metadata = getMetadata(project);
List<String> tags = getTags(project);
@@ -67,7 +77,14 @@ public class AiProjectBuilder {
double gridSpacingPx = project.getGridSpacing().convertToPixels();
List<AiComponent> components = project.getComponents().stream()
.map(c -> mapComponent(c, ids.get(c), gridSpacingPx, contacts, wireEndNets))
.map(c -> mapComponent(c, ids.get(c), gridSpacingPx, contacts, wireEndNets,
drawnBounds.apply(c)))
.toList();
Set<IDIYComponent<?>> selected = new HashSet<>(selection);
List<String> selectedIds = project.getComponents().stream()
.filter(selected::contains)
.map(ids::get)
.toList();
List<AiSwitch> switches =
@@ -76,7 +93,8 @@ public class AiProjectBuilder {
.map(x -> (ISwitch) x)
.map(sw -> mapSwitch(sw, ids)).toList();
return new AiProject(metadata, tags, components, nets, switches);
return new AiProject(metadata, tags, selectedIds.isEmpty() ? null : selectedIds, components,
nets, switches);
}
/**
@@ -134,9 +152,9 @@ public class AiProjectBuilder {
if (project.getTitle() != null) metadata.put("title", project.getTitle());
if (project.getAuthor() != null) metadata.put("author", project.getAuthor());
if (project.getDescription() != null) metadata.put("description", project.getDescription());
metadata.put("gridSpacing", project.getGridSpacing().toString());
metadata.put("width", project.getWidth().toString());
metadata.put("height", project.getHeight().toString());
metadata.put("gridSpacing", ComponentCatalogGenerator.formatMeasure(project.getGridSpacing()));
metadata.put("width", ComponentCatalogGenerator.formatMeasure(project.getWidth()));
metadata.put("height", ComponentCatalogGenerator.formatMeasure(project.getHeight()));
return metadata;
}
@@ -313,21 +331,52 @@ public class AiProjectBuilder {
List<AiSwitchPosition> positions = new ArrayList<>();
IDIYComponent<?> c = (IDIYComponent<?>) sw;
for (int p = 0; p < sw.getPositionCount(); p++) {
List<Set<Integer>> connections = new ArrayList<>();
for (int i = 0; i < c.getControlPointCount() - 1; i++) {
for (int j = i + 1; j < c.getControlPointCount(); j++) {
if (sw.arePointsConnected(i, j, p)) {
connections.add(Set.of(i, j));
}
}
}
positions.add(new AiSwitchPosition(sw.getPositionName(p), connections));
positions.add(new AiSwitchPosition(sw.getPositionName(p),
getConnectedGroups(sw, c.getControlPointCount(), p)));
}
return new AiSwitch(ids.get(c), positions);
}
/**
* Terminals that are connected in the given position, gathered into groups rather than listed in
* pairs, which grow with the square of the terminals that a pole ties together.
*/
private static List<Set<Integer>> getConnectedGroups(ISwitch sw, int pointCount, int position) {
List<Set<Integer>> groups = new ArrayList<>();
Set<Integer> grouped = new HashSet<>();
for (int i = 0; i < pointCount; i++) {
if (grouped.contains(i)) {
continue;
}
Set<Integer> group = new TreeSet<>();
Deque<Integer> pending = new ArrayDeque<>();
pending.push(i);
while (!pending.isEmpty()) {
int point = pending.pop();
if (!group.add(point)) {
continue;
}
for (int j = 0; j < pointCount; j++) {
if (!group.contains(j)
&& sw.arePointsConnected(Math.min(point, j), Math.max(point, j), position)) {
pending.push(j);
}
}
}
grouped.addAll(group);
if (group.size() > 1) {
groups.add(group);
}
}
return groups;
}
/**
* @param drawnBounds the outline the component was last drawn with, or null when it has not been
* drawn
*/
static AiComponent mapComponent(IDIYComponent<?> component, String id, double gridSpacingPx,
Map<Node, List<String>> contacts, Map<Node, String> wireEndNets) {
Map<Node, List<String>> contacts, Map<Node, String> wireEndNets, Rectangle2D drawnBounds) {
boolean connectivity = isConnectivity(component);
List<AiTerminal> terminals = new ArrayList<>();
@@ -347,10 +396,15 @@ public class AiProjectBuilder {
}
String compType = component.getClass().getCanonicalName().replace("org.diylc.components.", "");
Object rawValue = component.getValue();
String value = rawValue instanceof AbstractMeasure<?> measure
? ComponentCatalogGenerator.formatMeasure(measure)
: rawValue == null ? null : rawValue.toString();
Map<String, String> properties = getProperties(component);
return new AiComponent(id, compType,
component.getValue() == null ? null : component.getValue().toString(),
value == null || value.isEmpty() ? null : value,
properties.isEmpty() ? null : properties,
createAiBounds(component, drawnBounds, gridSpacingPx),
terminals.isEmpty() ? null : terminals);
}
@@ -379,11 +433,38 @@ public class AiProjectBuilder {
return result;
}
private static AiPoint createAiPoint(Point2D controlPoint, double gridSpacingPx) {
/**
* Bounds from the last repaint, which cover everything the component draws, such as a body that
* reaches past its leads or the text of a label, where its control points alone do not. A
* component that has not been drawn yet falls back to the rectangle around its control points.
*/
private static AiBounds createAiBounds(IDIYComponent<?> component, Rectangle2D drawnBounds,
double gridSpacingPx) {
Rectangle2D bounds = drawnBounds;
if (bounds == null) {
for (int i = 0; i < component.getControlPointCount(); i++) {
Point2D point = component.getControlPoint(i);
if (bounds == null) {
bounds = new Rectangle2D.Double(point.getX(), point.getY(), 0, 0);
} else {
bounds.add(point);
}
}
}
if (bounds == null) {
return null;
}
return new AiBounds(toGrid(bounds.getX(), gridSpacingPx), toGrid(bounds.getY(), gridSpacingPx),
toGrid(bounds.getWidth(), gridSpacingPx), toGrid(bounds.getHeight(), gridSpacingPx));
}
BigDecimal gridX = BigDecimal.valueOf(controlPoint.getX() / gridSpacingPx).setScale(2, RoundingMode.HALF_UP);
BigDecimal gridY = BigDecimal.valueOf(controlPoint.getY() / gridSpacingPx).setScale(2, RoundingMode.HALF_UP);
return new AiPoint(gridX, gridY);
private static AiPoint createAiPoint(Point2D controlPoint, double gridSpacingPx) {
return new AiPoint(toGrid(controlPoint.getX(), gridSpacingPx),
toGrid(controlPoint.getY(), gridSpacingPx));
}
private static BigDecimal toGrid(double px, double gridSpacingPx) {
return BigDecimal.valueOf(px / gridSpacingPx).setScale(2, RoundingMode.HALF_UP);
}
static String mapTerminal(Node terminalRef, Map<IDIYComponent<?>, String> ids) {
@@ -40,8 +40,10 @@ import org.diylc.plugins.chatbot.model.*;
import org.diylc.plugins.cloud.model.IServiceAPI;
import org.diylc.plugins.cloud.service.CloudException;
import org.diylc.plugins.cloud.service.NotLoggedInException;
import org.diylc.presenter.ComponentArea;
import org.diylc.presenter.ComponentProcessor;
import org.diylc.presenter.ContinuityArea;
import org.diylc.presenter.DrawingManager;
import org.diylc.utils.FileUtils;
import org.diylc.utils.ReflectionUtils;
@@ -309,8 +311,12 @@ public class ChatbotService {
if (plugInPort.getCurrentProject().getComponents().isEmpty()) {
return null;
}
List<ContinuityArea> continuityAreas = plugInPort.getDrawingManager().getContinuityAreas();
return AiProjectBuilder.build(plugInPort.getCurrentProject(), continuityAreas);
DrawingManager drawingManager = plugInPort.getDrawingManager();
List<ContinuityArea> continuityAreas = drawingManager.getContinuityAreas();
return AiProjectBuilder.build(plugInPort.getCurrentProject(), continuityAreas, component -> {
ComponentArea area = drawingManager.getComponentArea(component);
return area == null ? null : area.getOutlineBounds();
}, plugInPort.getSelectedComponents());
}
public SubscriptionEntity getSubscriptionInfo() throws NotLoggedInException {
@@ -350,7 +350,7 @@ public class ComponentCatalogGenerator {
// Measures - format with unit
if (value instanceof AbstractMeasure) {
return value.toString();
return formatMeasure((AbstractMeasure<?>) value);
}
// Enums
@@ -369,4 +369,18 @@ public class ComponentCatalogGenerator {
return null;
}
/**
* Formats a measure the way {@link AbstractMeasure#toString()} does, but always with a decimal
* point, where the measure's own format follows the locale and writes a decimal comma in some.
*/
static String formatMeasure(AbstractMeasure<?> measure) {
if (measure.getValue() == null) {
return null;
}
// four decimals rounded half-even, like the "0.####" DecimalFormat the measure uses
return java.math.BigDecimal.valueOf(measure.getValue())
.setScale(4, java.math.RoundingMode.HALF_EVEN).stripTrailingZeros().toPlainString()
+ measure.getUnit();
}
}
+44 -40
View File
@@ -664,51 +664,19 @@
</versionNumber>
<releaseDate>2026-10-17 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Rotating and mirroring the selection with 'Sticky Points' enabled now keeps the circuit connected, the same way moving it does - wires and leads stuck to the selection stay attached to their pins, and other rigid components stuck to it are rotated or mirrored along with it</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved sorting of switching combinations in 'Analyze Guitar Diagrams' tool</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed Guitar Analyzer logic that merges switching configuration that produce the same outcome; in some cases it would remove switches from the list when it shouldn't</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Gerber export on computers set to a language that uses a decimal comma (e.g. German, French, Serbian) produced files in which drill holes, pads and traces had the wrong size - a 0.8mm hole came out as 0mm and traces and the board outline had no width, so the files could not be used for fabrication</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Zooming out until the drawing is smaller than the window made the toolbar above the canvas grow taller and squeeze the canvas; the toolbar now keeps its height at every zoom level</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed a bug that makes it impossible to clear a property with dynamic values, e.g. 'Selected Position' of a switch</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed a bug that makes 'Selected Position' list sometimes shows wrong values</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
<org.diylc.appframework.update.Version>
<versionNumber>
<major>6</major>
<minor>10</minor>
<build>0</build>
</versionNumber>
<releaseDate>2026-10-19 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>AI Assistant can actively participate in circuit design, make changes to the project and designs circuits from scratch</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 6PDT and 6PDT On/Off/On toggle switch types</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Rotating and mirroring the selection with 'Sticky Points' enabled now keeps the circuit connected, the same way moving it does - wires and leads stuck to the selection stay attached to their pins, and other rigid components stuck to it are rotated or mirrored along with it</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Allow active position selection for 'Rotary Switch Sealed', 'DIP Switch', 'Latching Switch Symbol', 'LP Switch', 'S1 Switch', allowing for more accurate circuit analysis and debugging</description>
@@ -729,6 +697,22 @@
<changeType>IMPROVEMENT</changeType>
<description>'IEC Socket', 'Plastic DC Jack', 'Multi-Section Capacitor' and 'Mini Relay' terminals are now labeled by function (Earth, Line, Neutral; Sleeve, Switch, Center; the shared negative and each section's positive; coil, COM, NC and NO) instead of plain numbers, so netlists show what each terminal does</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved quality of output from the 'AI Circuit Analyzer'</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'AI Circuit Analyzer' offers to log in instead of just showing the error</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'AI Assistant' is now aware of what is selected so natural language prompts can refer to selected components</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved sorting of switching combinations in 'Analyze Guitar Diagrams' tool</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Single coil pickups rotated by 90 or 270 degrees had their start and finish lugs swapped relative to the pickup body</description>
@@ -737,6 +721,26 @@
<changeType>BUG_FIX</changeType>
<description>'Analyze Guitar Diagrams' ignored 'Stingray Bass Pickup', so a diagram built around it was reported as having no pickups connected to the output jack, and its coils were left out of the hum-cancelling and phase checks</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed 'Analyze Guitar Diagrams' logic that merges switching configuration that produce the same outcome; in some cases it would remove switches from the list when it shouldn't</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Gerber export on computers set to a language that uses a decimal comma (e.g. German, French, Serbian) produced files in which drill holes, pads and traces had the wrong size - a 0.8mm hole came out as 0mm and traces and the board outline had no width, so the files could not be used for fabrication</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Zooming out until the drawing is smaller than the window made the toolbar above the canvas grow taller and squeeze the canvas; the toolbar now keeps its height at every zoom level</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed a bug that makes it impossible to clear a property with dynamic values, e.g. 'Selected Position' of a switch</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed a bug that makes 'Selected Position' list sometimes shows wrong values</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
@@ -27,6 +27,7 @@ import static org.junit.Assert.assertTrue;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Point2D;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -162,6 +163,35 @@ public class AiEditScriptEditorTest {
assertTrue(editor.getWarnings().isEmpty());
}
@Test
public void testUnsupportedOperationIsSkippedWithWarning() {
DummyComponent comp = new DummyComponent();
comp.setName("R1");
project.getComponents().add(comp);
AiEditOperation unsupported = new AiEditOperation();
unsupported.setAction("teleport");
unsupported.setComponentName("R1");
AiEditOperation remove = new AiEditOperation();
remove.setAction("remove");
remove.setComponentName("R1");
AiEditScript script = new AiEditScript();
script.setExplanation("Teleport and remove R1");
script.setOperations(Arrays.asList(unsupported, remove));
AiEditScriptEditor editor = new AiEditScriptEditor(script);
editor.edit(project, new HashSet<>());
assertTrue(project.getComponents().isEmpty());
assertEquals(1, editor.getWarnings().size());
String warning = editor.getWarnings().get(0);
assertTrue(warning.contains("'teleport' on R1"));
assertTrue(warning.contains("not supported"));
assertTrue(warning.contains("upgrade"));
}
@Test
public void testModifyComponent() {
DummyComponent comp = new DummyComponent();