Skip to content

Commit f89ba19

Browse files
authored
Update Printer Functionality (#3092)
Implemented Print Preview and "standard engineering drawing template" as part of printouts
1 parent 036e82e commit f89ba19

13 files changed

Lines changed: 1762 additions & 65 deletions

File tree

ugs-core/src/resources/MessagesBundle_en_US.properties

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -838,4 +838,27 @@ designer.panel.shape-settings.transform.title=Transform options
838838
designer.panel.shape-settings.cutting.title=Cutting options
839839
designer.panel.shape-settings.text.title=Text options
840840
designer.panel.shape-settings.rectangle.title=Rectangle options
841-
designer.panel.shape-settings.raster.title=Image options
841+
designer.panel.shape-settings.raster.title=Image options
842+
designer.print.preview.title=Print Preview
843+
designer.print.preview.page=Page {0}
844+
designer.print.preview.paper=Paper
845+
designer.print.preview.orientation=Orientation
846+
designer.print.preview.orientation.portrait=Portrait
847+
designer.print.preview.orientation.landscape=Landscape
848+
designer.print.preview.page-setup=Page Setup...
849+
designer.print.preview.offset.x=Offset X (mm)
850+
designer.print.preview.offset.y=Offset Y (mm)
851+
designer.print.preview.scale=Scale
852+
designer.print.preview.author=Author
853+
designer.print.preview.fit-to-page=Fit to single page
854+
designer.print.preview.include-template=Include engineering template
855+
designer.print.preview.include-template.tooltip.multipage=The engineering template is only added when the design fits on a single page.
856+
designer.print.preview.print=Print...
857+
designer.print.preview.cancel=Cancel
858+
designer.print.page.disable=Disable
859+
designer.print.page.disable.tooltip.last=At least one page must remain enabled.
860+
designer.print.template.file=FILE
861+
designer.print.template.author=AUTHOR
862+
designer.print.template.scale=SCALE
863+
designer.print.template.dimensions=DIMENSIONS
864+
designer.print.template.date=DATE
Lines changed: 147 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2025 Damian Nikodem
2+
Copyright 2025-2026 Damian Nikodem
33
44
This file is part of Universal Gcode Sender (UGS).
55
@@ -18,21 +18,32 @@ This file is part of Universal Gcode Sender (UGS).
1818
*/
1919
package com.willwinder.ugs.designer.actions;
2020

21+
import com.willwinder.ugs.designer.entities.entities.Entity;
22+
import com.willwinder.ugs.designer.entities.entities.controls.GridControl;
23+
import com.willwinder.ugs.designer.gui.Drawing;
2124
import com.willwinder.ugs.designer.logic.Controller;
2225
import com.willwinder.ugs.designer.logic.ControllerFactory;
26+
import com.willwinder.ugs.designer.print.DesignPrintable;
27+
import com.willwinder.ugs.designer.print.EngineeringTemplateRenderer;
28+
import com.willwinder.ugs.designer.print.PrintConfig;
29+
import com.willwinder.ugs.designer.print.PrintPreviewDialog;
30+
import com.willwinder.universalgcodesender.model.BackendAPI;
31+
import com.willwinder.universalgcodesender.model.UnitUtils;
32+
import com.willwinder.universalgcodesender.services.LookupService;
2333
import com.willwinder.universalgcodesender.utils.SvgIconLoader;
2434

2535
import javax.swing.JOptionPane;
26-
import java.awt.Graphics;
36+
import java.awt.Color;
2737
import java.awt.Graphics2D;
28-
import java.awt.Point;
38+
import java.awt.RenderingHints;
2939
import java.awt.event.ActionEvent;
30-
import java.awt.geom.Point2D;
40+
import java.awt.geom.AffineTransform;
41+
import java.awt.geom.Rectangle2D;
3142
import java.awt.image.BufferedImage;
3243
import java.awt.print.PageFormat;
33-
import java.awt.print.Printable;
3444
import java.awt.print.PrinterException;
3545
import java.awt.print.PrinterJob;
46+
import java.time.LocalDate;
3647

3748
/**
3849
* @author Damian Nikodem
@@ -41,8 +52,11 @@ public class PrintDesignAction extends AbstractDesignAction {
4152
public static final String SMALL_ICON_PATH = "img/print.svg";
4253
public static final String LARGE_ICON_PATH = "img/print24.svg";
4354

44-
private final static double MM_IN_INCH = 0.0393700787401575; // 5 / 127
45-
private final static double PRINT_SYSTEM_DPI = 72.0;
55+
private static final double MM_IN_INCH = 0.0393700787401575; // 5 / 127
56+
private static final double PRINT_SYSTEM_DPI = 72.0;
57+
private static final double MM_PER_POINT = 25.4 / 72.0;
58+
59+
private String filenameHint = "";
4660

4761
public PrintDesignAction() {
4862
putValue("iconBase", SMALL_ICON_PATH);
@@ -52,78 +66,151 @@ public PrintDesignAction() {
5266
putValue(NAME, "Print");
5367
}
5468

55-
private int centerPos(double pageSize, double imageWidth) {
56-
int result = 0;
57-
if ((imageWidth / pageSize) < 1.0) {
58-
result = (int) ((pageSize - imageWidth) / 2.0);
59-
}
60-
return result;
61-
}
62-
63-
private Point centerPoint(PageFormat pageFormat, BufferedImage bi) {
64-
return new Point(
65-
centerPos(pageFormat.getImageableWidth(), bi.getWidth()),
66-
centerPos(pageFormat.getImageableHeight(), bi.getHeight())
67-
);
68-
}
69-
70-
private boolean translateGraphicsForPage(PageFormat pageFormat, BufferedImage bi, int pageIndex, Graphics2D g2d) {
71-
int xPageCount = (int) Math.ceil(bi.getWidth() / pageFormat.getImageableWidth());
72-
int yPageCount = (int) Math.ceil(bi.getHeight() / pageFormat.getImageableHeight());
73-
74-
int totalPages = xPageCount * yPageCount;
75-
76-
if (pageIndex >= totalPages) {
77-
return false;
78-
}
79-
80-
int yPageIndex = (int) Math.floor(pageIndex / xPageCount);
81-
int xPageIndex = pageIndex % xPageCount;
82-
83-
g2d.translate(pageFormat.getImageableX() - (xPageIndex * pageFormat.getImageableWidth()),
84-
pageFormat.getImageableY() - (yPageIndex * pageFormat.getImageableHeight()));
85-
// Page is valid
86-
return true;
69+
/**
70+
* Platform wrappers set this so the printed title block can show the current file's name.
71+
* The core {@link Controller} has no file-path concept of its own.
72+
*/
73+
public void setFilenameHint(String filenameHint) {
74+
this.filenameHint = filenameHint == null ? "" : filenameHint;
8775
}
8876

8977
@Override
9078
public void actionPerformed(ActionEvent e) {
91-
// Render image first.
9279
Controller controller = ControllerFactory.getController();
9380
controller.getSelectionManager().clearSelection();
9481

9582
PrinterJob job = PrinterJob.getPrinterJob();
96-
double oldScale = controller.getDrawing().getScale();
97-
Point2D.Double oldPos = controller.getDrawing().getPosition();
98-
99-
// Reset Scale so it draws properly.
100-
controller.getDrawing().setScale(MM_IN_INCH * PRINT_SYSTEM_DPI);
83+
PageFormat pageFormat = job.defaultPage();
84+
pageFormat.setOrientation(PageFormat.LANDSCAPE);
10185

102-
BufferedImage bi = controller.getDrawing().getImage();
86+
PrintConfig config = buildConfig(controller, pageFormat);
87+
BufferedImage bi = renderSourceImage(controller, config);
88+
config.setSourceImageSize(bi.getWidth(), bi.getHeight());
10389

104-
controller.getDrawing().setScale(oldScale);
105-
controller.getDrawing().setPosition(oldPos.x, oldPos.y);
90+
EngineeringTemplateRenderer templateRenderer = new EngineeringTemplateRenderer();
91+
DesignPrintable printable = new DesignPrintable(bi, config, templateRenderer);
10692

107-
job.setPrintable((Graphics graphics, PageFormat pageFormat, int pageIndex) -> {
108-
Graphics2D g2d = (Graphics2D) graphics;
93+
PrintPreviewDialog dialog = new PrintPreviewDialog(controller.getDrawing(), printable, job);
94+
dialog.setVisible(true);
95+
if (!dialog.isConfirmed()) {
96+
return;
97+
}
10998

110-
if (!translateGraphicsForPage(pageFormat, bi, pageIndex, g2d)) {
111-
return Printable.NO_SUCH_PAGE;
99+
job.setPrintable(printable, config.getPageFormat());
100+
if (job.printDialog()) {
101+
try {
102+
job.print();
103+
} catch (PrinterException exc) {
104+
JOptionPane.showMessageDialog(null, "Error Printing: " + exc.getMessage(), "Error Printing!",
105+
JOptionPane.ERROR_MESSAGE);
112106
}
107+
}
108+
}
113109

114-
Point imagePosition = centerPoint(pageFormat, bi);
115-
g2d.drawImage(bi, null, imagePosition.x, imagePosition.y);
110+
private PrintConfig buildConfig(Controller controller, PageFormat pageFormat) {
111+
PrintConfig config = new PrintConfig();
112+
config.setPageFormat(pageFormat);
113+
config.setFilename(filenameHint);
114+
config.setAuthor(System.getProperty("user.name", ""));
115+
config.setDate(LocalDate.now());
116+
config.setIncludeTemplate(true);
117+
118+
BackendAPI backend = LookupService.lookup(BackendAPI.class);
119+
if (backend != null && backend.getSettings() != null
120+
&& backend.getSettings().getPreferredUnits() != null) {
121+
config.setDimensionsUnits(backend.getSettings().getPreferredUnits());
122+
} else {
123+
config.setDimensionsUnits(UnitUtils.Units.MM);
124+
}
116125

117-
return Printable.PAGE_EXISTS;
118-
});
126+
Rectangle2D designBounds = controller.getDrawing().getRootEntity().getBounds();
127+
config.setDesignWidthMm(designBounds.getWidth());
128+
config.setDesignHeightMm(designBounds.getHeight());
129+
return config;
130+
}
119131

120-
if (job.printDialog()) {
132+
/**
133+
* Renders the current design to a {@link BufferedImage} at printer DPI.
134+
*
135+
* <p>Bypasses {@link Drawing#getImage()} because that method derives both image size and
136+
* transform from {@code globalRoot.getBounds()}, which unions with empty controls anchored at
137+
* the origin and silently clips designs whose minX/minY are non-zero. Here we own the bounds
138+
* decision: when the design fits on a single page the image becomes page-sized with the
139+
* design centered; otherwise the image matches the design bounds, so the grid clips and the
140+
* page count is driven purely by the drawing extent.
141+
*/
142+
private BufferedImage renderSourceImage(Controller controller, PrintConfig config) {
143+
Drawing drawing = controller.getDrawing();
144+
PageFormat pageFormat = config.getPageFormat();
145+
double oldScale = drawing.getScale();
146+
GridControl gridControl = drawing.getGridControl();
147+
148+
double pageWidthMm = pageFormat.getImageableWidth() * MM_PER_POINT;
149+
double pageHeightMm = pageFormat.getImageableHeight() * MM_PER_POINT;
150+
Rectangle2D designBounds = drawing.getRootEntity().getBounds();
151+
Rectangle2D targetBounds = computeTargetBounds(designBounds, pageWidthMm, pageHeightMm);
152+
153+
double scale = MM_IN_INCH * PRINT_SYSTEM_DPI;
154+
int imgWidth = Math.max(1, (int) Math.round(targetBounds.getWidth() * scale));
155+
int imgHeight = Math.max(1, (int) Math.round(targetBounds.getHeight() * scale));
156+
157+
BufferedImage bi = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);
158+
Graphics2D g = bi.createGraphics();
159+
try {
160+
g.setColor(Color.WHITE);
161+
g.fillRect(0, 0, imgWidth, imgHeight);
162+
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
163+
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
164+
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
165+
166+
// GridControl.drawLargeGridAndText reads getClipBounds() to skip off-image grid lines;
167+
// it crashes if no clip is set. Set the clip in pixel space; once we apply the transform
168+
// the clip becomes the design-space rectangle that maps to the full image.
169+
g.setClip(0, 0, imgWidth, imgHeight);
170+
171+
// Map design (minX, minY) → pixel (0, imgHeight) and (maxX, maxY) → (imgWidth, 0).
172+
AffineTransform tx = new AffineTransform();
173+
tx.scale(1, -1);
174+
tx.translate(0, -imgHeight);
175+
tx.scale(scale, scale);
176+
tx.translate(-targetBounds.getMinX(), -targetBounds.getMinY());
177+
g.setTransform(tx);
178+
179+
drawing.setScale(scale);
121180
try {
122-
job.print();
123-
} catch (PrinterException exc) {
124-
JOptionPane.showMessageDialog(null, "Error Printing: " + exc.getMessage(), "Error Printing!", JOptionPane.ERROR_MESSAGE);
181+
gridControl.setPrintBoundsOverride(targetBounds);
182+
try {
183+
gridControl.render(g, drawing);
184+
} finally {
185+
gridControl.setPrintBoundsOverride(null);
186+
}
187+
for (Entity entity : drawing.getRootEntity().getChildren()) {
188+
entity.render(g, drawing);
189+
}
190+
} finally {
191+
drawing.setScale(oldScale);
192+
drawing.refresh();
125193
}
194+
} finally {
195+
g.dispose();
126196
}
197+
return bi;
127198
}
128199

200+
private static Rectangle2D computeTargetBounds(Rectangle2D designBounds, double pageWidthMm, double pageHeightMm) {
201+
double designW = designBounds.getWidth();
202+
double designH = designBounds.getHeight();
203+
if (designW <= pageWidthMm && designH <= pageHeightMm) {
204+
// Single page — center a page-sized rectangle on the design's center so the design
205+
// ends up centered within the rendered image.
206+
double cx = (designBounds.getMinX() + designBounds.getMaxX()) / 2.0;
207+
double cy = (designBounds.getMinY() + designBounds.getMaxY()) / 2.0;
208+
return new Rectangle2D.Double(
209+
cx - pageWidthMm / 2.0, cy - pageHeightMm / 2.0, pageWidthMm, pageHeightMm);
210+
}
211+
// Multi-page — image matches design exactly so the grid clips at design bounds and the
212+
// page count follows the drawing's actual extent.
213+
return new Rectangle2D.Double(
214+
designBounds.getMinX(), designBounds.getMinY(), designW, designH);
215+
}
129216
}

ugs-designer/src/main/java/com/willwinder/ugs/designer/entities/entities/controls/GridControl.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,22 @@ public class GridControl extends AbstractEntity implements Control {
4646
public static final int LARGE_GRID_SIZE = 100;
4747
public static final int SMALL_GRID_SIZE = 20;
4848
private final Controller controller;
49+
private transient Rectangle2D printBoundsOverride;
4950

5051
public GridControl(Controller controller) {
5152
this.controller = controller;
5253
}
5354

55+
/**
56+
* When non-null, {@link #render} uses the supplied rectangle to size the grid instead of the
57+
* design's content bounds. This makes the grid extend to page/template edges during printing.
58+
* Must be called on the EDT; the caller is responsible for clearing it in a {@code finally}
59+
* block so the on-screen rendering behaviour stays unchanged.
60+
*/
61+
public void setPrintBoundsOverride(Rectangle2D bounds) {
62+
this.printBoundsOverride = bounds;
63+
}
64+
5465
private static void drawZeroLines(Graphics2D graphics, Drawing drawing, int startPosX, int startPosY, int endPosX, int endPosY) {
5566
// Draw zero lines
5667
graphics.setStroke(new BasicStroke((float) (0.5 / drawing.getScale())));
@@ -117,7 +128,9 @@ public void render(Graphics2D graphics, Drawing drawing) {
117128
double gridSize = LARGE_GRID_SIZE;
118129

119130

120-
Rectangle2D bounds = controller.getDrawing().getRootEntity().getBounds();
131+
Rectangle2D bounds = printBoundsOverride != null
132+
? printBoundsOverride
133+
: controller.getDrawing().getRootEntity().getBounds();
121134
double gridMargin = (gridSize * 100);
122135

123136

@@ -159,12 +172,17 @@ public Optional<Cursor> getHoverCursor() {
159172

160173
@Override
161174
public Shape getShape() {
162-
return new Rectangle();
175+
return printBoundsOverride != null ? (Shape) printBoundsOverride.clone() : new Rectangle();
163176
}
164177

165178
@Override
166179
public Shape getRelativeShape() {
167-
return new Rectangle();
180+
return printBoundsOverride != null ? (Shape) printBoundsOverride.clone() : new Rectangle();
181+
}
182+
183+
@Override
184+
public Rectangle2D getBounds() {
185+
return printBoundsOverride != null ? (Rectangle2D) printBoundsOverride.clone() : super.getBounds();
168186
}
169187

170188
@Override

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,14 @@ public class Drawing extends JPanel implements ISnapToGridListener {
8080
private transient DropHandler dropHandler;
8181
private transient DropTarget dropTarget;
8282

83+
private final transient GridControl gridControl;
84+
8385
public Drawing(Controller controller) {
8486
refreshThrottler = new Throttler(this::refresh, 1000);
8587

8688
globalRoot = new EntityGroup();
87-
globalRoot.addChild(new GridControl(controller));
89+
gridControl = new GridControl(controller);
90+
globalRoot.addChild(gridControl);
8891
globalRoot.addListener(event -> refreshThrottler.run());
8992

9093
entitiesRoot = new EntityGroup();
@@ -182,6 +185,10 @@ public EntityGroup getRootEntity() {
182185
return entitiesRoot;
183186
}
184187

188+
public GridControl getGridControl() {
189+
return gridControl;
190+
}
191+
185192
private void recursiveCollectEntities(Entity shape, List<Entity> result) {
186193
if (shape instanceof EntityGroup entityGroup) {
187194
List<Entity> shapes = entityGroup.getChildren();

0 commit comments

Comments
 (0)