Compare commits
No commits in common. "232f1119d972f3c08a3efa6444982cc62475827e" and "0d4e680e6f080f4834a436167a0e527a7b4a3a1b" have entirely different histories.
232f1119d9
...
0d4e680e6f
14 changed files with 4 additions and 989 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -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/
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
||||||
|
|
@ -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/)
|
|
||||||
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
|
||||||
28
tests/fixtures/rayforge-layer-model.json
vendored
28
tests/fixtures/rayforge-layer-model.json
vendored
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue