Skip to content

feat: add "Open in OrcaSlicer" export action - #347

Merged
lightningpixel merged 10 commits into
lightningpixel:devfrom
wenghaishi:feat/open-in-orcaslicer
Sep 19, 2026
Merged

lightningpixel merged 10 commits into
lightningpixel:devfrom
wenghaishi:feat/open-in-orcaslicer

Conversation

@wenghaishi

@wenghaishi wenghaishi commented Sep 14, 2026

Copy link
Copy Markdown

One-click hand-off from a generated model to OrcaSlicer via its orcaslicer://open?file=<url> deeplink, available in the Export dropdown.

let me know if this is alright or there might be some better place for this button.

Screenshot 2026-09-14 at 4 48 50 PM

What

  • Backend — new GET /export/slicer/{fmt}/{token}/model.{fmt} route that converts the workspace GLB to STL on the fly: bakes scene-graph transforms, reorients Y-up → Z-up, and normalizes the longest edge to a sane print size. The URL is intentionally path-only and ends in model.stl (no query string), because OrcaSlicer derives the import filename/format from the URL's final path segment. Uses ancestry-based path containment (not string-prefix).
  • Electron — a slicer:open IPC that fires the deeplink and reports success/failure so the UI can fall back when OrcaSlicer isn't installed.
  • Frontend — an Open in OrcaSlicer item in the Export dropdown, shown only for sliceable workspace meshes, plus a pure, unit-tested deeplink builder.

Why STL + deeplink

OrcaSlicer registers orcaslicer://open?file=<url>, downloads the URL, and imports it — and it cannot import GLB. So we convert to STL (which OrcaSlicer auto-repairs) and serve it at a deeplink-shaped URL.

Testing

  • api/tests/test_export_router.py — conversion, Y→Z reorientation, print-size normalization, and guards (unsupported format, extension mismatch, malformed token, path traversal, sibling-prefix escape, 404).
  • src/areas/generate/orcaSlicerLink.test.ts — exact deeplink string, no query string, url-safe base64 round-trip.
  • npm run lint clean; npm run test:node green. Manual E2E verified on macOS (imports upright, ~50 mm).

Notes / follow-ups

  • STL-first by design; 3MF (units + colour) is a natural fast-follow.
  • "Not installed" handling is a best-effort error toast; OS-level detection could be added later.

Closes #346

lightningpixel and others added 10 commits August 21, 2026 11:05
Modly had no SECURITY.md and no private channel for vulnerability
reports, which left email as the only route for researchers. Private
vulnerability reporting is now enabled on the repository; this points
people at it and sets expectations around it.

The policy leads with a threat model and lets the scope follow from it,
so that an excluded report comes with the reason it was excluded. Two
assumptions are deliberate: workflow files are untrusted input because
sharing them is normal, and any web page the user has open is an
untrusted caller of the loopback API. The second is why the
network-exposure exclusion is narrowed to deliberate exposure only --
a page in the user's own browser needs none.

Every claim was checked against the code. The policy does not call the
installer signed (no platform signs it), says nothing about PyTorch
(we do not ship it), and does not excuse social engineering on the
strength of UI warnings that do not exist.
…curity

docs: add a security policy with a private reporting route
Lets external contributors claim an issue without repo write access.
Commenting /assign self-assigns via a github-script Action (GITHUB_TOKEN
has the write permission the commenter doesn't); /unassign releases it.
CONTRIBUTING.md documents the full flow: claim -> fork -> PR with
`Closes #N` -> board moves through In progress / Ready to review /
Ready to test / Done.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDMd7LzfFJ7TXav5etBjRi
…buting-assign-command-to-main

docs: add CONTRIBUTING.md and /assign command bot
The /assign bot moved GitHub assignees but never touched the Project v2
board itself, so the "In progress" column stayed empty. Same for PRs:
opening one with `Closes #N` closed the issue on merge but never moved
the card to "Ready to review".

- assign-command.yml: on /assign, move the linked board item to
  "In progress"; on /unassign, move it back to "Backlog". Uses
  PROJECT_TOKEN since the default GITHUB_TOKEN has no Projects v2 scope.
- pr-board-sync.yml (new): on PR opened/edited/ready_for_review, parse
  closing keywords (Closes/Fixes/Resolves #N) from the description and
  move each linked issue's card to "Ready to review".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTLv6fJFA5MotMqWgStGrf
…status-automation-main

feat: sync project board status with /assign and linked PRs
One-click hand-off from a generated model to OrcaSlicer via its
orcaslicer://open?file=<url> deeplink, in the Export dropdown.

- Backend: GET /export/slicer/{fmt}/{token}/model.{fmt} converts the
  workspace GLB to STL on the fly — bakes scene-graph transforms,
  reorients Y-up->Z-up, normalizes print size. Path-only URL ending in
  the filename (no query string), since OrcaSlicer derives the import
  format from the URL's final segment; ancestry-based path containment.
- Electron: slicer:open IPC opens the deeplink and reports failure so the
  UI can fall back when OrcaSlicer isn't installed.
- Frontend: "Open in OrcaSlicer" item in the Export dropdown, shown for
  sliceable workspace meshes; pure deeplink builder with unit tests.

Tests: api/tests/test_export_router.py and orcaSlicerLink.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lindly

The slicer route assumed every source was a unit-sized, Y-up glTF straight from
a generator, but the Export action is reachable for any mesh in the viewer.

- Imported meshes (served through /optimize/serve-file) were excluded outright,
  so the most direct "I have a model, slice it" path offered no action at all
  and gave no hint why. They are now sliceable via an exact-membership registry
  of the files the user picked themselves this session, which keeps the route
  closed to arbitrary absolute paths rather than widening its path guard.

- The Y->Z rotation now applies only to glTF sources. STL/OBJ/PLY are already
  Z-up, and import converts them to GLB without touching the axes, so the
  original extension decides -- not the container's.

- Normalising to 50 mm now happens only for unit-sized meshes. A mesh that
  already carries a real-world size is the user's own, and silently shrinking a
  180 mm part would waste a print.

- slicer:open no longer claims it can detect a missing OrcaSlicer: on Windows an
  unregistered scheme still makes ShellExecuteEx succeed, so that error branch
  could never run.

test_normalizes_longest_edge_to_default_print_size encoded the unconditional
rescale, so its fixture becomes a unit-sized mesh, matching real generator
output.
@lightningpixel

lightningpixel commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Thanks for this — the path containment in the slicer route is solid, and the tests covering the .. and sibling-prefix escapes are exactly right.

I pushed a commit on your branch. It changes three things:

  • Imported meshes are now sliceable. Import serves files through /optimize/serve-file, so canOpenInOrcaSlicer hid the action for them — which happens to be the most direct "I have a model, slice it" path. Rather than widening the route's path guard, the backend now keeps an exact-membership registry of the files the user picked themselves this session.
  • Rotation only for glTF sources. STL/OBJ/PLY are already Z-up, and import converts them to GLB without touching the axes, so the original extension decides. Otherwise an upright STL lands in OrcaSlicer on its side.
  • Rescaling only for unit-sized meshes. A mesh that already carries a real-world size is the user's own — silently shrinking a 180 mm part to 50 mm would waste a print.

I also dropped the "make sure it is installed" error: on Windows an unregistered scheme still makes ShellExecuteEx succeed, so that branch could never run.

Worth flagging: I changed test_normalizes_longest_edge_to_default_print_size, since its fixture encoded the unconditional rescale. It's now a unit-sized mesh, matching what the generators actually emit.

@lightningpixel
lightningpixel merged commit db0a580 into lightningpixel:dev Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants