Skip to content

feat: Start From (Absolute / Current Position / User Origin) with a 9-point job origin - #538

Closed
switch87 wants to merge 11 commits into
barebaric:mainfrom
switch87:feat/job-origin
Closed

switch87 wants to merge 11 commits into
barebaric:mainfrom
switch87:feat/job-origin

Conversation

@switch87

@switch87 switch87 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Add a configurable Start From for laser jobs: Absolute coordinates
(default), Current Position, or a User Origin, with a placement outline on the
work surface so you see where the job will be cut.

Fixes #518.

What's in this PR

  • Start From control in the machine panel: pick Absolute / Current
    Position / User Origin and one of 9 anchor points (corners + edges + center).
  • Placement outline on the work surface: while a non-absolute origin is
    active, a blue dashed rectangle shows where the job will land, following the
    head when "Current Position" is selected.
  • Absolute keeps the dwgs placement; switching back to it clears the
    outline and jobs are placed as before.
  • The job preview, the pre-flight checks and the generated G-code all use the
    selected origin.

Overview

The job origin is a document-level property (persisted in the project file),
so the same choice applies to every job prepared from the document.

Tests

  • tests/core/test_job_origin.py — model, defaults and roundtrip.
  • tests/machine/test_job_placement.py — placement math for the 9 anchors
    and both non-absolute modes.
  • tests/pipeline/test_job_origin_gcode.py — G-code reflects the origin.
  • tests/ui_gtk/canvas2d/test_job_placement_overlay.py — outline visibility
    and follow-the-head behaviour.
  • tests/pipeline/test_job_design_rect.py — design rect used for placing.

All targeted tests pass (pytest tests/core/test_job_origin.py tests/machine/test_job_placement.py tests/pipeline/test_job_origin_gcode.py tests/pipeline/test_job_design_rect.py → 56 passed).

@switch87

Copy link
Copy Markdown
Contributor Author

The Windows exe workflow is not failing to build. Its build job runs the whole pytest suite, and 6 tests fail — all of them in the new tests/pipeline/test_job_origin_gcode.py. 7530 tests pass, and the workflow is green on every other open PR branch and on main, so this is specific to this PR and not to the Windows toolchain.

The failure

All six are _assert_golden(...) calls comparing generated G-code by exact string equality. The first difference:

At index 4 diff: 'G0 X0.06 Y9.94' != 'G0 X0.055 Y9.945'

-     'G0 X0.055 Y9.945',      (golden)
+     'G0 X0.06 Y9.94',        (Windows)
-     'G1 X9.945 F3000',
+     'G1 X9.94 F3000',
-     'G1 Y0.055 F3000',
+     'G1 Y0.06 F3000',

Failing: test_absolute_is_unchanged, test_user_origin_center, test_user_origin_output_is_independent_of_offset, test_current_position_bottom_left, test_back_to_absolute_restores_output, test_send_current_position_runs_placed_job.

This is a value difference, not a formatting one

gcode_precision defaults to 3 (Machine.__init__). At precision 3 a value only prints as 0.06 if it really is ~0.06; 0.055 cannot come out of a different rounding of the same number. So the same input and the same code produce coordinates about 0.005 mm apart depending on platform.

I re-ran the file locally on Linux: 15 passed. So the goldens are correct here and were not committed stale — the divergence is real and it is in the placement/offset arithmetic, not in the tests.

0.005 mm is far too large to be float noise in a single operation, which makes me think something in the offset or inset path sits near a tolerance and lands on the other side of a branch on Windows. Worth a look on its own, quite apart from this PR: if job placement is not bit-reproducible between platforms then the placement outline shown in the UI and the coordinates actually sent to the machine can disagree slightly depending on where rayforge was built.

Options for the tests

Comparing G-code by exact string equality will keep biting here for as long as the numbers are not reproducible. Two ways out:

  1. Compare numerically. Parse the coordinate words out of both files and compare with pytest.approx, keeping the structural lines (comments, M-codes, ordering) as exact string checks. That still catches real regressions in what the origin feature does, and stops the tests from encoding one platform's float results.
  2. Find the non-determinism in the placement/offset math and pin it. That fixes the underlying issue rather than working around it, and is probably the better long-term answer — but it is a separate investigation from the feature itself.

My suggestion is (1) to get this PR green and (2) as its own follow-up, since (2) should not hold up a feature that is otherwise passing everywhere else.

I have not run the executable on Windows and cannot — I have no Windows machine or licence, so my confirmation here is the CI log plus the local Linux run above.

knipknap added a commit to switch87/rayforge that referenced this pull request Oct 12, 2026
The SVG trace strategy rendered at sqrt(VTRACER_PIXEL_LIMIT) = 1104 px.
With the 2 px border the tracer adds, the image handed to vtracer is
1108x1108 and exceeds the limit, so _handle_oversized_image downscaled
it - by a different factor per platform, because the limit was
platform-dependent (1_220_000 on Linux, 1_000_000 on Windows). The
resulting traced geometry differs by about 0.005 mm, which made the
job-origin golden G-code tests fail on the Windows build.

Use the conservative 1_000_000 limit on every platform, and size the
SVG render to fit under it including the border so that tracing no
longer downscales at all. Regenerate the job-origin goldens: they now
carry the exact 0.05 mm stroke half-width instead of a downscale
artifact.

Refs barebaric#538
@knipknap

Copy link
Copy Markdown
Contributor

Thanks for the PR! I fixed the failing test so in theory this could be ready to merge.

However, this PR worries me because it touches on one of the most critical and error prone pieces of key functionality, and there are many special cases to consider:

  • Negative axis mode
  • Machine origin is work area origin mode
  • Machine margin configuration
  • Rotary layers
  • Does the UI clearly communicate the effect on existing buttons and plugins, or would any of the existing features yield unexpected results? E.g. print and cut, zero buttons, etc.
  • ...

So before merging this to main I would like to see real user feedback - IMO this one requires careful testing on more than just one machine type and use case.

@switch87

Copy link
Copy Markdown
Contributor Author

Thanks for the tracing fix, and fair point: this touches the most critical path, so it should not land on the strength of a single bottom-left test machine. I can't provide real-machine feedback for other machine types either. What I could do was trace each special case through the code, pin it with tests, and make every combination I couldn't make correct refuse the job with an explanation instead of guessing. Pushed on top of your commit:

  • a746d09ff test: the default output is pinned against main
  • 2cff9a91f fix: rotated panels, layers with their own WCS, Print & Cut
  • 173383a9b docs: the limits in all doc languages

Absolute Coordinates (the default) is byte-identical to main. I generated the expected G-code on the merge base c9ac56606, before Start From existed, for six setups. The branch reproduces it byte for byte (tests/pipeline/test_job_origin_absolute_baseline.py).

