From bc64cdd0be57dd74ba9f71c51f0338cbc27831f3 Mon Sep 17 00:00:00 2001 From: Bane Stojkovic Date: Fri, 30 Oct 2026 09:50:20 +0100 Subject: [PATCH] Ai edit (#1099) * 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 --- .../diylc/plugins/chatbot/model/AiBounds.java | 31 +++++ .../plugins/chatbot/model/AiComponent.java | 4 +- .../plugins/chatbot/model/AiProject.java | 6 + .../chatbot/model/AiSwitchPosition.java | 4 + .../chatbot/service/AiEditScriptEditor.java | 9 +- .../chatbot/service/AiProjectBuilder.java | 123 +++++++++++++++--- .../chatbot/service/ChatbotService.java | 10 +- .../service/ComponentCatalogGenerator.java | 16 ++- .../diylc-core/src/main/resources/update.xml | 84 ++++++------ .../service/AiEditScriptEditorTest.java | 30 +++++ .../chatbot/service/AiProjectBuilderTest.java | 94 ++++++++++++- .../service/ProjectFingerprintTest.java | 5 +- .../actions/analyze/AiAnalyzeAction.java | 22 ++++ .../plugins/cloud/actions/LoginAction.java | 26 ++-- .../src/main/resources/lang/Deutsch.txt | 1 + .../src/main/resources/lang/English.txt | 1 + .../src/main/resources/lang/Español.txt | 1 + .../src/main/resources/lang/Français.txt | 1 + .../src/main/resources/lang/Italiano.txt | 1 + .../src/main/resources/lang/Nederlands.txt | 1 + .../src/main/resources/lang/Português.txt | 1 + .../src/main/resources/lang/Srpski.txt | 1 + .../src/main/resources/lang/हिंदी.txt | 1 + .../src/main/resources/lang/简体中文.txt | 1 + 24 files changed, 394 insertions(+), 80 deletions(-) create mode 100644 diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiBounds.java 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 账户。是否立即登录?