mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 14:12:25 -05:00
feat: add Anycubic Slicer Next, Elegoo Slicer and Snapmaker Orca slicer options
This commit is contained in:
1 parent
6a6a87e38d
commit
9dd3e693e0
6 files changed
+109
-6
No files matched your search
@@ -1,10 +1,20 @@
|
||||
import { prisma } from "../db";
|
||||
|
||||
// Slicers offered for a future "open in {slicer}" launch. Most register their own URL protocol
|
||||
// (e.g. bambustudio://); a couple (bambustudio, prusaslicer, cura) instead route through the
|
||||
// Thingport Bridge helper -- see frontend's utils/slicerLaunch.ts BRIDGED_SLICERS for why.
|
||||
// (e.g. bambustudio://); some (bambustudio, prusaslicer, cura, anycubicslicernext) instead route
|
||||
// through the Thingport Bridge helper -- see frontend's utils/slicerLaunch.ts BRIDGED_SLICERS.
|
||||
// Anything else falls under "other".
|
||||
export const SLICER_IDS = ["bambustudio", "orcaslicer", "prusaslicer", "cura", "crealityprintlink", "other"] as const;
|
||||
export const SLICER_IDS = [
|
||||
"bambustudio",
|
||||
"orcaslicer",
|
||||
"prusaslicer",
|
||||
"cura",
|
||||
"crealityprintlink",
|
||||
"anycubicslicernext",
|
||||
"elegooslicer",
|
||||
"snapmaker-orca",
|
||||
"other",
|
||||
] as const;
|
||||
export type SlicerId = (typeof SLICER_IDS)[number];
|
||||
|
||||
function isSlicerId(value: string): value is SlicerId {
|
||||
|
||||
@@ -11,6 +11,9 @@ handlers won't take a link straight from a self-hosted Thingport instance:
|
||||
- Cura's `cura://open` handler isn't domain-restricted, but its OS protocol registration is
|
||||
unreliable across its AppImage/Flatpak/Microsoft Store builds, so it's routed the same way for
|
||||
consistency.
|
||||
- Anycubic Slicer Next is an OrcaSlicer fork that handles the same links, but it registers
|
||||
OrcaSlicer's own `orcaslicer://` protocol instead of one of its own -- with both installed, only
|
||||
one of them gets those links. The Bridge launches it by path, so the slicer you picked opens.
|
||||
|
||||
OrcaSlicer's protocol handler accepts any HTTP(S) URL directly with no such restriction, so it's
|
||||
launched straight from the browser and never needs this bridge.
|
||||
@@ -169,12 +172,22 @@ Environment override (per slicer): `THINGPORT_SLICER_BAMBUSTUDIO=/path/to/BambuS
|
||||
value and `config.json`/env-var override key)
|
||||
- prusaslicer
|
||||
- cura
|
||||
- anycubicslicernext (Anycubic Slicer Next -- an OrcaSlicer fork that registers OrcaSlicer's own
|
||||
`orcaslicer://` protocol, so it's launched by path to avoid opening OrcaSlicer instead when both
|
||||
are installed)
|
||||
- other (opens with the OS default app)
|
||||
|
||||
Cura's AppImage and Flatpak builds aren't on `PATH` and won't be auto-detected. If "Open in Cura"
|
||||
falls back to opening with your OS default app, add a `cura` entry to `config.json` (see
|
||||
`config.example.json`) or set `THINGPORT_SLICER_CURA=/path/to/cura`.
|
||||
|
||||
Anycubic Slicer Next is looked for as `AnycubicSlicerNext.app` or `Anycubic Slicer Next.app` in
|
||||
`/Applications` or `~/Applications` on macOS, under Program Files / AppData on Windows, and on `PATH`
|
||||
on Linux. If yours is elsewhere (e.g. an AppImage), add an `anycubicslicernext` entry to
|
||||
`config.json` or set `THINGPORT_SLICER_ANYCUBICSLICERNEXT=/path/to/it`.
|
||||
|
||||
On macOS, apps are looked for in both `/Applications` and your own `~/Applications`.
|
||||
|
||||
## Testing
|
||||
|
||||
After install, paste this into your browser's address bar (replace the URL):
|
||||
|
||||
@@ -340,11 +340,19 @@ func findLinuxCommand(id string, candidates map[string][]string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// findMacCommand checks each candidate app bundle in /Applications and then in the user's own
|
||||
// ~/Applications (a common drag-to-install target that needs no admin rights).
|
||||
func findMacCommand(id string, candidates map[string][]string) string {
|
||||
home, _ := os.UserHomeDir()
|
||||
for _, appPath := range candidates[id] {
|
||||
if fileExists(appPath) {
|
||||
return appPath
|
||||
}
|
||||
if home != "" && strings.HasPrefix(appPath, "/Applications/") {
|
||||
if userApp := filepath.Join(home, appPath); fileExists(userApp) {
|
||||
return userApp
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -362,6 +370,15 @@ func windowsCandidates() map[string][]string {
|
||||
"Ultimaker Cura */UltiMaker-Cura.exe",
|
||||
"Ultimaker Cura */Cura.exe",
|
||||
},
|
||||
// Anycubic's fork of OrcaSlicer. Its own orcaslicer:// handler would work too, but it
|
||||
// shares that protocol with OrcaSlicer itself (whichever registered last wins), so it's
|
||||
// launched by path instead. The install folder's exact name isn't documented, hence the
|
||||
// variants and the trailing glob.
|
||||
"anycubicslicernext": {
|
||||
"AnycubicSlicerNext\\AnycubicSlicerNext.exe",
|
||||
"Anycubic Slicer Next\\AnycubicSlicerNext.exe",
|
||||
"Anycubic*/AnycubicSlicerNext.exe", // "/" in globs, like Cura's: Windows accepts it too
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +391,8 @@ func linuxCandidates() map[string][]string {
|
||||
// AppImage and Flatpak builds aren't discoverable this way and need a config.json entry
|
||||
// or THINGPORT_SLICER_CURA override instead (see bridge/README.md).
|
||||
"cura": {"cura", "UltiMaker-Cura"},
|
||||
// Same PATH-only caveat as Cura: an AppImage needs a config.json entry instead.
|
||||
"anycubicslicernext": {"AnycubicSlicerNext", "anycubicslicernext", "anycubic-slicer-next"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +402,8 @@ func macCandidates() map[string][]string {
|
||||
"orcaslicer": {"/Applications/OrcaSlicer.app"},
|
||||
"prusaslicer": {"/Applications/PrusaSlicer.app", "/Applications/Original Prusa Drivers/PrusaSlicer.app"},
|
||||
// Rebranded from "Ultimaker Cura" to "UltiMaker Cura" around version 5.7.
|
||||
"cura": {"/Applications/UltiMaker Cura.app", "/Applications/Ultimaker Cura.app"},
|
||||
"cura": {"/Applications/UltiMaker Cura.app", "/Applications/Ultimaker Cura.app"},
|
||||
"anycubicslicernext": {"/Applications/AnycubicSlicerNext.app", "/Applications/Anycubic Slicer Next.app"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,40 @@ func TestFindWindowsCommandResolvesVersionedCuraInstall(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindWindowsCommandResolvesAnycubicSlicerNext(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
t.Setenv("ProgramFiles", base)
|
||||
t.Setenv("ProgramFiles(x86)", "")
|
||||
t.Setenv("LOCALAPPDATA", "")
|
||||
|
||||
// The folder name isn't documented, so an unexpected one must still be found by the glob.
|
||||
dir := filepath.Join(base, "Anycubic Slicer Next 1.3")
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
exePath := filepath.Join(dir, "AnycubicSlicerNext.exe")
|
||||
if err := os.WriteFile(exePath, nil, 0644); err != nil {
|
||||
t.Fatalf("write exe: %v", err)
|
||||
}
|
||||
|
||||
if got := findWindowsCommand("anycubicslicernext", windowsCandidates()); got != exePath {
|
||||
t.Fatalf("findWindowsCommand(\"anycubicslicernext\", ...) = %q, expected %q", got, exePath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindMacCommandChecksUserApplications(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
appPath := filepath.Join(home, "Applications", "AnycubicSlicerNext.app")
|
||||
if err := os.MkdirAll(appPath, 0755); err != nil {
|
||||
t.Fatalf("mkdir app: %v", err)
|
||||
}
|
||||
candidates := map[string][]string{"anycubicslicernext": {"/Applications/AnycubicSlicerNext.app"}}
|
||||
if got := findMacCommand("anycubicslicernext", candidates); got != appPath {
|
||||
t.Fatalf("findMacCommand(...) = %q, expected %q", got, appPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleProtocolRejectsUntrustedShapesBeforeDownload(t *testing.T) {
|
||||
tests := []string{
|
||||
"https://thingport.example/file",
|
||||
|
||||
@@ -12,6 +12,9 @@ export const SLICER_OPTIONS: SlicerOption[] = [
|
||||
{ id: "prusaslicer", label: "PrusaSlicer" },
|
||||
{ id: "cura", label: "Cura" },
|
||||
{ id: "crealityprintlink", label: "Creality Print" },
|
||||
{ id: "anycubicslicernext", label: "Anycubic Slicer Next" },
|
||||
{ id: "elegooslicer", label: "Elegoo Slicer" },
|
||||
{ id: "snapmaker-orca", label: "Snapmaker Orca" },
|
||||
{ id: "other", label: "Other / Manual" },
|
||||
];
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@ const BRIDGE_SCHEME = "thingport";
|
||||
// consistency. All three are handed to the Bridge instead, which downloads the file itself and
|
||||
// execs the local slicer directly, bypassing both the domain check and protocol registration.
|
||||
// OrcaSlicer accepts any HTTP(S) URL through its own handler, so it still bypasses the Bridge.
|
||||
const BRIDGED_SLICERS = new Set(["bambustudio", "prusaslicer", "cura"]);
|
||||
// Anycubic Slicer Next is an OrcaSlicer fork and understands the same links -- but it registers
|
||||
// OrcaSlicer's own orcaslicer:// protocol rather than one of its own, so with both installed only
|
||||
// one of them gets those links. Routing it through the Bridge, which launches it by path, opens
|
||||
// the one the user actually picked.
|
||||
const BRIDGED_SLICERS = new Set(["bambustudio", "prusaslicer", "cura", "anycubicslicernext"]);
|
||||
|
||||
/** Whether slicerId launches through the Thingport Bridge helper app rather than the slicer's
|
||||
* own URL protocol -- used to show the "install the Bridge" hint for the right slicers. */
|
||||
@@ -20,6 +24,24 @@ export function isBridgedSlicer(slicerId: string): boolean {
|
||||
return BRIDGED_SLICERS.has(slicerId);
|
||||
}
|
||||
|
||||
// Elegoo Slicer (elegooslicer://) and Snapmaker Orca (snapmaker-orca://) are OrcaSlicer forks
|
||||
// that, unlike Anycubic's, register a protocol of their own and accept any HTTP(S) URL through
|
||||
// it, so they launch directly like OrcaSlicer. Both name the downloaded file after the URL's last
|
||||
// path segment, which a prepared print's URL (.../prepared-print) doesn't end in -- so each is
|
||||
// also passed the filename the way its handler reads it: Elegoo from a filename= query parameter
|
||||
// on the file URL itself, Snapmaker Orca from a name= parameter after the file= one.
|
||||
function withFilenameHint(slicerId: string, fileUrl: string, filename: string): { fileUrl: string; extra: string } {
|
||||
if (slicerId === "elegooslicer") {
|
||||
const url = new URL(fileUrl, window.location.origin);
|
||||
url.searchParams.set("filename", filename);
|
||||
return { fileUrl: url.toString(), extra: "" };
|
||||
}
|
||||
if (slicerId === "snapmaker-orca") {
|
||||
return { fileUrl, extra: `&name=${encodeURIComponent(filename)}` };
|
||||
}
|
||||
return { fileUrl, extra: "" };
|
||||
}
|
||||
|
||||
// Builds the URL that "Open in {Slicer}" navigates to: either the slicer's own registered
|
||||
// protocol handler (e.g. orcaslicer://), or -- for a BRIDGED_SLICERS id -- the Thingport Bridge
|
||||
// protocol, which downloads fileUrl locally before handing it to the slicer. Only meaningful for
|
||||
@@ -30,5 +52,6 @@ export function slicerLaunchUrl(slicerId: string, fileUrl: string, filename?: st
|
||||
if (filename) params.set("filename", filename);
|
||||
return `${BRIDGE_SCHEME}://open?${params.toString()}`;
|
||||
}
|
||||
return `${slicerId}://open?file=${encodeURIComponent(fileUrl)}`;
|
||||
const hinted = filename ? withFilenameHint(slicerId, fileUrl, filename) : { fileUrl, extra: "" };
|
||||
return `${slicerId}://open?file=${encodeURIComponent(hinted.fileUrl)}${hinted.extra}`;
|
||||
}
|
||||
Reference in new issue
Block a user