Skip to content

feat: add rotatable panel orientation - #342

Merged
knipknap merged 9 commits into
mainfrom
feat/panel-orientation
Aug 15, 2026
Merged

feat: add rotatable panel orientation#342
knipknap merged 9 commits into
mainfrom
feat/panel-orientation

Conversation

@knipknap

@knipknap knipknap commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Ports the rotatable workspace orientation from PR #334 with the document model in WORLD space and the canvas presenting PANEL space.

  • MachinePanel exposes both WORLD (native) and PANEL (rotated) transforms; the encoder stays native WORLD→MACHINE
  • panel_orientation device-profile field and orientation controls in hardware settings + machine wizard
  • 2D canvas presents PANEL space: document content rotates via the layer transform, machine markers project machine→PANEL
  • 3D preview threads the world_to_panel rotation so ops, models, laser beams, and no-go zones follow the panel presentation
  • Rejects rotary layers under a rotated panel with a user-actionable pipeline error

Co-authored-by: Paulo Borges @pborges

@knipknap

Copy link
Copy Markdown
Contributor Author

@pborges so after many refactoring commits before this (which are already in main) I ported your PR to the now cleaned up latest version of main.

Due to the cleanups in main, the PR is now a lot smaller. I think I also fixed some bugs (WCS line direction and Ops positioning in the 3D canvas).

Can you please test? Does this work?

Port the rotatable panel orientation with the document model in WORLD
space and the canvas presenting PANEL space.

- MachinePanel now exposes both WORLD (native) and PANEL (rotated)
  transforms; the encoder stays native WORLD->MACHINE.
- Add panel_orientation to device profiles and orientation controls to
  the hardware settings and machine wizard.
- The 2D canvas presents PANEL space: document content rotates via the
  layer transform and machine markers project machine->PANEL.
- The 3D preview threads the world_to_panel rotation so ops, models,
  laser beams, and no-go zones follow the panel presentation.
- Reject rotary layers under a rotated panel with a user-actionable
  pipeline error.

Co-authored-by: Paulo Borges <pborges475@gmail.com>
@pborges

pborges commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

I sure can! Sorry I didn't get a notification for this tag, but I will list out some manual test cases and their results tonight

@pborges

pborges commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Manual Test Plan — Panel Orientation (LightBurn-style X/Y Swap)

Branch: feat/panel-orientation
Feature: Machine → Hardware → Panel Orientation (Native / Rotate Left /
Rotate Right) lets a physically portrait or landscape bed be designed and
jogged as if it were rotated 90°, without changing the underlying document
or the native machine coordinate mapping. Rotation is purely a
presentation transform composed on top of the existing origin-corner /
axis-reversal settings; the world→machine G-code mapping is untouched.

Use a real machine with a non-square bed (X extent ≠ Y extent) for
these tests — a square bed will hide swap bugs because width/height
mistakes are invisible.

Scope: hardware-only checks. UI-only items (settings screens, canvas
rendering, wizard, profile export/import) are intentionally excluded —
this list only covers things that require the physical machine, with
two exceptions (§2 — a confirmed bug, and §3 — suspected instances of
the same bug class — tracked here since they block sign-off). No
camera or rotary module — those are excluded too.

Safety: for every new orientation, first jog cautiously off the corners
and confirm direction before running any cut. Keep a hand near the
e-stop for the first job in each orientation, and start with reduced
power/speed on scrap material.


1. Jogging & Homing

