Expose import preflight in packaged Rayforge UI
This commit is contained in:
parent
8ab25827db
commit
f87135657c
9 changed files with 149 additions and 8 deletions
|
|
@ -94,9 +94,10 @@ element types and applies the evidence-bound Hz/ns contract only to LightBurn
|
||||||
2.1.03. The bridge binds source layer IDs, uses the host MOPA setters,
|
2.1.03. The bridge binds source layer IDs, uses the host MOPA setters,
|
||||||
serializes exact unit provenance and disables every staged step. Document
|
serializes exact unit provenance and disables every staged step. Document
|
||||||
preflight presents the remaining blockers and the offline driver rejects them
|
preflight presents the remaining blockers and the offline driver rejects them
|
||||||
before buffer preparation. It never authorizes encoding. The next import task
|
before buffer preparation. The packaged frontend now exposes that report as a
|
||||||
is packaged GTK presentation; absent pulse width and image equivalence remain
|
read-only Tools-menu dialog with stable blocker codes. It never authorizes
|
||||||
blockers.
|
encoding. Packaged interaction evidence, absent pulse width and image
|
||||||
|
equivalence remain blockers.
|
||||||
|
|
||||||
If a Windows artifact is available, keep the machine disconnected and run, in
|
If a Windows artifact is available, keep the machine disconnected and run, in
|
||||||
order:
|
order:
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@ unverified. Rayforge's relevant Step and LaserStep files are included in the
|
||||||
pinned upstream contract manifest.
|
pinned upstream contract manifest.
|
||||||
|
|
||||||
The addon now also exposes a transport-free document preflight report with
|
The addon now also exposes a transport-free document preflight report with
|
||||||
stable issue codes and human-readable blocker messages. The offline driver
|
stable issue codes and human-readable blocker messages. Its packaged frontend
|
||||||
|
adds a read-only `PirateRay Import Preflight` action to Rayforge's Tools menu;
|
||||||
|
the dialog renders those codes and keeps output disabled. The offline driver
|
||||||
runs this check before it prepares encoded buffers. A staged import is rejected;
|
runs this check before it prepares encoded buffers. A staged import is rejected;
|
||||||
manually re-enabling its step adds a distinct error, and malformed review data
|
manually re-enabling its step adds a distinct error, and malformed review data
|
||||||
fails closed. There is intentionally no accepted or promoted import state yet.
|
fails closed. There is intentionally no accepted or promoted import state yet.
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ required by real workshop jobs and supported by evidence.
|
||||||
| Preserve MOPA units and values | Galvo frequency is presented in kHz and Q-pulse width in ns | Rayforge's workflow uses Hz and ns; the addon maps complete LightBurn 2.1.03 vector settings with an evidence-bound raw-Hz/display-kHz and direct-ns contract | Proven offline for complete vector settings; image/absent-pulse cases gated | Verify editing and persistence in the packaged no-device GUI while retaining the version and provenance checks |
|
| Preserve MOPA units and values | Galvo frequency is presented in kHz and Q-pulse width in ns | Rayforge's workflow uses Hz and ns; the addon maps complete LightBurn 2.1.03 vector settings with an evidence-bound raw-Hz/display-kHz and direct-ns contract | Proven offline for complete vector settings; image/absent-pulse cases gated | Verify editing and persistence in the packaged no-device GUI while retaining the version and provenance checks |
|
||||||
| Apply a reusable material recipe | LightBurn libraries organize presets by material, thickness and description and apply/link them to layers | Rayforge recipes already match machine, head, material, thickness and step type and preserve Hz/ns settings; the external addon now validates versioned provenance plus exact source/lens/correction compatibility while withholding hardware authorization | Proven offline at model/source level; approval policy and portable collections remain | Integrate the validated metadata into recipe selection before changing the existing recipe UI |
|
| Apply a reusable material recipe | LightBurn libraries organize presets by material, thickness and description and apply/link them to layers | Rayforge recipes already match machine, head, material, thickness and step type and preserve Hz/ns settings; the external addon now validates versioned provenance plus exact source/lens/correction compatibility while withholding hardware authorization | Proven offline at model/source level; approval policy and portable collections remain | Integrate the validated metadata into recipe selection before changing the existing recipe UI |
|
||||||
| 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 |
|
| 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 |
|
| 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; the packaged addon exposes a read-only Tools-menu dialog for stable import blocker codes | Proven offline at encoder and frontend-contract boundaries; packaged interaction pending | Exercise the dialog in the packaged no-device checklist 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,8 @@ The remaining M1 release gates are deliberately narrow:
|
||||||
- vector project frequency and `QPulseWidth` now use a LightBurn 2.1.03,
|
- vector project frequency and `QPulseWidth` now use a LightBurn 2.1.03,
|
||||||
evidence-bound Hz/ns contract; absent pulse width and image equivalence stay
|
evidence-bound Hz/ns contract; absent pulse width and image equivalence stay
|
||||||
blocked;
|
blocked;
|
||||||
- packaged Windows/GTK interaction is blocked until a reviewed Rayforge
|
- the addon now registers a read-only GTK Tools-menu preflight action, but
|
||||||
|
packaged Windows/GTK interaction is blocked until a reviewed Rayforge
|
||||||
runtime with PyGObject and MSYS2/UCRT64 is available; the repository build
|
runtime with PyGObject and MSYS2/UCRT64 is available; the repository build
|
||||||
entry point fetches pinned source but never installs that runtime;
|
entry point fetches pinned source but never installs that runtime;
|
||||||
- three operator-verified projects establish project-level `frequency` and
|
- three operator-verified projects establish project-level `frequency` and
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
"""Packaged GTK presentation for PirateRay import preflight results."""
|
||||||
|
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
|
from gi.repository import Adw, Gio
|
||||||
|
|
||||||
|
from rayforge.core.hooks import hookimpl
|
||||||
|
from rayforge.ui_gtk.action_registry import MenuPlacement, action_registry
|
||||||
|
from rayforge.ui_gtk.actions import action_extension_registry
|
||||||
|
|
||||||
|
from .import_preflight import inspect_imported_workflow
|
||||||
|
from .preflight_presentation import format_preflight_report
|
||||||
|
|
||||||
|
|
||||||
|
ADDON_NAME = "the_pirate_ray_jcz"
|
||||||
|
ACTION_NAME = "pirateray-import-preflight"
|
||||||
|
|
||||||
|
|
||||||
|
def _register_actions(action_manager):
|
||||||
|
action = Gio.SimpleAction.new(ACTION_NAME, None)
|
||||||
|
|
||||||
|
def on_activate(_action, _parameter):
|
||||||
|
report = inspect_imported_workflow(action_manager.doc)
|
||||||
|
presentation = format_preflight_report(report)
|
||||||
|
dialog = Adw.MessageDialog(
|
||||||
|
transient_for=action_manager.win,
|
||||||
|
heading=presentation["heading"],
|
||||||
|
body=presentation["body"],
|
||||||
|
)
|
||||||
|
dialog.add_response("close", _("Close"))
|
||||||
|
dialog.set_default_response("close")
|
||||||
|
dialog.set_close_response("close")
|
||||||
|
dialog.present()
|
||||||
|
|
||||||
|
action.connect("activate", on_activate)
|
||||||
|
action_manager.win.add_action(action)
|
||||||
|
action_manager.actions[ACTION_NAME] = action
|
||||||
|
action_registry.register(
|
||||||
|
action_name=ACTION_NAME,
|
||||||
|
action=action,
|
||||||
|
addon_name=ADDON_NAME,
|
||||||
|
label=_("PirateRay Import Preflight"),
|
||||||
|
menu=MenuPlacement(menu_id="tools", priority=60),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def register_actions(_action_registry):
|
||||||
|
"""Expose the read-only preflight report in Rayforge's Tools menu."""
|
||||||
|
action_extension_registry.register_setup(_register_actions, ADDON_NAME)
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
"""Build a UI-neutral presentation of PirateRay import preflight results."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from gettext import gettext as _
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def _invalid_report() -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"status": "blocked",
|
||||||
|
"heading": _("Import preflight blocked"),
|
||||||
|
"body": _(
|
||||||
|
"The preflight report is malformed. Output remains disabled."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def format_preflight_report(report: Any) -> dict[str, str]:
|
||||||
|
"""Return localized heading and body text for the packaged GTK dialog."""
|
||||||
|
if not isinstance(report, Mapping):
|
||||||
|
return _invalid_report()
|
||||||
|
ready = report.get("ready")
|
||||||
|
issues = report.get("issues")
|
||||||
|
if not isinstance(ready, bool) or not isinstance(issues, list):
|
||||||
|
return _invalid_report()
|
||||||
|
if ready:
|
||||||
|
if issues:
|
||||||
|
return _invalid_report()
|
||||||
|
return {
|
||||||
|
"status": "ready",
|
||||||
|
"heading": _("Import preflight ready"),
|
||||||
|
"body": _("No unresolved PirateRay import blockers were found."),
|
||||||
|
}
|
||||||
|
if not issues:
|
||||||
|
return _invalid_report()
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for issue in issues:
|
||||||
|
if not isinstance(issue, Mapping):
|
||||||
|
return _invalid_report()
|
||||||
|
code = issue.get("code")
|
||||||
|
message = issue.get("message")
|
||||||
|
if not isinstance(code, str) or not code:
|
||||||
|
return _invalid_report()
|
||||||
|
if not isinstance(message, str) or not message:
|
||||||
|
return _invalid_report()
|
||||||
|
lines.append(f"{code}: {message}")
|
||||||
|
|
||||||
|
prefix = _(
|
||||||
|
"Resolve every issue before enabling output. Output remains disabled."
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"status": "blocked",
|
||||||
|
"heading": _("Import preflight blocked"),
|
||||||
|
"body": prefix + "\n\n" + "\n".join(lines),
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: the_pirate_ray_jcz
|
name: the_pirate_ray_jcz
|
||||||
display_name: The Pirate Ray JCZ (offline spike)
|
display_name: The Pirate Ray JCZ (offline spike)
|
||||||
description: Hardware-free binary-driver registration proof for Rayforge.
|
description: Hardware-free JCZ workflow and import-preflight integration.
|
||||||
version: 0.1.0
|
version: 0.2.0
|
||||||
api_version: 23
|
api_version: 23
|
||||||
depends:
|
depends:
|
||||||
- rayforge>=1.0.0
|
- rayforge>=1.0.0
|
||||||
|
|
@ -9,4 +9,5 @@ author:
|
||||||
name: The Pirate Ray contributors
|
name: The Pirate Ray contributors
|
||||||
provides:
|
provides:
|
||||||
worker: pirate_ray_jcz.worker
|
worker: pirate_ray_jcz.worker
|
||||||
|
frontend: pirate_ray_jcz.frontend
|
||||||
maturity: experimental
|
maturity: experimental
|
||||||
|
|
|
||||||
2
tests/fixtures/rayforge-addon-spike.json
vendored
2
tests/fixtures/rayforge-addon-spike.json
vendored
|
|
@ -17,8 +17,10 @@
|
||||||
],
|
],
|
||||||
"packaged_files": [
|
"packaged_files": [
|
||||||
"pirate_ray_jcz/__init__.py",
|
"pirate_ray_jcz/__init__.py",
|
||||||
|
"pirate_ray_jcz/frontend.py",
|
||||||
"pirate_ray_jcz/import_preflight.py",
|
"pirate_ray_jcz/import_preflight.py",
|
||||||
"pirate_ray_jcz/offline_session.py",
|
"pirate_ray_jcz/offline_session.py",
|
||||||
|
"pirate_ray_jcz/preflight_presentation.py",
|
||||||
"pirate_ray_jcz/profile_import.py",
|
"pirate_ray_jcz/profile_import.py",
|
||||||
"pirate_ray_jcz/project_import.py",
|
"pirate_ray_jcz/project_import.py",
|
||||||
"pirate_ray_jcz/rayforge_compat.py",
|
"pirate_ray_jcz/rayforge_compat.py",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ sys.path.insert(0, str(ADDON))
|
||||||
from pirate_ray_jcz.import_preflight import (
|
from pirate_ray_jcz.import_preflight import (
|
||||||
ImportPreflightError, assert_no_unresolved_imports, inspect_imported_workflow,
|
ImportPreflightError, assert_no_unresolved_imports, inspect_imported_workflow,
|
||||||
)
|
)
|
||||||
|
from pirate_ray_jcz.preflight_presentation import format_preflight_report
|
||||||
from pirate_ray_jcz.workflow_import import METADATA_KEY
|
from pirate_ray_jcz.workflow_import import METADATA_KEY
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -70,6 +71,31 @@ class ImportPreflightTests(unittest.TestCase):
|
||||||
prepare = source.index("self.offline_session.prepare(encoded)")
|
prepare = source.index("self.offline_session.prepare(encoded)")
|
||||||
self.assertLess(preflight, prepare)
|
self.assertLess(preflight, prepare)
|
||||||
|
|
||||||
|
def test_packaged_presentation_lists_stable_blocker_codes(self):
|
||||||
|
presentation = format_preflight_report(
|
||||||
|
inspect_imported_workflow(document(imported_step()))
|
||||||
|
)
|
||||||
|
self.assertEqual("blocked", presentation["status"])
|
||||||
|
self.assertEqual("Import preflight blocked", presentation["heading"])
|
||||||
|
self.assertIn("IMPORT_REVIEW_REQUIRED", presentation["body"])
|
||||||
|
self.assertIn("Output remains disabled", presentation["body"])
|
||||||
|
|
||||||
|
def test_packaged_presentation_fails_closed_on_malformed_report(self):
|
||||||
|
for report in (None, {}, {"ready": True, "issues": [{}]}):
|
||||||
|
with self.subTest(report=report):
|
||||||
|
presentation = format_preflight_report(report)
|
||||||
|
self.assertEqual("blocked", presentation["status"])
|
||||||
|
self.assertIn("malformed", presentation["body"])
|
||||||
|
|
||||||
|
def test_frontend_registers_read_only_tools_action(self):
|
||||||
|
source = (ADDON / "pirate_ray_jcz" / "frontend.py").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
self.assertIn('ACTION_NAME = "pirateray-import-preflight"', source)
|
||||||
|
self.assertIn('menu_id="tools"', source)
|
||||||
|
self.assertIn("inspect_imported_workflow(action_manager.doc)", source)
|
||||||
|
self.assertNotIn("machine_cmd", source)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue