Compare commits

..

No commits in common. "232f1119d972f3c08a3efa6444982cc62475827e" and "0d4e680e6f080f4834a436167a0e527a7b4a3a1b" have entirely different histories.

14 changed files with 4 additions and 989 deletions

1
.gitignore vendored
View file

@ -3,7 +3,6 @@ __pycache__/
.pytest_cache/ .pytest_cache/
.venv/ .venv/
.uv-cache/ .uv-cache/
/.build/
# Sensitive/raw acquisition data belongs in ThePirateRay-PrivateStash. # Sensitive/raw acquisition data belongs in ThePirateRay-PrivateStash.
/raw/ /raw/

View file

@ -97,11 +97,8 @@ project workspace or development destination.
- [Project charter](docs/project-charter.md) - [Project charter](docs/project-charter.md)
- [Product and research roadmap](docs/roadmap.md) - [Product and research roadmap](docs/roadmap.md)
- [MOPA workbench UX requirements](docs/design/mopa-workbench-ux.md)
- [Current Phase 0 status](docs/status/phase-0.md) - [Current Phase 0 status](docs/status/phase-0.md)
- [Open-source reuse landscape](docs/research/open-source-landscape.md) - [Open-source reuse landscape](docs/research/open-source-landscape.md)
- [LightBurn-to-Rayforge UX capability map](docs/research/lightburn-rayforge-ux-capability-map.md)
- [Rayforge layer visibility and output spike](docs/research/rayforge-layer-model-spike.md)
- [Proposed Rayforge/BalorMK reuse decision](docs/adr/0002-reuse-rayforge-and-balormk.md) - [Proposed Rayforge/BalorMK reuse decision](docs/adr/0002-reuse-rayforge-and-balormk.md)
- [Live USB enumeration](docs/research/live-usb-enumeration-2026-10-17.md) - [Live USB enumeration](docs/research/live-usb-enumeration-2026-10-17.md)
- [LightBurn device-export analysis](docs/research/lightburn-device-exports-2026-10-19.md) - [LightBurn device-export analysis](docs/research/lightburn-device-exports-2026-10-19.md)
@ -124,12 +121,6 @@ The checks verify manifest integrity, public/private evidence contracts,
disallowed binary types, private identifiers, local user paths, and other disallowed binary types, private identifiers, local user paths, and other
likely data leaks. They do not certify hardware safety. likely data leaks. They do not certify hardware safety.
Rayforge source and its Windows build stack are not vendored or downloaded by
these checks. The exact upstream revision is recorded in
`upstreams/rayforge.json`; run `scripts/prepare_rayforge_build.ps1` explicitly
to populate the ignored build cache. Dependency installation and bundle
creation require separate command switches.
## Safety boundary ## Safety boundary
- Simulation precedes equivalent hardware behavior. - Simulation precedes equivalent hardware behavior.

View file

@ -110,14 +110,6 @@ Subsequent offline evidence:
Rayforge's generated settings contract exposes frequency and pulse width, Rayforge's generated settings contract exposes frequency and pulse width,
preserves them through step serialization and supplies preview Ops without preserves them through step serialization and supplies preview Ops without
transport; packaged GTK interaction remains untested. transport; packaged GTK interaction remains untested.
- [LightBurn-to-Rayforge UX capability map](../research/lightburn-rayforge-ux-capability-map.md):
the casual-maker workflow maps mostly to existing Rayforge product seams;
the remaining UX gaps are narrow, testable extensions rather than evidence
for a separate desktop application.
- [Rayforge layer visibility and output spike](../research/rayforge-layer-model-spike.md):
canvas visibility and job participation are already independent serialized
states; LightBurn-style layer Output is a narrow aggregate UI concern, not a
new pipeline or document-model requirement.
These results support continuing toward complete list buffering and production These results support continuing toward complete list buffering and production
integration, but they do not yet satisfy every proof obligation or change this integration, but they do not yet satisfy every proof obligation or change this

View file

@ -1,311 +0,0 @@
# MOPA workbench UX requirements
- Status: Working product contract
- Recreated: 2026-10-20
- Scope: desktop application workflow; hardware-free through M1
## Purpose
The Pirate Ray should give a casual makerspace user a clear path from opening
a design to a validated job. It should feel capable without presenting machine
selection, controller configuration, lens data, or every galvo parameter as
the first decision.
This document records product behavior, not an application architecture. The
current reuse direction remains Rayforge plus the narrowest practical JCZ
integration. A prototype or mockup can illustrate these requirements, but it
does not become a second implementation and does not prove machine behavior.
This file was recreated because the UX document named by the repository
handoff was absent from the active checkout and had no Git history, branch, or
stash copy. The requirements below come from that handoff, the project charter
and roadmap, and the evidence-backed
[LightBurn-to-Rayforge capability map](../research/lightburn-rayforge-ux-capability-map.md).
## Product principles
1. **Start with the design.** Opening or importing artwork is the normal first
action. Choosing a controller, machine, or lens is an administrator task.
2. **Keep the canvas stable.** Panels may collapse and themes may change, but
the work-area geometry, artwork position, and semantic layer colors do not.
3. **Show the next useful decision.** Common layer settings are immediately
available; specialized settings appear only when the user's disclosure
level, operation, and active profile make them relevant.
4. **Preview before job control.** The planned geometry, enabled operations,
ordering, hatch, field boundary, and warnings are inspectable before any
physical action.
5. **Make uncertainty visible.** Unknown units, profile identity, correction,
lens compatibility, or parameter limits block encoding or execution rather
than receiving guessed defaults.
6. **Reuse instead of cloning.** Extend existing Rayforge concepts and UI
seams where possible. Do not recreate a canvas, geometry engine, material
database, or general LightBurn parser.
7. **Separate preparation from hardware.** Offline editing, preview, saving,
and simulation must remain useful when no controller backend is installed.
## Primary maker flow
The default flow is:
1. open or import a design;
2. inspect and arrange it on the machine-sized work area;
3. select an object or layer;
4. assign or edit the applicable operation settings;
5. optionally apply a compatible material recipe;
6. preview the planned path and operation order;
7. resolve preflight errors and review warnings;
8. save or export the portable project;
9. only when a separately supported hardware profile is active, enter the
gated framing and job-control flow.
The application must not force machine, controller, lens, or connection
selection into steps 1–6 when a configured default profile already exists.
## Workspace layout
The central canvas is the stable visual anchor. It represents the configured
work area and shows artwork, selection, field boundaries, planned paths, and
non-destructive overlays.
The right side contains independently collapsible panels in this order:
1. **Layers** — operation list, color, order, selection, visibility and output
state;
2. **Settings** — values applicable to the selected operation and current
disclosure level;
3. **Material Library** — compatible recipes and their provenance;
4. **Framing** — offline framing preview initially, physical framing only when
explicitly supported and gated.
**Job Control** remains fixed at the bottom rather than disappearing with the
other panels. In the offline product it shows simulation/no-device state and
must not imply that a controller is connected.
Collapsing panels may enlarge the available canvas viewport, but it must not
change document coordinates, work-area dimensions, geometry, layer colors, or
the meaning of the current selection.
## Layers
The layer list stays visible by default alongside the settings panel. Each
entry should expose, at minimum:
- semantic color and user-editable name;
- operation or mode;
- order;
- a visibility state for the canvas;
- a separate output-enabled state for preview and job planning;
- concise values for the most important applicable settings.
Visibility and output are independent. Hiding a layer must not silently
disable it, and disabling output must be visually distinct from hiding it.
Reordering operations must immediately affect preview order without moving
the source geometry.
The first supported operation types remain an open product decision. Until
that decision is reviewed, the UI must not imply support beyond operation
types already preserved end-to-end by tests.
## Settings and disclosure levels
The settings panel has three disclosure levels:
- **Beginner** — the smallest set needed for an ordinary, already-configured
job;
- **Advanced** — additional operation controls for a trained maker;
- **Expert** — specialized galvo, timing, diagnostic, and compatibility
details supported by the active profile.
Changing level reveals or hides rows; it must not replace the canvas, move the
panels into a different workflow, or reset values. Unsupported settings remain
absent rather than disabled with invented defaults.
Power, speed, passes, frequency, Q-pulse width, interval, hatch, scan angle,
and timing are candidate fields, not a promise that every operation exposes
all of them. Frequency and Q-pulse width require explicit units. Machine- and
source-specific ranges require verified profile evidence.
Exactly which fields belong in Beginner mode remains an open product decision.
The initial choice should be made from representative workshop jobs, not by
copying every field from LightBurn.
## Material library
Recipes are optional accelerators, not a mandatory wizard. A user can ignore
the library and edit job values directly.
A recipe can describe:
- material;
- thickness when relevant;
- operation or recipe variant, such as engraving or cutting;
- process settings with explicit units;
- source and validation status;
- compatible machine, source, lens, correction profile, and software schema;
- notes and revision information.
The library supports multiple collections:
- makerspace-shared;
- personal;
- optionally project-specific.
Personal collections are portable through import/export and require no cloud
account. Applying an incompatible or unverified recipe must produce an
explanation and must not silently override the active machine profile.
The evidence required for the label “makerspace-approved” remains an open
product decision. Exported LightBurn values and observed recipes are not, by
themselves, certified hardware limits or approved settings.
## Preview and preflight
Preview must work without a controller. It should show, where supported:
- field boundary and geometry;
- enabled versus disabled output layers;
- operation order;
- hatch or fill representation;
- an approximate path;
- warnings and blocking validation failures;
- estimated behavior only when the estimate's basis is explicit.
Preflight blocks at least:
- geometry outside the supported field;
- machine, source, lens, or correction-profile mismatch;
- unknown or ambiguous units;
- unsupported operations;
- invalid parameter values;
- incomplete MOPA import data required by the selected backend.
A warning must not be presented as a passed safety check. An approximate path
or time estimate must be labelled as approximate.
## Framing
Framing has two distinct meanings:
1. **Canvas framing preview** — an offline overlay showing the proposed bounds
or contour. This is safe to provide during M1 when clearly labelled as a
simulation.
2. **Physical galvo framing** — controller behavior that may move galvos or
activate a guide source. This remains unavailable until a separately
reviewed protocol authorizes the exact operation and safety conditions.
The UI must never present simulated framing as proof that the physical machine
has framed the job. `RequireFrameBeforeStart` is a profile observation and a
future policy input, not authorization to issue a framing command.
## Job control and states
Job Control remains visually separate from design editing. The interface must
make its current execution boundary explicit, for example:
- **Offline** — edit, save, validate and preview only;
- **Simulated** — run against a deterministic fake/no-device driver;
- **Blocked** — a named profile, validation, recovery, or evidence condition
prevents execution;
- **Hardware unavailable** — no reviewed production backend is reachable;
- future hardware states only after their protocols are reviewed.
The no-device workflow may expose Start Simulation, Hold, Resume, Cancel and
Recover when backed by deterministic tests. A physical Start action remains
absent or disabled while the production controller backend is gated.
Cancellation must clear pending work, and restart must require explicit
recovery when that is the driver contract. The UI must not translate a failed
or cancelled job into an apparently ready state without explanation.
## Machine and administrator settings
Machine, controller, source, lens, correction and connection settings belong
in an advanced administrator area. They must remain inspectable because they
are safety-relevant, but they are not routine choices at job start.
The normal workspace may show a concise active-profile summary and any
compatibility problem. Changing the lens or correction profile is an explicit
configuration action. The application must not offer a guessed replacement
when required profile evidence is missing.
## Themes and visual semantics
The default direction is clear and light. A dark theme may be provided, but it
must preserve:
- canvas geometry and scale;
- artwork position and appearance sufficiently for comparison;
- semantic layer colors;
- panel placement and collapse state;
- selection, warning, blocked, offline and simulated meanings.
Theme changes must not be used to hide safety or provenance information.
## Diagnostics and wording
Messages should state:
1. what is wrong or unknown;
2. why the action is blocked or limited;
3. which evidence or user action can resolve it;
4. whether the result is offline, simulated, inferred, or physically verified.
Avoid language that implies communication with a controller when only a model,
preview, mock transport, or UI prototype is active.
## Deferred capabilities
The following are not required for the first useful offline product unless a
representative workshop job proves otherwise:
- physical framing or marking;
- automatic Windows USB-driver switching;
- arbitrary raw controller commands;
- all LightBurn operation modes;
- sub-layers, cleanup passes, wobble, 3D sliced images, and specialized galvo
timing controls;
- a production material-test run flow;
- cloud accounts or cloud-only recipe storage;
- simultaneous support for every makerspace laser.
## Offline acceptance scenarios
The UX direction is ready for implementation decisions when a packaged
no-device desktop checkpoint can demonstrate:
1. opening representative synthetic geometry directly into the workbench;
2. selecting, naming, coloring and ordering layers;
3. independent visibility and output state, or a documented upstream gap;
4. editing supported settings with visible units and capability gates;
5. applying or declining a compatible synthetic material recipe;
6. previewing enabled geometry, hatch/order and field violations;
7. saving and reopening without losing supported settings;
8. starting a simulation, holding, resuming, cancelling and explicitly
recovering against the no-device driver;
9. keeping controller discovery and transport unreachable;
10. preserving canvas geometry and layer semantics across theme and disclosure
changes.
Passing these scenarios does not authorize physical framing or marking.
## Open product decisions
Kaz should decide these when a concrete implementation needs them:
1. Which operation types are included in the first useful release?
2. Which exact fields are visible in Beginner mode for each operation?
3. What evidence and review process makes a shared recipe
“makerspace-approved”?
All other choices should follow the casual-maker defaults above unless new
evidence reveals a conflict.
## Traceability
- [Project charter](../project-charter.md)
- [Product and research roadmap](../roadmap.md)
- [LightBurn-to-Rayforge UX capability map](../research/lightburn-rayforge-ux-capability-map.md)
- [Graphical workflow model spike](../research/rayforge-graphical-workflow-spike.md)
- [Windows GUI preflight](../research/rayforge-windows-gui-preflight.md)
- [Proposed reuse decision](../adr/0002-reuse-rayforge-and-balormk.md)

View file

@ -1,189 +0,0 @@
# LightBurn-to-Rayforge UX capability map
- Date: 2026-10-20
- Scope: desktop workflow and offline capability fit
- Rayforge evidence baseline: commit `c28918c87f8273dd1d244f5a057addc192807fa5`
- Hardware access: none
## Purpose
This note translates the useful parts of LightBurn's documented workflow into
testable requirements for The Pirate Ray and compares them with the evidence
already collected for Rayforge. It is a product-gap analysis, not permission
to copy LightBurn code, text, graphics, assets, or proprietary behavior.
The target is an approachable galvo job workbench. A normal maker should begin
by opening a design, working on the canvas, selecting a layer, adjusting the
applicable process settings, previewing the result, and optionally applying a
material recipe. Machine, controller, lens, correction, and driver selection
belong in an advanced or administrator path after initial configuration.
The previously referenced `docs/design/mopa-workbench-ux.md` was absent from
the active repository checkout and had no Git history, branch, or stash copy.
It has now been recreated as a traceable working product contract from the
repository handoff, charter, roadmap, and evidence cited in this note. The
missing HTML prototype was not treated as application evidence.
## Evidence labels
- **Proven offline**: exercised in the pinned code path with deterministic
output and no transport.
- **Source-inspected**: found in the pinned upstream source, but not exercised
in the packaged Windows application.
- **Product requirement**: agreed behavior that still needs an implementation
or interaction test.
- **Gated**: must remain unavailable until named evidence or safety conditions
are satisfied.
- **Deferred**: useful LightBurn behavior, but outside the first useful
product.
## Reference workflow
LightBurn's documentation provides a useful interaction vocabulary:
1. the workspace represents the machine work area and contains arranged
artwork;
2. colors/layers bind artwork to ordered operations;
3. the layer list controls visibility, output, order, mode, and abbreviated
process settings;
4. the full settings editor reveals controls according to machine type and
operation mode;
5. preview is used to inspect the planned output before job control;
6. material-library entries reapply grouped process settings;
7. framing is a separate physical alignment action, not merely another canvas
view;
8. advanced galvo controls include frequency, Q-pulse width, sub-layers,
timing, wobble, angle changes, and cleanup passes.
This vocabulary is useful, but The Pirate Ray should implement only the subset
required by real workshop jobs and supported by evidence.
## Capability map
| Maker need | LightBurn reference behavior | Current Rayforge evidence | Classification | Narrow next step |
| --- | --- | --- | --- | --- |
| Open a design directly into a work area | Artwork is imported or created on a machine-sized workspace | Rayforge has a desktop editor, projects, geometry and broad import support; the local spike proved a preview artifact from synthetic geometry | Source-inspected; preview proven offline | Exercise open/import in the packaged no-device Windows checkpoint |
| See and select operations as layers | The layer list uses color to identify operations and keeps order visible | Rayforge has layers and workflow steps, but the desired persistent layer/settings arrangement has not been interaction-tested | Source-inspected plus product requirement | Verify selection, ordering, naming and color semantics without creating a parallel layer model |
| Hide geometry without disabling output, and disable output without hiding geometry | LightBurn separates `Show` and `Output` | Rayforge separates `Layer.visible` canvas state from `Step.visible` job participation and serializes both; output is operation-level rather than a layer-level control | Proven offline at model/source level; aggregate UI untested | Add or propose a tri-state layer Output control over existing step states, then verify it in the packaged no-device GUI |
| Edit common settings quickly | The layer list exposes mode, speed, power, passes and applicable galvo fields | Rayforge's generated laser-step settings expose power, speed, frequency and pulse width; serialization is proven offline | Proven offline at the model/UI-contract level | Confirm editing and save/reload in the packaged no-device GUI |
| Reveal complexity progressively | LightBurn Beginner Mode hides some advanced settings; PirateRay calls for Beginner, Advanced and Expert levels without moving the canvas | Rayforge capability sections can hide machine-dependent fields, but three user-facing disclosure levels are not proven | Product requirement | Define the smallest beginner field set from real job needs before adding controls |
| Preserve MOPA units and values | Galvo frequency is presented in kHz and Q-pulse width in ns | Rayforge's workflow uses Hz and ns and preserves both; LightBurn project import does not yet map frequency or pulse width into workflow settings | Partly proven; import gated | Keep explicit conversion at the import boundary and wait for a known-working `.lbrn2` before mapping `QPulseWidth` |
| Apply a reusable material recipe | LightBurn libraries organize presets by material, thickness and description and apply/link them to layers | Rayforge has material recipes, but workshop provenance, approval, machine/lens compatibility and portable personal collections are not proven | Source-inspected plus product requirement | Map the existing recipe schema before designing a new library UI; specify provenance and compatibility gaps |
| Inspect planned behavior before any run | LightBurn Preview reflects enabled layers and planned order | Rayforge produced a populated `JobArtifact` with Ops, path length and no machine code | Proven offline; human interaction untested | Verify layer filtering, order, hatch and warnings in the packaged no-device GUI |
| Reject a visibly invalid job | Preview/preflight should reveal unintended output; the target product must reject profile mismatch and out-of-field geometry | Existing JCZ negative tests reject profile mismatch, out-of-field geometry, invalid values and unsupported Ops | Proven offline at encoder boundary | Surface these failures as understandable preflight messages without weakening the fail-closed gate |
| Keep machine and lens selection out of the normal start flow | LightBurn device settings are separate from ordinary layer work | Rayforge has machine profiles and capability-gated settings; the desired administrator-oriented placement is not interaction-tested | Product requirement | Verify default-profile startup and move profile/lens changes behind an advanced path if upstream behavior requires it |
| Save and reopen without losing job intent | Project files retain artwork and layer settings | The synthetic step retained power, speed, frequency and pulse width through serialization/deserialization; full project interaction is untested | Partly proven offline | Add save/reload to the packaged no-device checklist |
| Frame safely | Galvo framing is a distinct physical action used to align a job | No physical framing is authorized; galvo framing semantics and Rayforge fit remain unproven | Gated | Keep only an explicitly labelled on-canvas simulation until a reviewed hardware protocol authorizes exact behavior |
| Start, hold, cancel and recover | Job control is distinct from editing and preview | The no-device driver proves hold/resume, cancellation, cleared buffers and explicit recovery | Proven offline at driver level; GUI untested | Exercise the controls only against the no-device driver in the packaged GUI |
| Run a material test | LightBurn generates a grid varying two applicable process parameters | No production Rayforge/PirateRay material-test flow has been proven; a UI mock is not machine functionality | Deferred | Retain as a future offline planner feature; do not expose a physical run action |
| Use sub-layers, cleanup passes, wobble and per-layer galvo timings | LightBurn exposes these as advanced, mode- and device-specific settings | Hatch planning and basic MOPA operations are proven, but these specialized semantics are not covered end-to-end | Deferred | Select only from demonstrated workshop needs after the basic JPT vertical slice closes |
| Use identical geometry and semantic colors in light and dark themes | PirateRay requires theme changes not to reshape the work area | No current packaged-app evidence | Product requirement | Treat theme as presentation only and add a later visual regression check |
## What should be reused
The evidence supports reusing Rayforge's existing:
- canvas, document, layer, project and geometry concepts;
- import pipeline rather than creating a general LightBurn parser;
- workflow steps and generated capability-aware settings UI;
- material-recipe foundation;
- preview-facing `JobArtifact`, simulation and preflight boundaries;
- machine-profile, addon, encoder and asynchronous driver seams;
- project serialization and packaged desktop direction.
The PirateRay-specific work should remain narrow:
- JCZ profile recognition and provenance;
- evidence-backed LightBurn MOPA import conversion;
- JPT/lens/correction compatibility validation;
- fail-closed preflight and clear diagnostics;
- a casual-maker presentation of existing capabilities;
- a physically gated Balor transport after a separate reviewed protocol.
## Gaps that must not be papered over
1. A known-working private galvo `.lbrn2` is still required to establish
project-level `QPulseWidth` serialization and omission/default behavior.
2. The current Windows environment has not launched the pinned Rayforge GTK
application; the headless UI contract is not a human interaction test.
3. `RequireFrameBeforeStart` is observed in a LightBurn device export and
preserved by the narrow addon seam, but it is not proof of safe physical
framing behavior.
4. Machine-specific frequency, pulse-width, timing and correction limits are
not established by LightBurn documentation or exported recipe values.
5. Material recipes lack a proven contract for provenance, approval and
machine/lens/correction compatibility.
6. The HTML prototype named by the handoff is absent from the active checkout;
its historical controls must not be treated as implemented capability.
## Recommended implementation order
### P0 — recover the product contract (completed 2026-10-20)
- `docs/design/mopa-workbench-ux.md` has been recreated from traceable project
requirements and this capability analysis.
- Resolve the open product decisions only when they block a concrete task:
first supported operation types, beginner-visible fields, and the evidence
required for a makerspace-approved recipe.
### P1 — complete the no-device desktop checkpoint
- Use a reviewed Windows GTK runtime for the pinned Rayforge checkout.
- Load the synthetic JCZ-capable profile with no discovery or transport.
- Open geometry, select a layer, edit power/speed/frequency/pulse width,
preview, save, reload, hold, cancel and recover.
- Record the actual placement and behavior of layers, settings, recipes,
preflight and job controls.
### P2 — close only evidenced gaps
- Propose small upstream or addon changes for independent Show/Output state,
casual-maker disclosure and safety diagnostics only when P1 proves they are
absent.
- Extend material recipes with provenance and compatibility metadata using the
existing model where possible.
- Add LightBurn MOPA field conversion only after private project evidence
establishes serialization and units.
### P3 — defer specialized and physical behavior
- Keep material-test execution, physical framing, controller status and laser
start outside the offline product.
- Add sub-layers, cleanup passes, wobble and timing controls only in response
to supported workshop jobs and end-to-end tests.
## Decision impact
This analysis finds no UX requirement that currently justifies a separate
desktop application. Rayforge covers the expensive general-product foundation,
while the identified gaps are narrow enough to test as addon or upstream
extensions. ADR 0002 should remain **Proposed** until the packaged no-device
interaction checkpoint and the LightBurn MOPA fixture gate are complete.
No USB enumeration, controller command, galvo movement, physical framing,
arming, or laser output was performed for this analysis.
## Sources
Project evidence:
- [Open-source landscape](open-source-landscape.md)
- [Graphical workflow model spike](rayforge-graphical-workflow-spike.md)
- [Layer visibility and output spike](rayforge-layer-model-spike.md)
- [Windows GUI preflight](rayforge-windows-gui-preflight.md)
- [Synthetic LightBurn MOPA import spike](rayforge-lightburn-mopa-import-spike.md)
- [Synthetic LightBurn JCZ device-import spike](rayforge-lightburn-device-import-spike.md)
- [Rayforge JCZ negative cases](rayforge-jcz-negative-cases.md)
- [Rayforge cancellation and recovery spike](rayforge-jcz-cancel-recovery-spike.md)
- [ADR 0002](../adr/0002-reuse-rayforge-and-balormk.md)
Official LightBurn behavior references:
- [Workspace / Edit Window](https://docs.lightburnsoftware.com/latest/Reference/UI/Workspace/)
- [Cuts / Layers Window](https://docs.lightburnsoftware.com/latest/Reference/CutsLayersWindow/)
- [Galvo-Specific Cut Settings](https://docs.lightburnsoftware.com/latest/Reference/CutSettingsEditor/GalvoSpecificCutSettings/)
- [Sub-Layers](https://docs.lightburnsoftware.com/latest/Reference/CutSettingsEditor/SubLayers/)
- [Previewing](https://docs.lightburnsoftware.com/latest/GetStarted/PreviewBeginner/)
- [Material Test](https://docs.lightburnsoftware.com/latest/GetStarted/FirstMaterialTest/)
- [Galvo Ports and Laser Settings](https://docs.lightburnsoftware.com/latest/Reference/DeviceSettings/GalvoPorts/)

View file

@ -1,103 +0,0 @@
# Rayforge layer visibility and output spike
- Date: 2026-10-20
- Rayforge commit: `c28918c87f8273dd1d244f5a057addc192807fa5`
- Scope: layer model, workflow output, ordering and serialization
- Transport: none
## Result
Rayforge already separates the two behaviors required by the PirateRay UX,
but at different levels of its model:
- `Layer.visible` controls whether a layer is rendered on the canvas and in
visual previews;
- `Step.visible` controls whether a workflow operation participates in the
generated job pipeline.
The states are independent. A hidden layer can retain an enabled operation,
which matches LightBurn's rule that hiding artwork does not silently disable
output. Conversely, disabling a workflow step removes that operation from job
generation without changing the layer's canvas visibility.
The pinned model round-trip preserved:
- layer name `Engraving`;
- layer color `#123456`;
- hidden layer state;
- operation name `Mark outline`;
- enabled operation state;
- an explicitly reordered layer list (`Second`, then `First`).
The deterministic result is stored in
`tests/fixtures/rayforge-layer-model.json`.
## UX fit
Rayforge's existing layer header exposes one eye control for
`Layer.visible`. Its operation editor exposes a visibility switch for each
`Step.visible`. The pipeline checks step visibility and does not check layer
visibility when constructing job nodes.
This means the required Show/Output semantics do **not** require a new pipeline
field or a duplicate layer model. The smallest PirateRay-facing extension is
an aggregate **Output** control in the layer presentation:
- on: all supported workflow steps in the layer are enabled;
- off: all workflow steps in the layer are disabled;
- mixed: some workflow steps are enabled and some are disabled.
An aggregate toggle must preserve or deliberately define what happens to a
previous mixed selection. The safest interaction is to expose the mixed state,
let the layer toggle affect all steps, and leave per-operation switches
available in Advanced view. This is a UI policy decision over existing model
state, not a serialization migration.
## Ordering
`Doc.layers` is the single ordered layer list. Drag-and-drop calls the
undoable `reorder_layers` command, canvas stacking follows the same list, and
the intent builder iterates it in order when constructing job nodes. The
serialized document retains that order.
The source documentation states that layers execute left to right. A packaged
no-device GUI checkpoint should still confirm that the visible ordering affordance
is understandable to a casual maker and that preview communicates the same
order.
## Remaining interaction evidence
The source and model evidence close the data-model question, but not the human
interaction checkpoint. A packaged no-device test should verify:
1. the layer eye changes only canvas/preview visibility;
2. a disabled operation remains visibly distinct from a hidden layer;
3. the proposed aggregate Output control reports on/off/mixed correctly;
4. save/reload retains names, colors, order and both states;
5. preview and no-device job output omit disabled steps but retain hidden,
enabled layers.
No USB enumeration, controller command, motion, physical framing, or emission
occurred.
## Reproduce
Run against the pinned checkout with the already isolated Rayforge Python
environment:
```powershell
python scripts/spike_rayforge_layer_model.py <rayforge-checkout> `
--output tests/fixtures/rayforge-layer-model.json
```
## Source anchors
- `rayforge/core/layer.py`: layer visibility, color and serialization
- `rayforge/core/step.py`: operation visibility and serialization
- `rayforge/pipeline/intent_builder.py`: step filtering and ordered job nodes
- `rayforge/ui_gtk/doceditor/layer_column.py`: layer eye control
- `rayforge/ui_gtk/doceditor/step_box.py`: operation visibility control
- `rayforge/ui_gtk/doceditor/layers_tab.py`: undoable drag ordering
- `rayforge/ui_gtk/canvas2d/surface.py`: canvas stacking order
- `website/docs/features/multi-layer.md`: documented execution semantics

View file

@ -37,22 +37,9 @@ toolchain is a separate, reviewable action: upstream's setup includes MSYS2,
GTK, compiler/build packages and network downloads, so it should not be an GTK, compiler/build packages and network downloads, so it should not be an
implicit side effect of a workflow test. implicit side effect of a workflow test.
## On-demand source and build policy
The public repository records the upstream URL and exact commit in
`upstreams/rayforge.json`. `scripts/prepare_rayforge_build.ps1` fetches that
revision into the ignored `.build/upstreams/` cache only when explicitly
invoked. Its default action prepares source only; it neither installs the
Windows toolchain nor starts a build.
Windows dependencies and bundle creation require separate
`-InstallWindowsDependencies` and `-BuildWindowsBundle` switches. This keeps a
multi-gigabyte MSYS2/UCRT64 toolchain out of normal clone, test and documentation
workflows and makes a CI-produced bundle the preferred interaction-test input.
## Next action ## Next action
Produce or obtain an artifact for the pinned revision through the explicit Provide an already reviewed Rayforge Windows build/runtime, or explicitly
on-demand build path, then rerun the preflight and perform the packaged approve installation of the pinned checkout's MSYS2/UCRT64 dependencies. Then
no-device interaction checklist from `rayforge-graphical-workflow-spike.md`. rerun the preflight and perform the packaged no-device interaction checklist
Do not make the Windows build toolchain an implicit project dependency. from `rayforge-graphical-workflow-spike.md`.

View file

@ -41,10 +41,6 @@ hardware command is possible. The real controller backend remains absent or
physically gated until its protocol and safety behavior are supported by physically gated until its protocol and safety behavior are supported by
evidence. evidence.
The corresponding casual-maker workflow, layout, disclosure, preview and
offline acceptance requirements are recorded in the
[MOPA workbench UX requirements](design/mopa-workbench-ux.md).
## What the installer snapshot gives us ## What the installer snapshot gives us
The collected installer snapshot establishes: The collected installer snapshot establishes:

View file

@ -1,94 +0,0 @@
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$CacheRoot,
[switch]$InstallWindowsDependencies,
[switch]$BuildWindowsBundle
)
$ErrorActionPreference = "Stop"
$repositoryRoot = Split-Path -Parent $PSScriptRoot
$manifestPath = Join-Path $repositoryRoot "upstreams\rayforge.json"
$manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json
if (-not $CacheRoot) {
$CacheRoot = Join-Path $repositoryRoot ".build\upstreams"
}
$shortCommit = $manifest.commit.Substring(0, 12)
$checkout = Join-Path $CacheRoot "rayforge-$shortCommit"
if (-not (Test-Path -LiteralPath $checkout)) {
if (-not $PSCmdlet.ShouldProcess($checkout, "Clone pinned Rayforge source")) {
return
}
New-Item -ItemType Directory -Force -Path $CacheRoot | Out-Null
& git clone --filter=blob:none --no-checkout $manifest.repository $checkout
if ($LASTEXITCODE -ne 0) {
throw "Rayforge clone failed with exit code $LASTEXITCODE"
}
}
if (-not (Test-Path -LiteralPath (Join-Path $checkout ".git"))) {
throw "Existing cache path is not a Git checkout: $checkout"
}
$currentCommit = (& git -C $checkout rev-parse HEAD 2>$null)
if ($LASTEXITCODE -ne 0 -or $currentCommit -ne $manifest.commit) {
if (-not $PSCmdlet.ShouldProcess($checkout, "Fetch and check out pinned commit $($manifest.commit)")) {
return
}
& git -C $checkout fetch --depth 1 origin $manifest.commit
if ($LASTEXITCODE -ne 0) {
throw "Rayforge fetch failed with exit code $LASTEXITCODE"
}
& git -C $checkout checkout --detach $manifest.commit
if ($LASTEXITCODE -ne 0) {
throw "Rayforge checkout failed with exit code $LASTEXITCODE"
}
}
$verifiedCommit = (& git -C $checkout rev-parse HEAD).Trim()
if ($verifiedCommit -ne $manifest.commit) {
throw "Rayforge pin mismatch: expected $($manifest.commit), got $verifiedCommit"
}
Write-Output "Rayforge source ready at $checkout"
Write-Output "Verified commit: $verifiedCommit"
if (-not $InstallWindowsDependencies -and -not $BuildWindowsBundle) {
Write-Output "No dependencies were installed and no build was started."
return
}
$bash = "C:\msys64\usr\bin\bash.exe"
if (-not (Test-Path -LiteralPath $bash)) {
throw "MSYS2 is required for the upstream Windows build. Install it explicitly before using build flags."
}
$msysCheckout = (& $bash -lc "cygpath -u '$checkout'").Trim()
if ($InstallWindowsDependencies) {
if ($PSCmdlet.ShouldProcess("C:\msys64", "Install Rayforge Windows build dependencies")) {
& $bash -lc "cd '$msysCheckout' && bash scripts/win/win_setup.sh"
if ($LASTEXITCODE -ne 0) {
throw "Rayforge Windows dependency setup failed with exit code $LASTEXITCODE"
}
}
}
if ($BuildWindowsBundle) {
$environmentFile = Join-Path $checkout ".msys2_env"
if (-not (Test-Path -LiteralPath $environmentFile)) {
throw "Windows dependencies are not prepared. Re-run with -InstallWindowsDependencies."
}
if ($PSCmdlet.ShouldProcess($checkout, "Build pinned Rayforge Windows bundle")) {
& $bash -lc "cd '$msysCheckout' && bash scripts/win/win_build.sh 'pirateray-$shortCommit'"
if ($LASTEXITCODE -ne 0) {
throw "Rayforge Windows build failed with exit code $LASTEXITCODE"
}
}
}

View file

@ -1,134 +0,0 @@
"""Characterize Rayforge layer visibility, output, order, and persistence."""
from __future__ import annotations
import argparse
import hashlib
import inspect
import json
import subprocess
import sys
from pathlib import Path
RAYFORGE_COMMIT = "c28918c87f8273dd1d244f5a057addc192807fa5"
def checkout_commit(root: Path) -> str:
result = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=root,
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
def characterize(root: Path) -> dict:
commit = checkout_commit(root)
if commit != RAYFORGE_COMMIT:
raise RuntimeError(
f"Expected Rayforge {RAYFORGE_COMMIT}, found {commit}"
)
sys.path.insert(0, str(root))
from rayforge.core.doc import Doc
from rayforge.core.layer import Layer
from rayforge.core.step import Step
from rayforge.pipeline.intent_builder import IntentBuilder
layer = Layer("Engraving")
layer.set_color("#123456")
workflow = layer.workflow
if workflow is None:
raise RuntimeError("Test layer has no workflow")
step = Step("Contour", "Mark outline")
workflow.add_step(step)
layer.set_visible(False)
step.set_visible(True)
restored = Layer.from_dict(layer.to_dict())
restored_step = restored.workflow.steps[0] if restored.workflow else None
first = Layer("First")
second = Layer("Second")
doc = Doc()
doc.set_layers([second, first])
serialized_order = [
child["name"]
for child in doc.to_dict()["children"]
if child.get("type") == "layer"
]
build_source = inspect.getsource(IntentBuilder.build)
render_source = inspect.getsource(Layer.get_renderable_items)
layer_ui = (
root / "rayforge" / "ui_gtk" / "doceditor" / "layer_column.py"
).read_text(encoding="utf-8")
step_ui = (
root / "rayforge" / "ui_gtk" / "doceditor" / "step_box.py"
).read_text(encoding="utf-8")
return {
"rayforge_commit": commit,
"hardware_access": False,
"model": {
"layer_visible": layer.visible,
"step_visible": step.visible,
"states_are_independent": (
layer.visible is False and step.visible is True
),
"roundtrip": {
"layer_name": restored.name,
"layer_color": restored.color,
"layer_visible": restored.visible,
"step_name": restored_step.name if restored_step else None,
"step_visible": (
restored_step.visible if restored_step else None
),
},
"serialized_layer_order": serialized_order,
},
"source_contract": {
"canvas_checks_layer_visibility": (
"if not self.visible" in render_source
),
"job_pipeline_checks_step_visibility": (
"if not step.visible" in build_source
),
"job_pipeline_checks_layer_visibility": (
"layer.visible" in build_source
),
"layer_visibility_control": (
"Toggle layer visibility" in layer_ui
),
"step_visibility_control": "Toggle step visibility" in step_ui,
},
"conclusion": (
"Rayforge separates canvas visibility at layer level from output "
"participation at workflow-step level; a LightBurn-style layer "
"Output control can be an aggregate UI over existing step states."
),
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("rayforge_checkout", type=Path)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
result = characterize(args.rayforge_checkout.resolve())
payload = json.dumps(result, indent=2, sort_keys=True) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(payload, encoding="utf-8")
else:
print(payload, end="")
print(f"sha256={hashlib.sha256(payload.encode()).hexdigest()}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -1,28 +0,0 @@
{
"conclusion": "Rayforge separates canvas visibility at layer level from output participation at workflow-step level; a LightBurn-style layer Output control can be an aggregate UI over existing step states.",
"hardware_access": false,
"model": {
"layer_visible": false,
"roundtrip": {
"layer_color": "#123456",
"layer_name": "Engraving",
"layer_visible": false,
"step_name": "Mark outline",
"step_visible": true
},
"serialized_layer_order": [
"Second",
"First"
],
"states_are_independent": true,
"step_visible": true
},
"rayforge_commit": "c28918c87f8273dd1d244f5a057addc192807fa5",
"source_contract": {
"canvas_checks_layer_visibility": true,
"job_pipeline_checks_layer_visibility": false,
"job_pipeline_checks_step_visibility": true,
"layer_visibility_control": true,
"step_visibility_control": true
}
}

View file

@ -1,49 +0,0 @@
import json
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "tests" / "fixtures" / "rayforge-layer-model.json"
SCRIPT = ROOT / "scripts" / "spike_rayforge_layer_model.py"
class RayforgeLayerModelSpikeTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.result = json.loads(FIXTURE.read_text(encoding="utf-8"))
def test_visibility_and_output_states_are_independent(self):
self.assertTrue(self.result["model"]["states_are_independent"])
self.assertFalse(self.result["model"]["layer_visible"])
self.assertTrue(self.result["model"]["step_visible"])
def test_states_name_color_and_order_survive_serialization(self):
restored = self.result["model"]["roundtrip"]
self.assertEqual(restored["layer_name"], "Engraving")
self.assertEqual(restored["layer_color"], "#123456")
self.assertFalse(restored["layer_visible"])
self.assertEqual(restored["step_name"], "Mark outline")
self.assertTrue(restored["step_visible"])
self.assertEqual(
self.result["model"]["serialized_layer_order"],
["Second", "First"],
)
def test_canvas_and_job_pipeline_use_different_states(self):
contract = self.result["source_contract"]
self.assertTrue(contract["canvas_checks_layer_visibility"])
self.assertTrue(contract["job_pipeline_checks_step_visibility"])
self.assertFalse(contract["job_pipeline_checks_layer_visibility"])
self.assertTrue(contract["layer_visibility_control"])
self.assertTrue(contract["step_visibility_control"])
def test_spike_is_hardware_free(self):
self.assertFalse(self.result["hardware_access"])
source = SCRIPT.read_text(encoding="utf-8").lower()
for forbidden in ("pyusb", "usb.core", "9588", "9899"):
self.assertNotIn(forbidden, source)
if __name__ == "__main__":
unittest.main()

View file

@ -1,35 +0,0 @@
import json
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "upstreams" / "rayforge.json"
PREPARE = ROOT / "scripts" / "prepare_rayforge_build.ps1"
PIN = "c28918c87f8273dd1d244f5a057addc192807fa5"
class UpstreamBuildReferenceTests(unittest.TestCase):
def test_rayforge_reference_is_exact_and_https(self):
data = json.loads(MANIFEST.read_text(encoding="utf-8"))
self.assertEqual(data["commit"], PIN)
self.assertEqual(
data["repository"], "https://github.com/barebaric/rayforge.git"
)
def test_prepare_script_is_on_demand_and_uses_ignored_cache(self):
script = PREPARE.read_text(encoding="utf-8")
self.assertIn('".build\\upstreams"', script)
self.assertIn("SupportsShouldProcess", script)
self.assertIn("InstallWindowsDependencies", script)
self.assertIn("BuildWindowsBundle", script)
self.assertIn("No dependencies were installed", script)
def test_prepare_script_has_no_hardware_path(self):
script = PREPARE.read_text(encoding="utf-8").lower()
for forbidden in ("pyusb", "usb.core", "9588", "9899", "frame", "mark"):
self.assertNotIn(forbidden, script)
if __name__ == "__main__":
unittest.main()

View file

@ -1,7 +0,0 @@
{
"name": "Rayforge",
"repository": "https://github.com/barebaric/rayforge.git",
"commit": "c28918c87f8273dd1d244f5a057addc192807fa5",
"license": "MIT",
"purpose": "Pinned host application for offline reuse and JCZ addon validation"
}