For each orientation (Native, Rotate Left, Rotate Right):

  • Jog "north/south/east/west" buttons move the head in the
    direction that matches the on-screen bed orientation, not the
    raw native machine axes. Verified — correct in all three
    orientations (Native, Rotate Left, Rotate Right).
  • Jog distance is physically correct (e.g. a 10mm jog moves 10mm).
    Verified — correct in all three orientations.
  • Reported head position on-screen tracks the physical head
    correctly while jogging (no lag, no axis swap in the readout).
    Verified working.
  • Homing behaves correctly and the reported position after homing
    matches the configured origin corner as displayed. Verified —
    "Home All" lands at the correct displayed origin corner in every
    orientation.
    Noted expected (not buggy) behavior: the
    per-axis "Home X" / "Home Y" buttons home the machine's
    native physical axis (tied to that motor's limit switch — can't
    be rotated), so e.g. "Home X" while rotated visibly sends the
    laser "north" on screen. This is correct/intentional, distinct
    from the presented-space jog buttons in the first check above.
  • Ground-truth check: sent raw G0 X60 Y60 via the Console
    (bypasses the panel/UI layer entirely — native machine
    coordinates). Verified — landed at the physical spot the UI
    labels "60, 60"
    in the active orientation. Confirms the
    underlying WORLD↔MACHINE mapping and on-screen coordinate labels
    agree; the confirmed bugs so far are all in UI-side operations
    that skip the panel-rotation composition, not in the base
    coordinate mapping itself.

2. Canvas Keyboard Nudge (Arrow Keys)

🐛 Bug found in testing: arrow-key nudging of a selected workpiece
does not honor panel rotation — it moves along the raw native
axes instead of the visually-presented (rotated) direction. File this
against the branch and block sign-off until it's fixed; re-test once
resolved.

  • With a rotated orientation active (Rotate Left / Rotate Right),
    select a workpiece and press Up/Down/Left/Right — confirm it
    moves in the direction shown on screen, matching the rotated bed
    outline (not the machine's native axes).
  • Confirm Shift+Arrow (10x) and Ctrl/Cmd+Arrow (0.1x) nudge amounts
    also move in the correct rotated direction.

Likely root cause: WorkSurface.on_key_pressed in
rayforge/ui_gtk/canvas2d/surface.py:1497-1523 computes the nudge
delta and passes it straight to TransformCmd.nudge_items
(rayforge/doceditor/transform_cmd.py:100-136), which applies it
directly to the item's canonical world matrix — neither call goes
through machine.panel.get_world_to_panel_2d(). Contrast with
mouse-drag (rayforge/ui_gtk/canvas/canvas.pytransform.py:98-127),
which un-rotates the drag delta via the parent LayerElement's world
transform before applying it. The fix likely needs the arrow-key
handler to rotate (move_x, move_y) through the panel transform the
same way, before calling nudge_items.

3. Other Canvas Transform Operations (Suspected Same Bug Class)

Static analysis flagged these as sharing the arrow-key nudge's root
cause (writing to a DocItem's canonical WORLD-space matrix without
composing machine.panel.get_world_to_panel_2d()). Results below are
from live testing on this branch and override the code-level guess
where they disagree with it.

  • Flip Horizontal / Flip Vertical on a selected workpiece while
    rotated — verified working correctly. Mirrors along the axis
    shown on screen as expected, despite the code path looking
    suspect (transform_cmd.py:138-210). Not investigated further —
    may be that the reflection composes with the 90° rotation in a
    way that still lands correctly, or the code path isn't what's
    actually wired to the UI button. Not a bug.
  • Align Left / Align Right / Align Top / Align Bottom on a
    single workpiece while rotated.
    🐛 CONFIRMED BROKEN — does not snap to the screen edge the
    button implies. Matches the suspected cause:
    BboxAlignLeftStrategy in doceditor/layout/align.py aligns to
    raw WORLD x=0, never composing the panel rotation. Blocks
    sign-off until fixed.
  • "Move to Lower-Left / Center / Upper-Right" jog shortcut
    buttons
    (bottom panel, Move tab) while rotated.
    🐛 CONFIRMED BROKEN — does not honor panel rotation, same
    symptom as Flip/Align. Root cause: _on_move_to_position in
    rayforge/ui_gtk/doceditor/bottom_panel.py:543-573 converts the
    target corner via panel.world_point_to_machine(world_x, world_y)
    — the unrotated native delegate — instead of a rotation-aware
    one. Same class of bug as §2's arrow-key nudge. Blocks sign-off
    until fixed.
  • Center Horizontally / Center Vertically on a single item while
    rotated, on a non-square bed.
    🐛 CONFIRMED BROKEN — worse than plain Align: the H/V axis is
    flipped (same as Align), and the centering offset amount
    itself is wrong, not just misdirected. Matches the predicted
    cause: BboxAlignCenterStrategy mixes the already-rotated
    surface_width_mm with a raw WORLD-x target, so the numeric
    target is wrong on top of landing on the wrong axis. Blocks
    sign-off until fixed.
  • Spread Horizontally / Spread Vertically with 3+ items.
    ⚠️ Inconclusive, likely out of scope for this branch — tester
    reports the 3 selected items didn't move at all, including in
    Native orientation
    . Since it also fails with no rotation
    involved, this doesn't match the panel-rotation bug class being
    tracked here (Align/Center/move-to-corner all fail specifically
    because of rotation, not unconditionally). Tester notes they're
    still new to Rayforge and may not have triggered the feature
    correctly (e.g. selection state, minimum item count/spacing).
    Needs re-verification by someone familiar with the feature before
    filing as a real bug — if confirmed, it's likely a pre-existing
    issue unrelated to panel orientation, not a blocker for this
    branch's sign-off.
  • Transform sidebar numeric X/Y position fields while rotated.
    Verified — matches code analysis: "X" still moves the item
    north/south and "Y" still moves it east/west
    (native-axis
    semantics, not screen/panel direction), consistent with
    property_providers/transform.py reading/writing via the
    unrotated world_item_to_machine / machine_item_to_world
    delegates. Tester's call: not treating this as a bug
    "weird but expected," same reasoning as the per-axis Home X/Y
    buttons (§1) intentionally staying native-axis. Not a sign-off
    blocker, but worth a UX note to the team since it's inconsistent
    with every other rotation-aware surface (canvas drag, jog,
    Flip).

4. Origin Corner + Axis Reversal Combinations

  • Try at least one non-default Origin Corner (e.g. Top Left) with
    each Panel Orientation — jog to each corner and confirm direction
    is still correct. Verified. Tested native origin Bottom Left
    → Bottom Right while rotated: the on-screen origin marker
    relocated to the correct rotated corner, and jog directions
    stayed correct. North/south jog direction appeared "reversed"
    after the origin change — this is expected, not a bug: moving
    the origin to a different corner inherently flips which screen
    direction is "positive" for that axis, independent of rotation.
  • Try Reverse X-Axis and/or Reverse Y-Axis combined with a rotated
    orientation — verify jog direction and coordinate readback are
    still correct (this composition is easy to get backwards).
    ⚠️ Inconclusive, possibly out of scope, but flagged for safety.
    Tester (new to this feature) reports: after enabling a reverse
    axis and homing, the directional jog pad correctly grays out
    North and West (consistent with being homed into that corner),
    but pressing South or East — the directions the UI implies are
    safe — instead rams into the limit and produces a hard stop.

    Also reproduces in Native orientation with the reversed axis,
    so — same as the Spread finding — this doesn't look like a
    rotation-specific regression from this branch; more likely the
    jog-button sensitivity logic (jog_widget.py
    _update_limit_status / _can_jog_direction) doesn't correctly
    account for a reversed axis. Needs verification by someone
    familiar with the feature. Safety note: until this is
    understood, avoid free-jogging into unknown directions with a
    reverse axis enabled — enable soft limits first (§7) or step
    cautiously and watch for stalls, to avoid lost steps/mechanical
    stress from repeated hard stops. Confirmed: the hard stop put
    GRBL into an ALARM state (hard limit trigger), requiring
    unlock/re-home before further motion — re-home the machine before
    trusting any subsequent position-critical test (e.g. §5 Job
    Execution), since an alarm means GRBL's position reference may no
    longer be trusted.

5. Job Execution — G-code Correctness (critical)

  • Design a simple asymmetric shape (e.g. an "L" or a shape with a
    marked corner) near one edge of the design canvas.
  • Run/engrave it in Native orientation; note where it physically
    lands on the bed. Verified — landed and looked as expected.
  • Switch to Rotate Left, re-run the same design (same on-canvas
    position relative to the rotated bed outline), and confirm the
    physical cut lands in the equivalent bed location for that
    orientation (not shifted, mirrored, or off-bed). Verified —
    material left in place, re-run traced the previous shape
    perfectly.
    Confirms switching orientation doesn't move where
    the document's WORLD-space content physically cuts (expected,
    since orientation is purely a display/interaction transform and
    never touches the document data or the WORLD→MACHINE G-code
    mapping).
  • Repeat for Rotate Right. Verified — same result, traced
    perfectly.
  • Confirm cut/engrave dimensions (width vs. height) are not swapped
    or distorted in the physical output compared to the design.
    Verified — covered by the test shape used in Checks 1-3
    above: the "L" had a distinct short side and long side, and came
    out correct (not swapped/distorted) in all three orientations.
  • Multi-workpiece job: run a job with 2+ workpieces placed near
    different corners; confirm all land correctly in a rotated
    orientation. Covered by reasoning, not separately re-tested
    each workpiece maps through the same orientation-independent
    WORLD→MACHINE pipeline proven correct in Checks 1-3; no
    per-workpiece special-casing exists that rotation could break.

6. Framing (outline/boundary preview)

  • "Frame" a job (laser traces the bounding box at low power) in each
    orientation and confirm the traced rectangle matches the design's
    footprint and location on the physical bed. Verified
    actually tested (not just reasoned about): framed the §5 test
    shape in Rotate Right, traced correctly.

7. Soft Limits

  • With custom soft limits configured, confirm they still correctly
    constrain jogging/motion after switching to a rotated orientation
    (attempt a jog that should be blocked and confirm it is).
    Verified — set X=100 / Y=200 soft limits while rotated;
    jogging correctly stopped at the configured limits before
    reaching a hard stop. Reconfirms the recurring pattern: soft
    limit X/Y fields are native-axis (X still corresponds to
    north/south, Y to east/west on screen), consistent with the
    Transform sidebar fields (§3) and Home X/Y buttons (§1).

8. Work Coordinate Systems (WCS)

  • With multiple WCS configured (e.g. G54/G55), switch active WCS
    in each Panel Orientation, jog/go-to-zero, and confirm the head
    physically arrives at the expected rotated location each time.
    NOT TESTED — operator ignorance. Tester doesn't currently
    have a second WCS configured with a distinct offset and isn't
    familiar enough with the feature to set one up meaningfully for
    this pass. Needs a tester familiar with multi-WCS workflows
    before this can be signed off.

9. 3D Simulation vs. Real Machine

  • Run a job in each orientation and compare the 3D sim preview's
    laser head / gantry motion against the real machine in real time
    — motion path and head position should match throughout the run.
    🐛 CONFIRMED BROKEN — test could not be completed. Tester
    reports something wrong with the 3D sim viewport's camera
    translations, and confirmed it's a regression (not a pre-existing
    issue) by A/B testing against the main branch build of
    Rayforge, where it doesn't happen. Likely area: this branch's
    sim3d camera/viewport composition changes
    (rayforge/ui_gtk/sim3d/viewport.py,
    render_context/viewport.py, scene_presenter.py, and the
    renderers that now compose margin_shift @ world_to_panel
    instead of just margin_shift). Blocks sign-off until fixed and
    re-tested.

10. Regression Sweep (Native orientation only)

Run a quick smoke pass in Native orientation only, on real hardware,
to confirm nothing regressed for the default/common case:

  • Jogging and homing. Verified — no regression (exercised
    throughout §1).
  • Soft limits enforcement. Verified — no regression (exercised
    in §7).
  • A basic cut/engrave job end-to-end. Verified — no regression
    (exercised in §5, Check 1).

Sign-off

  • All applicable sections above passed on real hardware.
    NOT SIGNED OFF — full pass completed, 4 confirmed bugs block
    merge.
  • Any failures filed as follow-up issues with orientation + steps to
    reproduce. Not yet filed as separate issues — currently only
    tracked inline in this document.

Result summary (full pass complete)

🐛 Confirmed bugs — block sign-off:

  1. §2 — Arrow-key nudge doesn't honor panel rotation.
  2. §3 — Align Left/Right/Top/Bottom doesn't honor panel rotation.
  3. §3 — Center H/V doesn't honor panel rotation, and gets the offset
    amount wrong too.
  4. §3 — "Move to Lower-Left/Center/Upper-Right" jog shortcuts don't
    honor panel rotation.
  5. §9 — 3D sim viewport camera translation is wrong under rotation;
    confirmed as a regression via A/B test against main.

✅ Verified working (core physical pipeline is sound):

  • Jog direction/distance/readout, homing, in all 3 orientations (§1).
  • Ground-truth raw G-code vs. UI coordinate labels agree (§1).
  • Origin Corner × Orientation composition (§4).
  • Flip Horizontal/Vertical (§3).
  • Actual physical cut/engrave placement and dimensions in all 3
    orientations, single and (by reasoning) multi-workpiece (§5).
  • Framing (§6).
  • Soft limits under rotation (§7).
  • Full Native-orientation regression sweep (§10).

⚠️ Needs follow-up, not necessarily blocking:

  • §4 — Reverse-axis + hard-limit jog button sensitivity looks wrong,
    but reproduces in Native too — likely pre-existing/out of scope, but
    flagged for safety (real GRBL alarm triggered).
  • §3 — Spread H/V appeared to do nothing at all, including in Native —
    likely pre-existing/out of scope, needs a tester familiar with the
    feature to confirm.
  • §3 — Transform sidebar X/Y fields use native-axis semantics, not
    screen-relative — tester judged this acceptable/expected (consistent
    with Home X/Y and soft-limit fields), but it's inconsistent with
    every other rotation-aware surface and worth a design decision from
    the team.
  • §8 — WCS switching under rotation not tested (tester lacked a second
    configured WCS to test against).

@knipknap

Copy link
Copy Markdown
Contributor Author

Thank you for testing, I will work on porting the other functionality to the new PANEL coordinate system.

One thing I cannot reproduce: spread vertically / horizontally seems to work fine for me (when unrotated). How did you test this? You need a minimum of three workpieces selected, then they are placed equidistant.

@pborges

pborges commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Turns out I was "holding it wrong" I didn't realize the "spread" works within the extents of the selected objects, I had objects overlapping each other and expected them to align to a grid and spread out, once I manually spread them out, the alignment did work, chalk that up to operator ignorance.

However vertical and horizontal spreading do seem like they need to be translated.
i.e
Spread Horizontal (in rotate right) spreads the vertical axis, and vice versa

@knipknap

Copy link
Copy Markdown
Contributor Author

All fixes done. Can you confirm it is ready to merge?

@pborges

pborges commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Manual Retest Plan — Panel Orientation Fixes

1. Arrow-key nudging

Repeat in Rotate Left and Rotate Right:

  • Each arrow key moves the selected item 1 mm in the visible arrow
    direction, with no movement on the perpendicular screen axis.
  • Shift+Arrow moves 10 mm and Ctrl/Cmd+Arrow moves 0.1 mm in the
    visible arrow direction.
  • Opposite arrows return the item exactly to its starting position.
  • With multiple items selected, all move together without changing their
    relative positions.
  • Undo and redo restore the correct positions.

Result: Nudging worked correctly throughout the Rotate Left and Rotate
Right checks.

2. Align and center

Repeat in Rotate Left and Rotate Right, undoing after each command:

  • Align Left, Right, Top, and Bottom place a single item's visible edge
    on the corresponding displayed work-area edge.
  • Center Horizontally and Center Vertically place the item at the true
    center of the displayed non-square bed.
  • With multiple items selected, the Align commands make the requested
    visible edges coincide.
  • With multiple items selected, Center Horizontally gives the items
    the same displayed X center, placing them in a vertical stack, while
    Center Vertically gives them the same displayed Y center, placing
    them side by side.
  • Item size and orientation do not change.
  • Each command is one undoable action.

Finding: The Center commands initially appeared to be flipped because
Center Horizontally placed the items on top of one another and Center
Vertically placed them side by side. This matches the commands' axis-based
semantics: the former aligns horizontal/X centers and the latter aligns
vertical/Y centers. Recorded as expected behavior rather than a failure.

Result: Align and Center worked correctly in both rotated orientations.

3. Spread and flip

Repeat in both rotated orientations:

  • Select three workpieces with unequal horizontal gaps and choose
    Spread Horizontally. The outer items stay fixed and the visible
    edge-to-edge gaps become equal.
  • Undo, arrange them with unequal vertical gaps, and choose
    Spread Vertically. The outer items stay fixed and the visible gaps
    become equal.
  • Horizontal Flip mirrors an asymmetric item left-to-right on screen;
    Vertical Flip mirrors it top-to-bottom.
  • Flipping twice returns the item exactly to its original appearance and
    position.

Result: Spread and Flip worked correctly in both rotated orientations.

4. Move shortcuts on real hardware

These buttons move the head, not the workpiece. Do not fire the laser.
Place the selected item safely away from all limits.

Repeat in Rotate Left and Rotate Right:

  • Move to Lower-Left sends the physical head to the selected item's
    lower-left corner as displayed on screen.
  • Move to Center sends it to the displayed center.
  • Move to Upper-Right sends it to the displayed upper-right corner.
  • Repeat with multiple items selected; the buttons target the displayed
    corners and center of the collective selection bounds.
  • The reported and on-screen head positions agree with the physical head
    after every move.

Result — single item: Move to Lower-Left, Center, and Upper-Right reached
the expected displayed points in Rotate Left and Rotate Right. Reported,
on-screen, and physical head positions agreed.

Result — multiple items: The collective lower-left, center, and
upper-right targets worked correctly in both rotated orientations.

5. 3D simulation

Use the default WCS; no additional WCS configuration is required.

For Native, Rotate Left, and Rotate Right:

  • The 3D bed/grid matches the dimensions and orientation of the 2D
    canvas after resetting the view as described below.
  • Stock, workpieces, toolpaths, machine model, and work-area boundaries
    remain aligned with one another after the view reset.
  • Nothing is left in Native orientation or shifted away from the bed when
    switching to a rotated orientation after the view reset.
  • Resetting the view restores correct behavior. Observation: The 3D
    view initially did not respond correctly in either Native or rotated
    orientations. Choosing View → Top View, followed by
    View → Isometric View, corrected it; the 3D view then behaved
    correctly in Native and the tested rotated orientations.
  • Jog the physical head to three safe, asymmetric points. The 3D head,
    2D position, and physical head agree at each point.

Result — head tracking: Physical, 2D, and 3D head positions tracked
correctly at asymmetric points in Rotate Left and Rotate Right.

6. Quick regression and sign-off

  • In Native, smoke-test one nudge, Align, Center, Spread, and Flip.
  • Switch Native → Left → Right → Native without editing the document.
    Returning to Native restores the original view and item placement.
  • Repeat one formerly failing action after restarting Rayforge to confirm
    the saved orientation initializes correctly.

Result: The Native smoke test, orientation round-trip, and restart check
all passed. The saved orientation initialized correctly on this retest.

Result

  • PASS: All checks pass in both rotated orientations and the Native
    smoke test passes.
  • FAIL: Record the orientation, selected items, command, expected
    screen direction or point, actual result, and whether it also fails in
    Native.

Overall assessment: The panel-orientation behavior feels good. No issues
found in this retest block sign-off.

@pborges

pborges commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

I think it's ready to merge!

The last thing to keep in mind is when a sketch is added while the canvas is rotated, the newly added sketch
appears rotated by 90° on the canvas. This is slightly unexpected because the
sketch does not initially appear in the orientation in which it was created,
but it is not considered a bug or a sign-off blocker. It would be nice if a
new sketch initially appeared in its expected visual orientation on the
rotated canvas. It is unclear whether this behavior was present during the
first manual test pass.

@knipknap
knipknap merged commit 5c27bae into main Aug 15, 2026
29 checks passed
@knipknap

Copy link
Copy Markdown
Contributor Author

Thanks again for the thorough testing! I think the sketcher topic could be a follow up.

@pborges

pborges commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

I agree! Thanks again for this

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.

2 participants