Skip to content

Commit f151c14

Browse files
authored
Added Tool Library (#3093)
* "Pick from Library..." button added to the existing Tool Settings dialog. Picking a tool populates the fields and shows its name; editing a field away from the library value turns it amber until it matches again. The picker also has a "Manage Library..." button for adding/editing tools. * Tool Library manager — list on the left, editor on the right, with Add / Duplicate / Delete / Revert. Built-in tools can be edited and reverted, but not deleted. * 41 pre-seeded tools: Upcut, Downcut, Ball, Compression, Straight in 1/16", 1/8", 1/4", 1/2", 1" sizes; V-bits in 30°, 60°, 90° across the same sizes; plus a "Custom" entry. * Per-tool diameter units stick with the tool — 1/4" endmills always display as 0.25 in, 3 mm endmills always as 3 mm. Toggling the unit in the editor converts the value. * Feed / plunge / depth stay in machine units and now show unit labels (mm/min, mm) next to each field. * Library persists across projects at ~/.ugs/tool-library.json. * Project files remember which tool they were built with. On opening a project whose tool differs from your library, UGS prompts: use the project's tool for this session, update the library, or keep your library version. If the project's tool isn't in your library at all, it offers to import it. * Existing projects load unchanged — no prompt, no overwriting of your current tool settings.
1 parent 436e9d3 commit f151c14

29 files changed

Lines changed: 3222 additions & 33 deletions
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright 2026 Damian Nikodem
3+
4+
This file is part of Universal Gcode Sender (UGS).
5+
6+
UGS is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
UGS is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with UGS. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
package com.willwinder.ugs.designer.actions;
20+
21+
import com.willwinder.ugs.designer.gui.toollibrary.ToolLibraryDialog;
22+
import com.willwinder.ugs.designer.logic.Controller;
23+
import com.willwinder.universalgcodesender.uielements.DialogUtils;
24+
25+
import java.awt.Window;
26+
import java.awt.event.ActionEvent;
27+
import java.awt.event.ActionListener;
28+
29+
public class OpenToolLibraryAction implements ActionListener {
30+
private final Controller controller;
31+
32+
public OpenToolLibraryAction(Controller controller) {
33+
this.controller = controller;
34+
}
35+
36+
@Override
37+
public void actionPerformed(ActionEvent e) {
38+
Window parent = DialogUtils.getParentWindow(controller.getDrawing());
39+
ToolLibraryDialog.show(parent, controller.getSettings().getPreferredUnits());
40+
}
41+
}

ugs-designer/src/main/java/com/willwinder/ugs/designer/gui/ToolSettingsPanel.java

Lines changed: 173 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
Copyright 2021-2024 Will Winder
3+
Copyright 2026 Damian Nikodem
34
45
This file is part of Universal Gcode Sender (UGS).
56
@@ -18,120 +19,253 @@ This file is part of Universal Gcode Sender (UGS).
1819
*/
1920
package com.willwinder.ugs.designer.gui;
2021

22+
import com.willwinder.ugs.designer.gui.toollibrary.DeviationHighlighter;
23+
import com.willwinder.ugs.designer.gui.toollibrary.ToolLibraryPickerDialog;
2124
import com.willwinder.ugs.designer.logic.Controller;
25+
import com.willwinder.ugs.designer.logic.ToolLibraryService;
2226
import com.willwinder.ugs.designer.model.Settings;
27+
import com.willwinder.ugs.designer.model.toollibrary.ToolDefinition;
2328
import com.willwinder.universalgcodesender.Utils;
2429
import com.willwinder.universalgcodesender.model.Unit;
30+
import com.willwinder.universalgcodesender.model.UnitUtils;
31+
import com.willwinder.universalgcodesender.services.LookupService;
2532
import com.willwinder.universalgcodesender.uielements.TextFieldWithUnit;
2633
import net.miginfocom.swing.MigLayout;
2734

2835
import javax.swing.DefaultComboBoxModel;
36+
import javax.swing.JButton;
2937
import javax.swing.JCheckBox;
3038
import javax.swing.JComboBox;
3139
import javax.swing.JLabel;
3240
import javax.swing.JPanel;
3341
import javax.swing.JSeparator;
3442
import javax.swing.JTextField;
3543
import javax.swing.SwingConstants;
44+
import javax.swing.SwingUtilities;
3645
import java.awt.Dimension;
3746
import java.text.ParseException;
47+
import java.util.Optional;
3848

3949
/**
4050
* @author Joacim Breiler
4151
*/
4252
public class ToolSettingsPanel extends JPanel {
4353
public static final String TOOL_FIELD_CONSTRAINT = "grow, wrap";
4454
private final transient Controller controller;
45-
private JTextField toolDiameter;
46-
private JTextField feedSpeed;
47-
private JTextField plungeSpeed;
48-
private JTextField depthPerPass;
49-
private JTextField stepOver;
55+
private final transient ToolLibraryService libraryService;
56+
57+
private JButton pickFromLibraryButton;
58+
private JLabel selectedToolLabel;
59+
private JPanel diameterSlot;
60+
private TextFieldWithUnit toolDiameter;
61+
private UnitUtils.Units diameterDisplayUnit = UnitUtils.Units.MM;
62+
private TextFieldWithUnit feedSpeed;
63+
private TextFieldWithUnit plungeSpeed;
64+
private TextFieldWithUnit depthPerPass;
65+
private TextFieldWithUnit stepOver;
5066
private JTextField safeHeight;
5167
private JCheckBox detectMaxSpindleSpeed;
5268
private TextFieldWithUnit laserDiameter;
5369
private TextFieldWithUnit maxSpindleSpeed;
5470
private JComboBox<String> spindleDirection;
5571
private TextFieldWithUnit flatnessPrecision;
5672

73+
private transient ToolDefinition librarySnapshot;
74+
5775
public ToolSettingsPanel(Controller controller) {
5876
this.controller = controller;
77+
this.libraryService = LookupService.lookupOptional(ToolLibraryService.class).orElse(null);
5978
initComponents();
60-
setMinimumSize(new Dimension(300, 400));
61-
setPreferredSize(new Dimension(300, 400));
79+
setMinimumSize(new Dimension(360, 480));
80+
setPreferredSize(new Dimension(360, 480));
81+
initialiseLibrarySnapshot();
82+
attachDeviationHighlighters();
6283
}
84+
6385
private void initComponents() {
64-
setLayout(new MigLayout("fill", "[20%][80%]" ));
86+
setLayout(new MigLayout("fill", "[pref!][grow,fill]"));
87+
88+
pickFromLibraryButton = new JButton("Pick from Library…");
89+
pickFromLibraryButton.addActionListener(e -> onPickFromLibrary());
90+
add(pickFromLibraryButton, "spanx, growx, split 2");
91+
selectedToolLabel = new JLabel(" ");
92+
add(selectedToolLabel, "wrap, growx");
93+
94+
add(new JSeparator(SwingConstants.HORIZONTAL), "spanx, grow, wrap, hmin 2");
6595

66-
add(new JLabel("Tool diameter" ));
67-
toolDiameter = new TextFieldWithUnit(Unit.MM, 3, controller.getSettings().getToolDiameter());
68-
add(toolDiameter, TOOL_FIELD_CONSTRAINT);
96+
add(new JLabel("Tool diameter"));
97+
diameterSlot = new JPanel(new MigLayout("insets 0, fill"));
98+
rebuildDiameterField(UnitUtils.Units.MM, controller.getSettings().getToolDiameter());
99+
add(diameterSlot, TOOL_FIELD_CONSTRAINT);
69100

70-
add(new JLabel("Tool step over" ));
101+
add(new JLabel("Tool step over (%)"));
71102
stepOver = new TextFieldWithUnit(Unit.PERCENT, 2,
72103
controller.getSettings().getToolStepOver());
73104
add(stepOver, TOOL_FIELD_CONSTRAINT);
74105

75-
add(new JSeparator(SwingConstants.HORIZONTAL), "spanx, grow, wrap, hmin 2" );
106+
add(new JSeparator(SwingConstants.HORIZONTAL), "spanx, grow, wrap, hmin 2");
76107

77-
add(new JLabel("Default feed speed" ));
108+
add(new JLabel("Default feed speed (" + Unit.MM_PER_MINUTE.getAbbreviation() + ")"));
78109
feedSpeed = new TextFieldWithUnit(Unit.MM_PER_MINUTE, 0, controller.getSettings().getFeedSpeed());
79110
add(feedSpeed, TOOL_FIELD_CONSTRAINT);
80111

81-
add(new JLabel("Plunge speed" ));
112+
add(new JLabel("Plunge speed (" + Unit.MM_PER_MINUTE.getAbbreviation() + ")"));
82113
plungeSpeed = new TextFieldWithUnit(Unit.MM_PER_MINUTE, 0, controller.getSettings().getPlungeSpeed());
83114
add(plungeSpeed, TOOL_FIELD_CONSTRAINT);
84115

85-
add(new JLabel("Depth per pass" ));
116+
add(new JLabel("Depth per pass (" + Unit.MM.getAbbreviation() + ")"));
86117
depthPerPass = new TextFieldWithUnit(Unit.MM, 2, controller.getSettings().getDepthPerPass());
87118
add(depthPerPass, TOOL_FIELD_CONSTRAINT);
88119

89-
add(new JLabel("Safe height" ));
120+
add(new JLabel("Safe height (" + Unit.MM.getAbbreviation() + ")"));
90121
safeHeight = new TextFieldWithUnit(Unit.MM, 2, controller.getSettings().getSafeHeight());
91122
add(safeHeight, TOOL_FIELD_CONSTRAINT);
92123

93-
add(new JSeparator(SwingConstants.HORIZONTAL), "spanx, grow, wrap, hmin 2" );
124+
add(new JSeparator(SwingConstants.HORIZONTAL), "spanx, grow, wrap, hmin 2");
94125

95-
add(new JLabel("Detect max spindle speed" ));
126+
add(new JLabel("Detect max spindle speed"));
96127
detectMaxSpindleSpeed = new JCheckBox("", controller.getSettings().getDetectMaxSpindleSpeed());
97128
add(detectMaxSpindleSpeed, TOOL_FIELD_CONSTRAINT);
98129

99-
add(new JLabel("Max spindle speed" ));
130+
add(new JLabel("Max spindle speed"));
100131
maxSpindleSpeed = new TextFieldWithUnit(Unit.REVOLUTIONS_PER_MINUTE, 0, controller.getSettings().getMaxSpindleSpeed());
101132
add(maxSpindleSpeed, TOOL_FIELD_CONSTRAINT);
102133

103-
add(new JSeparator(SwingConstants.HORIZONTAL), "spanx, grow, wrap, hmin 2" );
134+
add(new JSeparator(SwingConstants.HORIZONTAL), "spanx, grow, wrap, hmin 2");
104135

105-
add(new JLabel("Laser diameter" ));
136+
add(new JLabel("Laser diameter"));
106137
laserDiameter = new TextFieldWithUnit(Unit.MM, 3, controller.getSettings().getLaserDiameter());
107138
add(laserDiameter, TOOL_FIELD_CONSTRAINT);
108139

109-
add(new JSeparator(SwingConstants.HORIZONTAL), "spanx, grow, wrap, hmin 2" );
140+
add(new JSeparator(SwingConstants.HORIZONTAL), "spanx, grow, wrap, hmin 2");
110141

111-
add(new JLabel("Spindle Start Command" ));
112-
spindleDirection = new JComboBox<>(new DefaultComboBoxModel<>(new String[]{"M3","M4","M5"}));
142+
add(new JLabel("Spindle Start Command"));
143+
spindleDirection = new JComboBox<>(new DefaultComboBoxModel<>(new String[]{"M3", "M4", "M5"}));
113144
spindleDirection.setSelectedItem(controller.getSettings().getSpindleDirection());
114145
add(spindleDirection, TOOL_FIELD_CONSTRAINT);
115146

116-
add(new JLabel("Arc precision" ));
147+
add(new JLabel("Arc precision"));
117148
flatnessPrecision = new TextFieldWithUnit(Unit.MM, 3, controller.getSettings().getFlatnessPrecision());
118149
add(flatnessPrecision, TOOL_FIELD_CONSTRAINT);
119150
}
120151

152+
private void rebuildDiameterField(UnitUtils.Units unit, double valueInFieldUnit) {
153+
diameterDisplayUnit = unit == null ? UnitUtils.Units.MM : unit;
154+
Unit fieldUnit = diameterDisplayUnit == UnitUtils.Units.INCH ? Unit.INCH : Unit.MM;
155+
int decimals = diameterDisplayUnit == UnitUtils.Units.INCH ? 4 : 3;
156+
diameterSlot.removeAll();
157+
toolDiameter = new TextFieldWithUnit(fieldUnit, decimals, valueInFieldUnit);
158+
diameterSlot.add(toolDiameter, "grow");
159+
diameterSlot.revalidate();
160+
diameterSlot.repaint();
161+
}
162+
163+
private void initialiseLibrarySnapshot() {
164+
Settings settings = controller.getSettings();
165+
String activeId = settings.getCurrentToolId();
166+
if (activeId != null && libraryService != null) {
167+
Optional<ToolDefinition> tool = libraryService.getById(activeId);
168+
if (tool.isPresent() && !tool.get().isCustomSentinel()) {
169+
librarySnapshot = tool.get();
170+
SwingUtilities.invokeLater(() -> applyLibrarySnapshotToFields(false));
171+
return;
172+
}
173+
}
174+
ToolDefinition snapshot = settings.getCurrentToolSnapshot();
175+
if (snapshot != null && !snapshot.isCustomSentinel()) {
176+
librarySnapshot = new ToolDefinition(snapshot);
177+
}
178+
updateSelectedToolLabel();
179+
}
180+
181+
private void attachDeviationHighlighters() {
182+
DeviationHighlighter.attachDouble(toolDiameter, () -> librarySnapshot == null ? null
183+
: valueInDisplayUnit(librarySnapshot.getDiameterInMm()));
184+
DeviationHighlighter.attachDouble(maxSpindleSpeed,
185+
() -> librarySnapshot == null ? null : (double) librarySnapshot.getMaxSpindleSpeed());
186+
DeviationHighlighter.attachDouble(flatnessPrecision,
187+
() -> librarySnapshot == null ? null : null);
188+
DeviationHighlighter.attachDouble(laserDiameter,
189+
() -> librarySnapshot == null ? null : null);
190+
191+
DeviationHighlighter.attachDouble(feedSpeed,
192+
() -> librarySnapshot == null ? null : (double) librarySnapshot.getFeedSpeed());
193+
DeviationHighlighter.attachDouble(plungeSpeed,
194+
() -> librarySnapshot == null ? null : (double) librarySnapshot.getPlungeSpeed());
195+
DeviationHighlighter.attachDouble(depthPerPass,
196+
() -> librarySnapshot == null ? null : librarySnapshot.getDepthPerPass());
197+
DeviationHighlighter.attachDouble(stepOver,
198+
() -> librarySnapshot == null ? null : librarySnapshot.getStepOverPercent());
199+
DeviationHighlighter.attachCombo(spindleDirection,
200+
() -> librarySnapshot == null ? null : librarySnapshot.getSpindleDirection());
201+
}
202+
203+
private Double valueInDisplayUnit(double mm) {
204+
if (diameterDisplayUnit == UnitUtils.Units.INCH) {
205+
return mm * UnitUtils.scaleUnits(UnitUtils.Units.MM, UnitUtils.Units.INCH);
206+
}
207+
return mm;
208+
}
209+
210+
private void onPickFromLibrary() {
211+
if (libraryService == null) {
212+
return;
213+
}
214+
Optional<ToolDefinition> picked = ToolLibraryPickerDialog.pick(
215+
SwingUtilities.getWindowAncestor(this),
216+
controller.getSettings().getPreferredUnits());
217+
picked.ifPresent(tool -> {
218+
if (tool.isCustomSentinel()) {
219+
librarySnapshot = null;
220+
updateSelectedToolLabel();
221+
} else {
222+
librarySnapshot = tool;
223+
applyLibrarySnapshotToFields(true);
224+
}
225+
});
226+
}
227+
228+
private void applyLibrarySnapshotToFields(boolean populateAll) {
229+
if (librarySnapshot == null) return;
230+
rebuildDiameterField(librarySnapshot.getDiameterUnit(), librarySnapshot.getDiameter());
231+
// Re-attach highlighter to the new field
232+
DeviationHighlighter.attachDouble(toolDiameter, () -> librarySnapshot == null ? null
233+
: valueInDisplayUnit(librarySnapshot.getDiameterInMm()));
234+
if (populateAll) {
235+
try {
236+
feedSpeed.setDoubleValue(librarySnapshot.getFeedSpeed());
237+
plungeSpeed.setDoubleValue(librarySnapshot.getPlungeSpeed());
238+
depthPerPass.setDoubleValue(librarySnapshot.getDepthPerPass());
239+
stepOver.setDoubleValue(librarySnapshot.getStepOverPercent());
240+
maxSpindleSpeed.setDoubleValue(librarySnapshot.getMaxSpindleSpeed());
241+
spindleDirection.setSelectedItem(librarySnapshot.getSpindleDirection());
242+
} catch (RuntimeException ignored) {
243+
// Bad format — leave field as-is
244+
}
245+
}
246+
updateSelectedToolLabel();
247+
}
248+
249+
private void updateSelectedToolLabel() {
250+
if (librarySnapshot == null) {
251+
selectedToolLabel.setText("— Custom —");
252+
} else {
253+
selectedToolLabel.setText(librarySnapshot.getName() == null
254+
? "— Custom —" : librarySnapshot.getName());
255+
}
256+
}
257+
121258
public double getToolDiameter() {
122259
try {
123-
return Utils.formatter.parse(toolDiameter.getText()).doubleValue();
260+
double displayed = Utils.formatter.parse(toolDiameter.getText()).doubleValue();
261+
return displayed * UnitUtils.scaleUnits(diameterDisplayUnit, UnitUtils.Units.MM);
124262
} catch (ParseException e) {
125263
return controller.getSettings().getToolDiameter();
126264
}
127265
}
128266

129267
public double getStepOver() {
130-
try {
131-
return Utils.formatter.parse(stepOver.getText()).doubleValue() / 100;
132-
} catch (ParseException e) {
133-
return controller.getSettings().getToolStepOver();
134-
}
268+
return stepOver.getDoubleValue();
135269
}
136270

137271
public double getDepthPerPass() {
@@ -212,6 +346,13 @@ public Settings getSettings() {
212346
settings.setDetectMaxSpindleSpeed(getDetectMaxSpindleSpeed());
213347
settings.setSpindleDirection(getSpindleDirection());
214348
settings.setFlatnessPrecision(getFlatnessPrecision());
349+
if (librarySnapshot != null) {
350+
settings.setCurrentToolId(librarySnapshot.getId());
351+
settings.setCurrentToolSnapshot(new ToolDefinition(librarySnapshot));
352+
} else {
353+
settings.setCurrentToolId(null);
354+
settings.setCurrentToolSnapshot(null);
355+
}
215356
return settings;
216357
}
217358
}

0 commit comments

Comments
 (0)