Skip to content

Commit 85251d3

Browse files
authored
Added basic support for the designer in ugs-fx (#3129)
1 parent 1cc7d7c commit 85251d3

123 files changed

Lines changed: 6352 additions & 166 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ugs-core/src/resources/MessagesBundle_en_US.properties

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ platform.visualizer.color.background = Background Color
340340
platform.visualizer.tool = Show tool location
341341
platform.visualizer.color.tool = Tool color
342342
platform.visualizer.tool.desc = Shows the tool location
343+
platform.visualizer.design = Show design
344+
platform.visualizer.design.desc = Shows the design shapes
343345
platform.visualizer.model = Show model
344346
platform.visualizer.model.desc = Shows the tool path for the model
345347
platform.visualizer.mouseover = Show the mouse position
@@ -365,6 +367,41 @@ platform.visualizer.color.rapid = Rapid Movement Color (G0)
365367
platform.visualizer.color.arc = Arc Movement Color (G2/G3)
366368
platform.visualizer.color.plunge = Plunge/Raise Movement Color
367369
platform.visualizer.color.completed = Color of lines which have been completed
370+
platform.visualizer.color.ruler.lines = Ruler Line Color
371+
platform.visualizer.color.ruler.text = Ruler Text Color
372+
platform.visualizer.ruler = Ruler
373+
platform.visualizer.ruler.show = Show ruler
374+
platform.visualizer.toggleRuler = Toggle ruler
375+
platform.visualizer.toggleGrid = Toggle grid
376+
platform.visualizer.toggleAxes = Toggle axes
377+
platform.visualizer.centerCamera = Center view on workspace
378+
actions.category.designer = Designer
379+
platform.designer.select = Select and move shapes
380+
platform.designer.rectangle = Draw rectangle
381+
platform.designer.circle = Draw ellipse
382+
platform.designer.point = Draw point
383+
platform.designer.text = Draw text
384+
platform.designer.line = Draw line
385+
platform.designer.import = Import file
386+
platform.designer.clipart = Insert clipart
387+
platform.designer.trace = Trace image
388+
platform.designer.undo = Undo
389+
platform.designer.redo = Redo
390+
platform.designer.copy = Copy
391+
platform.designer.paste = Paste
392+
platform.designer.delete = Delete
393+
platform.designer.alignLeft = Align left
394+
platform.designer.alignRight = Align right
395+
platform.designer.alignTop = Align top
396+
platform.designer.alignBottom = Align bottom
397+
platform.designer.alignCenter = Align center
398+
platform.designer.alignMiddle = Align middle
399+
platform.designer.union = Union
400+
platform.designer.subtract = Subtract
401+
platform.designer.intersection = Intersection
402+
platform.designer.breakApart = Break apart
403+
platform.designer.stitch = Stitch
404+
platform.designer.group = Group
368405
platform.visualizer.grid = Toggles grid
369406
platform.visualizer.grid.desc = Toggles the grid
370407
platform.visualizer.plane = Toggle plane
@@ -827,6 +864,7 @@ settings.firmware = Firmware
827864
settings.metric = Metric
828865
settings.imperial = Imperial
829866
settings.probe = Probe
867+
settings.mouse = Mouse
830868
settings.terminal = Terminal
831869
settings.pendant.clear = Clear
832870
settings.visualizer.machine = Machine model

ugs-fx/src/main/java/com/willwinder/universalgcodesender/fx/Main.java

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,19 @@ This file is part of Universal Gcode Sender (UGS).
2525
import com.willwinder.universalgcodesender.fx.component.dro.MachineStatusPane;
2626
import com.willwinder.universalgcodesender.fx.component.jog.JogPane;
2727
import com.willwinder.universalgcodesender.fx.component.visualizer.Visualizer;
28+
import com.willwinder.universalgcodesender.fx.component.visualizer.designer.InspectorPane;
29+
import com.willwinder.universalgcodesender.fx.model.UgsdWorkspaceContext;
2830
import com.willwinder.universalgcodesender.services.LookupService;
2931
import com.willwinder.universalgcodesender.fx.helper.FontRegistry;
32+
import com.willwinder.universalgcodesender.fx.helper.SplitPaneDividerPersistence;
3033
import com.willwinder.universalgcodesender.fx.helper.SvgLoader;
3134
import com.willwinder.universalgcodesender.fx.service.ActionRegistry;
3235
import com.willwinder.universalgcodesender.fx.service.JogActionRegistry;
3336
import com.willwinder.universalgcodesender.fx.service.MacroActionService;
3437
import com.willwinder.universalgcodesender.fx.service.ShortcutService;
38+
import com.willwinder.universalgcodesender.fx.service.WorkspaceManager;
3539
import com.willwinder.universalgcodesender.fx.settings.Settings;
3640
import com.willwinder.universalgcodesender.i18n.Localization;
37-
import com.willwinder.universalgcodesender.model.BackendAPI;
3841
import com.willwinder.universalgcodesender.model.GUIBackend;
3942
import com.willwinder.universalgcodesender.pendantui.PendantUI;
4043
import com.willwinder.universalgcodesender.utils.SettingsFactory;
@@ -56,16 +59,15 @@ This file is part of Universal Gcode Sender (UGS).
5659

5760
import javax.swing.UIManager;
5861
import java.io.File;
62+
import java.util.Objects;
5963
import java.util.logging.Level;
6064
import java.util.logging.Logger;
6165

6266
public class Main extends Application {
6367
private static final Logger LOGGER = Logger.getLogger(Main.class.getName());
64-
private SplitPane leftSplitPane;
68+
private SplitPane motionSplitPane;
6569
private SplitPane contentSplitPane;
6670
private StackPane contentPanel;
67-
private SplitPane.Divider contentPaneDivider;
68-
private SplitPane.Divider leftPaneDivider;
6971

7072
@Override
7173
public void init() throws Exception {
@@ -109,7 +111,7 @@ public void start(Stage primaryStage) {
109111
ShortcutService.registerListener(scene);
110112
FontRegistry.registerFonts();
111113

112-
scene.getStylesheets().add(Main.class.getResource("/styles/root.css").toExternalForm());
114+
scene.getStylesheets().add(Objects.requireNonNull(Main.class.getResource("/styles/root.css")).toExternalForm());
113115
root.getChildren().addAll(toolBarMenu, contentSplitPane);
114116

115117
primaryStage.setTitle("Universal G-code Sender - " + Version.getVersion());
@@ -120,24 +122,35 @@ public void start(Stage primaryStage) {
120122

121123
Parameters params = getParameters();
122124
if (!params.getUnnamed().isEmpty()) {
123-
BackendAPI backendAPI = LookupService.lookup(BackendAPI.class);
124125
try {
125126
File file = new File(params.getUnnamed().get(0));
126-
backendAPI.setGcodeFile(file);
127-
backendAPI.getSettings().setLastWorkingDirectory(file.getParent());
127+
WorkspaceManager.getInstance().openWorkspace(file);
128128
} catch (Exception e) {
129129
throw new RuntimeException(e);
130130
}
131+
} else {
132+
openDefaultWorkspace();
131133
}
132134
}
133135

134-
private void registerLayoutListeners(Stage primaryStage) {
136+
/**
137+
* Opens an empty UGSD design workspace when the application is started without a file argument,
138+
* so the designer is ready to use straight away.
139+
*/
140+
private void openDefaultWorkspace() {
141+
try {
142+
UgsdWorkspaceContext workspace = new UgsdWorkspaceContext(null);
143+
WorkspaceManager.getInstance().setWorkspace(workspace);
144+
} catch (Exception e) {
145+
LOGGER.log(Level.SEVERE, "Could not open the default design workspace", e);
146+
}
147+
}
148+
149+
private void registerWindowBoundsListeners(Stage primaryStage) {
135150
primaryStage.widthProperty().addListener((observable, oldValue, newValue) -> Settings.getInstance().windowWidthProperty().set(newValue.doubleValue()));
136151
primaryStage.heightProperty().addListener((observable, oldValue, newValue) -> Settings.getInstance().windowHeightProperty().set(newValue.doubleValue()));
137152
primaryStage.xProperty().addListener((observable, oldValue, newValue) -> Settings.getInstance().windowPositionXProperty().set(newValue.doubleValue()));
138153
primaryStage.yProperty().addListener((observable, oldValue, newValue) -> Settings.getInstance().windowPositionYProperty().set(newValue.doubleValue()));
139-
leftPaneDivider.positionProperty().addListener((obs, oldVal, newVal) -> Settings.getInstance().windowDividerLeftProperty().set(newVal.doubleValue()));
140-
contentPaneDivider.positionProperty().addListener((obs, oldVal, newVal) -> Settings.getInstance().windowDividerContentProperty().set(newVal.doubleValue()));
141154
}
142155

143156
private void registerListeners(Stage primaryStage) {
@@ -146,16 +159,13 @@ private void registerListeners(Stage primaryStage) {
146159
primaryStage.setY(Settings.getInstance().windowPositionYProperty().get());
147160
primaryStage.setWidth(Settings.getInstance().windowWidthProperty().get());
148161
primaryStage.setHeight(Settings.getInstance().windowHeightProperty().get());
149-
leftPaneDivider.setPosition(Settings.getInstance().windowDividerLeftProperty().get());
150-
contentPaneDivider.setPosition(Settings.getInstance().windowDividerContentProperty().get());
162+
registerWindowBoundsListeners(primaryStage);
151163

152-
153-
// Hack to make sure that the window is shown with correct size before setting the dividers
154-
ThreadHelper.invokeLater(() -> {
155-
leftPaneDivider.setPosition(Settings.getInstance().windowDividerLeftProperty().get());
156-
contentPaneDivider.setPosition(Settings.getInstance().windowDividerContentProperty().get());
157-
registerLayoutListeners(primaryStage);
158-
}, 200);
164+
Platform.runLater(() -> {
165+
SplitPaneDividerPersistence.install(motionSplitPane, 0, Settings.getInstance().windowDividerLeftProperty());
166+
SplitPaneDividerPersistence.install(contentSplitPane, 0, Settings.getInstance().windowDividerContentProperty());
167+
SplitPaneDividerPersistence.install(contentSplitPane, 1, Settings.getInstance().windowDividerInspectorProperty());
168+
});
159169
});
160170

161171
primaryStage.setOnCloseRequest(event -> {
@@ -179,24 +189,18 @@ private void createContentPane() {
179189
contentSplitPane = new SplitPane();
180190
contentSplitPane.setMinWidth(200);
181191
contentSplitPane.setOrientation(Orientation.HORIZONTAL);
182-
contentSplitPane.getItems().addAll(leftSplitPane, contentPanel);
192+
contentSplitPane.getItems().addAll(motionSplitPane, contentPanel);
183193
SplitPane.setResizableWithParent(contentSplitPane, false);
184-
185-
contentPaneDivider = contentSplitPane.getDividers().get(0);
186-
contentPaneDivider.setPosition(Settings.getInstance().windowDividerContentProperty().get());
194+
new InspectorPane(contentSplitPane);
187195
}
188196

189197
private void createLeftPane() {
190-
leftSplitPane = new SplitPane();
191-
leftSplitPane.setStyle("-fx-border-color: transparent;");
192-
193-
leftSplitPane.setMinWidth(200);
194-
leftSplitPane.setOrientation(Orientation.VERTICAL);
195-
leftSplitPane.getItems().addAll(new MachineStatusPane(), new JogPane());
196-
SplitPane.setResizableWithParent(leftSplitPane, false);
198+
motionSplitPane = new SplitPane();
197199

198-
leftPaneDivider = leftSplitPane.getDividers().get(0);
199-
leftPaneDivider.setPosition(Settings.getInstance().windowDividerLeftProperty().get());
200+
motionSplitPane.setOrientation(Orientation.VERTICAL);
201+
motionSplitPane.getItems().addAll(new MachineStatusPane(), new JogPane());
202+
motionSplitPane.setMinWidth(200);
203+
SplitPane.setResizableWithParent(motionSplitPane, false);
200204
}
201205

202206
private void registerShortCuts(Scene scene) {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
Copyright 2026 Joacim Breiler
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.universalgcodesender.fx.actions;
20+
21+
import com.willwinder.universalgcodesender.i18n.Localization;
22+
import javafx.event.ActionEvent;
23+
24+
import javax.swing.SwingUtilities;
25+
26+
/**
27+
* Base class for design toolbar commands that reuse an existing Swing based design action that
28+
* opens a modal Swing dialog (such as importing a file or inserting clipart). Unlike the other
29+
* designer actions, which mutate the model on the JavaFX thread (see {@link AbstractDesignEditAction}
30+
* and {@link AbstractDesignToolAction}), the wrapped action here must be dispatched on the Swing
31+
* event dispatch thread because that is where its dialog has to run. The model mutation that follows
32+
* the dialog is serialized behind the modal dialog, so it does not overlap the FX-thread gestures.
33+
*/
34+
public abstract class AbstractDesignCommandAction extends BaseAction {
35+
36+
private final transient javax.swing.Action delegate;
37+
38+
protected AbstractDesignCommandAction(javax.swing.Action delegate, String title, String icon) {
39+
super(title, title, Localization.getString("actions.category.designer"), icon);
40+
this.delegate = delegate;
41+
}
42+
43+
@Override
44+
public void handleAction(ActionEvent event) {
45+
SwingUtilities.invokeLater(() -> delegate.actionPerformed(null));
46+
}
47+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
Copyright 2026 Joacim Breiler
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.universalgcodesender.fx.actions;
20+
21+
import com.willwinder.ugs.designer.logic.Controller;
22+
import com.willwinder.ugs.designer.logic.ControllerFactory;
23+
import com.willwinder.universalgcodesender.i18n.Localization;
24+
import javafx.application.Platform;
25+
import javafx.event.ActionEvent;
26+
27+
/**
28+
* Base class for the context aware design edit actions (undo/redo/copy/paste/delete) that operate
29+
* directly on the designer {@link Controller}, in the same spirit as {@link AbstractDesignToolAction}.
30+
* <p>
31+
* The action body runs on the JavaFX thread, which is the thread that owns the designer model in
32+
* the FX visualizer: the draw-gesture handlers mutate the drawing on the JavaFX thread and the
33+
* visualizer rebuilds the scene graph from it on the JavaFX thread. Running the edits on the same
34+
* thread keeps every mutation and the resulting scene refresh serialized and race free. The JavaFX
35+
* {@link #enabledProperty()} is updated by the subclasses as the relevant designer state (the
36+
* current selection or the undo history) changes.
37+
*/
38+
public abstract class AbstractDesignEditAction extends BaseAction {
39+
40+
protected final transient Controller controller;
41+
42+
protected AbstractDesignEditAction(String title, String icon) {
43+
super(title, title, Localization.getString("actions.category.designer"), icon);
44+
this.controller = ControllerFactory.getController();
45+
}
46+
47+
@Override
48+
public void handleAction(ActionEvent event) {
49+
// Mutate the designer model on the JavaFX thread so the visualizer refreshes immediately
50+
Platform.runLater(this::performAction);
51+
}
52+
53+
/**
54+
* Performs the action on the designer model. Always invoked on the JavaFX thread.
55+
*/
56+
protected abstract void performAction();
57+
58+
/**
59+
* Updates the enabled state of the action on the JavaFX thread.
60+
*/
61+
protected void setEnabledLater(boolean enabled) {
62+
Platform.runLater(() -> enabledProperty().set(enabled));
63+
}
64+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
Copyright 2026 Joacim Breiler
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.universalgcodesender.fx.actions;
20+
21+
import com.willwinder.ugs.designer.actions.AbstractDesignAction;
22+
import com.willwinder.ugs.designer.entities.entities.selection.SelectionEvent;
23+
import com.willwinder.ugs.designer.entities.entities.selection.SelectionListener;
24+
25+
/**
26+
* Base class for the design operation actions, both the alignment actions (align left, top, ...)
27+
* and the geometric operations (union, subtract, group, ...). Each of them reuses the existing
28+
* Swing based {@link AbstractDesignAction} that already implements the operation against the
29+
* designer model.
30+
* <p>
31+
* The wrapped action mutates the drawing directly without opening a dialog, so — like the other
32+
* context aware edit actions (see {@link AbstractDesignEditAction}) — it is dispatched on the
33+
* JavaFX thread that owns the designer model in the FX visualizer.
34+
* <p>
35+
* The enabled state is delegated to the wrapped action, which already knows the selection rules
36+
* for the operation (alignment needs more than one selected entity, a union needs at least two,
37+
* a break apart needs a single compound path, ...). Each selection change is forwarded to the
38+
* delegate so its enabled state is recomputed before it is mirrored onto the JavaFX
39+
* {@link #enabledProperty()}.
40+
*/
41+
public abstract class AbstractDesignOperationAction extends AbstractDesignEditAction implements SelectionListener {
42+
43+
private final transient AbstractDesignAction delegate;
44+
45+
protected AbstractDesignOperationAction(AbstractDesignAction delegate, String title, String icon) {
46+
super(title, icon);
47+
this.delegate = delegate;
48+
controller.getSelectionManager().addSelectionListener(this);
49+
enabledProperty().set(delegate.isEnabled());
50+
}
51+
52+
@Override
53+
protected void performAction() {
54+
delegate.actionPerformed(null);
55+
}
56+
57+
@Override
58+
public void onSelectionEvent(SelectionEvent selectionEvent) {
59+
if (delegate instanceof SelectionListener selectionListener) {
60+
selectionListener.onSelectionEvent(selectionEvent);
61+
}
62+
setEnabledLater(delegate.isEnabled());
63+
}
64+
}

0 commit comments

Comments
 (0)