Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3f3ddab
Work in progress
breiler Apr 25, 2026
4494053
Add action for saving changes to the design and update gcode
breiler May 9, 2026
902ab46
Add ruler
breiler May 14, 2026
376cdb1
Add settings for toggling ruler and gcode model
breiler May 14, 2026
aba67a5
Add settings for toggling ruler and gcode model
breiler May 14, 2026
d710cd7
Make the objects unselectable when clicked outside
breiler May 16, 2026
e98c086
Disable depth test to prevent flickering
breiler May 16, 2026
9401a36
Added entity shape settings panel
breiler May 30, 2026
afc531b
Add support for drawing new shapes
breiler Jun 15, 2026
f7dd77f
Replaced actions with JavaFX variants
breiler Jun 16, 2026
d064255
Moved the inspector to its own class
breiler Jun 16, 2026
8eb753b
Make control handles scale and add hover feedback
breiler Jun 16, 2026
69d4f25
Add live update
breiler Jun 16, 2026
b7d8c51
Add possibility to create a new design
breiler Jun 16, 2026
9fea8d5
Add context aware designer actions
breiler Jun 16, 2026
548573c
Load extents from the design
breiler Jun 16, 2026
3181ba0
Add more actions
breiler Jun 17, 2026
1b2911f
Open a default UGSD design
breiler Jun 17, 2026
821f663
Add an objects tree
breiler Jun 17, 2026
3d813c4
Moved visualizer toggle buttons
breiler Jun 17, 2026
47fac72
Recenters on workspace when file is loaded
breiler Jun 17, 2026
4376aeb
Use JavaFX transform for applying transformations to shape
breiler Jun 17, 2026
10fc74c
Fix problem with divider persistence
breiler Jun 17, 2026
bab31be
Fixes after code review
breiler Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions ugs-core/src/resources/MessagesBundle_en_US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ platform.visualizer.color.background = Background Color
platform.visualizer.tool = Show tool location
platform.visualizer.color.tool = Tool color
platform.visualizer.tool.desc = Shows the tool location
platform.visualizer.design = Show design
platform.visualizer.design.desc = Shows the design shapes
platform.visualizer.model = Show model
platform.visualizer.model.desc = Shows the tool path for the model
platform.visualizer.mouseover = Show the mouse position
Expand All @@ -365,6 +367,41 @@ platform.visualizer.color.rapid = Rapid Movement Color (G0)
platform.visualizer.color.arc = Arc Movement Color (G2/G3)
platform.visualizer.color.plunge = Plunge/Raise Movement Color
platform.visualizer.color.completed = Color of lines which have been completed
platform.visualizer.color.ruler.lines = Ruler Line Color
platform.visualizer.color.ruler.text = Ruler Text Color
platform.visualizer.ruler = Ruler
platform.visualizer.ruler.show = Show ruler
platform.visualizer.toggleRuler = Toggle ruler
platform.visualizer.toggleGrid = Toggle grid
platform.visualizer.toggleAxes = Toggle axes
platform.visualizer.centerCamera = Center view on workspace
actions.category.designer = Designer
platform.designer.select = Select and move shapes
platform.designer.rectangle = Draw rectangle
platform.designer.circle = Draw ellipse
platform.designer.point = Draw point
platform.designer.text = Draw text
platform.designer.line = Draw line
platform.designer.import = Import file
platform.designer.clipart = Insert clipart
platform.designer.trace = Trace image
platform.designer.undo = Undo
platform.designer.redo = Redo
platform.designer.copy = Copy
platform.designer.paste = Paste
platform.designer.delete = Delete
platform.designer.alignLeft = Align left
platform.designer.alignRight = Align right
platform.designer.alignTop = Align top
platform.designer.alignBottom = Align bottom
platform.designer.alignCenter = Align center
platform.designer.alignMiddle = Align middle
platform.designer.union = Union
platform.designer.subtract = Subtract
platform.designer.intersection = Intersection
platform.designer.breakApart = Break apart
platform.designer.stitch = Stitch
platform.designer.group = Group
platform.visualizer.grid = Toggles grid
platform.visualizer.grid.desc = Toggles the grid
platform.visualizer.plane = Toggle plane
Expand Down Expand Up @@ -827,6 +864,7 @@ settings.firmware = Firmware
settings.metric = Metric
settings.imperial = Imperial
settings.probe = Probe
settings.mouse = Mouse
settings.terminal = Terminal
settings.pendant.clear = Clear
settings.visualizer.machine = Machine model
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,19 @@ This file is part of Universal Gcode Sender (UGS).
import com.willwinder.universalgcodesender.fx.component.dro.MachineStatusPane;
import com.willwinder.universalgcodesender.fx.component.jog.JogPane;
import com.willwinder.universalgcodesender.fx.component.visualizer.Visualizer;
import com.willwinder.universalgcodesender.fx.component.visualizer.designer.InspectorPane;
import com.willwinder.universalgcodesender.fx.model.UgsdWorkspaceContext;
import com.willwinder.universalgcodesender.services.LookupService;
import com.willwinder.universalgcodesender.fx.helper.FontRegistry;
import com.willwinder.universalgcodesender.fx.helper.SplitPaneDividerPersistence;
import com.willwinder.universalgcodesender.fx.helper.SvgLoader;
import com.willwinder.universalgcodesender.fx.service.ActionRegistry;
import com.willwinder.universalgcodesender.fx.service.JogActionRegistry;
import com.willwinder.universalgcodesender.fx.service.MacroActionService;
import com.willwinder.universalgcodesender.fx.service.ShortcutService;
import com.willwinder.universalgcodesender.fx.service.WorkspaceManager;
import com.willwinder.universalgcodesender.fx.settings.Settings;
import com.willwinder.universalgcodesender.i18n.Localization;
import com.willwinder.universalgcodesender.model.BackendAPI;
import com.willwinder.universalgcodesender.model.GUIBackend;
import com.willwinder.universalgcodesender.pendantui.PendantUI;
import com.willwinder.universalgcodesender.utils.SettingsFactory;
Expand All @@ -56,16 +59,15 @@ This file is part of Universal Gcode Sender (UGS).