Special case What Start From does Pinned by
Default (Absolute) on BL, TR + negative axes, TL/BR + "work area is origin" + margins, layer with own WCS, rotary layer Nothing: identical to main test_job_origin_absolute_baseline.py (6 goldens from main)
All four origin corners, with and without negative axes User Origin: the anchor lands on X0 Y0, and the design extends in the direction the machine's coordinates grow towards the top right of the canvas (literal table of 8 expected extents) test_job_origin_machines.py::test_user_origin_bottom_left_on_every_setup, ::test_user_origin_center_lands_on_zero_point
Machine origin = work area origin, with margins (every corner, ± negative axes) User Origin = the work area corner, i.e. the margin offset (signed for negative axes) same tests, workarea_origin=True (table WORKAREA_ZERO)
Current Position on every corner / negative axes / work-area origin The anchor lands on the reported head position. Checked in the G-code (head minus zero point) and in the world-space preview ops ::test_current_position_center_lands_on_head
Framing + return to start on negative axes The frame traces around the head, then the head is moved back to its start position in command coordinates ::test_frame_and_return_on_negative_axes, plus the existing test_job_origin_gcode.py frame/send tests
Rotated panel (Rotate Left/Right) Bug, fixed: the anchor was applied in world space, so on a rotated panel "top left" placed a different corner. It now maps through the panel rotation, for both the G-code and the outline marker ::test_rotated_panel_uses_anchor_as_seen_on_screen, test_job_placement_overlay.py::test_anchor_marker_is_where_chosen_on_screen
Layer with its own WCS (≠ active) Made safe: such a layer would be emitted relative to another zero point than the one the job is placed on, so User Origin / Current Position are now refused with a message naming the layer and WCS. This is skipped when the work area is the origin, because WCS offsets are not used then ::test_layer_with_own_wcs_is_refused, ::test_layer_naming_the_active_wcs_is_accepted, ::test_layer_wcs_is_ignored_when_workarea_is_origin
Rotary layers Refused in both non-absolute modes (already the case, now pinned for both). The Start From subtitle says why, and no outline is drawn ::test_rotary_layer_is_refused_in_every_mode, test_job_origin_rows.py::test_subtitle_explains_unsupported_layer, test_job_placement_overlay.py::test_unsupported_layer_shows_no_outline
Print & Cut Made safe: the alignment places the design on the printed marks, and a non-absolute Start From would move it off them again. Applying an alignment now switches back to Absolute with a notice test_print_and_cut_wizard.py::test_apply_switches_start_from_to_absolute
Zero / set-origin buttons, WCS switch They don't touch the job. With User Origin the G-code stays WCS-relative and the outline follows the new zero. With Current Position they have no effect on the placement existing test_user_origin_output_is_independent_of_offset, overlay tests
Machine extents / no-go zones A shifted job that leaves the travel or enters a zone is refused before send/frame existing test_send_refuses_job_beyond_machine_extents, test_frame_refuses_job_in_nogo_zone
Pointer alignment Current Position uses the pointer dot. The return move restores the beam position existing test_job_placement.py, frame tests

Still open (not covered by the tests above):

  • The 3D material-fold simulation on stock items still uses the unshifted placement. It's cosmetic and doesn't reach the G-code, but the burn preview on the stock is in the canvas position.
  • Non-GRBL drivers (Ruida, Marlin, Smoothie, OctoPrint) only go through the same transform path. I haven't checked their move_to semantics for the return-to-start move on real hardware.

I'm fine with this staying unmerged until people with other machines have tried the branch. Alternatively it could be merged behind a setting (e.g. an "experimental" toggle that hides the Start From rows and forces Absolute). Do you want it flagged as experimental that way, or kept on the branch until there is user feedback?

switch87 and others added 11 commits October 13, 2026 16:58
A document now carries a job origin: the Start From mode (Absolute
Coordinates, Current Position or User Origin) and one of nine anchor
points on the job's bounding box. It is saved with the project and
older projects load as Absolute Coordinates.

job_placement resolves it against a machine: User Origin is the zero
point of the active WCS (or the work area origin), Current Position is
the head position the controller reports (the pointer dot while pointer
alignment is on) and needs a connected, idle machine.

Refs barebaric#518
The job origin is applied as one rigid XY translation of the whole job
in world space, folded into the workpiece placement of the step
aggregates. Everything downstream (machine transform, encoder, 3D
preview, framing, sanity checks) sees the placed job, while the G-code
stays absolute in the active WCS: no G92 or G10 is emitted and nothing
is written to the controller.

- The anchor refers to the bounding box of the designs on the canvas
  (job_design_rect), not the toolpath with overscan or lead-ins.
- Current Position re-reads the head position for every job request,
  so send, frame and export never reuse a stale placement.
- Rotary layers only support Absolute Coordinates.
- MachineCmd refuses a placed job that leaves the machine extents or
  enters a no-go zone, and in Current Position mode moves the head back
  to the start position after framing or a finished job, because the
  dialect postscript usually returns to X0 Y0.
- workpiece_world_aabb used the size on top of the world transform,
  which already scales the unit square, so workpieces without vector
  geometry (images) got a far too large box.

Golden G-code tests cover every mode on GRBL.

Refs barebaric#518
The Control Panel gets a Start From row and a 3x3 Job Origin selector
under the WCS selector; changes are undoable and the subtitle shows
where the job will start. When the mode is not Absolute, the 2D canvas
draws a dashed outline of the placed job with a marker on its origin
point, following the head in Current Position mode.

