Ship one preset cache per vendor in place of the profile JSONs

Each vendor's system presets serialize into a single <vendor>.opc built at
package time, and a shipped build carries that file alone — the profile JSON
and its sub-file tree are pruned. The vendor loader, the setup wizard's profile
list and the resource installer all read a vendor through its cache, falling
back to parsing whenever one is absent, stale or unreadable, so the cache stays
an optimization and never a source of truth. Caches hold presets in source form
and resolve inheritance at load, through the same code the JSON path uses.
This commit is contained in:
SoftFever
2026-08-05 21:16:41 +08:00
parent 55fb4703c9
commit 8c050894db
26 changed files with 2675 additions and 1372 deletions

5
.gitattributes vendored
View File

@@ -1,2 +1,7 @@
# Set the default behavior, in case people don't have core.autocrlf set. # Set the default behavior, in case people don't have core.autocrlf set.
* text=auto * text=auto
# Shell scripts are run by Git Bash on Windows CI, which cannot read a script
# with CRLF line endings: it fails on the first line. Windows checkouts default
# to core.autocrlf=true, so keep these LF whatever the platform.
*.sh text eol=lf

View File

@@ -162,14 +162,13 @@ jobs:
retention-days: 5 retention-days: 5
if-no-files-found: error if-no-files-found: error
- name: Generate system presets cache (macOS) - name: Build system preset cache (macOS)
if: runner.os == 'macOS' && !inputs.macos-combine-only if: runner.os == 'macOS' && !inputs.macos-combine-only
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
shell: bash shell: bash
run: | # The bundle was already packed from resources/, so the caches have to be
tool=$(find build/${{ inputs.arch }} -name generate_system_cache -type f | head -1) # installed into it here; the source tree keeps its JSONs for later jobs.
profiles=$(find build/${{ inputs.arch }} -path "*/Resources/profiles" -type d | head -1) run: ./scripts/build_preset_cache.sh -b build/${{ inputs.arch }} build/${{ inputs.arch }}/OrcaSlicer/OrcaSlicer.app/Contents/Resources/profiles
"$tool" --path "$profiles" --log_level 2
- name: Pack macOS app bundle ${{ inputs.arch }} - name: Pack macOS app bundle ${{ inputs.arch }}
if: runner.os == 'macOS' && !inputs.macos-combine-only if: runner.os == 'macOS' && !inputs.macos-combine-only
@@ -399,6 +398,13 @@ jobs:
if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 tests } else { .\build_release_vs.bat slicer tests } if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 tests } else { .\build_release_vs.bat slicer tests }
shell: pwsh shell: pwsh
- name: Build system preset cache (Windows)
if: runner.os == 'Windows'
shell: cmd
# Shipped into both the already-installed tree (portable zip, MSIX) and
# the checkout cpack re-installs from when it builds the NSIS installer.
run: scripts\build_preset_cache.bat "%BUILD_DIR%" "resources\profiles" "%BUILD_DIR%\OrcaSlicer\resources\profiles"
- name: Pack unit tests Win - name: Pack unit tests Win
if: runner.os == 'Windows' if: runner.os == 'Windows'
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
@@ -415,23 +421,6 @@ jobs:
retention-days: 5 retention-days: 5
if-no-files-found: error if-no-files-found: error
- name: Generate system presets cache (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$buildDir = $env:BUILD_DIR
$tool = Get-ChildItem -Recurse -Path $buildDir -Filter "generate_system_cache.exe" | Select-Object -First 1
if (-not $tool) { Write-Error "generate_system_cache.exe not found in $buildDir"; exit 1 }
$profiles = Get-ChildItem -Recurse -Path $buildDir -Directory -Filter profiles |
Where-Object { $_.FullName -match 'resources' } | Select-Object -First 1
if (-not $profiles) { Write-Error "profiles directory not found in $buildDir"; exit 1 }
# Add the slicer's runtime DLL directory to PATH so generate_system_cache.exe
# can resolve its dependencies (TKernel.dll etc.) without a full install step.
$dll_dir = Get-ChildItem -Recurse -Path $buildDir -Filter "TKernel.dll" |
Select-Object -First 1 | Select-Object -ExpandProperty DirectoryName
if ($dll_dir) { $env:PATH = "$dll_dir;$env:PATH" }
& $tool.FullName --path $profiles.FullName --log_level 2
# NSIS is x86-only; it runs (and the installer it emits runs) under ARM64's # NSIS is x86-only; it runs (and the installer it emits runs) under ARM64's
# x86 emulation, packaging the native arm64 payload from build-arm64. # x86 emulation, packaging the native arm64 payload from build-arm64.
- name: Create installer Win - name: Create installer Win
@@ -565,6 +554,20 @@ jobs:
retention-days: 5 retention-days: 5
if-no-files-found: error if-no-files-found: error
- name: Build system preset cache (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
# Both were packed from resources/ before the caches existed, so the
# AppImage is unpacked first and the caches shipped into it and into
# the package tree; the source tree keeps its JSONs for later steps.
appimage=$(find build -maxdepth 1 -name "OrcaSlicer_Linux_AppImage*.AppImage" | head -1)
chmod +x "$appimage"
"$appimage" --appimage-extract
./scripts/build_preset_cache.sh -b build build/package/resources/profiles squashfs-root/resources/profiles
appimagetool=$(find build -name "appimagetool.AppImage" | head -1)
ARCH=$(uname -m) "$appimagetool" --appimage-extract-and-run squashfs-root "$appimage"
rm -rf squashfs-root
# Ship the freshly-built validator so slice_check_linux (build_all.yml) # Ship the freshly-built validator so slice_check_linux (build_all.yml)
# can slice-sweep the shipped profiles with this PR's engine. Taken from # can slice-sweep the shipped profiles with this PR's engine. Taken from
# the aarch64 leg so the sweep also exercises the arm build; x86_64 on # the aarch64 leg so the sweep also exercises the arm build; x86_64 on
@@ -579,22 +582,6 @@ jobs:
retention-days: 5 retention-days: 5
if-no-files-found: error if-no-files-found: error
- name: Generate system presets cache (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
tool=$(find build -name generate_system_cache -type f | head -1)
if [ -z "$tool" ]; then echo "ERROR: generate_system_cache not found in build tree" >&2; exit 1; fi
"$tool" --path build/package/resources/profiles --log_level 2
# Re-pack the AppImage so the per-vendor caches are included
appimage=$(find build -maxdepth 1 -name "OrcaSlicer_Linux_AppImage*.AppImage" | head -1)
chmod +x "$appimage"
"$appimage" --appimage-extract
cp build/package/resources/profiles/*.cache squashfs-root/resources/profiles/
appimagetool=$(find build -name "appimagetool.AppImage" | head -1)
ARCH=$(uname -m) "$appimagetool" --appimage-extract-and-run squashfs-root "$appimage"
rm -rf squashfs-root
- name: Run external slicer regression tests - name: Run external slicer regression tests
if: runner.os == 'Linux' && inputs.arch != 'aarch64' if: runner.os == 'Linux' && inputs.arch != 'aarch64'
timeout-minutes: 20 timeout-minutes: 20

2
.gitignore vendored
View File

@@ -49,4 +49,4 @@ internal_docs/
# Python bytecode # Python bytecode
__pycache__/ __pycache__/
*.pyc *.pyc
*.cache *.opc

288
docs/HLSD/preset-cache.md Normal file
View File

@@ -0,0 +1,288 @@
# System Preset Cache — High Level Design
## Why it exists
OrcaSlicer ships tens of thousands of system preset JSON files. Every launch used to
parse all of them: read each vendor profile, walk its machine, process and filament
sub-files, resolve inheritance, and build the preset collections from scratch. That
parse dominated startup, and it produced the same result every time, because system
presets only change when the app is updated or a profile update is installed.
The preset cache replaces that parse with a read. Each vendor's presets are serialized
once — at build time, in CI — into a single binary file the app reads in one pass. The
read replaces the file walk and the JSON parsing, which is where the time went;
resolving inheritance and registering the presets still runs at load, through the same
code the JSON path uses, so the result is the parse's result without the parse.
The cache is **only ever an optimization**. Every rule below exists to guarantee that a
cache is either provably equivalent to parsing the JSONs, or rejected. There is no
"mostly right" cache.
## The unit is one vendor
A cache covers exactly one vendor. `BBL.opc` sits beside `BBL.json` and holds
everything `BBL.json` and the `BBL/` sub-file tree would have produced.
Per-vendor granularity is what makes the system practical:
- A vendor whose profile is bumped invalidates only its own cache. The other 60-odd
vendors keep theirs — even when the bumped vendor is the shared Orca filament
library everyone else inherits from.
- The setup wizard, which loads vendors one at a time, gets the same speedup as
startup without a second code path.
- A vendor with no cache, or a broken one, costs only that vendor a parse.
A cache holds *system* presets only. User presets, project settings and modified
presets are never serialized — they have their own storage and their own lifecycle.
## Where the files live
| Location | Contents on a shipped build | Role |
|---|---|---|
| `resources/profiles/` | `<vendor>.opc` alone — the profile and its preset JSONs both pruned | What the app ships with; the fallback everything falls back to |
| `<data_dir>/system/` | `<vendor>.opc` alone, or `<vendor>.json` + `<vendor>/` after an update | What the user has installed |
| `<data_dir>/system/` (dev build) | `<vendor>.json` + `<vendor>/` + `<vendor>.opc` written at runtime | A developer tree caches as it parses |
Two forms of the same vendor therefore exist, and the system's central rule is that
**a vendor's cache is the whole of it**. Where a cache ships or is installed, no profile
and no preset JSONs sit beside it: the cache carries the presets, the vendor profile,
and the version stamp that says which release it came from. A vendor is "installed" if
either form is present, and its installed version is read from whichever form is there.
What stays beside the caches in `resources/profiles/` is everything that is not a
preset: each vendor's directory of printer thumbnails, cover images, bed models and
hotend meshes, which are read from disk by path and were never part of the cache. Files
that are not vendors at all, `blacklist.json` chief among them, are untouched.
The alternative — shipping both and treating the cache as a sidecar — was rejected. It
doubles the installed size, and it creates a class of bug where the two disagree and
the app's behavior depends on which one a given code path happened to read.
## What a cache file is
A fixed-size header followed by one binary stream.
The header carries a magic number, the cache format version, the payload size and a
CRC32 of the payload. It exists so that a truncated download, a half-written file or a
file from an entirely different program is rejected in microseconds, before anything
tries to interpret it.
The payload opens with the stamps that decide whether the cache may be used at all —
format version, schema fingerprint, vendor name, vendor version — and then the
vendor's data: its vendor profile, three lists of preset entries (process, filament,
machine), and the count of errors the original parse hit.
Each entry is one preset **in source form**: what its JSON sub-file states and nothing
that resolving it derives — the preset's own config diff, the name of the preset it
inherits, and the parse metadata (name, sub-path, description, instantiation, setting
and filament ids, renames). Non-instantiated base presets are stored too; the children
that inherit from them cannot resolve without them.
Three deliberate choices in the layout:
- **Stamps come first**, so the question "what version is this vendor installed at?"
can be answered by reading the first kilobyte. The updater asks that question for
every vendor on every launch; reading tens of megabytes to answer it would give back
the startup time the cache saved.
- **Nothing inherited is baked in.** A filament preset that inherits from the shared
library is stored as its own diff plus its parent's name, and the parent is looked up
when the entry is installed, against whatever library is loaded then. A cache
therefore carries no other vendor's values, and no other vendor's update — the
library's included — can make it stale.
- **Nothing derived is stored.** Default presets, flattened configs, aliases and
lookup maps are all reconstructed at load by the same code the JSON path runs, and
state that path never fills (obsolete-preset lists) is not stored either. This keeps
the cache a record of the vendor's data, not a memory image of the program's state.
## When a cache may be used
A cache is accepted only if every gate below passes. Any failure means "parse the
JSONs instead" — never a hard error, never a partial load.
**1. Integrity.** Magic number, plausible size, CRC32 over the payload.
**2. Cache format version.** A single integer bumped by hand whenever the binary layout
changes in a way nothing else would catch: reordering or retyping a hand-written
serialized field, or changing what the cache's own stamps mean.
**3. Schema fingerprint.** A checksum over the app version and the entire print-config
option schema — every option's key, type, wire ordinal and enum values. This is the
gate that makes the cache safe across development: adding a config option, changing its
type, or reordering the enum values of an existing one all change the fingerprint, so
caches from before the change are rejected without anyone having to remember to bump
anything. It also means a cache never crosses app versions.
**4. Vendor identity and version.** The cache names the vendor it holds and the profile
version it was built from. It is accepted only if that version is at least as new as
the profile now on disk. Where no profile sits beside the cache — the shipped,
cache-only form — the comparison is skipped, because nothing on disk can be newer than
a cache that is the installation.
**5. Every entry installs.** Entries are installed as they are read, and an entry that
cannot be — typically one that inherits a parent the currently loaded filament library
no longer provides — rejects the whole cache, never just the entry. A partial vendor is
not a vendor.
There is deliberately no stamp for the shared filament library. A cache stores its
filaments' inheritance by name and resolves it at load, so a library update changes
what a cache load *produces*, never whether the cache is *valid* — the same file yields
the updated result. This matters most on a shipped build, where a vendor is its cache
and nothing else: a profile update that delivered only the library would otherwise have
stranded every other vendor with a cache it invalidated and no JSONs to fall back on.
A vendor profile with no parsable version is never cached and never served from a
cache. There would be no way to tell later whether the cache had gone stale, and a
cache nothing can invalidate is worse than no cache.
## How a vendor is loaded
Vendors load in a fixed order, because filament inheritance crosses exactly one
boundary: any vendor's filament may inherit from the shared Orca filament library,
and nothing else reaches across vendors. The library therefore goes first, alone;
every other vendor follows in parallel, resolving against it; and the results are
merged in a stable order:
```mermaid
flowchart LR
lib["1 · OrcaFilamentLibrary<br/>loaded first, synchronously"] --> par["2 · every other vendor in parallel,<br/>each into its own bundle, filaments<br/>resolving against the loaded library"] --> merge["3 · bundles merged into one,<br/>sequentially, in stable vendor order"]
```
Whether a vendor comes from its cache or from a parse changes nothing in that
order — both produce the same bundle, so cached and parsed vendors mix freely in
one startup. Each vendor load — startup's and the setup wizard's alike — tries,
in order:
1. The cache in the directory it was asked to load from — normally `<data_dir>/system/`.
2. The shipped cache in `resources/profiles/` — judged against the same installed
profile, so it cannot resurrect a version an update has superseded.
3. Parsing the JSONs — from the data directory if the profile is installed there, and
from `resources/profiles/` otherwise, which on a shipped build only has JSONs for a
vendor that has no cache.
The same decision drawn out — "the gates" are the five acceptance checks above:
```mermaid
flowchart TB
start["load vendor V from a directory dir<br/>— normally &lt;data_dir&gt;/system/"]
start --> stamp["installed version = version of dir/V.json<br/>— or ∞ with no profile there,<br/>the cache then being the installation"]
stamp --> g1{"dir/V.opc<br/>passes all five gates?"}
g1 -- "yes" --> hit(["served from the<br/>installed cache"])
g1 -- "no" --> g2{"resources/profiles/V.opc passes them,<br/>against the same installed version?"}
g2 -- "yes" --> hit2(["served from the<br/>shipped cache"])
g2 -- "no" --> src{"dir/V.json<br/>present?"}
src -- "yes" --> pd["parse the JSONs in dir"]
src -- "no" --> pr["parse the JSONs in<br/>resources/profiles/"]
pd --> ver{"profile version<br/>parsable?"}
pr --> ver
ver -- "yes" --> save(["loaded; dir/V.opc written back —<br/>the next load takes the top path"])
ver -- "no" --> raw(["loaded, never cached"])
```
Serving from a cache is not a memory-image restore. The entries are deserialized and
then installed one by one — inheritance resolved against the presets installed before
them and the currently loaded filament library, configs flattened onto the collection
defaults, validated and registered — by the same function the JSON path calls straight
after parsing a sub-file. The two paths share everything below the parse, which is what
makes a cache-loaded bundle indistinguishable from a JSON-loaded one by construction
rather than by test coverage. Installation also rebuilds each preset's file path from
the local data directory, so a shipped cache never carries the generating machine's
paths.
The second tier is what makes app upgrades work. After an upgrade, a cache the previous
version installed fails the fingerprint gate; the new build's own shipped cache answers
instead, and the user never sees a parse. The stale installed file is simply ignored
until the next profile update overwrites it.
If a parse does happen and the vendor's profile carries a version, the app writes the
cache back beside where it looked for the vendor. That is how a developer build warms
itself up on second launch, and how a vendor delivered by a profile update becomes
cached without waiting for the next release.
## How a vendor is installed
Installing copies from `resources/profiles/` into `<data_dir>/system/`. A shipped build
offers only a cache and a source tree only JSONs, but a partially-generated tree can
have both, at different versions, so the installer picks the form that ships at the
**newer version** and installs only that one:
- Cache newer or equal, and readable → copy the `.opc`, and delete any profile and
vendor directory a previous install left behind, so nothing can shadow it.
- Profile newer, or the cache unreadable or absent → copy the profile and the vendor's
preset JSONs exactly as the app did before caches existed, and delete any stale `.opc`.
The result is that only one form of a vendor is ever present, and it is the newest one
the build has. This matters most for the update check, which compares what is installed
against what installing *would* lay down: if those two disagreed about which form
counts, a vendor could reinstall on every launch forever, or silently never update.
Profile updates delivered over the air always arrive as JSONs, and they win — an
updated vendor's real profile lands in the data directory, the shipped cache is older
and gets rejected, and the vendor is parsed and re-cached. An update that touches only
the filament library needs nothing more: every other vendor's cache stays valid and
simply resolves against the new library on its next load.
## How the caches are produced
Cache generation is a build step, not something a user ever runs.
One script per platform does the whole job, and CI calls it once on each. It builds a
small dev-utility that loads a profiles directory exactly as the app would, with cache
writing enabled, dropping a `<vendor>.opc` beside every vendor profile it parses; then
it copies those caches into each packaged application it was pointed at and deletes
every preset JSON they replace — the vendor's own profile included. Only a vendor that
actually has a cache is pruned, so a vendor the generator skipped keeps its JSONs and is
simply parsed at startup.
Because the schema fingerprint includes the app version, caches must be generated by
the same build that ships them. Generation runs after the build, in the same job.
## Behavior when things go wrong
The system is designed so that no cache problem is fatal:
- **Corrupt, truncated or foreign file** — rejected at the header, vendor parsed.
- **Cache from another app version or schema** — rejected at the fingerprint, vendor
parsed or served from the shipped cache.
- **Stale cache** — rejected on the vendor version stamp, vendor parsed and re-cached.
- **Failure part-way through loading** — a deserialization error, or any entry that
fails to install — rejects the whole cache, and the bundle is reset to a clean state
before falling back, so a half-loaded cache can never leak into the parsed result.
- **A vendor that can be neither read nor parsed** — logged, and left out. The setup
wizard drops that vendor from its list and opens with the rest; startup records the
error alongside the vendors that did load. One broken vendor never takes the app down.
The one genuine limit: on a shipped build a vendor is its cache and nothing else, so a
rejected cache has nothing to fall back to for that vendor. This is by design — the
alternative is shipping every preset twice — and it is why the acceptance gates are
conservative and why CI generates the caches with the same build that ships them. The
recovery path is a profile update, which delivers real JSONs.
It also means nothing may quietly assume a `<vendor>.json` exists. Discovery, version
checks and the update decision all read whichever form is present, and a code path that
enumerates only `*.json` will find no vendors at all in a packaged build.
## Maintenance rules
- **Adding or changing a config option** needs nothing. The fingerprint covers it.
- **Changing a hand-written `serialize()`** — `PresetBundle::CachedPreset`,
`VendorProfile` or its nested types — or the cache's own layout or stamps requires
bumping the cache format version by hand. Nothing else is serialized by hand; the
config payload is covered by the fingerprint.
- **Bumping a vendor profile's version** invalidates that vendor's cache and nothing
else — the filament library's included. Other vendors' caches resolve against the
new library the next time they load.
- **Caches are never committed.** They are build artifacts, generated per build,
ignored by git.
## Where this lives in the tree
| Area | Files |
|---|---|
| Cache format, entry serialization, read/write, load and save | `src/libslic3r/PresetBundle.{hpp,cpp}` |
| Vendor profile serialization | `src/libslic3r/Preset.hpp` |
| Vendor discovery, installed/shipped versions, installation | `src/libslic3r/PresetBundle.cpp` |
| Update and reinstall decisions | `src/slic3r/Utils/PresetUpdater.cpp` |
| Setup wizard and printer-selection dialog | `src/slic3r/GUI/ConfigWizard.cpp`, `src/slic3r/GUI/WebGuideDialog.cpp` |
| Generator tool | `src/dev-utils/generate_system_cache.cpp` |
| Build and packaging script | `scripts/build_preset_cache.{sh,bat}` |
| Tests | `tests/libslic3r/test_vendor_cache.cpp` |

View File

@@ -0,0 +1,122 @@
@echo off
rem Build the per-vendor system preset caches (one <vendor>.opc per vendor) by
rem running the generate_system_cache.exe dev tool against a profiles directory,
rem and make every profiles directory named on the command line ship-ready:
rem install the caches into it and delete the preset JSONs they replace, so a
rem build ships one copy of its presets instead of two.
rem
rem scripts\build_preset_cache.bat [build_dir] [target_dir ...]
rem
rem build_dir defaults to "build"
rem target_dir profiles directories to ship into. Caches are generated into
rem the source tree's resources\profiles, which is what every
rem packaging step copies from; a target may be that same
rem directory, which then only gets pruned.
rem
rem Shipping deletes, so it is a CI packaging step. A vendor's own <vendor>.json
rem goes along with its preset JSONs: the cache carries the vendor profile and
rem the version it was built at, so discovery, version checks and installing all
rem read it there. Only a vendor that has a cache is pruned, so non-vendor JSONs
rem (blacklist.json) are left alone, as are the vendor directories themselves -
rem thumbnails, covers and bed models still live there.
rem
rem set CONFIG=<cfg> to pin the build config for multi-config generators
rem (default: the config of the tool already in the build tree, else Release)
setlocal enabledelayedexpansion
set "REPO_ROOT=%~dp0.."
set "BUILD_DIR=%~1"
if "%BUILD_DIR%"=="" set "BUILD_DIR=build"
if not exist "%BUILD_DIR%\" (
echo ERROR: build tree not found: %BUILD_DIR% 1>&2
exit /b 1
)
if not "%~1"=="" shift
rem Newest match wins: a stale binary silently produces a stale cache layout.
call :find_tool
if not defined CONFIG (
for %%c in (Debug Release RelWithDebInfo MinSizeRel) do (
echo !TOOL! | findstr /i "\\%%c\\" >nul && set "CONFIG=%%c"
)
)
if not defined CONFIG set "CONFIG=Release"
echo Building generate_system_cache in %BUILD_DIR% (%CONFIG%)
cmake --build "%BUILD_DIR%" --config %CONFIG% --target generate_system_cache
if errorlevel 1 (
echo ERROR: could not build generate_system_cache - configure the build tree with -DORCA_TOOLS=ON: 1>&2
echo cmake -S "%REPO_ROOT%" -B "%BUILD_DIR%" -DORCA_TOOLS=ON 1>&2
exit /b 1
)
call :find_tool
if not defined TOOL (
echo ERROR: generate_system_cache.exe not found under %BUILD_DIR% - build with -DORCA_TOOLS=ON 1>&2
exit /b 1
)
set "PROFILES=%REPO_ROOT%\resources\profiles"
if not exist "%PROFILES%\" (
echo ERROR: profiles directory not found: %PROFILES% 1>&2
exit /b 1
)
for %%d in ("%PROFILES%") do set "PROFILES=%%~fd"
rem Add the slicer's runtime DLL directory to PATH so generate_system_cache.exe
rem can resolve its dependencies (TKernel.dll etc.) without a full install step.
set "DLL_DIR="
for /f "delims=" %%f in ('dir /s /b "%BUILD_DIR%\TKernel.dll" 2^>nul') do (
if not defined DLL_DIR set "DLL_DIR=%%~dpf"
)
if defined DLL_DIR set "PATH=%DLL_DIR%;%PATH%"
echo Generating per-vendor preset caches in %PROFILES%
rem Start clean so vendors that went away - and caches written by older tool
rem versions - don't linger next to the freshly generated ones.
del /q "%PROFILES%\*.opc" 2>nul
del /q "%PROFILES%\*.cache" 2>nul
"%TOOL%" --path "%PROFILES%" --log_level 2
if errorlevel 1 exit /b %errorlevel%
:next_target
if "%~1"=="" exit /b 0
call :ship "%~1"
if errorlevel 1 exit /b 1
shift
goto :next_target
:ship
set "TARGET=%~1"
if not exist "%TARGET%\" (
echo ERROR: profiles directory not found: %TARGET% 1>&2
exit /b 1
)
for %%d in ("%TARGET%") do set "TARGET=%%~fd"
if /i not "%TARGET%"=="%PROFILES%" copy /y "%PROFILES%\*.opc" "%TARGET%\" >nul
set /a SHIPPED=0
set /a PRUNED=0
for %%c in ("%PROFILES%\*.opc") do (
set /a SHIPPED+=1
set "VENDOR=%%~nc"
if exist "%TARGET%\!VENDOR!.json" (
del /q "%TARGET%\!VENDOR!.json"
set /a PRUNED+=1
)
if exist "%TARGET%\!VENDOR!\" (
for /f %%n in ('dir /s /b "%TARGET%\!VENDOR!\*.json" 2^>nul ^| find /c /v ""') do set /a PRUNED+=%%n
del /s /q "%TARGET%\!VENDOR!\*.json" >nul 2>&1
rem Deepest first, so a directory the delete above emptied goes too; rd
rem refuses the ones still holding covers or meshes.
for /f "delims=" %%d in ('dir /s /b /ad "%TARGET%\!VENDOR!" 2^>nul ^| sort /r') do rd "%%d" 2>nul
)
)
echo %TARGET%: !SHIPPED! caches, dropped !PRUNED! preset JSONs
exit /b 0
:find_tool
set "TOOL="
for /f "delims=" %%f in ('dir /s /b /o-d "%BUILD_DIR%\generate_system_cache.exe" 2^>nul') do (
if not defined TOOL set "TOOL=%%f"
)
exit /b 0

143
scripts/build_preset_cache.sh Executable file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env bash
# Build the per-vendor system preset caches (one <vendor>.opc per vendor) by
# running the generate_system_cache dev tool against a profiles directory, and
# make every profiles directory named on the command line ship-ready: install
# the caches into it and delete the preset JSONs they replace, so a build ships
# one copy of its presets instead of two.
#
# ./scripts/build_preset_cache.sh # caches into resources/profiles
# ./scripts/build_preset_cache.sh -b build/arm64 # search this build tree for the tool
# ./scripts/build_preset_cache.sh <dir> [<dir> ...] # and ship into these profiles dirs
#
# Caches are generated into the source tree's resources/profiles, which is what
# every packaging step copies from. Shipping deletes, so it is a CI packaging
# step: pass packaged output directories, or the checkout of a build that is
# about to be packaged from it.
#
# A vendor's own <vendor>.json goes along with its preset JSONs: the cache
# carries the vendor profile and the version it was built at, so discovery,
# version checks and installing all read it there. A shipped vendor is its cache
# and nothing else. Only a vendor that has a cache is pruned, so an ungenerated
# vendor keeps its JSONs and is simply parsed at startup; non-vendor JSONs
# (blacklist.json) are left alone, as are the vendor directories themselves —
# thumbnails, covers and bed models still live there.
#
# -b <dir> build tree holding the tool
# (default: build/arm64, build/x86_64, or build — first that exists)
# -p <dir> profiles directory to generate caches into
# (default: <repo>/resources/profiles)
# -c <cfg> build config for multi-config generators
# (default: the config of the tool already in the build tree, else
# the build tree's CMAKE_BUILD_TYPE)
# -n skip the rebuild and run the tool already in the build tree
# -l <level> tool log level (default: 2)
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/.." && pwd -P)"
build_dir=""
profiles_dir=""
config=""
build_tool=1
log_level=2
while getopts "b:p:c:l:nh" opt; do
case $opt in
b) build_dir="$OPTARG" ;;
p) profiles_dir="$OPTARG" ;;
c) config="$OPTARG" ;;
n) build_tool=0 ;;
l) log_level="$OPTARG" ;;
h) sed -n '2,${/^#/!q;p;}' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) exit 1 ;;
esac
done
shift $((OPTIND - 1))
if [ -z "$build_dir" ]; then
for candidate in "$repo_root/build/arm64" "$repo_root/build/x86_64" "$repo_root/build"; do
if [ -d "$candidate" ]; then build_dir="$candidate"; break; fi
done
fi
if [ -z "$build_dir" ] || [ ! -d "$build_dir" ]; then
echo "ERROR: build tree not found (pass -b <build_dir>)" >&2
exit 1
fi
# Newest match wins: multi-config trees keep one binary per config, and a stale
# one silently produces a stale cache layout.
find_tool() {
local best="" f
while IFS= read -r f; do
[ -n "$f" ] || continue
if [ -z "$best" ] || [ "$f" -nt "$best" ]; then best="$f"; fi
done < <(find "$build_dir" -name generate_system_cache -type f 2>/dev/null)
printf '%s' "$best"
}
tool=$(find_tool)
if [ -z "$config" ]; then
case "$tool" in
*/Debug/*) config=Debug ;;
*/Release/*) config=Release ;;
*/RelWithDebInfo/*) config=RelWithDebInfo ;;
*/MinSizeRel/*) config=MinSizeRel ;;
*) config=$(sed -n 's/^CMAKE_BUILD_TYPE:[A-Z]*=\(.\+\)$/\1/p' "$build_dir/CMakeCache.txt" 2>/dev/null | head -1 || true) ;;
esac
fi
if [ "$build_tool" = 1 ]; then
echo "Building generate_system_cache in $build_dir${config:+ ($config)}"
build_args=(--build "$build_dir" --target generate_system_cache)
if [ -n "$config" ]; then build_args+=(--config "$config"); fi
if ! cmake "${build_args[@]}"; then
echo "ERROR: could not build generate_system_cache — configure the build tree with -DORCA_TOOLS=ON:" >&2
echo " cmake -S \"$repo_root\" -B \"$build_dir\" -DORCA_TOOLS=ON" >&2
exit 1
fi
tool=$(find_tool)
fi
if [ -z "$tool" ]; then
echo "ERROR: generate_system_cache not found under $build_dir — build with -DORCA_TOOLS=ON" >&2
exit 1
fi
if [ -z "$profiles_dir" ]; then profiles_dir="$repo_root/resources/profiles"; fi
if [ ! -d "$profiles_dir" ]; then
echo "ERROR: profiles directory not found: $profiles_dir" >&2
exit 1
fi
profiles_dir=$(cd "$profiles_dir" && pwd -P)
# Start clean so vendors that went away — and caches written by older tool
# versions — don't linger next to the freshly generated ones.
echo "Generating per-vendor preset caches in $profiles_dir"
rm -f "$profiles_dir"/*.opc "$profiles_dir"/*.cache
"$tool" --path "$profiles_dir" --log_level "$log_level"
for target in "$@"; do
resolved=$(cd "$target" 2>/dev/null && pwd -P) || {
echo "ERROR: profiles directory not found: $target" >&2
exit 1
}
if [ "$resolved" != "$profiles_dir" ]; then
cp "$profiles_dir"/*.opc "$resolved"/
fi
pruned=0
shipped=0
for cache in "$profiles_dir"/*.opc; do
vendor=$(basename "$cache" .opc)
shipped=$(( shipped + 1 ))
if [ -f "$resolved/$vendor.json" ]; then
rm -f "$resolved/$vendor.json"
pruned=$(( pruned + 1 ))
fi
[ -d "$resolved/$vendor" ] || continue
n=$(find "$resolved/$vendor" -name '*.json' | wc -l)
find "$resolved/$vendor" -name '*.json' -delete
find "$resolved/$vendor" -type d -empty -delete
pruned=$(( pruned + n ))
done
echo "$resolved: $shipped caches, dropped $pruned preset JSONs"
done

View File

@@ -115,8 +115,6 @@ if(ORCA_TOOLS)
endif() endif()
target_link_libraries(OrcaSlicer_profile_validator libslic3r boost_headeronly libcurl OpenSSL::SSL OpenSSL::Crypto) target_link_libraries(OrcaSlicer_profile_validator libslic3r boost_headeronly libcurl OpenSSL::SSL OpenSSL::Crypto)
target_compile_definitions(OrcaSlicer_profile_validator PRIVATE -DBOOST_ALL_NO_LIB -DBOOST_USE_WINAPI_VERSION=0x602 -DBOOST_SYSTEM_USE_UTF8) target_compile_definitions(OrcaSlicer_profile_validator PRIVATE -DBOOST_ALL_NO_LIB -DBOOST_USE_WINAPI_VERSION=0x602 -DBOOST_SYSTEM_USE_UTF8)
endif() endif()
# Create a slic3r executable # Create a slic3r executable

View File

@@ -23,7 +23,7 @@ endif()
if (ORCA_TOOLS) if (ORCA_TOOLS)
set(_DEV_DEFS -DBOOST_ALL_NO_LIB -DBOOST_USE_WINAPI_VERSION=0x602 -DBOOST_SYSTEM_USE_UTF8) set(_DEV_DEFS -DBOOST_ALL_NO_LIB -DBOOST_USE_WINAPI_VERSION=0x602 -DBOOST_SYSTEM_USE_UTF8)
# generate_system_cache: pre-generates per-vendor resources/profiles/<id>.cache files for CI bundling. # generate_system_cache: pre-generates per-vendor <vendor>.opc files under resources/profiles for CI bundling.
add_executable(generate_system_cache generate_system_cache.cpp) add_executable(generate_system_cache generate_system_cache.cpp)
target_link_libraries(generate_system_cache libslic3r boost_headeronly) target_link_libraries(generate_system_cache libslic3r boost_headeronly)
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS}) target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})

View File

@@ -2,10 +2,10 @@
#include "libslic3r/Preset.hpp" #include "libslic3r/Preset.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/program_options.hpp> #include <boost/program_options.hpp>
#include <boost/system/error_code.hpp>
#include <iostream> #include <iostream>
using namespace Slic3r; using namespace Slic3r;
@@ -58,31 +58,27 @@ int main(int argc, char* argv[])
auto preset_bundle = std::make_unique<PresetBundle>(); auto preset_bundle = std::make_unique<PresetBundle>();
preset_bundle->set_is_validation_mode(true); preset_bundle->set_is_validation_mode(true);
preset_bundle->set_default_suppressed(true); preset_bundle->set_default_suppressed(true);
preset_bundle->set_generate_vendor_caches(true);
std::cout << "Loading system presets from: " << profiles_path << "\n"; std::cout << "Loading system presets from: " << profiles_path << "\n";
try { try {
// In validation mode data_dir() is the profiles directory set above, so the
// loader writes each <vendor>.opc next to its <vendor>.json as it parses it.
preset_bundle->load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent); preset_bundle->load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent);
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
std::cerr << "Failed to load presets: " << ex.what() << "\n"; std::cerr << "Failed to load presets: " << ex.what() << "\n";
return 1; return 1;
} }
const std::string output_path = size_t cache_count = 0;
(fs::path(profiles_path) / "system_presets.cache").make_preferred().string(); for (auto& entry : fs::directory_iterator(profiles_path))
if (boost::iends_with(entry.path().string(), ".opc"))
std::cout << "Saving single-bundle cache to: " << output_path << "\n"; ++ cache_count;
if (cache_count == 0) {
const auto stats = preset_bundle->save_system_presets_cache(profiles_path, output_path); std::cerr << "No vendor cache files were generated under " << profiles_path << "\n";
if (!stats.ok) {
std::cerr << "ERROR: verification failed\n";
return 1; return 1;
} }
std::cout << "Generated " << cache_count << " vendor cache file(s) under " << profiles_path << "\n";
std::cout << "[ok] system_presets.cache\n"
<< " Total print presets: " << stats.print_presets << "\n"
<< " Total filament presets: " << stats.filament_presets << "\n"
<< " Total printer presets: " << stats.printer_presets << "\n";
return 0; return 0;
} }

View File

@@ -28,6 +28,9 @@
#include <cereal/access.hpp> #include <cereal/access.hpp>
#include <cereal/types/base_class.hpp> #include <cereal/types/base_class.hpp>
// The serialize() members below archive ConfigOption hierarchies through
// cereal::base_class, whose registration machinery lives in polymorphic.hpp.
#include <cereal/types/polymorphic.hpp>
namespace Slic3r { namespace Slic3r {
struct FloatOrPercent struct FloatOrPercent

View File

@@ -152,17 +152,6 @@ Semver get_version_from_json(std::string file_path)
} }
} }
std::string get_vendor_cache_key(const std::string& json_path)
{
const Semver ver = get_version_from_json(json_path);
if (ver.valid())
return ver.to_string();
// No version field — use mtime as change fingerprint so edits invalidate the cache.
boost::system::error_code ec;
const std::time_t mtime = boost::filesystem::last_write_time(json_path, ec);
return ec ? std::string{} : ("mtime:" + std::to_string(mtime));
}
//BBS: add a function to load the key-values from xxx.json //BBS: add a function to load the key-values from xxx.json
int get_values_from_json(std::string file_path, std::vector<std::string>& keys, std::map<std::string, std::string>& key_values) int get_values_from_json(std::string file_path, std::vector<std::string>& keys, std::map<std::string, std::string>& key_values)
{ {
@@ -275,18 +264,28 @@ void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil
} }
}; };
// The four variant sets are immutable after static init and probed for every
// key of every preset loaded; one merged map makes that a single lookup.
// emplace keeps the first insertion, preserving the first-set-wins priority
// of the else-if chain this replaces.
static const std::unordered_map<std::string, int> variant_class = [] {
std::unordered_map<std::string, int> m;
for (const std::string& k : print_options_with_variant) m.emplace(k, 0);
for (const std::string& k : filament_options_with_variant) m.emplace(k, 1);
for (const std::string& k : printer_options_with_variant_1) m.emplace(k, 2);
for (const std::string& k : printer_options_with_variant_2) m.emplace(k, 3);
return m;
}();
for(auto& key :config.keys()){ for(auto& key :config.keys()){
if(auto iter = print_options_with_variant.find(key); iter != print_options_with_variant.end()){ auto iter = variant_class.find(key);
replace_nil_and_resize(key, process_variant_length); if (iter == variant_class.end())
} continue;
else if(auto iter = filament_options_with_variant.find(key); iter != filament_options_with_variant.end()){ switch (iter->second) {
replace_nil_and_resize(key, filament_variant_length); case 0: replace_nil_and_resize(key, process_variant_length); break;
} case 1: replace_nil_and_resize(key, filament_variant_length); break;
else if(auto iter = printer_options_with_variant_1.find(key); iter != printer_options_with_variant_1.end()){ case 2: replace_nil_and_resize(key, machine_variant_length); break;
replace_nil_and_resize(key, machine_variant_length); case 3: replace_nil_and_resize(key, machine_variant_length * 2); break;
}
else if(auto iter = printer_options_with_variant_2.find(key); iter != printer_options_with_variant_2.end()){
replace_nil_and_resize(key, machine_variant_length * 2);
} }
} }
} }
@@ -758,7 +757,6 @@ void Preset::save(DynamicPrintConfig* parent_config)
idx_file.replace_extension(".info"); idx_file.replace_extension(".info");
this->save_info(idx_file.string()); this->save_info(idx_file.string());
} }
} }
void Preset::reload(Preset const &parent) void Preset::reload(Preset const &parent)

View File

@@ -16,14 +16,6 @@
#include "Semver.hpp" #include "Semver.hpp"
#include "ProjectTask.hpp" #include "ProjectTask.hpp"
#include <cereal/archives/binary.hpp>
#include <cereal/cereal.hpp>
#include <cereal/types/map.hpp>
#include <cereal/types/polymorphic.hpp>
#include <cereal/types/set.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
//BBS: change system directories //BBS: change system directories
#define PRESET_SYSTEM_DIR "system" #define PRESET_SYSTEM_DIR "system"
#define PRESET_USER_DIR "user" #define PRESET_USER_DIR "user"
@@ -122,10 +114,6 @@ extern Semver get_version_from_json(std::string file_path);
//BBS: add a function to load the key-values from xxx.json //BBS: add a function to load the key-values from xxx.json
extern int get_values_from_json(std::string file_path, std::vector<std::string>& keys, std::map<std::string, std::string>& key_values); extern int get_values_from_json(std::string file_path, std::vector<std::string>& keys, std::map<std::string, std::string>& key_values);
// Returns the cache key for a vendor JSON: the Semver string for versioned
// vendors, or "mtime:<unix_timestamp>" for vendors without a version field.
extern std::string get_vendor_cache_key(const std::string& json_path);
extern ConfigFileType guess_config_file_type(const boost::property_tree::ptree &tree); extern ConfigFileType guess_config_file_type(const boost::property_tree::ptree &tree);
extern void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil_to_default, const DynamicPrintConfig& defaults); extern void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil_to_default, const DynamicPrintConfig& defaults);
@@ -144,8 +132,9 @@ public:
PrinterVariant(const std::string &name) : name(name) {} PrinterVariant(const std::string &name) : name(name) {}
std::string name; std::string name;
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
template<class Archive> template<class Archive>
void serialize(Archive& ar) { ar(name); } void serialize(Archive& ar) { ar(name); } // PrinterVariant
}; };
struct PrinterModel { struct PrinterModel {
@@ -154,7 +143,7 @@ public:
std::string name; std::string name;
//BBS: this is internal id for the printer. Currently only used for searching in database //BBS: this is internal id for the printer. Currently only used for searching in database
std::string model_id; std::string model_id;
PrinterTechnology technology; PrinterTechnology technology = ptFFF;
std::string family; std::string family;
std::vector<PrinterVariant> variants; std::vector<PrinterVariant> variants;
std::vector<std::string> default_materials; std::vector<std::string> default_materials;
@@ -178,13 +167,15 @@ public:
const PrinterVariant* variant(const std::string &name) const { return const_cast<PrinterModel*>(this)->variant(name); } const PrinterVariant* variant(const std::string &name) const { return const_cast<PrinterModel*>(this)->variant(name); }
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
template<class Archive> template<class Archive>
void serialize(Archive& ar) void serialize(Archive& ar) // PrinterModel
{ {
ar(id, name, model_id, family, technology, variants, default_materials, ar(id, name, model_id, technology, family, variants, default_materials,
not_support_bed_types, bed_model, bed_texture, image_bed_type, not_support_bed_types, bed_model, bed_texture, image_bed_type,
bottom_texture_end_name, use_double_extruder_default_texture, bottom_texture_end_name, use_double_extruder_default_texture,
bottom_texture_rect, middle_texture_rect, hotend_model); bottom_texture_rect, bottom_texture_rect_longer, middle_texture_rect,
hotend_model);
} }
}; };
std::vector<PrinterModel> models; std::vector<PrinterModel> models;
@@ -197,10 +188,11 @@ public:
bool valid() const { return ! name.empty() && ! id.empty() && config_version.valid(); } bool valid() const { return ! name.empty() && ! id.empty() && config_version.valid(); }
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
template<class Archive> template<class Archive>
void serialize(Archive& ar) void serialize(Archive& ar) // VendorProfile
{ {
ar(id, name, config_version, config_update_url, changelog_url, ar(name, id, config_version, config_update_url, changelog_url,
models, default_filaments, default_sla_materials); models, default_filaments, default_sla_materials);
} }
@@ -456,31 +448,12 @@ public:
// BBS: move constructor to public // BBS: move constructor to public
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {} Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
// Default constructor is public so cereal can default-construct elements when
// deserializing std::vector<Preset> (std::allocator is not a cereal::access friend).
Preset() = default;
protected: protected:
friend class PresetCollection; friend class PresetCollection;
friend class PresetBundle; friend class PresetBundle;
friend class cereal::access;
// Serializes all value fields of Preset for the binary vendor cache. Preset() = default;
// Raw pointers (vendor, loading_substitutions) are excluded — vendor is reconstructed
// by apply_vendor_cache() from the VendorProfile stored alongside the presets.
template<class Archive>
void serialize(Archive& ar)
{
ar(type, name, alias, file, version,
filament_id, setting_id, description,
renamed_from, is_system, is_visible,
is_default, is_external, is_dirty, is_compatible,
is_project_embedded, loaded,
m_from_orca_filament_lib, m_excluded_from,
bundle_id, user_id, base_id, sync_info,
updated_time, key_values, ini_str,
config);
}
}; };
bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer); bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer);

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
#include "enum_bitmask.hpp" #include "enum_bitmask.hpp"
#include <memory> #include <memory>
#include <set>
#include <shared_mutex> #include <shared_mutex>
#include <unordered_map> #include <unordered_map>
#include <optional> #include <optional>
@@ -13,7 +14,6 @@
#include <boost/filesystem/path.hpp> #include <boost/filesystem/path.hpp>
#include <unordered_set> #include <unordered_set>
#define DEFAULT_USER_FOLDER_NAME "default" #define DEFAULT_USER_FOLDER_NAME "default"
#define BUNDLE_STRUCTURE_JSON_NAME "bundle_structure.json" #define BUNDLE_STRUCTURE_JSON_NAME "bundle_structure.json"
@@ -171,31 +171,72 @@ struct PresetBundleMetadata
class PresetBundle class PresetBundle
{ {
public: public:
// ---- System presets single-bundle cache -------------------------------- // ---- Per-vendor preset cache --------------------------------------------
// All vendor profiles + all system presets in one file. // One cache file per vendor (plus the Orca filament library), stamped with
// Bundled (CI): resources/profiles/system_presets.cache // the vendor's own profile version rather than a directory scan.
// User runtime: data_dir/system/system_presets.cache
struct SaveCacheResult { // The cache is not something a caller loads from: a vendor is loaded with
bool ok = false; // load_vendor_configs_from_json, which comes from the cache whenever one covers
size_t print_presets = 0; // it. What is public here is what the cache's own tests drive directly.
size_t filament_presets = 0;
size_t printer_presets = 0; // One preset as its JSON subfile states it: the config diff, the name of the
// preset it inherits, and the parse metadata — everything the parse phase of
// load_vendor_configs_from_json extracts and nothing it derives. Inheritance
// is resolved when the entry is installed, against whatever filament library
// is loaded then, so a cache carries no other vendor's values and no other
// vendor's update can make it stale.
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
struct CachedPreset
{
std::string name;
std::string sub_path; // path under the vendor's directory
DynamicPrintConfig config_src; // the preset's own diff, nothing inherited
std::string inherits;
std::string description;
std::string instantiation; // "true"/"false" as stated; anything else was already counted as a parse error
std::string setting_id;
std::string filament_id;
std::vector<std::string> renamed_from;
template<class Archive>
void serialize(Archive& ar)
{
ar(name, sub_path, config_src, inherits, description, instantiation,
setting_id, filament_id, renamed_from);
}
}; };
static std::string bundled_system_presets_cache_path(); // Save one vendor (vendor_name at vendor_version): its vendor profile and its
static std::string user_system_presets_cache_path(); // presets in source form, plus how many errors their parse counted.
static bool save_vendor_cache(const std::string& cache_path, const std::string& vendor_name,
const std::string& vendor_version, const VendorMap& vendors,
const std::vector<CachedPreset>& process_entries,
const std::vector<CachedPreset>& filament_entries,
const std::vector<CachedPreset>& machine_entries,
uint64_t parse_errors);
// Capture the currently-loaded PresetBundle and write a single-bundle cache // Load a validated per-vendor cache into this bundle by installing its
// to output_path. profiles_dir contains the vendor JSON files (for the key). // entries, with base_bundle's filament library as the inheritance base.
// Used by generate_system_cache and tests. // Rejects (returns false, with this bundle left clean) unless the cache
SaveCacheResult save_system_presets_cache(const std::string& profiles_dir, // version, schema fingerprint and vendor name match, the cache was built
const std::string& output_path) const; // from a vendor profile at least as new as the expected one, and every
// entry installs. An invalid expected version (a profile whose version
// cannot be judged) is never served from cache; Semver::inf() (no profile
// beside the cache at all) accepts whatever is cached.
bool load_vendor_cache(const std::string& cache_path, const std::string& expected_vendor_name,
const Semver& expected_vendor_version, const PresetBundle* base_bundle = nullptr);
// Load a single-bundle cache into out_bundle for the guide wizard. // Read the profile version a cache was stamped with, without deserializing its
// Returns false if the cache is missing, stale, or corrupt. // presets. Empty if the file is unreadable, not a cache this build understands,
static bool load_system_presets_cache_for_guide(const std::string& cache_path, // or not this vendor's. This is how an installed vendor's version is known when
PresetBundle& out_bundle); // only its cache is installed.
static std::string peek_vendor_cache_version(const std::string& cache_path, const std::string& expected_vendor_name);
// Enable writing a per-vendor cache after a JSON parse (off by default). Cache
// content is pure parse output, so the guard is policy, not correctness: only
// the deliberate generators (load_system_presets_from_json, the cache build
// tool) write files, not every incidental load a dialog performs.
void set_generate_vendor_caches(bool enable) { m_generate_vendor_caches = enable; }
static DynamicPrintConfig construct_full_config(Preset &in_printer_preset, static DynamicPrintConfig construct_full_config(Preset &in_printer_preset,
Preset &in_print_preset, Preset &in_print_preset,
@@ -471,8 +512,12 @@ public:
/*std::pair<PresetsConfigSubstitutions, size_t> load_configbundle( /*std::pair<PresetsConfigSubstitutions, size_t> load_configbundle(
const std::string &path, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule);*/ const std::string &path, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule);*/
//Orca: load config bundle from json, pass the base bundle to support cross vendor inheritance //Orca: load config bundle from json, pass the base bundle to support cross vendor inheritance
// Orca: `dir` is where the vendor is looked for — its own directory, whether or
// not the profile JSONs are still there. A whole-vendor load comes from the
// vendor's preset cache whenever one covers the profile on disk, and is parsed
// from the JSONs (falling back to the ones in resources) only when none does.
std::pair<PresetsConfigSubstitutions, size_t> load_vendor_configs_from_json( std::pair<PresetsConfigSubstitutions, size_t> load_vendor_configs_from_json(
const std::string &path, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr); const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr);
// Export a config bundle file containing all the presets and the names of the active presets. // Export a config bundle file containing all the presets and the names of the active presets.
//void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false); //void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false);
@@ -544,19 +589,55 @@ public:
// Orca: for validation only. // Orca: for validation only.
bool has_errors(bool check_duplicate_filament_subtypes = false) const; bool has_errors(bool check_duplicate_filament_subtypes = false) const;
// Errors the last load recorded. What the cache's error accounting promises —
// a cache-served vendor reports what its parse would — is pinned against this.
int error_count() const { return m_errors; }
// Orca: for validation only. Flag any system preset whose inherits / compatible_printers / // Orca: for validation only. Flag any system preset whose inherits / compatible_printers /
// compatible_prints references a deleted (unknown) or renamed (old) preset name. // compatible_prints references a deleted (unknown) or renamed (old) preset name.
bool check_preset_references() const; bool check_preset_references() const;
// Merge one vendor's presets with the other vendor's presets, report duplicates.
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
std::vector<std::string> merge_presets(PresetBundle &&other);
private: private:
// Compute combined invalidation key from all vendor JSON files in system_dir. // Load one vendor from its preset cache: the one in `dir`, or — when that is
static std::string compute_system_presets_cache_key(const std::string& system_dir); // missing or stale — the one shipped in resources/profiles, both judged against
// Try user cache then bundled cache; apply all presets on hit. // the vendor as installed in `dir`. False, with this bundle left clean, when
bool try_load_system_presets_from_cache(const std::string& expected_key); // neither is usable and the vendor has to be parsed. This is how
// load_vendor_configs_from_json reads a cache.
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
// Read raw cache blob: verify magic, size, CRC. // Read raw cache blob: verify magic, size, CRC.
static bool read_cache_blob(const std::string& path, std::string& out_blob); static bool read_cache_blob(const std::string& path, std::string& out_blob);
// Write a cache blob with the standard 20-byte file header. // Write a cache blob with the standard 20-byte file header. False when the
static void write_cache_blob(const std::string& path, const std::string& blob); // file could not be opened or written whole.
static bool write_cache_blob(const std::string& path, const std::string& blob);
// Install one source-form preset entry into this bundle: resolve `inherits`,
// flatten, validate and register the preset. Returns the reason installation
// failed, empty on success. See the definition for the sharing contract
// between the JSON parse and the cache load.
// retain_configs, when non-null, names the only presets registered into
// config_maps (a full config copy each). The cache load passes the names its
// entries inherit — the only ones ever looked up again; the JSON parse
// retains all, not knowing what later subfiles inherit.
std::string install_vendor_preset(const CachedPreset& entry,
const std::string& path, const std::string& vendor_name,
const PresetBundle* base_bundle,
LoadConfigBundleAttributes flags,
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps,
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
const std::set<std::string>* retain_configs = nullptr);
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
void clear_printer_hold_aliases();
// Whether to (re)write a per-vendor cache after a JSON parse.
bool m_generate_vendor_caches { false };
// Orca: validation only - flag any printer with two or more compatible // Orca: validation only - flag any printer with two or more compatible
// filament presets sharing one filament_id (ambiguous AMS subtype match). // filament presets sharing one filament_id (ambiguous AMS subtype match).
@@ -565,8 +646,6 @@ private:
//std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule); //std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule);
//BBS: add json related logic //BBS: add json related logic
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule); std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule);
// Merge one vendor's presets with the other vendor's presets, report duplicates.
std::vector<std::string> merge_presets(PresetBundle &&other);
// Update the multicolor information for filaments. // Update the multicolor information for filaments.
void update_filament_multi_color(); void update_filament_multi_color();
// Update renamed_from and alias maps of system profiles. // Update renamed_from and alias maps of system profiles.
@@ -601,6 +680,38 @@ private:
ENABLE_ENUM_BITMASK_OPERATORS(PresetBundle::LoadConfigBundleAttribute) ENABLE_ENUM_BITMASK_OPERATORS(PresetBundle::LoadConfigBundleAttribute)
// True if `vendor` is installed in data_dir()/system. A build that ships preset
// caches installs the cache alone, so it — not the profile — marks a vendor
// installed, and either one on its own counts.
extern bool is_vendor_installed(const std::string& vendor);
// The version of the installed vendor: what its profile claims, or what its cache
// was stamped with where only the cache is installed. Invalid Semver if neither is.
extern Semver installed_vendor_version(const std::string& vendor);
// Remove every form `vendor` can be installed as from data_dir()/system: its
// profile, its preset cache, and its preset directory.
extern void remove_installed_vendor(const std::string& vendor);
// The vendors `dir` holds, sorted: one is named by its profile or, in a build that
// ships preset caches instead of the raw profile JSONs, by its cache alone.
extern std::set<std::string> vendor_names_in(const boost::filesystem::path& dir);
// The version a build ships `vendor` at: whichever of its preset cache and its
// profile is newer, that being the one installing lays down. Invalid Semver if the
// build ships neither.
extern Semver resource_vendor_version(const std::string& vendor);
// Install vendors from the resources directory into the data directory, each as
// its preset cache or as its profile and preset JSONs — whichever of the two the
// build ships at the newer version. Anything the previous install of that vendor
// left behind goes, so only the form just installed is there to be loaded.
// bundle_names: vendor names, without extension.
// Returns false on the first vendor that cannot be installed.
extern bool install_vendor_bundles_from_resources(const std::vector<std::string>& bundle_names,
const std::string& resource_subdir = "profiles",
const std::string& data_subdir = "system");
} // namespace Slic3r } // namespace Slic3r
#endif /* slic3r_PresetBundle_hpp_ */ #endif /* slic3r_PresetBundle_hpp_ */

View File

@@ -197,7 +197,10 @@ public:
std::string save_minimal(const Archive&) const { return to_string_sf(); } std::string save_minimal(const Archive&) const { return to_string_sf(); }
template<class Archive> template<class Archive>
void load_minimal(const Archive&, const std::string& s) { void load_minimal(const Archive&, const std::string& s) {
if (auto v = Semver::parse(s)) *this = std::move(*v); auto v = Semver::parse(s);
if (! v)
throw std::runtime_error("Semver: cannot parse serialized version: " + s);
*this = std::move(*v);
} }
private: private:

View File

@@ -722,15 +722,6 @@ void copy_directory_recursively(const boost::filesystem::path& source,
std::function<bool(const std::string)> filter = nullptr, std::function<bool(const std::string)> filter = nullptr,
bool merge_mode = false); bool merge_mode = false);
// Install vendor bundles from resources directory to data directory
// bundle_names: vector of vendor bundle names (without .json extension)
// resource_subdir: subdirectory under resources_dir() (default: "profiles")
// data_subdir: subdirectory under data_dir() (default: "system")
// Returns: true if all bundles installed successfully, false otherwise
bool install_vendor_bundles_from_resources(const std::vector<std::string>& bundle_names,
const std::string& resource_subdir = "profiles",
const std::string& data_subdir = "system");
// Orca: Since 1.7.9 Boost deprecated save_string_file and load_string_file, copy and modified from boost 1.7.8 // Orca: Since 1.7.9 Boost deprecated save_string_file and load_string_file, copy and modified from boost 1.7.8
void save_string_file(const boost::filesystem::path& p, const std::string& str); void save_string_file(const boost::filesystem::path& p, const std::string& str);
void load_string_file(const boost::filesystem::path& p, std::string& str); void load_string_file(const boost::filesystem::path& p, std::string& str);