import javax.swing.UIManager;
import java.io.File;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Main extends Application {
private static final Logger LOGGER = Logger.getLogger(Main.class.getName());
private SplitPane leftSplitPane;
private SplitPane motionSplitPane;
private SplitPane contentSplitPane;
private StackPane contentPanel;
private SplitPane.Divider contentPaneDivider;
private SplitPane.Divider leftPaneDivider;

@Override
public void init() throws Exception {
Expand Down Expand Up @@ -109,7 +111,7 @@ public void start(Stage primaryStage) {
ShortcutService.registerListener(scene);
FontRegistry.registerFonts();

scene.getStylesheets().add(Main.class.getResource("/styles/root.css").toExternalForm());
scene.getStylesheets().add(Objects.requireNonNull(Main.class.getResource("/styles/root.css")).toExternalForm());
root.getChildren().addAll(toolBarMenu, contentSplitPane);

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

Parameters params = getParameters();
if (!params.getUnnamed().isEmpty()) {
BackendAPI backendAPI = LookupService.lookup(BackendAPI.class);
try {
File file = new File(params.getUnnamed().get(0));
backendAPI.setGcodeFile(file);
backendAPI.getSettings().setLastWorkingDirectory(file.getParent());
WorkspaceManager.getInstance().openWorkspace(file);
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
openDefaultWorkspace();
}
}

private void registerLayoutListeners(Stage primaryStage) {
/**
* Opens an empty UGSD design workspace when the application is started without a file argument,
* so the designer is ready to use straight away.
*/
private void openDefaultWorkspace() {
try {
UgsdWorkspaceContext workspace = new UgsdWorkspaceContext(null);
WorkspaceManager.getInstance().setWorkspace(workspace);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Could not open the default design workspace", e);
}
}

private void registerWindowBoundsListeners(Stage primaryStage) {
primaryStage.widthProperty().addListener((observable, oldValue, newValue) -> Settings.getInstance().windowWidthProperty().set(newValue.doubleValue()));
primaryStage.heightProperty().addListener((observable, oldValue, newValue) -> Settings.getInstance().windowHeightProperty().set(newValue.doubleValue()));
primaryStage.xProperty().addListener((observable, oldValue, newValue) -> Settings.getInstance().windowPositionXProperty().set(newValue.doubleValue()));
primaryStage.yProperty().addListener((observable, oldValue, newValue) -> Settings.getInstance().windowPositionYProperty().set(newValue.doubleValue()));
leftPaneDivider.positionProperty().addListener((obs, oldVal, newVal) -> Settings.getInstance().windowDividerLeftProperty().set(newVal.doubleValue()));
contentPaneDivider.positionProperty().addListener((obs, oldVal, newVal) -> Settings.getInstance().windowDividerContentProperty().set(newVal.doubleValue()));
}

private void registerListeners(Stage primaryStage) {
Expand All @@ -146,16 +159,13 @@ private void registerListeners(Stage primaryStage) {
primaryStage.setY(Settings.getInstance().windowPositionYProperty().get());
primaryStage.setWidth(Settings.getInstance().windowWidthProperty().get());
primaryStage.setHeight(Settings.getInstance().windowHeightProperty().get());
leftPaneDivider.setPosition(Settings.getInstance().windowDividerLeftProperty().get());
contentPaneDivider.setPosition(Settings.getInstance().windowDividerContentProperty().get());
registerWindowBoundsListeners(primaryStage);


// Hack to make sure that the window is shown with correct size before setting the dividers
ThreadHelper.invokeLater(() -> {
leftPaneDivider.setPosition(Settings.getInstance().windowDividerLeftProperty().get());
contentPaneDivider.setPosition(Settings.getInstance().windowDividerContentProperty().get());
registerLayoutListeners(primaryStage);
}, 200);
Platform.runLater(() -> {
SplitPaneDividerPersistence.install(motionSplitPane, 0, Settings.getInstance().windowDividerLeftProperty());
SplitPaneDividerPersistence.install(contentSplitPane, 0, Settings.getInstance().windowDividerContentProperty());
SplitPaneDividerPersistence.install(contentSplitPane, 1, Settings.getInstance().windowDividerInspectorProperty());
});
});

primaryStage.setOnCloseRequest(event -> {
Expand All @@ -179,24 +189,18 @@ private void createContentPane() {
contentSplitPane = new SplitPane();
contentSplitPane.setMinWidth(200);
contentSplitPane.setOrientation(Orientation.HORIZONTAL);
contentSplitPane.getItems().addAll(leftSplitPane, contentPanel);
contentSplitPane.getItems().addAll(motionSplitPane, contentPanel);
SplitPane.setResizableWithParent(contentSplitPane, false);

contentPaneDivider = contentSplitPane.getDividers().get(0);
contentPaneDivider.setPosition(Settings.getInstance().windowDividerContentProperty().get());
new InspectorPane(contentSplitPane);
}

private void createLeftPane() {
leftSplitPane = new SplitPane();
leftSplitPane.setStyle("-fx-border-color: transparent;");

leftSplitPane.setMinWidth(200);
leftSplitPane.setOrientation(Orientation.VERTICAL);
leftSplitPane.getItems().addAll(new MachineStatusPane(), new JogPane());
SplitPane.setResizableWithParent(leftSplitPane, false);
motionSplitPane = new SplitPane();

leftPaneDivider = leftSplitPane.getDividers().get(0);
leftPaneDivider.setPosition(Settings.getInstance().windowDividerLeftProperty().get());
motionSplitPane.setOrientation(Orientation.VERTICAL);
motionSplitPane.getItems().addAll(new MachineStatusPane(), new JogPane());
motionSplitPane.setMinWidth(200);
SplitPane.setResizableWithParent(motionSplitPane, false);
}

private void registerShortCuts(Scene scene) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
Copyright 2026 Joacim Breiler

This file is part of Universal Gcode Sender (UGS).

UGS 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.

UGS 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 UGS. If not, see <http://www.gnu.org/licenses/>.
*/
package com.willwinder.universalgcodesender.fx.actions;

import com.willwinder.universalgcodesender.i18n.Localization;
import javafx.event.ActionEvent;

import javax.swing.SwingUtilities;

/**
* Base class for design toolbar commands that reuse an existing Swing based design action that
* opens a modal Swing dialog (such as importing a file or inserting clipart). Unlike the other
* designer actions, which mutate the model on the JavaFX thread (see {@link AbstractDesignEditAction}
* and {@link AbstractDesignToolAction}), the wrapped action here must be dispatched on the Swing
* event dispatch thread because that is where its dialog has to run. The model mutation that follows
* the dialog is serialized behind the modal dialog, so it does not overlap the FX-thread gestures.
*/
public abstract class AbstractDesignCommandAction extends BaseAction {

private final transient javax.swing.Action delegate;

protected AbstractDesignCommandAction(javax.swing.Action delegate, String title, String icon) {
super(title, title, Localization.getString("actions.category.designer"), icon);
this.delegate = delegate;
}

@Override
public void handleAction(ActionEvent event) {
SwingUtilities.invokeLater(() -> delegate.actionPerformed(null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2026 Joacim Breiler

This file is part of Universal Gcode Sender (UGS).

UGS 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.

UGS 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 UGS. If not, see <http://www.gnu.org/licenses/>.
*/
package com.willwinder.universalgcodesender.fx.actions;

import com.willwinder.ugs.designer.logic.Controller;
import com.willwinder.ugs.designer.logic.ControllerFactory;
import com.willwinder.universalgcodesender.i18n.Localization;
import javafx.application.Platform;
import javafx.event.ActionEvent;

/**
* Base class for the context aware design edit actions (undo/redo/copy/paste/delete) that operate
* directly on the designer {@link Controller}, in the same spirit as {@link AbstractDesignToolAction}.
* <p>
* The action body runs on the JavaFX thread, which is the thread that owns the designer model in
* the FX visualizer: the draw-gesture handlers mutate the drawing on the JavaFX thread and the
* visualizer rebuilds the scene graph from it on the JavaFX thread. Running the edits on the same
* thread keeps every mutation and the resulting scene refresh serialized and race free. The JavaFX
* {@link #enabledProperty()} is updated by the subclasses as the relevant designer state (the
* current selection or the undo history) changes.
*/
public abstract class AbstractDesignEditAction extends BaseAction {

protected final transient Controller controller;

protected AbstractDesignEditAction(String title, String icon) {
super(title, title, Localization.getString("actions.category.designer"), icon);
this.controller = ControllerFactory.getController();
}

@Override
public void handleAction(ActionEvent event) {
// Mutate the designer model on the JavaFX thread so the visualizer refreshes immediately
Platform.runLater(this::performAction);
}

/**
* Performs the action on the designer model. Always invoked on the JavaFX thread.
*/
protected abstract void performAction();

/**
* Updates the enabled state of the action on the JavaFX thread.
*/
protected void setEnabledLater(boolean enabled) {
Platform.runLater(() -> enabledProperty().set(enabled));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2026 Joacim Breiler

This file is part of Universal Gcode Sender (UGS).

UGS 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.

UGS 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 UGS. If not, see <http://www.gnu.org/licenses/>.
*/
package com.willwinder.universalgcodesender.fx.actions;

import com.willwinder.ugs.designer.actions.AbstractDesignAction;
import com.willwinder.ugs.designer.entities.entities.selection.SelectionEvent;
import com.willwinder.ugs.designer.entities.entities.selection.SelectionListener;

/**
* Base class for the design operation actions, both the alignment actions (align left, top, ...)
* and the geometric operations (union, subtract, group, ...). Each of them reuses the existing
* Swing based {@link AbstractDesignAction} that already implements the operation against the
* designer model.
* <p>
* The wrapped action mutates the drawing directly without opening a dialog, so — like the other
* context aware edit actions (see {@link AbstractDesignEditAction}) — it is dispatched on the
* JavaFX thread that owns the designer model in the FX visualizer.
* <p>
* The enabled state is delegated to the wrapped action, which already knows the selection rules
* for the operation (alignment needs more than one selected entity, a union needs at least two,
* a break apart needs a single compound path, ...). Each selection change is forwarded to the
* delegate so its enabled state is recomputed before it is mirrored onto the JavaFX
* {@link #enabledProperty()}.
*/
public abstract class AbstractDesignOperationAction extends AbstractDesignEditAction implements SelectionListener {

private final transient AbstractDesignAction delegate;

protected AbstractDesignOperationAction(AbstractDesignAction delegate, String title, String icon) {
super(title, icon);
this.delegate = delegate;
controller.getSelectionManager().addSelectionListener(this);
enabledProperty().set(delegate.isEnabled());
}

@Override
protected void performAction() {
delegate.actionPerformed(null);
}

@Override
public void onSelectionEvent(SelectionEvent selectionEvent) {
if (delegate instanceof SelectionListener selectionListener) {
selectionListener.onSelectionEvent(selectionEvent);
}
setEnabledLater(delegate.isEnabled());
}
}
Loading
Loading