diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiBounds.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiBounds.java
new file mode 100644
index 00000000..fec13683
--- /dev/null
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiBounds.java
@@ -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 .
+
+*/
+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) {
+}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiComponent.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiComponent.java
index 678287cd..0d49d636 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiComponent.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiComponent.java
@@ -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 properties,
- List terminals) {
+ AiBounds bounds, List terminals) {
}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiProject.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiProject.java
index 27343255..09d20716 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiProject.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiProject.java
@@ -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 metadata,
List tags,
+ List selection,
List components,
Map> nets,
List switches) {
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiSwitchPosition.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiSwitchPosition.java
index 8eb0027b..be588474 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiSwitchPosition.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiSwitchPosition.java
@@ -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> connectedTerminalGroups) {
}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/AiEditScriptEditor.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/AiEditScriptEditor.java
index 5c436004..c702540f 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/AiEditScriptEditor.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/AiEditScriptEditor.java
@@ -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 {
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/AiProjectBuilder.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/AiProjectBuilder.java
index b08398d5..6159c79f 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/AiProjectBuilder.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/AiProjectBuilder.java
@@ -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 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 continuityAreas,
+ Function, Rectangle2D> drawnBounds,
+ Collection> selection) {
Map metadata = getMetadata(project);
List tags = getTags(project);
@@ -67,7 +77,14 @@ public class AiProjectBuilder {
double gridSpacingPx = project.getGridSpacing().convertToPixels();
List 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> selected = new HashSet<>(selection);
+ List selectedIds = project.getComponents().stream()
+ .filter(selected::contains)
+ .map(ids::get)
.toList();
List 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 positions = new ArrayList<>();
IDIYComponent> c = (IDIYComponent>) sw;
for (int p = 0; p < sw.getPositionCount(); p++) {
- List> 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> getConnectedGroups(ISwitch sw, int pointCount, int position) {
+ List> groups = new ArrayList<>();
+ Set grouped = new HashSet<>();
+ for (int i = 0; i < pointCount; i++) {
+ if (grouped.contains(i)) {
+ continue;
+ }
+ Set group = new TreeSet<>();
+ Deque 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> contacts, Map wireEndNets) {
+ Map> contacts, Map wireEndNets, Rectangle2D drawnBounds) {
boolean connectivity = isConnectivity(component);
List 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 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, String> ids) {
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/ChatbotService.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/ChatbotService.java
index 1813f2f6..8de50e7e 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/ChatbotService.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/ChatbotService.java
@@ -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 continuityAreas = plugInPort.getDrawingManager().getContinuityAreas();
- return AiProjectBuilder.build(plugInPort.getCurrentProject(), continuityAreas);
+ DrawingManager drawingManager = plugInPort.getDrawingManager();
+ List 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 {
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/ComponentCatalogGenerator.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/ComponentCatalogGenerator.java
index 5af83aec..69a70b42 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/ComponentCatalogGenerator.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/ComponentCatalogGenerator.java
@@ -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();
+ }
}
diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml
index 90098b29..6e5ae39f 100644
--- a/diylc/diylc-core/src/main/resources/update.xml
+++ b/diylc/diylc-core/src/main/resources/update.xml
@@ -664,51 +664,19 @@
2026-10-17 00:00:00.000 CET
-
-
- IMPROVEMENT
- 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
-
-
- IMPROVEMENT
- Improved sorting of switching combinations in 'Analyze Guitar Diagrams' tool
-
-
- BUG_FIX
- 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
-
-
- BUG_FIX
- 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
-
-
- BUG_FIX
- 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
-
-
- BUG_FIX
- Fixed a bug that makes it impossible to clear a property with dynamic values, e.g. 'Selected Position' of a switch
-
-
- BUG_FIX
- Fixed a bug that makes 'Selected Position' list sometimes shows wrong values
-
-
- https://github.com/bancika/diy-layout-creator/releases
-
-
-
- 6
- 10
- 0
-
- 2026-10-19 00:00:00.000 CET
-
NEW_FEATURE
AI Assistant can actively participate in circuit design, make changes to the project and designs circuits from scratch
+
+ NEW_FEATURE
+ Added 6PDT and 6PDT On/Off/On toggle switch types
+
+
+ IMPROVEMENT
+ 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
+
IMPROVEMENT
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
@@ -729,6 +697,22 @@
IMPROVEMENT
'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
+
+ IMPROVEMENT
+ Improved quality of output from the 'AI Circuit Analyzer'
+
+
+ IMPROVEMENT
+ 'AI Circuit Analyzer' offers to log in instead of just showing the error
+
+
+ IMPROVEMENT
+ 'AI Assistant' is now aware of what is selected so natural language prompts can refer to selected components
+
+
+ IMPROVEMENT
+ Improved sorting of switching combinations in 'Analyze Guitar Diagrams' tool
+
BUG_FIX
Single coil pickups rotated by 90 or 270 degrees had their start and finish lugs swapped relative to the pickup body
@@ -737,6 +721,26 @@
BUG_FIX
'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
+
+ BUG_FIX
+ 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
+
+
+ BUG_FIX
+ 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
+
+
+ BUG_FIX
+ 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
+
+
+ BUG_FIX
+ Fixed a bug that makes it impossible to clear a property with dynamic values, e.g. 'Selected Position' of a switch
+
+
+ BUG_FIX
+ Fixed a bug that makes 'Selected Position' list sometimes shows wrong values
+
https://github.com/bancika/diy-layout-creator/releases
diff --git a/diylc/diylc-core/src/test/java/org/diylc/plugins/chatbot/service/AiEditScriptEditorTest.java b/diylc/diylc-core/src/test/java/org/diylc/plugins/chatbot/service/AiEditScriptEditorTest.java
index 46bb629a..2145735e 100644
--- a/diylc/diylc-core/src/test/java/org/diylc/plugins/chatbot/service/AiEditScriptEditorTest.java
+++ b/diylc/diylc-core/src/test/java/org/diylc/plugins/chatbot/service/AiEditScriptEditorTest.java
@@ -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();
diff --git a/diylc/diylc-library/src/test/java/org/diylc/plugins/chatbot/service/AiProjectBuilderTest.java b/diylc/diylc-library/src/test/java/org/diylc/plugins/chatbot/service/AiProjectBuilderTest.java
index 024925b4..217b56aa 100644
--- a/diylc/diylc-library/src/test/java/org/diylc/plugins/chatbot/service/AiProjectBuilderTest.java
+++ b/diylc/diylc-library/src/test/java/org/diylc/plugins/chatbot/service/AiProjectBuilderTest.java
@@ -27,9 +27,12 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -37,18 +40,27 @@ import java.util.UUID;
import org.junit.Test;
import org.diylc.common.Orientation;
+import org.diylc.components.boards.PerfBoard;
import org.diylc.components.boards.TagStrip;
import org.diylc.components.connectivity.HookupWire;
import org.diylc.components.connectivity.SolderLug;
import org.diylc.components.connectivity.SolderPad;
+import org.diylc.components.guitar.LPSwitch;
import org.diylc.components.guitar.SingleCoilPickup;
import org.diylc.components.passive.Resistor;
+import org.diylc.components.semiconductors.TransistorTO92;
import org.diylc.core.IDIYComponent;
import org.diylc.core.Project;
+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.plugins.chatbot.model.AiBounds;
import org.diylc.plugins.chatbot.model.AiComponent;
import org.diylc.plugins.chatbot.model.AiEditOperation;
import org.diylc.plugins.chatbot.model.AiEditScript;
import org.diylc.plugins.chatbot.model.AiProject;
+import org.diylc.plugins.chatbot.model.AiSwitch;
import org.diylc.plugins.chatbot.model.AiTerminal;
/**
@@ -240,6 +252,86 @@ public class AiProjectBuilderTest {
assertEquals("_180", component(export, "Neck").properties().get("Orientation"));
}
+ @Test
+ public void measuresUseADecimalPointInAnyLocale() {
+ Locale locale = Locale.getDefault();
+ Locale.setDefault(Locale.GERMANY);
+ try {
+ Resistor resistor = resistor();
+ resistor.setValue(new Resistance(4.7d, ResistanceUnit.K));
+ PerfBoard board = new PerfBoard();
+ board.setName("Board1");
+ board.setSpacing(new Size(0.15d, SizeUnit.in));
+
+ AiProject export = export(resistor, board);
+
+ assertEquals("4.7K", component(export, "R1").value());
+ assertEquals("0.15in", component(export, "Board1").properties().get("Spacing"));
+ assertEquals("0.1in", export.metadata().get("gridSpacing"));
+ } finally {
+ Locale.setDefault(locale);
+ }
+ }
+
+ @Test
+ public void emptyValueIsLeftOut() {
+ TransistorTO92 transistor = new TransistorTO92();
+ transistor.setName("Q1");
+
+ assertNull(component(export(transistor), "Q1").value());
+ }
+
+ @Test
+ public void drawnBoundsAreExportedInGridUnits() {
+ PerfBoard board = new PerfBoard();
+ board.setName("Board1");
+ Project project = project(board);
+ double grid = project.getGridSpacing().convertToPixels();
+ Rectangle2D drawn = new Rectangle2D.Double(2 * grid, 3 * grid, 10 * grid, 5.5 * grid);
+
+ AiProject export = AiProjectBuilder.build(project, new ArrayList<>(),
+ c -> c == board ? drawn : null, List.of());
+
+ assertEquals(new AiBounds(new BigDecimal("2.00"), new BigDecimal("3.00"),
+ new BigDecimal("10.00"), new BigDecimal("5.50")), component(export, "Board1").bounds());
+ }
+
+ @Test
+ public void undrawnComponentIsBoundedByItsControlPoints() {
+ Project project = project(resistor());
+ double grid = project.getGridSpacing().convertToPixels();
+
+ AiBounds bounds = component(export(resistor()), "R1").bounds();
+
+ assertEquals(FIRST_LEAD.getX() / grid, bounds.gridX().doubleValue(), 0.01);
+ assertEquals((SECOND_LEAD.getX() - FIRST_LEAD.getX()) / grid,
+ bounds.gridWidth().doubleValue(), 0.01);
+ assertEquals(0, bounds.gridHeight().doubleValue(), 0.01);
+ }
+
+ @Test
+ public void selectionListsTheIdsOfSelectedComponents() {
+ HookupWire wire = wire("W1", SECOND_LEAD, LOOSE_END);
+ Project project = project(resistor(), wire);
+
+ assertEquals(List.of("W1"), AiProjectBuilder.build(project, new ArrayList<>(), c -> null,
+ List.of(wire)).selection());
+ assertNull(export(resistor()).selection());
+ }
+
+ @Test
+ public void switchTerminalsTiedTogetherFormOneGroup() {
+ LPSwitch lpSwitch = new LPSwitch();
+ lpSwitch.setName("SW1");
+
+ AiSwitch exported = export(lpSwitch).switches().get(0);
+
+ assertEquals(List.of(Set.of(1, 2)), exported.positions().get(0).connectedTerminalGroups());
+ assertEquals(List.of(Set.of(1, 2, 3)),
+ exported.positions().get(1).connectedTerminalGroups());
+ assertEquals(List.of(Set.of(2, 3)), exported.positions().get(2).connectedTerminalGroups());
+ }
+
@Test
public void identityAndCosmeticPropertiesAreLeftOut() {
Map properties = component(export(resistor()), "R1").properties();
@@ -267,7 +359,7 @@ public class AiProjectBuilderTest {
}
private static AiProject export(IDIYComponent>... components) {
- return AiProjectBuilder.build(project(components), new ArrayList<>());
+ return AiProjectBuilder.build(project(components), new ArrayList<>(), c -> null, List.of());
}
private static Project project(IDIYComponent>... components) {
diff --git a/diylc/diylc-library/src/test/java/org/diylc/plugins/chatbot/service/ProjectFingerprintTest.java b/diylc/diylc-library/src/test/java/org/diylc/plugins/chatbot/service/ProjectFingerprintTest.java
index 93735998..3e86df28 100644
--- a/diylc/diylc-library/src/test/java/org/diylc/plugins/chatbot/service/ProjectFingerprintTest.java
+++ b/diylc/diylc-library/src/test/java/org/diylc/plugins/chatbot/service/ProjectFingerprintTest.java
@@ -126,10 +126,11 @@ public class ProjectFingerprintTest {
}
private static String fingerprint(Project project) {
- return ChatbotService.fingerprint(AiProjectBuilder.build(project, new ArrayList<>()));
+ return ChatbotService.fingerprint(
+ AiProjectBuilder.build(project, new ArrayList<>(), c -> null, List.of()));
}
private static AiProject aiProject(Map> nets) {
- return new AiProject(Map.of(), List.of(), List.of(), nets, List.of());
+ return new AiProject(Map.of(), List.of(), null, List.of(), nets, List.of());
}
}
diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/analyze/AiAnalyzeAction.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/analyze/AiAnalyzeAction.java
index 2fbc3644..92b14fab 100644
--- a/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/analyze/AiAnalyzeAction.java
+++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/actions/analyze/AiAnalyzeAction.java
@@ -1,7 +1,10 @@
package org.diylc.swing.actions.analyze;
import org.diylc.common.IPlugInPort;
+import org.diylc.lang.LangUtil;
+import org.diylc.plugins.cloud.service.NotLoggedInException;
import org.diylc.swing.ISwingUI;
+import org.diylc.swing.plugins.cloud.actions.LoginAction;
import org.diylc.swingframework.TextDialog;
import org.diylc.utils.IconLoader;
@@ -55,6 +58,10 @@ public class AiAnalyzeAction extends AbstractAction {
@Override
public void actionPerformed(ActionEvent e) {
+ if (!plugInPort.getCloudService().isLoggedIn()) {
+ offerLogin(e);
+ return;
+ }
String message = WAITING_MESSAGES[(int) (Math.random() * WAITING_MESSAGES.length)];
JOptionPane optionPane = new JOptionPane(message,
@@ -86,6 +93,10 @@ public class AiAnalyzeAction extends AbstractAction {
new TextDialog(swingUI.getOwnerFrame().getRootPane(), res, "AI Circuit Analyzer",
new Dimension(800, 600)).setVisible(true);
} catch (Exception ex) {
+ if (ex.getCause() instanceof NotLoggedInException) {
+ offerLogin(e);
+ return;
+ }
String message =
Optional.ofNullable(ex.getCause()).map(Throwable::getMessage).orElse(ex.getMessage());
swingUI.showMessage(message, "AI Circuit Analyzer", ISwingUI.ERROR_MESSAGE);
@@ -100,4 +111,15 @@ public class AiAnalyzeAction extends AbstractAction {
worker.cancel(true);
}
}
+
+ private void offerLogin(ActionEvent e) {
+ int choice = swingUI.showConfirmDialog(
+ LangUtil.translate(
+ "AI Circuit Analyzer requires a DIYLC Cloud account. Would you like to log in now?"),
+ LangUtil.translate("AI Circuit Analyzer"), ISwingUI.YES_NO_OPTION,
+ ISwingUI.QUESTION_MESSAGE);
+ if (choice == ISwingUI.YES_OPTION && LoginAction.logIn(plugInPort, swingUI)) {
+ actionPerformed(e);
+ }
+ }
}
diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/cloud/actions/LoginAction.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/cloud/actions/LoginAction.java
index ca5c3b24..53ebc642 100644
--- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/cloud/actions/LoginAction.java
+++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/cloud/actions/LoginAction.java
@@ -23,6 +23,7 @@ package org.diylc.swing.plugins.cloud.actions;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
+import org.diylc.common.IPlugInPort;
import org.diylc.common.ITask;
import org.diylc.core.IView;
import org.diylc.plugins.cloud.service.CloudException;
@@ -46,27 +47,30 @@ public class LoginAction extends AbstractAction {
@Override
public void actionPerformed(ActionEvent e) {
+ if (logIn(cloudPlugIn.getPlugInPort(), cloudPlugIn.getSwingUI())) {
+ cloudPlugIn.loggedIn();
+ }
+ }
+
+ public static boolean logIn(IPlugInPort plugInPort, ISwingUI swingUI) {
LoginDialog dialog = DialogFactory.getInstance().createLoginDialog();
do {
dialog.setVisible(true);
if (ButtonDialog.OK.equals(dialog.getSelectedButtonCaption())) {
try {
- if (cloudPlugIn.getPlugInPort().getCloudService().logIn(dialog.getUserName(), dialog.getPassword())) {
- cloudPlugIn.getSwingUI().showMessage(
- "You have successfully logged into the system. You will remain logged in from this machine until logged out.",
- "Login Successful", IView.INFORMATION_MESSAGE);
- cloudPlugIn.loggedIn();
- break;
+ if (plugInPort.getCloudService().logIn(dialog.getUserName(), dialog.getPassword())) {
+ return true;
} else {
- cloudPlugIn.getSwingUI().showMessage(
+ swingUI.showMessage(
"Could not login. Possible reasons are wrong credentials or lack of internet connection.",
"Login Error", IView.ERROR_MESSAGE);
}
} catch (CloudException e1) {
- cloudPlugIn.getSwingUI().showMessage("Could not login. Error: " + e1.getMessage(), "Login Error", IView.ERROR_MESSAGE);
+ swingUI.showMessage("Could not login. Error: " + e1.getMessage(), "Login Error", IView.ERROR_MESSAGE);
}
- } else
- break;
+ } else {
+ return false;
+ }
} while (true);
}
-}
+}
diff --git a/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt b/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt
index 4872fbcc..80e5edef 100644
--- a/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt
+++ b/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt
@@ -713,3 +713,4 @@ Replace %s with %s|%s durch %s ersetzen
Reconnect %s to %s|%s mit %s neu verbinden
Editing your circuit:|Der Bearbeitung Ihrer Schaltung:
Generating new layouts:|Der Erstellung neuer Layouts:
+AI Circuit Analyzer requires a DIYLC Cloud account. Would you like to log in now?|Der KI-Schaltungsanalysator erfordert ein DIYLC-Cloud-Konto. Möchten Sie sich jetzt anmelden?
diff --git a/diylc/diylc-swing/src/main/resources/lang/English.txt b/diylc/diylc-swing/src/main/resources/lang/English.txt
index 7faf124a..98236dd4 100644
--- a/diylc/diylc-swing/src/main/resources/lang/English.txt
+++ b/diylc/diylc-swing/src/main/resources/lang/English.txt
@@ -706,3 +706,4 @@ Replace %s with %s
Reconnect %s to %s
Editing your circuit:
Generating new layouts:
+AI Circuit Analyzer requires a DIYLC Cloud account. Would you like to log in now?
diff --git a/diylc/diylc-swing/src/main/resources/lang/Español.txt b/diylc/diylc-swing/src/main/resources/lang/Español.txt
index 82218c6c..3179d263 100644
--- a/diylc/diylc-swing/src/main/resources/lang/Español.txt
+++ b/diylc/diylc-swing/src/main/resources/lang/Español.txt
@@ -709,3 +709,4 @@ Replace %s with %s|Reemplazar %s por %s
Reconnect %s to %s|Reconectar %s a %s
Editing your circuit:|Edición de su circuito:
Generating new layouts:|Generación de nuevos diseños:
+AI Circuit Analyzer requires a DIYLC Cloud account. Would you like to log in now?|El Analizador de Circuitos IA requiere una cuenta de DIYLC Cloud. ¿Desea iniciar sesión ahora?
diff --git a/diylc/diylc-swing/src/main/resources/lang/Français.txt b/diylc/diylc-swing/src/main/resources/lang/Français.txt
index 330c58dd..14d14f0b 100644
--- a/diylc/diylc-swing/src/main/resources/lang/Français.txt
+++ b/diylc/diylc-swing/src/main/resources/lang/Français.txt
@@ -708,3 +708,4 @@ Replace %s with %s|Remplacer %s par %s
Reconnect %s to %s|Reconnecter %s à %s
Editing your circuit:|La modification de votre circuit :
Generating new layouts:|La génération de nouvelles implantations :
+AI Circuit Analyzer requires a DIYLC Cloud account. Would you like to log in now?|L'analyseur de circuit IA nécessite un compte DIYLC Cloud. Voulez-vous vous connecter maintenant ?
diff --git a/diylc/diylc-swing/src/main/resources/lang/Italiano.txt b/diylc/diylc-swing/src/main/resources/lang/Italiano.txt
index 87a34ec8..bdfe58c3 100644
--- a/diylc/diylc-swing/src/main/resources/lang/Italiano.txt
+++ b/diylc/diylc-swing/src/main/resources/lang/Italiano.txt
@@ -710,3 +710,4 @@ Replace %s with %s|Sostituisci %s con %s
Reconnect %s to %s|Ricollega %s a %s
Editing your circuit:|Modifica del tuo circuito:
Generating new layouts:|Generazione di nuovi layout:
+AI Circuit Analyzer requires a DIYLC Cloud account. Would you like to log in now?|L'Analizzatore di Circuiti IA richiede un account DIYLC Cloud. Vuoi accedere ora?
diff --git a/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt b/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt
index 97e5e1af..81df420b 100644
--- a/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt
+++ b/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt
@@ -714,3 +714,4 @@ Replace %s with %s|%s vervangen door %s
Reconnect %s to %s|%s opnieuw verbinden met %s
Editing your circuit:|Uw schakeling bewerken:
Generating new layouts:|Nieuwe layouts genereren:
+AI Circuit Analyzer requires a DIYLC Cloud account. Would you like to log in now?|De AI Circuit Analysator vereist een DIYLC Cloud-account. Wilt u nu inloggen?
diff --git a/diylc/diylc-swing/src/main/resources/lang/Português.txt b/diylc/diylc-swing/src/main/resources/lang/Português.txt
index eaf95c4b..512c7f3b 100644
--- a/diylc/diylc-swing/src/main/resources/lang/Português.txt
+++ b/diylc/diylc-swing/src/main/resources/lang/Português.txt
@@ -709,3 +709,4 @@ Replace %s with %s|Substituir %s por %s
Reconnect %s to %s|Reconectar %s a %s
Editing your circuit:|Edição do seu circuito:
Generating new layouts:|Geração de novos layouts:
+AI Circuit Analyzer requires a DIYLC Cloud account. Would you like to log in now?|O Analisador de Circuito IA requer uma conta DIYLC Cloud. Deseja entrar agora?
diff --git a/diylc/diylc-swing/src/main/resources/lang/Srpski.txt b/diylc/diylc-swing/src/main/resources/lang/Srpski.txt
index 4ee85fc4..caef215b 100644
--- a/diylc/diylc-swing/src/main/resources/lang/Srpski.txt
+++ b/diylc/diylc-swing/src/main/resources/lang/Srpski.txt
@@ -705,3 +705,4 @@ Replace %s with %s|Zameni %s sa %s
Reconnect %s to %s|Ponovo poveži %s sa %s
Editing your circuit:|Izmenama vašeg kola:
Generating new layouts:|Generisanju novih rasporeda:
+AI Circuit Analyzer requires a DIYLC Cloud account. Would you like to log in now?|AI Analizator Kola zahteva DIYLC Cloud nalog. Da li želite da se prijavite sada?
diff --git a/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt b/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt
index 05c615d7..7d9b9abc 100644
--- a/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt
+++ b/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt
@@ -707,3 +707,4 @@ Replace %s with %s|%s को %s से बदलें
Reconnect %s to %s|%s को %s से फिर से जोड़ें
Editing your circuit:|आपके सर्किट को संपादित करना:
Generating new layouts:|नए लेआउट बनाना:
+AI Circuit Analyzer requires a DIYLC Cloud account. Would you like to log in now?|AI सर्किट विश्लेषक के लिए DIYLC Cloud खाते की आवश्यकता है। क्या आप अभी लॉग इन करना चाहेंगे?
diff --git a/diylc/diylc-swing/src/main/resources/lang/简体中文.txt b/diylc/diylc-swing/src/main/resources/lang/简体中文.txt
index 9505d80a..ff0553d6 100644
--- a/diylc/diylc-swing/src/main/resources/lang/简体中文.txt
+++ b/diylc/diylc-swing/src/main/resources/lang/简体中文.txt
@@ -707,3 +707,4 @@ Replace %s with %s|将 %s 替换为 %s
Reconnect %s to %s|将 %s 重新连接到 %s
Editing your circuit:|编辑您的电路:
Generating new layouts:|生成新的布局:
+AI Circuit Analyzer requires a DIYLC Cloud account. Would you like to log in now?|AI电路分析器需要 DIYLC Cloud 账户。是否立即登录?