Refs barebaric#518
The pre-flight check reused the cached job, which in Current Position
mode was placed where the head was when it was assembled. It now
assembles the job afresh in that mode.

Refs barebaric#518
The SVG trace strategy rendered at sqrt(VTRACER_PIXEL_LIMIT) = 1104 px.
With the 2 px border the tracer adds, the image handed to vtracer is
1108x1108 and exceeds the limit, so _handle_oversized_image downscaled
it - by a different factor per platform, because the limit was
platform-dependent (1_220_000 on Linux, 1_000_000 on Windows). The
resulting traced geometry differs by about 0.005 mm, which made the
job-origin golden G-code tests fail on the Windows build.

Use the conservative 1_000_000 limit on every platform, and size the
SVG render to fit under it including the border so that tracing no
longer downscales at all. Regenerate the job-origin goldens: they now
carry the exact 0.05 mm stroke half-width instead of a downscale
artifact.

Refs barebaric#538
Absolute Coordinates is the default and must not change any existing
setup. The expected G-code was generated on the merge base before Start
From existed (c9ac566) for a bottom-left bed, top-right with negative
axes, top-left and bottom-right with "machine origin is work area
origin" and margins, a layer with its own WCS, and a rotary layer. The
default job origin must reproduce it byte for byte.

Refs barebaric#518
- The Job Origin is picked on the canvas, but was applied in world
  space. On a rotated panel "top left" therefore placed another corner
  of the job on the start point. Map the anchor through the panel
  rotation, for the G-code and for the outline marker.
- A layer with its own WCS is emitted relative to another zero point
  than the one the job is placed on. Refuse User Origin and Current
  Position for such layers (unless the work area origin is the zero
  point, where WCS offsets are not used), as for rotary layers.
- Show why Start From is not available in its subtitle, and hide the
  placement outline instead of drawing one for a refused job.
- Applying a Print & Cut alignment places the design on the printed
  marks; switch Start From back to Absolute Coordinates with a notice
  so the job is not moved away from them.

Pin Start From on every origin corner, with and without negative axes
and with "machine origin is work area origin" plus margins: User Origin
and Current Position G-code, the preview placement, and framing with
the return to the start position.

Refs barebaric#518

@knipknap knipknap left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I tried to give this one a deeper review, but I am sorry that I gave up - this has severe architectural layer violations to the point that I have a hard time understanding what happens or explaining it properly. Some examples include: decentralizes coordinate space conversions, geometry math in the pipeline, artifacts from past refactoring, and other code quality issues. I am sorry but this is not mergeable.

If a feature like this lands, I would recommend going in tiny, iterative steps - starting from the core, e.g. introduce the new coordinate system with strictly defined terminology and well documented central conversion function first (which probably would already require some discussion), proving equivalence, then extending the data model, ... or something like this.

This PR touches several critical code paths at once, which would make any regressions extremely hard to pin down.

self._editor.notification_requested.send(
self,
message=_(
"Start From was set to \u201cAbsolute Coordinates\u201d "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not use \ sequences for UTF chars; all modern versions of Python support UTF8 natively

from raygeo.geo.types import Point

# A world-space bounding box as (min_x, min_y, max_x, max_y).
BBox = tuple[float, float, float, float]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raygeo has a Rect type for this

Comment thread rayforge/machine/cmd.py
artifact_store.release(handle)
raise

def _start_position_in_command_space(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coordinate space conversion is responsibility of coordspace.py

@switch87

Copy link
Copy Markdown
Contributor Author

Thanks for taking the time for a deep review, and fair enough: this touched too many critical paths at once, and the coordinate handling isn't where it belongs. I'm closing it. If the feature comes back, it will start small, as you suggest: first an issue to agree on the coordinate-system terminology and a single, documented conversion function, then a PR that only adds that and proves the output is unchanged, and the data model and UI after that, one step at a time.

@switch87
switch87 deleted the feat/job-origin branch October 15, 2026 03:47

This branch was successfully deployed

1 active deployment
pypi — 9314c2ed Deployed Oct 13, 2026 by switch87 via Build and Publish Wheel #2289
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Start From (Absolute / Current Position / User Origin) with a 9-point job origin

2 participants