View File

@@ -1724,76 +1724,6 @@ void copy_directory_recursively(const boost::filesystem::path& source,
return; return;
} }
bool install_vendor_bundles_from_resources(
const std::vector<std::string>& bundle_names,
const std::string& resource_subdir,
const std::string& data_subdir)
{
namespace fs = boost::filesystem;
fs::path rsrc_path = fs::path(Slic3r::resources_dir()) / resource_subdir;
fs::path vendor_path = fs::path(Slic3r::data_dir()) / data_subdir;
BOOST_LOG_TRIVIAL(info) << "Installing " << bundle_names.size() << " bundles from resources...";
for (const auto &bundle : bundle_names) {
try {
// Install the JSON file
auto path_in_rsrc = (rsrc_path / bundle).replace_extension(".json");
auto path_in_vendors = (vendor_path / bundle).replace_extension(".json");
if (!fs::exists(path_in_rsrc)) {
BOOST_LOG_TRIVIAL(warning) << "Bundle not found in resources: " << bundle;
return false;
}
// Create target directory if needed
if (!fs::exists(vendor_path))
fs::create_directories(vendor_path);
// Copy JSON file
std::string error_message;
CopyFileResult cfr = copy_file(path_in_rsrc.string(), path_in_vendors.string(), error_message, false);
if (cfr != CopyFileResult::SUCCESS) {
BOOST_LOG_TRIVIAL(error) << "Failed to copy " << bundle << ".json: " << error_message;
return false;
}
// Copy the vendor directory (if it exists)
auto dir_in_rsrc = rsrc_path / bundle;
auto dir_in_vendors = vendor_path / bundle;
if (fs::exists(dir_in_rsrc) && fs::is_directory(dir_in_rsrc)) {
// Remove existing directory
if (fs::exists(dir_in_vendors))
fs::remove_all(dir_in_vendors);
fs::create_directories(dir_in_vendors);
// Copy with file filter (same as PresetUpdater::install_bundles_rsrc)
// Filter out certain file types: .stl, .png, .svg, .jpeg, .jpg, .3mf
auto file_filter = [](const std::string name) -> bool {
return boost::iends_with(name, ".stl") ||
boost::iends_with(name, ".png") ||
boost::iends_with(name, ".svg") ||
boost::iends_with(name, ".jpeg") ||
boost::iends_with(name, ".jpg") ||
boost::iends_with(name, ".3mf");
};
copy_directory_recursively(dir_in_rsrc, dir_in_vendors, file_filter);
}
BOOST_LOG_TRIVIAL(info) << "Successfully installed bundle: " << bundle;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "Exception installing bundle " << bundle << ": " << e.what();
return false;
}
}
return true;
}
void save_string_file(const boost::filesystem::path& p, const std::string& str) void save_string_file(const boost::filesystem::path& p, const std::string& str)
{ {
boost::nowide::ofstream file; boost::nowide::ofstream file;

View File

@@ -432,14 +432,9 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot:
cfg.models_variants_installed.erase(it ++); cfg.models_variants_installed.erase(it ++);
else else
++ it; ++ it;
// Read the active config bundle, parse the config version. // Orca: the version the vendor is installed at, read from its profile or —
PresetBundle bundle; // where the cache is the whole installation — from the cache's own stamp.
//BBS: change directoties by design cfg.version.config_version = installed_vendor_version(cfg.name);
//bundle.load_configbundle((data_dir / PRESET_SYSTEM_DIR / (cfg.name + ".ini")).string(), PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent);
bundle.load_vendor_configs_from_json((data_dir/PRESET_SYSTEM_DIR).string(), cfg.name, PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent);
for (const auto &vp : bundle.vendors)
if (vp.second.id == cfg.name)
cfg.version.config_version = vp.second.config_version;
snapshot.vendor_configs.emplace_back(std::move(cfg)); snapshot.vendor_configs.emplace_back(std::move(cfg));
} }

View File

@@ -66,41 +66,41 @@ using Config::SnapshotDB;
// Configuration data structures extensions needed for the wizard // Configuration data structures extensions needed for the wizard
//BBS: set BBL as default //BBS: set BBL as default
bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_bbl_bundle) bool Bundle::load(fs::path dir, const std::string &vendor_name, bool ais_in_resources, bool ais_bbl_bundle)
{ {
this->preset_bundle = std::make_unique<PresetBundle>(); this->preset_bundle = std::make_unique<PresetBundle>();
this->is_in_resources = ais_in_resources; this->is_in_resources = ais_in_resources;
this->is_bbl_bundle = ais_bbl_bundle; this->is_bbl_bundle = ais_bbl_bundle;
std::string path_string = source_path.string();
std::string parent_path = source_path.parent_path().string();
//BBS: add json logic for vendor bundles //BBS: add json logic for vendor bundles
std::string vendor_name = source_path.filename().string(); // Orca: served from the vendor's preset cache where one covers it — which is
if (Slic3r::is_json_file(path_string)) { // how a shipped build carries its vendors — and parsed from the JSONs otherwise.
// Remove the .json suffix. // A vendor that can be neither read nor parsed — a cache the build cannot use
vendor_name.erase(vendor_name.size() - 5); // with the preset JSONs behind it pruned, say — is one the wizard cannot offer.
} // Every other vendor still can be, so it is left out rather than thrown over.
else size_t presets_loaded = 0;
try {
auto [config_substitutions, loaded] = preset_bundle->load_vendor_configs_from_json(
dir.string(), vendor_name, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable);
UNUSED(config_substitutions);
// No substitutions shall be reported when loading a system config bundle, no substitutions are allowed.
assert(config_substitutions.empty());
presets_loaded = loaded;
} catch (const std::exception &e) {
BOOST_LOG_TRIVIAL(fatal) << boost::format("Vendor bundle: `%1%`: cannot be loaded, leaving it out: %2%") % vendor_name % e.what();
return false; return false;
}
// Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air.
//BBS: add json logic for vendor bundles
auto [config_substitutions, presets_loaded] = preset_bundle->load_vendor_configs_from_json(
parent_path, vendor_name, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable);
UNUSED(config_substitutions);
// No substitutions shall be reported when loading a system config bundle, no substitutions are allowed.
assert(config_substitutions.empty());
auto first_vendor = preset_bundle->vendors.begin(); auto first_vendor = preset_bundle->vendors.begin();
if (first_vendor == preset_bundle->vendors.end()) { if (first_vendor == preset_bundle->vendors.end()) {
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % vendor_name;
return false; return false;
} }
if (presets_loaded == 0) { if (presets_loaded == 0) {
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string; BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % vendor_name;
return false; return false;
} }
BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded; BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % vendor_name % presets_loaded;
this->vendor_profile = &first_vendor->second; this->vendor_profile = &first_vendor->second;
return true; return true;
} }
@@ -125,15 +125,10 @@ BundleMap BundleMap::load()
//Orca: add custom as default //Orca: add custom as default
//Orca: add json logic for vendor bundle //Orca: add json logic for vendor bundle
auto orca_bundle_path = (vendor_dir / PresetBundle::ORCA_DEFAULT_BUNDLE).replace_extension(".json");
auto orca_bundle_rsrc = false;
if (!boost::filesystem::exists(orca_bundle_path)) {
orca_bundle_path = (rsrc_vendor_dir / PresetBundle::ORCA_DEFAULT_BUNDLE).replace_extension(".json");
orca_bundle_rsrc = true;
}
{ {
const bool from_rsrc = ! is_vendor_installed(PresetBundle::ORCA_DEFAULT_BUNDLE);
Bundle bbl_bundle; Bundle bbl_bundle;
if (bbl_bundle.load(std::move(orca_bundle_path), orca_bundle_rsrc, true)) if (bbl_bundle.load(from_rsrc ? rsrc_vendor_dir : vendor_dir, PresetBundle::ORCA_DEFAULT_BUNDLE, from_rsrc, true))
res.emplace(PresetBundle::ORCA_DEFAULT_BUNDLE, std::move(bbl_bundle)); res.emplace(PresetBundle::ORCA_DEFAULT_BUNDLE, std::move(bbl_bundle));
} }
@@ -141,18 +136,13 @@ BundleMap BundleMap::load()
// and then additionally from resources/profiles. // and then additionally from resources/profiles.
bool is_in_resources = false; bool is_in_resources = false;
for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) { for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) {
for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) { for (const std::string &id : vendor_names_in(*dir)) {
//BBS: add json logic for vendor bundle // Don't load this bundle if we've already loaded it.
if (Slic3r::is_json_file(dir_entry.path().string())) { if (res.find(id) != res.end()) { continue; }
std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".json" part
// Don't load this bundle if we've already loaded it. Bundle bundle;
if (res.find(id) != res.end()) { continue; } if (bundle.load(*dir, id, is_in_resources))
res.emplace(id, std::move(bundle));
Bundle bundle;
if (bundle.load(dir_entry.path(), is_in_resources))
res.emplace(std::move(id), std::move(bundle));
}
} }
is_in_resources = true; is_in_resources = true;

View File

@@ -71,9 +71,11 @@ struct Bundle
Bundle() = default; Bundle() = default;
Bundle(Bundle&& other); Bundle(Bundle&& other);
// Load the vendor `vendor_name` as it is installed in `dir`, from its preset
// cache or its profile JSONs, whichever is usable.
// Returns false if not loaded. Reason for that is logged as boost::log error. // Returns false if not loaded. Reason for that is logged as boost::log error.
//BBS: set BBL as default //BBS: set BBL as default
bool load(fs::path source_path, bool is_in_resources, bool is_bbl_bundle = false); bool load(fs::path dir, const std::string &vendor_name, bool is_in_resources, bool is_bbl_bundle = false);
const std::string& vendor_id() const { return vendor_profile->id; } const std::string& vendor_id() const { return vendor_profile->id; }
}; };

View File

@@ -2201,25 +2201,14 @@ bool CreatePrinterPresetDialog::load_system_and_user_presets_with_curr_model(Pre
} else { } else {
selected_vendor_id = m_printer_preset_vendor_selected.id; selected_vendor_id = m_printer_preset_vendor_selected.id;
if (boost::filesystem::exists(boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR / selected_vendor_id)) {
preset_path = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string();
} else if (boost::filesystem::exists(boost::filesystem::path(Slic3r::resources_dir()) / "profiles" / selected_vendor_id)) {
preset_path = (boost::filesystem::path(Slic3r::resources_dir()) / "profiles").string();
}
if (preset_path.empty()) {
BOOST_LOG_TRIVIAL(info) << "Preset path was not found";
MessageDialog dlg(this, _L("Preset path was not found; please reselect vendor."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES_NO | wxYES_DEFAULT | wxCENTRE);
dlg.ShowModal();
return false;
}
try { try {
// Pass the app's preset bundle (which already holds OrcaFilamentLibrary) as the base // Pass the app's preset bundle (which already holds OrcaFilamentLibrary) as the base
// bundle so vendor filaments that inherit OFL bases resolve via the existing // bundle so vendor filaments that inherit OFL bases resolve via the existing
// cross-vendor inheritance path. // cross-vendor inheritance path.
temp_preset_bundle.load_vendor_configs_from_json(preset_path, selected_vendor_id, // Orca: served from the vendor's preset cache where one covers it — a shipped
// build carries that instead of the raw preset JSONs — and parsed otherwise.
temp_preset_bundle.load_vendor_configs_from_json((boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string(),
selected_vendor_id,
PresetBundle::LoadConfigBundleAttribute::LoadSystem, PresetBundle::LoadConfigBundleAttribute::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent, ForwardCompatibilitySubstitutionRule::EnableSilent,
wxGetApp().preset_bundle); wxGetApp().preset_bundle);

View File

@@ -1,6 +1,7 @@
#include "WebGuideDialog.hpp" #include "WebGuideDialog.hpp"
#include "ConfigWizard.hpp" #include "ConfigWizard.hpp"
#include <boost/algorithm/string/join.hpp>
#include <boost/filesystem/operations.hpp> #include <boost/filesystem/operations.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
#include <boost/filesystem/path.hpp> #include <boost/filesystem/path.hpp>
@@ -43,33 +44,6 @@ using namespace nlohmann;
namespace Slic3r { namespace GUI { namespace Slic3r { namespace GUI {
static std::string guide_json_cache_version_key(const boost::filesystem::path& rsrc_dir,
const boost::filesystem::path& user_dir)
{
std::vector<std::string> parts;
auto collect = [&](const boost::filesystem::path& dir) {
boost::system::error_code ec;
if (!boost::filesystem::exists(dir, ec)) return;
for (const auto& e : boost::filesystem::directory_iterator(dir, ec)) {
if (e.path().extension().string() != ".json") continue;
const std::string k = get_vendor_cache_key(e.path().string());
if (!k.empty())
parts.push_back(e.path().filename().string() + "=" + k);
}
};
collect(rsrc_dir);
if (user_dir != rsrc_dir) collect(user_dir);
std::sort(parts.begin(), parts.end());
std::string result;
for (const auto& p : parts) { result += p; result += ';'; }
return result;
}
static boost::filesystem::path guide_json_cache_path()
{
return boost::filesystem::path(data_dir()) / "guide_profile_cache.json";
}
static wxString update_custom_filaments() static wxString update_custom_filaments()
{ {
json m_Res = json::object(); json m_Res = json::object();
@@ -217,8 +191,7 @@ GuideFrame::GuideFrame(GUI_App *pGUI, long style)
GuideFrame::~GuideFrame() GuideFrame::~GuideFrame()
{ {
m_destroy = true; *m_cancel_token = true; // stop the loading thread and any queued CallAfter lambdas before join
*m_cancel_token = true; // signal any queued CallAfter lambdas before join
if (m_load_task && m_load_task->joinable()) if (m_load_task && m_load_task->joinable())
m_load_task->join(); m_load_task->join();
m_load_task.reset(); m_load_task.reset();
@@ -327,13 +300,19 @@ void GuideFrame::OnNavigationRequest(wxWebViewEvent &evt)
/** /**
* Callback invoked when a navigation request was accepted * Callback invoked when a navigation request was accepted
*/ */
void GuideFrame::init_guide_paths() // The empty shape every profile-loading path starts from or falls back to.
void GuideFrame::reset_profile_json()
{ {
m_ProfileJson = json::parse("{}");
m_ProfileJson["model"] = json::array(); m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object(); m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object(); m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array(); m_ProfileJson["process"] = json::array();
}
void GuideFrame::init_guide_paths()
{
m_ProfileJson = json::parse("{}");
reset_profile_json();
vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
@@ -376,27 +355,7 @@ void GuideFrame::OnNavigationComplete(wxWebViewEvent &evt)
try { try {
init_guide_paths(); init_guide_paths();
if (BuildProfileDataFromPresetBundle()) { if (BuildProfileDataFromPresetBundle()) {
// Persist so future opens that start before preset_bundle is ready if (!*m_cancel_token)
// can skip the slower loading paths. Capture by value so the
// thread is safe even if the dialog closes before it finishes.
boost::thread([data = m_ProfileJson,
rsrc = rsrc_vendor_dir,
user = vendor_dir] {
try {
json cache;
cache["version"] = guide_json_cache_version_key(rsrc, user);
if (cache["version"].get<std::string>().empty()) return;
json base = data;
for (auto& entry : base["model"]) entry["nozzle_selected"] = "";
cache["data"] = std::move(base);
boost::nowide::ofstream ofs(guide_json_cache_path().string());
ofs << cache.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: guide JSON cache saved";
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: failed to save guide JSON cache: " << e.what();
}
}).detach();
if (!m_destroy)
on_profile_loaded(); on_profile_loaded();
} else { } else {
// Presets not yet in memory — delegate to background thread. // Presets not yet in memory — delegate to background thread.
@@ -858,11 +817,9 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle
bool check_unsaved_preset_changes = false; bool check_unsaved_preset_changes = false;
std::vector<std::string> install_bundles; std::vector<std::string> install_bundles;
std::vector<std::string> remove_bundles; std::vector<std::string> remove_bundles;
const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
for (const auto &it : enabled_vendors) { for (const auto &it : enabled_vendors) {
if (it.second.size() > 0) { if (it.second.size() > 0) {
auto vendor_file = vendor_dir/(it.first + ".json"); if (!is_vendor_installed(it.first)) {
if (!fs::exists(vendor_file)) {
install_bundles.emplace_back(it.first); install_bundles.emplace_back(it.first);
} }
} }
@@ -873,8 +830,7 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle
if (it.second.size() > 0) { if (it.second.size() > 0) {
if (enabled_vendors.find(it.first) != enabled_vendors.end()) if (enabled_vendors.find(it.first) != enabled_vendors.end())
continue; continue;
auto vendor_file = vendor_dir/(it.first + ".json"); if (is_vendor_installed(it.first)) {
if (fs::exists(vendor_file)) {
remove_bundles.emplace_back(it.first); remove_bundles.emplace_back(it.first);
} }
} }
@@ -1223,72 +1179,20 @@ int GuideFrame::GetFilamentInfo( std::string VendorDirectory, json & pFilaList,
return status; return status;
} }
bool GuideFrame::TryLoadGuideJsonCache() bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors)
{ {
const auto path = guide_json_cache_path();
boost::system::error_code ec;
if (!boost::filesystem::exists(path, ec)) return false;
try {
boost::nowide::ifstream ifs(path.string());
json cache;
ifs >> cache;
if (!cache.contains("version") || !cache.contains("data")) return false;
const std::string expected = guide_json_cache_version_key(rsrc_vendor_dir, vendor_dir);
if (expected.empty() || cache["version"].get<std::string>() != expected) return false;
m_ProfileJson = cache["data"];
if (m_ProfileJson["machine"].empty()) return false;
BOOST_LOG_TRIVIAL(info) << "GuideFrame: loaded profile data from guide JSON cache ("
<< m_ProfileJson["model"].size() << " models, "
<< m_ProfileJson["machine"].size() << " machines, "
<< m_ProfileJson["filament"].size() << " filaments)";
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: guide JSON cache load failed: " << e.what();
return false;
}
}
void GuideFrame::SaveGuideJsonCache()
{
try {
json cache;
cache["version"] = guide_json_cache_version_key(rsrc_vendor_dir, vendor_dir);
if (cache["version"].get<std::string>().empty()) return;
json base = m_ProfileJson;
// Strip user-specific state — SaveProfileData() re-applies it from AppConfig.
for (auto& entry : base["model"])
entry["nozzle_selected"] = "";
cache["data"] = std::move(base);
boost::nowide::ofstream ofs(guide_json_cache_path().string());
ofs << cache.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: guide JSON cache saved";
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: failed to save guide JSON cache: " << e.what();
}
}
bool GuideFrame::BuildProfileDataFromPresetBundle()
{
PresetBundle* pb = wxGetApp().preset_bundle;
if (!pb || pb->vendors.empty())
return false;
try { try {
// Models from vendor profiles // Models from vendor profiles
for (const auto& [vendor_id, vp] : pb->vendors) { for (const auto& [vendor_id, vp] : bundle.vendors) {
for (const auto& model : vp.models) { for (const auto& model : vp.models) {
std::string nozzle_str; std::string nozzle_str;
for (const auto& v : model.variants) { for (const auto& v : model.variants) {
if (!nozzle_str.empty()) nozzle_str += ";"; if (!nozzle_str.empty()) nozzle_str += ";";
nozzle_str += v.name; nozzle_str += v.name;
} }
std::string materials_str; const std::string materials_str = boost::algorithm::join(model.default_materials, ";");
for (const auto& m : model.default_materials) {
if (!materials_str.empty()) materials_str += ";";
materials_str += m;
}
boost::filesystem::path cover_path = boost::filesystem::path cover_path =
(boost::filesystem::path(resources_dir()) / "profiles" / vendor_id / (model.id + "_cover.png")) (boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (model.id + "_cover.png"))
.make_preferred(); .make_preferred();
if (!boost::filesystem::exists(cover_path)) if (!boost::filesystem::exists(cover_path))
cover_path = cover_path =
@@ -1298,7 +1202,7 @@ bool GuideFrame::BuildProfileDataFromPresetBundle()
json entry; json entry;
entry["model"] = model.id; entry["model"] = model.id;
entry["name"] = model.name; entry["name"] = model.name;
entry["vendor"] = vendor_id; entry["vendor"] = vp.id;
entry["nozzle_diameter"] = nozzle_str; entry["nozzle_diameter"] = nozzle_str;
entry["materials"] = materials_str; entry["materials"] = materials_str;
entry["cover"] = cover_path.string(); entry["cover"] = cover_path.string();
@@ -1309,8 +1213,8 @@ bool GuideFrame::BuildProfileDataFromPresetBundle()
} }
// Machine map: preset name -> {model, nozzle variant} // Machine map: preset name -> {model, nozzle variant}
for (const Preset& p : pb->printers()) { for (const Preset& p : bundle.printers()) {
if (!p.is_system) continue; if (!p.is_system || !p.vendor) continue;
const auto* printer_model = p.config.option<ConfigOptionString>("printer_model"); const auto* printer_model = p.config.option<ConfigOptionString>("printer_model");
const auto* printer_variant = p.config.option<ConfigOptionString>("printer_variant"); const auto* printer_variant = p.config.option<ConfigOptionString>("printer_variant");
if (!printer_model || printer_model->value.empty() || !printer_variant) continue; if (!printer_model || printer_model->value.empty() || !printer_variant) continue;
@@ -1322,8 +1226,9 @@ bool GuideFrame::BuildProfileDataFromPresetBundle()
} }
// Filament map from system filament presets (vendor/type already resolved in config) // Filament map from system filament presets (vendor/type already resolved in config)
for (const Preset& p : pb->filaments()) { const json& machines = m_ProfileJson["machine"];
if (!p.is_system) continue; for (const Preset& p : bundle.filaments()) {
if (!p.is_system || !p.vendor) continue;
const auto* fila_vendor = p.config.option<ConfigOptionStrings>("filament_vendor"); const auto* fila_vendor = p.config.option<ConfigOptionStrings>("filament_vendor");
const auto* fila_type = p.config.option<ConfigOptionStrings>("filament_type"); const auto* fila_type = p.config.option<ConfigOptionStrings>("filament_type");
const auto* compat_printers = p.config.option<ConfigOptionStrings>("compatible_printers"); const auto* compat_printers = p.config.option<ConfigOptionStrings>("compatible_printers");
@@ -1334,9 +1239,10 @@ bool GuideFrame::BuildProfileDataFromPresetBundle()
std::string model_list; std::string model_list;
if (compat_printers) { if (compat_printers) {
for (const std::string& pname : compat_printers->values) { for (const std::string& pname : compat_printers->values) {
if (m_ProfileJson["machine"].contains(pname)) { auto it = machines.find(pname);
std::string m = m_ProfileJson["machine"][pname]["model"]; if (it != machines.end()) {
std::string n = m_ProfileJson["machine"][pname]["nozzle"]; const std::string m = (*it)["model"];
const std::string n = (*it)["nozzle"];
model_list += "[" + m + "++" + n + "]"; model_list += "[" + m + "++" + n + "]";
} }
} }
@@ -1353,157 +1259,103 @@ bool GuideFrame::BuildProfileDataFromPresetBundle()
} }
// Process list from visible system print presets // Process list from visible system print presets
for (const Preset& p : pb->prints()) { for (const Preset& p : bundle.prints()) {
if (!p.is_system || !p.is_visible) continue; if (!p.is_system || !p.vendor || !p.is_visible) continue;
json entry; json entry;
entry["name"] = p.name; entry["name"] = p.name;
entry["sub_path"] = p.file; entry["sub_path"] = p.file;
m_ProfileJson["process"].push_back(entry); m_ProfileJson["process"].push_back(entry);
} }
// If rsrc_vendor_dir has vendor JSONs not covered by the current bundle, the if (require_all_resource_vendors) {
// bundle is incomplete (e.g. dev env where data_dir/system only has // If rsrc_vendor_dir has vendors (profile JSONs, or the preset caches a
// OrcaFilamentLibrary+Custom). Fall back so LoadProfileFamily reads both dirs. // packaged build ships instead) not covered by the current bundle, the
try { // bundle is incomplete (e.g. dev env where data_dir/system only has
for (const auto& e : boost::filesystem::directory_iterator(rsrc_vendor_dir)) { // OrcaFilamentLibrary+Custom). Fall back so the slow path reads both dirs.
if (e.path().extension().string() != ".json") continue; try {
const std::string stem = e.path().stem().string(); for (const std::string& name : vendor_names_in(rsrc_vendor_dir)) {
if (pb->vendors.find(stem) == pb->vendors.end()) { if (bundle.vendors.find(name) == bundle.vendors.end()) {
BOOST_LOG_TRIVIAL(info) << "GuideFrame: vendor '" << stem BOOST_LOG_TRIVIAL(info) << "GuideFrame: vendor '" << name
<< "' in resources but not in preset_bundle — falling back to JSON loading"; << "' in resources but not in preset_bundle — falling back to JSON loading";
m_ProfileJson["model"] = json::array(); reset_profile_json();
m_ProfileJson["machine"] = json::object(); return false;
m_ProfileJson["filament"] = json::object(); }
m_ProfileJson["process"] = json::array();
return false;
} }
} } catch (const std::exception&) {}
} catch (const std::exception&) {} }
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from preset_bundle (" BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data ("
<< m_ProfileJson["model"].size() << " models, " << m_ProfileJson["model"].size() << " models, "
<< m_ProfileJson["machine"].size() << " machines, " << m_ProfileJson["machine"].size() << " machines, "
<< m_ProfileJson["filament"].size() << " filaments)"; << m_ProfileJson["filament"].size() << " filaments)";
return !m_ProfileJson["machine"].empty(); return !m_ProfileJson["machine"].empty();
} catch (const std::exception& e) { } catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromPresetBundle failed: " << e.what() BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileJson failed: " << e.what()
<< " — falling back to JSON loading"; << " — falling back to JSON loading";
m_ProfileJson["model"] = json::array(); reset_profile_json();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
return false; return false;
} }
} }
// Builds guide profile JSON from the per-vendor bundled caches bool GuideFrame::BuildProfileDataFromPresetBundle()
// (resources/profiles/system_presets.cache, generated by CI).
// This avoids the 90-second LoadProfileFamily fallback on first launch.
bool GuideFrame::BuildProfileDataFromBundledCache()
{ {
const std::string cache_path = PresetBundle::bundled_system_presets_cache_path(); PresetBundle* pb = wxGetApp().preset_bundle;
if (!boost::filesystem::exists(cache_path)) if (!pb || pb->vendors.empty())
return false; return false;
return BuildProfileJson(*pb, /*require_all_resource_vendors=*/true);
}
bool GuideFrame::BuildProfileDataFromVendors()
{
try { try {
// Same vendor set and precedence as the JSON scan in LoadProfileData: a
// vendor in the user's system dir shadows the bundled one of that name.
// A vendor is named by its profile or, where a build ships preset caches
// instead, by its cache alone — so both forms name one here.
std::map<std::string, boost::filesystem::path> vendor_files;
auto collect = [&vendor_files](const boost::filesystem::path& dir) {
boost::system::error_code ec;
if (!boost::filesystem::exists(dir, ec))
return;
for (const auto& e : boost::filesystem::directory_iterator(dir, ec))
if (Slic3r::is_json_file(e.path().string()) || e.path().extension() == ".opc")
vendor_files.emplace(e.path().stem().string(), e.path()); // first wins
};
collect(vendor_dir);
collect(rsrc_vendor_dir);
// Each vendor comes from its preset cache where one covers it, which is what
// makes this worth doing instead of the scan below; the filament library goes
// first because the others' filaments inherit from it, and resolving those on
// the vendors the cache does not cover needs it already loaded.
PresetBundle bundle; PresetBundle bundle;
if (!PresetBundle::load_system_presets_cache_for_guide(cache_path, bundle)) auto load_vendor = [this](PresetBundle& into, const std::string& vendor, const PresetBundle* base) {
into.load_vendor_configs_from_json(vendor_dir.string(), vendor, PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent, base);
};
const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY);
if (vendor_files.count(filament_library))
load_vendor(bundle, filament_library, nullptr);
for (const auto& entry : vendor_files) {
if (*m_cancel_token)
return false; // as in the scan below: a vendor without a cache is parsed, and that takes time
const std::string& vendor = entry.first;
// A cache is only ever written for a versioned vendor; a JSON has to be
// asked, so that an unversioned one (blacklist.json) carrying no presets
// is passed over.
if (vendor == filament_library ||
(entry.second.extension() != ".opc" && ! get_version_from_json(entry.second.string()).valid()))
continue;
PresetBundle tmp;
load_vendor(tmp, vendor, &bundle);
bundle.merge_presets(std::move(tmp));
}
if (bundle.vendors.empty())
return false; return false;
return BuildProfileJson(bundle, /*require_all_resource_vendors=*/false);
for (const auto& [vendor_id, vp] : bundle.vendors) { } catch (const std::exception& e) {
for (const auto& cm : vp.models) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed: " << e.what();
std::string nozzle_str; reset_profile_json();
for (const auto& v : cm.variants) {
if (!nozzle_str.empty()) nozzle_str += ";";
nozzle_str += v.name;
}
std::string materials_str;
for (const auto& m : cm.default_materials) {
if (!materials_str.empty()) materials_str += ";";
materials_str += m;
}
boost::filesystem::path cover_path =
(boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (cm.id + "_cover.png"))
.make_preferred();
if (!boost::filesystem::exists(cover_path))
cover_path =
(boost::filesystem::path(resources_dir()) / "web/image/printer" / (cm.id + "_cover.png"))
.make_preferred();
json entry;
entry["model"] = cm.id;
entry["name"] = cm.name;
entry["vendor"] = vp.id;
entry["nozzle_diameter"] = nozzle_str;
entry["materials"] = materials_str;
entry["cover"] = cover_path.string();
entry["nozzle_selected"] = "";
entry["sub_path"] = "";
m_ProfileJson["model"].push_back(entry);
}
}
for (const Preset& cp : bundle.printers()) {
if (!cp.is_system || !cp.vendor) continue;
const auto* pm = cp.config.option<ConfigOptionString>("printer_model");
const auto* pv = cp.config.option<ConfigOptionString>("printer_variant");
if (!pm || pm->value.empty() || !pv) continue;
json mach;
mach["model"] = pm->value;
mach["nozzle"] = pv->value;
m_ProfileJson["machine"][cp.name] = mach;
}
for (const Preset& cp : bundle.filaments()) {
if (!cp.is_system || !cp.vendor) continue;
const auto* fv = cp.config.option<ConfigOptionStrings>("filament_vendor");
const auto* ft = cp.config.option<ConfigOptionStrings>("filament_type");
const auto* compat = cp.config.option<ConfigOptionStrings>("compatible_printers");
std::string vendor = (fv && !fv->values.empty()) ? fv->values[0] : "";
std::string type = (ft && !ft->values.empty()) ? ft->values[0] : "";
std::string model_list;
if (compat) {
for (const std::string& pname : compat->values) {
if (m_ProfileJson["machine"].contains(pname)) {
std::string m = m_ProfileJson["machine"][pname]["model"];
std::string n = m_ProfileJson["machine"][pname]["nozzle"];
model_list += "[" + m + "++" + n + "]";
}
}
}
json ff;
ff["name"] = cp.name;
ff["sub_path"] = cp.file;
ff["vendor"] = vendor;
ff["type"] = type;
ff["models"] = model_list;
ff["selected"] = 0;
m_ProfileJson["filament"][cp.name] = ff;
}
for (const Preset& cp : bundle.prints()) {
if (!cp.is_system || !cp.vendor || !cp.is_visible) continue;
json entry;
entry["name"] = cp.name;
entry["sub_path"] = cp.file;
m_ProfileJson["process"].push_back(entry);
}
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from single-bundle cache ("
<< m_ProfileJson["model"].size() << " models, "
<< m_ProfileJson["machine"].size() << " machines, "
<< m_ProfileJson["filament"].size() << " filaments)";
return !m_ProfileJson["machine"].empty();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromBundledCache failed: " << ex.what();
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
return false; return false;
} }
} }
@@ -1512,55 +1364,48 @@ int GuideFrame::LoadProfileData()
{ {
// Background thread: the fast path in OnNavigationComplete failed (presets not yet loaded). // Background thread: the fast path in OnNavigationComplete failed (presets not yet loaded).
// Loading order (fastest to slowest): // Loading order (fastest to slowest):
// 1. Guide JSON cache (data_dir/guide_profile_cache.json, sub-second) // 1. Load every vendor, from its preset cache wherever one covers it
// 2. Bundled per-vendor binary caches (CI-generated, ~1-2s) // 2. Read all vendor JSONs by hand
// 3. Read all vendor JSONs (~90s)
// After paths 2 or 3 the guide JSON cache is written so next open uses path 1.
try { try {
if (!TryLoadGuideJsonCache()) { if (!BuildProfileDataFromVendors()) {
if (!BuildProfileDataFromBundledCache()) { // Last resort — read all vendor JSONs
// Last resort — read all vendor JSONs (~90s) std::set<std::string> loaded_vendors;
std::set<std::string> loaded_vendors; auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json"); if (boost::filesystem::exists(vendor_dir / filament_library_name))
if (boost::filesystem::exists(vendor_dir / filament_library_name)) LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string());
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string()); else
else LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string());
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string()); loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
boost::filesystem::directory_iterator endIter; boost::filesystem::directory_iterator endIter;
for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) { for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) { if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.'); wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\'); strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/'); strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower(); wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end()) if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue; continue;
LoadProfileFamily(w2s(strVendor), iter->path().string()); LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor)); loaded_vendors.insert(w2s(strVendor));
}
if (m_destroy) return 0;
}
boost::filesystem::directory_iterator others_endIter;
for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
}
if (m_destroy) return 0;
} }
if (*m_cancel_token) return 0;
}
boost::filesystem::directory_iterator others_endIter;
for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
}
if (*m_cancel_token) return 0;
} }
// Persist the result so subsequent opens skip both the bundled cache and
// the slow JSON loading path entirely.
SaveGuideJsonCache();
} }
// Capture the cancel token by value (shared_ptr) so the lambda doesn't // Capture the cancel token by value (shared_ptr) so the lambda doesn't

View File

@@ -84,10 +84,10 @@ public:
int LoadProfileFamily(std::string strVendor, std::string strFilePath); int LoadProfileFamily(std::string strVendor, std::string strFilePath);
void init_guide_paths(); void init_guide_paths();
void on_profile_loaded(); void on_profile_loaded();
bool BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors);
bool BuildProfileDataFromPresetBundle(); bool BuildProfileDataFromPresetBundle();
bool BuildProfileDataFromBundledCache(); bool BuildProfileDataFromVendors();
bool TryLoadGuideJsonCache(); void reset_profile_json();
void SaveGuideJsonCache();
int SaveProfile(); int SaveProfile();
int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType); int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType);
@@ -122,9 +122,9 @@ private:
//First Load //First Load
bool bFirstComplete{false}; bool bFirstComplete{false};
std::atomic<bool> m_destroy{false}; // Set once in the destructor. Read through `this` by the loading thread
// Shared cancel token captured by CallAfter lambdas so they don't touch // (joined before `this` dies) and captured as the shared_ptr by CallAfter
// `this` after the destructor has run and the object is freed. // lambdas so they don't touch `this` after the object is freed.
std::shared_ptr<std::atomic<bool>> m_cancel_token{std::make_shared<std::atomic<bool>>(false)}; std::shared_ptr<std::atomic<bool>> m_cancel_token{std::make_shared<std::atomic<bool>>(false)};
std::unique_ptr<boost::thread> m_load_task; std::unique_ptr<boost::thread> m_load_task;

View File

@@ -1044,46 +1044,42 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const
std::set<std::string> bundles; std::set<std::string> bundles;
// Orca: always install filament library // Orca: always install filament library
bundles.insert(PresetBundle::ORCA_FILAMENT_LIBRARY); bundles.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
for (auto &dir_entry : boost::filesystem::directory_iterator(rsrc_path)) { // A vendor is named by its profile or, where the build ships preset caches
const auto &path = dir_entry.path(); // instead of the raw profile JSONs, by its cache alone.
std::string file_path = path.string(); for (const std::string &vendor_name : vendor_names_in(rsrc_path)) {
if (is_json_file(file_path)) { if (bundles.find(vendor_name) != bundles.end())continue;
const auto path_in_vendor = vendor_path / path.filename();
std::string vendor_name = path.filename().string();
// Remove the .json suffix.
vendor_name.erase(vendor_name.size() - 5);
if (bundles.find(vendor_name) != bundles.end())continue;
const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE
|| (enabled_vendors.find(vendor_name) != enabled_vendors.end()); || (enabled_vendors.find(vendor_name) != enabled_vendors.end());
if (enabled_config_update) { if (enabled_config_update) {
if ( fs::exists(path_in_vendor)) { if (is_vendor_installed(vendor_name)) {
if (is_vendor_enabled) { if (is_vendor_enabled) {
Semver resource_ver = get_version_from_json(file_path); // Orca: whichever form of the vendor resources ships at the newer
Semver vendor_ver = get_version_from_json(path_in_vendor.string()); // version is the one installing lays down, and the one to judge
// what is installed against.
Semver resource_ver = resource_vendor_version(vendor_name);
// Orca: a vendor installed as a preset cache has no profile
// beside it; the version it was installed at is in the cache.
Semver vendor_ver = installed_vendor_version(vendor_name);
if (vendor_ver < resource_ver) { if (vendor_ver < resource_ver) {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version " BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version "
<< resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string(); << resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string();
bundles.insert(vendor_name); bundles.insert(vendor_name);
}
}
else {
//need to be removed because not installed
fs::remove(path_in_vendor);
const auto path_of_vendor = vendor_path / vendor_name;
if (fs::exists(path_of_vendor))
fs::remove_all(path_of_vendor);
} }
} }
else if (is_vendor_enabled) { else {
bundles.insert(vendor_name); //need to be removed because not installed
remove_installed_vendor(vendor_name);
} }
} }
else if (is_vendor_enabled) { else if (is_vendor_enabled) {
bundles.insert(vendor_name); bundles.insert(vendor_name);
} }
} }
else if (is_vendor_enabled) {
bundles.insert(vendor_name);
}
} }
if (bundles.size() > 0) { if (bundles.size() > 0) {
@@ -1163,11 +1159,12 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME); auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME);
auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME); auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME);
if (( fs::exists(path_in_vendor)) if (is_vendor_installed(vendor_name)
|| fs::exists(print_in_cache) || fs::exists(print_in_cache)
|| fs::exists(filament_in_cache) || fs::exists(filament_in_cache)
|| fs::exists(machine_in_cache)) { || fs::exists(machine_in_cache)) {
Semver vendor_ver = get_version_from_json(path_in_vendor.string()); // Orca: a vendor installed as a preset cache carries its version there.
Semver vendor_ver = installed_vendor_version(vendor_name);
std::map<std::string, std::string> key_values; std::map<std::string, std::string> key_values;
std::vector<std::string> keys(3); std::vector<std::string> keys(3);

View File

@@ -146,7 +146,7 @@ TEST_CASE("Current vendor type tolerates missing printer model", "[Preset][Bundl
{ {
PresetBundle bundle; PresetBundle bundle;
VendorProfile orca_vendor("ORCA"); VendorProfile orca_vendor; orca_vendor.id = "ORCA";
VendorProfile::PrinterModel model; VendorProfile::PrinterModel model;
model.name = "Orca Test"; model.name = "Orca Test";
orca_vendor.models.emplace_back(model); orca_vendor.models.emplace_back(model);

File diff suppressed because it is too large Load Diff