Compare commits

...
Author SHA1 Message Date
Ian Chua ce41b5a3cb fix: stale audit mode on_lifecycle_event 2026-09-18 22:33:46 +08:00
Ian Chua 8fd93907fd Merge branch 'main' into feat/plugin-lifecycle-evts 2026-09-18 21:35:09 +08:00
Ian Chua 080f27f602 fix: windows unsubscribed loaded plugin not allowed (#15751)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->
On windows, after installing and loading a plugin, if you try to
unsubscribe from a loaded plugin, and on refresh, it will be an orphaned
plugin.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
On unsubscribing from a loaded cloud plugin
<img width="1418" height="862" alt="image"
src="https://github.com/user-attachments/assets/9ba8353c-1645-4b21-86ff-2ed235f4d259"
/>

On plugin refresh
<img width="1418" height="862" alt="image"
src="https://github.com/user-attachments/assets/fd58c144-7970-4448-ba3b-e72e52980467"
/>

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-18 21:32:57 +08:00
Ian Chua 23c77f15cf feat: add CI to generate OPC for OTA workflow (#15624)
# Description

Adds the CI half of the profile OTA pipeline: a push-triggered workflow
that
rebuilds a vendor's binary preset cache (`<vendor>.opc`) whenever its
profile
changes on `main` / `release/*`, and publishes it as a versioned release
asset
for OrcaCloud's OTA Manager to pick up.

### `.github/workflows/post_merge_profiles.yml` (new)

Push-triggered counterpart to `check_profiles.yml` (which only gates
PRs):

- Diffs the push to find which vendors under `resources/profiles/**`
changed.
- Reads the Orca version from `version.inc` and each vendor's 4-part
`version`
from `resources/profiles/<vendor>.json` (fails the run if it isn't
`A.B.C.D`).
- Downloads the prebuilt `generate_system_cache` from this repo's
`nightly-builds` release and builds one `<vendor>.opc` per changed
vendor.
- Packages each as
`<orca_ver>_<vendor>_<profile_version>_<UTCyyyymmddHHMM>.zip` (zip root
`<vendor>.opc`) — the asset-name contract OrcaCloud's release scanner
expects.
- Uploads them to a per-Orca-version release on the profiles repo via a
scoped
  GitHub App token.

It stops there: no changelog, no R2, no OTA webhook — a maintainer still
publishes from the OTA Manager. Job is guarded to
`OrcaSlicer/OrcaSlicer`;
workflow permissions are `contents: read` (the cross-repo write uses the
App
token only).

### `.github/workflows/build_orca.yml`

Two steps on the Linux leg: upload `generate_system_cache` as a CI
artifact,
and (on `main`) deploy it to the `nightly-builds` release as
`generate_system_cache_Linux_Ubuntu2404_nightly` so the workflow above
has a
tool to download. `.opc` is 64-bit little-endian and
platform-independent, so
only the Linux binary is shipped.

### `src/dev-utils/generate_system_cache.cpp`

New `-v` / `--vendor` option to generate the cache for a single vendor
(plus the
always-loaded Orca filament library) instead of all vendors, and an
error if the
named vendor produced no `.opc` (catches typos). Reuses
`PresetBundle::set_vendor_to_validate()` from #14217.

## Operational prerequisites

- Repo secrets `PROFILES_APP_ID` / `PROFILES_APP_PRIVATE_KEY` for a
GitHub App
  with `contents: write` on the target profiles repo.
- `env.PROFILES_OWNER` / `env.PROFILES_REPO` in
`post_merge_profiles.yml` must
  point at the production profiles repo OrcaCloud reads.
- `generate_system_cache_Linux_Ubuntu2404_nightly` only appears after
the first
post-merge nightly `build_orca` run; a profile-only push before then
fails at
  the download step.

## Screenshots/Recordings/Graphs

n/a — CI only.

## Tests

Exercised end-to-end in a staging environment: per-vendor `.opc` zips
published
in the expected `<orca_ver>_<vendor>_<profile_version>_<timestamp>.zip`
layout,
and `generate_system_cache -v <vendor>` confirmed to emit only that
vendor's
`.opc`.

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-18 18:53:26 +08:00
Ian Chua 00f78c18cf Merge branch 'main' into fix/windows-unsub-plugin 2026-09-18 18:51:50 +08:00
SoftFever 2dd3ef7cff fix build errors on mac 2026-09-18 17:24:47 +08:00
peachismomo 2bfab589ee fix: windows unsubsribe loaded plugin not allowed 2026-09-18 16:07:25 +08:00
Ian Chua 4a72a3bba2 Merge branch 'main' into feat/ota-opc-ci 2026-09-18 14:56:00 +08:00
Ian Chua 173706750d Merge branch 'main' into feat/ota-opc-ci 2026-09-15 15:19:43 +08:00
Ian Chua ffb2946b20 feat: compare with environment variable FOLDER_MERGERS for verified folders 2026-09-10 20:09:37 +08:00
Ian Chua e89a05b1e4 fix: repo name 2026-09-10 16:38:03 +08:00
Ian Chua 02a8011b97 feat: add CI to generate OPC for OTA workflow 2026-09-10 16:27:23 +08:00
SoftFever 8056b32450 Merge branch 'main' into feat/plugin-lifecycle-evts 2026-08-26 14:43:41 +08:00
Ian Chua fabd08af1e update GCodeExport to use model id for ctx.name 2026-08-25 15:59:17 +08:00
Ian Chua 33a8265a11 fix: slicer regression test 2026-08-25 15:37:15 +08:00
Ian Chua d96682a798 Merge branch 'main' into feat/plugin-lifecycle-evts 2026-08-25 13:50:00 +08:00
Ian Chua 64d04a75f3 feat: additional events 2026-08-24 11:36:37 +08:00
Ian Chua bf22ef2a82 Merge branch 'main' into feat/plugin-lifecycle-evts 2026-08-20 12:21:50 +08:00
Ian Chua dfd3444ae7 feat: initial draft of lifecycle events API for plugins 2026-08-19 19:20:31 +08:00
36 changed files with 1162 additions and 49 deletions
+23
View File
@@ -685,6 +685,18 @@ jobs:
name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }} name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/Release/OrcaSlicer_profile_validator' path: './build/src/Release/OrcaSlicer_profile_validator'
# generate_system_cache is what scripts/build_preset_cache.sh bakes the
# <vendor>.opc caches with; it was already built by the "Build system
# preset cache (Linux)" step above. The .opc format is 64-bit
# little-endian native, i.e. identical across every platform Orca ships,
# so only the Linux binary is published.
- name: Upload generate_system_cache Ubuntu
if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: actions/upload-artifact@v7
with:
name: generate_system_cache_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/dev-utils/Release/generate_system_cache'
- name: Deploy Ubuntu release - name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0 uses: WebFreak001/deploy-nightly@v3.2.0
@@ -715,6 +727,17 @@ jobs:
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
max_releases: 1 max_releases: 1
- name: Deploy Ubuntu generate_system_cache release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./build/src/dev-utils/Release/generate_system_cache
asset_name: generate_system_cache_Linux${{ env.ubuntu-ver-str }}_nightly
asset_content_type: application/octet-stream
max_releases: 1
- name: Deploy orca_custom_preset_tests - name: Deploy orca_custom_preset_tests
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0 uses: WebFreak001/deploy-nightly@v3.2.0
+253
View File
@@ -0,0 +1,253 @@
name: Post-merge profiles
# Push-triggered counterpart to check_profiles.yml (which only gates PRs). When a
# profile change lands on main or a release branch, rebuild the affected vendors'
# binary preset caches (<vendor>.opc) and publish each as a versioned ZIP asset on
# a per-Orca-version release of the profiles repo. From there OrcaCloud's OTA
# Manager lists the asset, a maintainer attaches a changelog and hits Publish, and
# only then does it become a live OTA update - this workflow does none of that
# last part (no changelog, no R2, no webhook).
#
# Asset contract expected by OrcaCloud's release scanner:
# ^(\d+\.\d+\.\d+)_([^_]+)_(\d+(?:\.\d+){3})_(\d{12})\.zip$
# <orca_ver>_<vendor>_<profile_version>_<UTC yyyymmddHHMM>.zip (zip root: <vendor>.opc)
#
# Setup (App + secrets): docs/ota/post-merge-profiles-setup.md
on:
push:
branches:
- main
- release/*
paths:
- 'resources/profiles/**'
- '.github/workflows/post_merge_profiles.yml'
workflow_dispatch:
permissions:
contents: read
# One run per branch; let a run finish rather than cancel it, since it publishes.
concurrency:
group: post-merge-profiles-${{ github.ref }}
cancel-in-progress: false
env:
# generate_system_cache is published to this repo's own nightly-builds release
# by build_orca.yml's Linux leg. The job guard pins github.repository to
# OrcaSlicer/OrcaSlicer, so this resolves there.
TOOL_REPO: ${{ github.repository }}
TOOL_ASSET: generate_system_cache_Linux_Ubuntu2404_nightly
# Where per-vendor ZIP assets are published; OrcaCloud's OTA reads this repo.
PROFILES_OWNER: OrcaSlicer
PROFILES_REPO: orcaslicer-profiles
jobs:
publish_profile_caches:
name: Publish profile caches
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
# FOLDER_MERGERS is an environment-scoped variable, shared with the PR
# merge bot. Keep this environment free of protection rules so this
# push-triggered job does not wait for a reviewer.
environment: merge-delegation
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
# Enough history to reach github.event.before for the changed-vendor
# diff on a normal push; deeper pushes fall back to HEAD^..HEAD in the
# step below. fetch-depth: 0 would clone all of OrcaSlicer's history.
fetch-depth: 50
- name: Resolve changed vendors
id: vendors
shell: bash
env:
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
run: |
set -euo pipefail
base='${{ github.event.before }}'
head='${{ github.sha }}'
# Zero SHA (branch created / force push) or manual dispatch: fall back
# to this commit's own diff.
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
base="$head^"
fi
echo "Diffing $base..$head"
mapfile -t candidates < <(
git diff --name-only "$base" "$head" -- resources/profiles \
| sed -nE 's#^resources/profiles/([^/]+)/.*#\1#p; s#^resources/profiles/([^/]+)\.json$#\1#p' \
| sort -u
)
vendors=()
for v in "${candidates[@]:-}"; do
[ -n "$v" ] || continue
json="resources/profiles/$v.json"
# A vendor has a manifest plus either a preset directory or a version
# field; this drops non-vendor files such as blacklist.json.
if [ -f "$json" ] && { [ -d "resources/profiles/$v" ] || jq -e '.version' "$json" >/dev/null 2>&1; }; then
vendors+=("$v")
fi
done
if [ "${#vendors[@]}" -eq 0 ]; then
echo "No changed vendor profiles in this push; nothing to publish."
echo "vendors=" >> "$GITHUB_OUTPUT"
exit 0
fi
# A vendor is eligible only when both the profile directory and its
# sibling bundle JSON are covered by at least one FOLDER_MERGERS
# grant. The account part is intentionally ignored here: this is a
# post-merge safety check, not an authorization check for a command.
grants=()
while IFS= read -r raw_line; do
line="${raw_line#"${raw_line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[ -n "$line" ] || continue
[[ "$line" == \#* ]] && continue
[[ "$line" == *:* ]] || continue
grant="${line#*:}"
grant="${grant#"${grant%%[![:space:]]*}"}"
grant="${grant%"${grant##*[![:space:]]}"}"
while [[ "$grant" == */ ]]; do grant="${grant%/}"; done
grants+=("$grant")
done <<< "${FOLDER_MERGERS:-}"
is_granted() {
local path="$1"
local grant
for grant in "${grants[@]:-}"; do
if [[ "$path" == "$grant" || "$path" == "$grant/"* ]]; then
return 0
fi
done
return 1
}
unauthorized=()
for v in "${vendors[@]}"; do
if ! is_granted "resources/profiles/$v" || ! is_granted "resources/profiles/$v.json"; then
unauthorized+=("$v")
fi
done
if [ "${#unauthorized[@]}" -ne 0 ]; then
echo "Changed vendor profiles are not covered by FOLDER_MERGERS: ${unauthorized[*]}"
echo "No profile caches will be published for this push."
echo "vendors=" >> "$GITHUB_OUTPUT"
exit 0
fi
printf 'Changed vendors: %s\n' "${vendors[*]}"
echo "vendors=${vendors[*]}" >> "$GITHUB_OUTPUT"
- name: Resolve Orca version
id: orca
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
raw="$(sed -nE 's/^set\(SoftFever_VERSION "([^"]+)".*/\1/p' version.inc | head -1)"
[ -n "$raw" ] || { echo "::error::could not read SoftFever_VERSION from version.inc"; exit 1; }
orca_ver="${raw%%-*}"
if ! [[ "$orca_ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Orca version '$orca_ver' (from '$raw') is not X.Y.Z"; exit 1
fi
# release_tag is what the desktop client sends as orca_version and what
# OrcaCloud keys R2 on; orca_ver (X.Y.Z) is the asset-name prefix.
echo "release_tag=$raw" >> "$GITHUB_OUTPUT"
echo "orca_ver=$orca_ver" >> "$GITHUB_OUTPUT"
echo "Orca version: release_tag=$raw asset_prefix=$orca_ver"
- name: Validate profile versions
id: pver
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
: > "$RUNNER_TEMP/pver.tsv"
for v in ${{ steps.vendors.outputs.vendors }}; do
pv="$(jq -r '.version // empty' "resources/profiles/$v.json")"
if ! [[ "$pv" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::vendor $v version '${pv:-<none>}' must be 4 numeric parts (A.B.C.D) for the OTA asset name; fix resources/profiles/$v.json"
exit 1
fi
printf '%s\t%s\n' "$v" "$pv" >> "$RUNNER_TEMP/pver.tsv"
echo "$v -> $pv"
done
- name: Download generate_system_cache
if: steps.vendors.outputs.vendors != ''
shell: bash
env:
# gh (with the default token) rather than an unauthenticated curl: keeps
# working if TOOL_REPO is ever private and avoids anonymous rate limits.
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh release download nightly-builds --repo "$TOOL_REPO" \
--pattern "$TOOL_ASSET" --output generate_system_cache --clobber
chmod +x generate_system_cache
- name: Build caches and package assets
id: pkg
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
# One timestamp for the whole run so a multi-vendor merge groups together.
ts="$(date -u +%Y%m%d%H%M)"
orca_ver='${{ steps.orca.outputs.orca_ver }}'
out="$RUNNER_TEMP/assets"
mkdir -p "$out"
for v in ${{ steps.vendors.outputs.vendors }}; do
./generate_system_cache -p "$GITHUB_WORKSPACE/resources/profiles" -v "$v" -l 2
opc="resources/profiles/$v.opc"
[ -f "$opc" ] || { echo "::error::$opc was not generated"; exit 1; }
pv="$(awk -F'\t' -v v="$v" '$1==v{print $2}' "$RUNNER_TEMP/pver.tsv")"
name="${orca_ver}_${v}_${pv}_${ts}.zip"
( cd resources/profiles && zip -q -j "$out/$name" "$v.opc" )
echo "packaged $name"
done
echo "dir=$out" >> "$GITHUB_OUTPUT"
- name: Mint profiles-repo token
id: token
if: steps.vendors.outputs.vendors != ''
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.PROFILES_APP_ID }}
private-key: ${{ secrets.PROFILES_APP_PRIVATE_KEY }}
owner: ${{ env.PROFILES_OWNER }}
repositories: ${{ env.PROFILES_REPO }}
- name: Publish assets to profiles release
if: steps.vendors.outputs.vendors != ''
shell: bash
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
RELEASE_TAG: ${{ steps.orca.outputs.release_tag }}
ASSET_DIR: ${{ steps.pkg.outputs.dir }}
run: |
set -euo pipefail
repo="$PROFILES_OWNER/$PROFILES_REPO"
if ! gh release view "$RELEASE_TAG" --repo "$repo" >/dev/null 2>&1; then
echo "Creating release $RELEASE_TAG on $repo"
gh release create "$RELEASE_TAG" --repo "$repo" \
--title "$RELEASE_TAG" --notes "Profile cache assets for Orca $RELEASE_TAG." \
--latest=false
fi
# Asset names are timestamp-unique; a clash means a bug, so don't --clobber.
gh release upload "$RELEASE_TAG" --repo "$repo" "$ASSET_DIR"/*.zip
{
echo "### Published to \`$repo\` release \`$RELEASE_TAG\`"
for f in "$ASSET_DIR"/*.zip; do echo "- \`$(basename "$f")\`"; done
} >> "$GITHUB_STEP_SUMMARY"
+1
View File
@@ -292,5 +292,6 @@ src/slic3r/GUI/AMSDryControl.cpp
src/slic3r/GUI/AMSDryControl.hpp src/slic3r/GUI/AMSDryControl.hpp
src/libslic3r/PresetBundle.cpp src/libslic3r/PresetBundle.cpp
src/slic3r/GUI/CAD/DesignPanel.cpp src/slic3r/GUI/CAD/DesignPanel.cpp
src/slic3r/GUI/CAD/SketchInlineEditor.cpp
src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp
src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp
+16 -2
View File
@@ -23,7 +23,8 @@ int main(int argc, char* argv[])
#else #else
("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory") ("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory")
#endif #endif
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)"); ("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)")
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional; generate the cache for this vendor only (the Orca filament library is always included as the inheritance base). All vendors if not specified.");
// clang-format on // clang-format on
po::variables_map vm; po::variables_map vm;
@@ -38,6 +39,7 @@ int main(int argc, char* argv[])
const std::string profiles_path = vm["path"].as<std::string>(); const std::string profiles_path = vm["path"].as<std::string>();
const int log_level = vm["log_level"].as<int>(); const int log_level = vm["log_level"].as<int>();
const std::string vendor = vm["vendor"].as<std::string>();
if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) { if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) {
std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n"; std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n";
@@ -59,8 +61,12 @@ int main(int argc, char* argv[])
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); preset_bundle->set_generate_vendor_caches(true);
// Empty == every vendor. Otherwise only this vendor (plus the always-loaded
// Orca filament library) is parsed, so only its <vendor>.opc is written.
preset_bundle->set_vendor_to_validate(vendor);
std::cout << "Loading system presets from: " << profiles_path << "\n"; std::cout << "Loading system presets from: " << profiles_path
<< (vendor.empty() ? "" : " (vendor: " + vendor + ")") << "\n";
try { try {
// In validation mode data_dir() is the profiles directory set above, so the // In validation mode data_dir() is the profiles directory set above, so the
@@ -71,6 +77,14 @@ int main(int argc, char* argv[])
return 1; return 1;
} }
// A specific vendor must have produced its own cache; the always-loaded
// filament library alone would otherwise mask a misspelt or removed name.
if (!vendor.empty() && !fs::exists(fs::path(profiles_path) / (vendor + ".opc"))) {
std::cerr << "No cache was generated for vendor '" << vendor << "' under " << profiles_path
<< " - check the vendor name.\n";
return 1;
}
size_t cache_count = 0; size_t cache_count = 0;
for (auto& entry : fs::directory_iterator(profiles_path)) for (auto& entry : fs::directory_iterator(profiles_path))
if (boost::iends_with(entry.path().string(), ".opc")) if (boost::iends_with(entry.path().string(), ".opc"))
+46 -5
View File
@@ -8,6 +8,7 @@
#include "I18N.hpp" #include "I18N.hpp"
#include "GCode.hpp" #include "GCode.hpp"
#include "Exception.hpp" #include "Exception.hpp"
#include "LifecycleEvents.hpp"
#include "ExtrusionEntity.hpp" #include "ExtrusionEntity.hpp"
#include "EdgeGrid.hpp" #include "EdgeGrid.hpp"
#include "Geometry/ConvexHull.hpp" #include "Geometry/ConvexHull.hpp"
@@ -2476,6 +2477,14 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
m_writer.set_is_bbl_machine(print->is_BBL_printer()); m_writer.set_is_bbl_machine(print->is_BBL_printer());
print->set_started(psGCodeExport); print->set_started(psGCodeExport);
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx);
}
// check if any custom gcode contains keywords used by the gcode processor to // check if any custom gcode contains keywords used by the gcode processor to
// produce time estimation and gcode toolpaths // produce time estimation and gcode toolpaths
std::vector<std::pair<std::string, std::string>> validation_res = DoExport::validate_custom_gcode(*print); std::vector<std::pair<std::string, std::string>> validation_res = DoExport::validate_custom_gcode(*print);
@@ -2511,12 +2520,22 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
m_processor.set_print(print); m_processor.set_print(print);
GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor); GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor);
if (! file.is_open()) { if (! file.is_open()) {
BOOST_LOG_TRIVIAL(error) << std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n" << std::endl; std::string err_msg = std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n";
BOOST_LOG_TRIVIAL(error) << err_msg << std::endl;
if (!fs::exists(folder)) { if (!fs::exists(folder)) {
//fs::create_directory(folder); //fs::create_directory(folder);
BOOST_LOG_TRIVIAL(error) << "the parent path " + folder.string() +" is not there!!!" << std::endl; std::string add_err_msg = "the parent path " + folder.string() +" is not there!!!";
BOOST_LOG_TRIVIAL(error) << add_err_msg << std::endl;
err_msg += add_err_msg;
} }
throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"); {
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + err_msg;
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError(err_msg);
} }
try { try {
@@ -2527,11 +2546,18 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
boost::nowide::remove(path_tmp.c_str()); boost::nowide::remove(path_tmp.c_str());
throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n"); throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n");
} }
} catch (std::exception & /* ex */) { } catch (std::exception &ex) {
// Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown. // Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown.
// Close and remove the file. // Close and remove the file.
file.close(); file.close();
boost::nowide::remove(path_tmp.c_str()); boost::nowide::remove(path_tmp.c_str());
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + ex.what();
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw; throw;
} }
file.close(); file.close();
@@ -2637,6 +2663,13 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
std::error_code ret = rename_file(path_tmp, path); std::error_code ret = rename_file(path_tmp, path);
if (ret) { if (ret) {
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError( throw Slic3r::RuntimeError(
std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "error code " + ret.message() + '\n' + std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "error code " + ret.message() + '\n' +
"Is " + path_tmp + " locked?" + '\n'); "Is " + path_tmp + " locked?" + '\n');
@@ -2647,7 +2680,15 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
BOOST_LOG_TRIVIAL(info) << "Exporting G-code finished" << log_memory_info(); BOOST_LOG_TRIVIAL(info) << "Exporting G-code finished" << log_memory_info();
print->set_done(psGCodeExport); print->set_done(psGCodeExport);
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
// Orca: label_object_enabled reflects whether objects are labeled in the g-code (EXCLUDE_OBJECT / // Orca: label_object_enabled reflects whether objects are labeled in the g-code (EXCLUDE_OBJECT /
// M486), which is driven by exclude_object for every printer // M486), which is driven by exclude_object for every printer
if(result != nullptr) if(result != nullptr)
+181
View File
@@ -0,0 +1,181 @@
#pragma once
// LifecycleEvents.hpp
// --------------------
// Application lifecycle events (project, slicing, plate editing, preset, printer connection, and
// job activity) that other subsystems -- chiefly the plugin layer above libslic3r -- may want to
// observe. Lives in libslic3r rather than the plugin layer because some events fire from inside
// the slicing engine itself; see fire_lifecycle_event() below.
#include <functional>
#include <string>
#include <utility>
namespace Slic3r
{
enum class LifecycleEvent {
// Project (3mf)
NewProject,
ProjectOpened,
ProjectBeforeSave,
ProjectAfterSave,
ProjectClosed,
ProjectDirtyChanged,
// Slicing pipeline
SliceStarted,
SliceGeometryFinished,
GCodeExportStarted,
GCodeExportFinished,
SlicingJobComplete,
// Plate/model editing
ObjectAdded,
ObjectDeleted,
ObjectTransformed,
ObjectChanged,
ObjectRenamed,
PlateCreated,
PlateDeleted,
PlateSelected,
PlateRenamed,
// Preset
PresetSelected,
PresetSaved,
// Printer/device
PrintStateChanged,
DeviceOnlineChanged,
DeviceDiscovered,
DeviceSelected,
DeviceConnected,
DeviceDisconnected,
UploadStarted,
UploadFinished,
// Print/send jobs
PrintJobStarted,
PrintJobFinished,
SendJobStarted,
SendJobFinished,
};
// Scoped so callers must qualify (LifecycleEvtCode::Error, not ERROR) -- ERROR/OK collide with
// Windows macros (wingdi.h) as unqualified names.
enum class LifecycleEvtCode { Ok, Error, Warn };
struct LifecycleEventContext
{
// The primary subject identifier for the event. This is event-specific (for example, a
// project/output path, preset name, device id, or object name), must not contain status or
// prose, and may be empty when the event has no single subject.
std::string name;
// Outcome of the operation represented by the event. For state-change and start events,
// Ok means that the event occurred; it does not imply that a future operation succeeded.
LifecycleEvtCode code = LifecycleEvtCode::Ok;
// Optional human-readable detail or diagnostic text. It is not a stable parsing contract;
// machine-readable data should be represented by a dedicated field or event instead.
std::string msg;
// Stable subject/object identifier, when the source model provides one.
std::string id;
// Previous value for rename and other before/after events.
std::string previous_name;
// Device identifier for printer and job events.
std::string device_id;
// Job identifier when the originating queue/task provides one.
std::string job_id;
// Source subsystem or operation detail, suitable for filtering but not guaranteed to be
// exhaustive across versions.
std::string source;
// Plate, object, or volume index when the source uses an index rather than a stable id.
int index = -1;
// Aggregate project dirty state for ProjectDirtyChanged.
bool dirty = false;
};
inline std::string lifecycle_event_to_string(LifecycleEvent event)
{
switch (event) {
case LifecycleEvent::NewProject: return "NewProject";
case LifecycleEvent::ProjectOpened: return "ProjectOpened";
case LifecycleEvent::ProjectBeforeSave: return "ProjectBeforeSave";
case LifecycleEvent::ProjectAfterSave: return "ProjectAfterSave";
case LifecycleEvent::ProjectClosed: return "ProjectClosed";
case LifecycleEvent::ProjectDirtyChanged: return "ProjectDirtyChanged";
case LifecycleEvent::SliceStarted: return "SliceStarted";
case LifecycleEvent::SliceGeometryFinished: return "SliceGeometryFinished";
case LifecycleEvent::GCodeExportStarted: return "GCodeExportStarted";
case LifecycleEvent::GCodeExportFinished: return "GCodeExportFinished";
case LifecycleEvent::SlicingJobComplete: return "SlicingJobComplete";
case LifecycleEvent::ObjectAdded: return "ObjectAdded";
case LifecycleEvent::ObjectDeleted: return "ObjectDeleted";
case LifecycleEvent::ObjectTransformed: return "ObjectTransformed";
case LifecycleEvent::ObjectChanged: return "ObjectChanged";
case LifecycleEvent::ObjectRenamed: return "ObjectRenamed";
case LifecycleEvent::PlateCreated: return "PlateCreated";
case LifecycleEvent::PlateDeleted: return "PlateDeleted";
case LifecycleEvent::PlateSelected: return "PlateSelected";
case LifecycleEvent::PlateRenamed: return "PlateRenamed";
case LifecycleEvent::PresetSelected: return "PresetSelected";
case LifecycleEvent::PresetSaved: return "PresetSaved";
case LifecycleEvent::PrintStateChanged: return "PrintStateChanged";
case LifecycleEvent::DeviceOnlineChanged: return "DeviceOnlineChanged";
case LifecycleEvent::DeviceDiscovered: return "DeviceDiscovered";
case LifecycleEvent::DeviceSelected: return "DeviceSelected";
case LifecycleEvent::DeviceConnected: return "DeviceConnected";
case LifecycleEvent::DeviceDisconnected: return "DeviceDisconnected";
case LifecycleEvent::UploadStarted: return "UploadStarted";
case LifecycleEvent::UploadFinished: return "UploadFinished";
case LifecycleEvent::PrintJobStarted: return "PrintJobStarted";
case LifecycleEvent::PrintJobFinished: return "PrintJobFinished";
case LifecycleEvent::SendJobStarted: return "SendJobStarted";
case LifecycleEvent::SendJobFinished: return "SendJobFinished";
default: return "Unknown";
}
}
inline std::string lifecycle_evt_code_to_string(LifecycleEvtCode code)
{
switch (code) {
case LifecycleEvtCode::Ok: return "Ok";
case LifecycleEvtCode::Error: return "Error";
case LifecycleEvtCode::Warn: return "Warn";
default: return "Unknown";
}
}
// Global cross-layer seam (mirrors ConfigBase::set_resolve_capability_fn): any libslic3r code can
// fire a lifecycle event without depending on the plugin layer above it, which installs the
// dispatcher here at startup. Not tied to Print/GCode specifically, since nothing here should
// require callers to hold a Print& just to report an event.
using LifecycleHookFn = std::function<void(LifecycleEvent, const LifecycleEventContext&)>;
inline LifecycleHookFn& lifecycle_hook_fn()
{
static LifecycleHookFn fn;
return fn;
}
inline void set_lifecycle_hook_fn(LifecycleHookFn fn) { lifecycle_hook_fn() = std::move(fn); }
inline void fire_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx)
{
if (const LifecycleHookFn& fn = lifecycle_hook_fn(); fn)
fn(event, ctx);
}
}
+10
View File
@@ -47,6 +47,7 @@
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include "libslic3r.h" #include "libslic3r.h"
#include "LifecycleEvents.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "Time.hpp" #include "Time.hpp"
#include "PlaceholderParser.hpp" #include "PlaceholderParser.hpp"
@@ -2972,6 +2973,7 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
// 1) Find the preset with a new_name or create a new one, // 1) Find the preset with a new_name or create a new one,
// initialize it with the edited config. // initialize it with the edited config.
auto it = this->find_preset_internal(new_name); auto it = this->find_preset_internal(new_name);
const bool preset_existed = (it != m_presets.end() && it->name == new_name);
if (it != m_presets.end() && it->name == new_name) { if (it != m_presets.end() && it->name == new_name) {
// Preset with the same name found. // Preset with the same name found.
Preset &preset = *it; Preset &preset = *it;
@@ -3079,6 +3081,14 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
this->get_selected_preset().save(&(parent_preset->config)); this->get_selected_preset().save(&(parent_preset->config));
else else
this->get_selected_preset().save(nullptr); this->get_selected_preset().save(nullptr);
{
LifecycleEventContext ctx;
ctx.name = new_name;
ctx.msg = preset_existed ? "overwrite" : "new";
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::PresetSaved, ctx);
}
} }
// A detached standalone preset for the Full Publish receiver: create a user preset holding // A detached standalone preset for the Full Publish receiver: create a user preset holding
+39 -1
View File
@@ -14,6 +14,7 @@
#include "Flow.hpp" #include "Flow.hpp"
#include "Geometry/ConvexHull.hpp" #include "Geometry/ConvexHull.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include "LifecycleEvents.hpp"
#include "ShortestPath.hpp" #include "ShortestPath.hpp"
#include "Thread.hpp" #include "Thread.hpp"
#include "Time.hpp" #include "Time.hpp"
@@ -2694,6 +2695,13 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (m_objects.empty()) if (m_objects.empty())
return; return;
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
}
for (PrintObject *obj : m_objects) for (PrintObject *obj : m_objects)
obj->clear_shared_object(); obj->clear_shared_object();
@@ -3312,6 +3320,13 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
} }
BOOST_LOG_TRIVIAL(info) << "Slicing process finished." << log_memory_info(); BOOST_LOG_TRIVIAL(info) << "Slicing process finished." << log_memory_info();
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
}
} }
// G-code export process, running at a background thread. // G-code export process, running at a background thread.
@@ -4911,6 +4926,14 @@ void Print::set_gcode_file_invalidated()
//BBS: add gcode file preload logic //BBS: add gcode file preload logic
void Print::export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb) void Print::export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb)
{ {
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx);
}
try { try {
GCodeProcessor processor; GCodeProcessor processor;
GCodeProcessor::s_IsBBLPrinter = is_BBL_printer(); GCodeProcessor::s_IsBBLPrinter = is_BBL_printer();
@@ -4930,13 +4953,28 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
*result = std::move(processor.extract_result()); *result = std::move(processor.extract_result());
result->filament_change_sequence = filament_seq_loaded; result->filament_change_sequence = filament_seq_loaded;
result->nozzle_change_sequence = nozzle_seq_loaded; result->nozzle_change_sequence = nozzle_seq_loaded;
} catch (std::exception & /* ex */) { } catch (std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str(); BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = file + "\n" + ex.what();
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError( throw Slic3r::RuntimeError(
std::string("Failed to process the G-code file ") + file + " from previous 3mf\n"); std::string("Failed to process the G-code file ") + file + " from previous 3mf\n");
} }
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": process the G-code file %1% successfully")%file.c_str(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": process the G-code file %1% successfully")%file.c_str();
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
} }
std::tuple<float, float> Print::object_skirt_offset(double margin_height) const std::tuple<float, float> Print::object_skirt_offset(double margin_height) const
+10 -10
View File
@@ -403,7 +403,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// bind to their default mode; the other modes stay in the toolbar flyout. Registered here // bind to their default mode; the other modes stay in the toolbar flyout. Registered here
// where select_tool is in scope; the closures run at key-press time (members are live by then). // where select_tool is in scope; the closures run at key-press time (members are live by then).
auto sk_key = [this, select_tool](int ch, DesignSketchTool::Mode m, const wxString& h) { auto sk_key = [this, select_tool](int ch, DesignSketchTool::Mode m, const wxString& h) {
m_keys_sketch[ch] = [this, select_tool, m, h] { select_tool(m, h); }; m_keys_sketch[ch] = [select_tool, m, h] { select_tool(m, h); };
}; };
sk_key('L', DesignSketchTool::Mode::Line, _L("Line — click start, then end")); sk_key('L', DesignSketchTool::Mode::Line, _L("Line — click start, then end"));
sk_key('R', DesignSketchTool::Mode::CornerRect, _L("Rectangle — click two opposite corners")); sk_key('R', DesignSketchTool::Mode::CornerRect, _L("Rectangle — click two opposite corners"));
@@ -1445,16 +1445,16 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Choose FIRST, then open: open_tool() titles the card from m_dressup_type, so setting // Choose FIRST, then open: open_tool() titles the card from m_dressup_type, so setting
// it afterwards left the header reading "Fillet 1" over a chamfer. Nothing in the // it afterwards left the header reading "Fillet 1" over a chamfer. Nothing in the
// opener resets the combo, so this order is safe. // opener resets the combo, so this order is safe.
m_verb_actions["btn:dress#0"] = [this, open_feature, k_dress] { m_verb_actions["btn:dress#0"] = [this, open_feature] {
if (m_dressup_type) m_dressup_type->SetSelection(0); open_feature(k_dress); }; if (m_dressup_type) m_dressup_type->SetSelection(0); open_feature(k_dress); };
m_verb_actions["btn:dress#1"] = [this, open_feature, k_dress] { m_verb_actions["btn:dress#1"] = [this, open_feature] {
if (m_dressup_type) m_dressup_type->SetSelection(1); open_feature(k_dress); }; if (m_dressup_type) m_dressup_type->SetSelection(1); open_feature(k_dress); };
for (int op = 0; op < 3; ++op) for (int op = 0; op < 3; ++op)
m_verb_actions["btn:bool#" + std::to_string(op)] = [this, open_feature, k_bool, op] { m_verb_actions["btn:bool#" + std::to_string(op)] = [this, open_feature, op] {
open_feature(k_bool); open_feature(k_bool);
if (m_bool_op) { m_bool_op->SetSelection(op); refresh_preview(); } }; if (m_bool_op) { m_bool_op->SetSelection(op); refresh_preview(); } };
for (int t = 0; t < 2; ++t) for (int t = 0; t < 2; ++t)
m_verb_actions["btn:pat#" + std::to_string(t)] = [this, open_feature, k_pat, t] { m_verb_actions["btn:pat#" + std::to_string(t)] = [this, open_feature, t] {
open_feature(k_pat); if (m_pattern_type) m_pattern_type->SetSelection(t); }; open_feature(k_pat); if (m_pattern_type) m_pattern_type->SetSelection(t); };
auto* b_poly = icon_btn("design_polygon", _L("Polygon")); auto* b_poly = icon_btn("design_polygon", _L("Polygon"));
@@ -6625,7 +6625,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
mate_ghost = false; mate_ghost = false;
}; };
menu.Bind(wxEVT_MENU_HIGHLIGHT, menu.Bind(wxEVT_MENU_HIGHLIGHT,
[this, cs_a, cs_b, opts, mate_base, &mate_ghost, drop_ghost](wxMenuEvent& e) { [this, cs_a, cs_b, opts, &mate_ghost, drop_ghost](wxMenuEvent& e) {
const int i = e.GetMenuId() - (mate_base + 1); const int i = e.GetMenuId() - (mate_base + 1);
// Off the palette (a verb row, the header, or nothing) — a stale ghost from the row // Off the palette (a verb row, the header, or nothing) — a stale ghost from the row
// you just left is worse than none, so it goes as soon as the cursor does. // you just left is worse than none, so it goes as soon as the cursor does.
@@ -6635,7 +6635,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
m_viewport->repaint_now(); // synchronous: the popup owns the loop, a queued repaint is never serviced m_viewport->repaint_now(); // synchronous: the popup owns the loop, a queued repaint is never serviced
}); });
menu.Bind(wxEVT_MENU, [this, cs_a, cs_b, opts, mate_base, drop_ghost](wxCommandEvent& e) { menu.Bind(wxEVT_MENU, [this, cs_a, cs_b, opts, drop_ghost](wxCommandEvent& e) {
const int i = e.GetId() - (mate_base + 1); const int i = e.GetId() - (mate_base + 1);
if (i < 0 || i >= int(opts.size()) || !opts[i].viable) return; if (i < 0 || i >= int(opts.size()) || !opts[i].viable) return;
drop_ghost(); // the real bodies are about to become the ghost's pose drop_ghost(); // the real bodies are about to become the ghost's pose
@@ -6666,7 +6666,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
// status message on screen, and clicking one created nothing at all. The whole mate palette // status message on screen, and clicking one created nothing at all. The whole mate palette
// enumerated perfectly and fired nothing. Restricting the range keeps each half to its own ids // enumerated perfectly and fired nothing. Restricting the range keeps each half to its own ids
// regardless of bind order. // regardless of bind order.
menu.Bind(wxEVT_MENU_HIGHLIGHT, [this, &bound, base](wxMenuEvent& e) { menu.Bind(wxEVT_MENU_HIGHLIGHT, [this, &bound](wxMenuEvent& e) {
const int i = e.GetMenuId() - base; const int i = e.GetMenuId() - base;
if (i < 0 || i >= int(bound.size()) || bound[i] == nullptr || bound[i]->hint == nullptr) if (i < 0 || i >= int(bound.size()) || bound[i] == nullptr || bound[i]->hint == nullptr)
return; return;
@@ -6674,7 +6674,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
set_status(wxGetTranslation(wxString::FromUTF8(bound[i]->hint))); set_status(wxGetTranslation(wxString::FromUTF8(bound[i]->hint)));
m_status->Update(); // the popup owns the loop; without this the line repaints late m_status->Update(); // the popup owns the loop; without this the line repaints late
}, base, base + 499); // 499: the mate section starts at base + 500 (see mate_base) }, base, base + 499); // 499: the mate section starts at base + 500 (see mate_base)
menu.Bind(wxEVT_MENU, [this, &bound, base](wxCommandEvent& e) { menu.Bind(wxEVT_MENU, [this, &bound](wxCommandEvent& e) {
const int i = e.GetId() - base; const int i = e.GetId() - base;
if (i >= 0 && i < int(bound.size()) && bound[i]) if (i >= 0 && i < int(bound.size()) && bound[i])
run_offer_action(bound[i]->action); run_offer_action(bound[i]->action);
@@ -7935,7 +7935,7 @@ void DesignPanel::apply_live_constraint(SketchConstraintType type)
constraint_reject_text(plan.reason, type), int(sel.size()))); constraint_reject_text(plan.reason, type), int(sel.size())));
return; return;
case ConstraintPlan::Kind::AskValue: case ConstraintPlan::Kind::AskValue:
m_viewport->open_inline_value(plan.prefill, [this, plan, commit](double v) { m_viewport->open_inline_value(plan.prefill, [plan, commit](double v) {
commit(plan.defs, v); commit(plan.defs, v);
}); });
return; return;
+1 -1
View File
@@ -121,7 +121,7 @@ void SketchInlineEditor::do_commit()
// just sits there and the user has no idea what it wants. Say so in the title line and // just sits there and the user has no idea what it wants. Say so in the title line and
// keep editing. // keep editing.
ux_trace("refused", m_title, std::string("typed=") + m_buf); ux_trace("refused", m_title, std::string("typed=") + m_buf);
m_err = (m_buf[0] == '\0') ? into_u8(_L("Enter a number")) : into_u8(_L("Not a number")); m_err = (m_buf[0] == '\0') ? _u8L("Enter a number") : _u8L("Not a number");
m_focus_pending = true; m_focus_pending = true;
return; return;
} }
+25 -2
View File
@@ -10,6 +10,7 @@
#include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/Plater.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Time.hpp" #include "libslic3r/Time.hpp"
@@ -355,7 +356,10 @@ namespace Slic3r
obj->bind_state = "free"; obj->bind_state = "free";
obj->last_alive = Slic3r::Utils::get_current_time_utc(); obj->last_alive = Slic3r::Utils::get_current_time_utc();
obj->m_is_online = true; // Route through set_online_state() (rather than writing m_is_online directly) so the
// DeviceOnlineChanged lifecycle event fires consistently; same effective value/behavior
// here since the object was already online in the common case.
obj->set_online_state(true);
obj->set_dev_name(dev_name); obj->set_dev_name(dev_name);
/* if (!obj->dev_ip.empty()) { /* if (!obj->dev_ip.empty()) {
Slic3r::GUI::wxGetApp().app_config->set_str("ip_address", obj->dev_id, obj->dev_ip); Slic3r::GUI::wxGetApp().app_config->set_str("ip_address", obj->dev_id, obj->dev_ip);
@@ -373,6 +377,10 @@ namespace Slic3r
obj->bind_sec_link = sec_link; obj->bind_sec_link = sec_link;
obj->dev_connection_name = connection_name; obj->dev_connection_name = connection_name;
obj->bind_ssdp_version = ssdp_version; obj->bind_ssdp_version = ssdp_version;
// Discovery establishes the initial reachability state. Do not report it as an
// online transition; DeviceDiscovered below is the lifecycle event for a new
// device. Subsequent updates route through set_online_state(), so a known device
// still emits DeviceOnlineChanged when its reachability actually changes.
obj->m_is_online = true; obj->m_is_online = true;
//load access code //load access code
@@ -389,6 +397,15 @@ namespace Slic3r
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " New Machine, dev_id= " << dev_id BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " New Machine, dev_id= " << dev_id
<< ", ip = " << dev_ip <<", printer_name = " << dev_name << ", ip = " << dev_ip <<", printer_name = " << dev_name
<< ", con_type= " << connect_type <<", signal= " << printer_signal << ", bind_state= " << bind_state; << ", con_type= " << connect_type <<", signal= " << printer_signal << ", bind_state= " << bind_state;
// First discovery of a genuinely new device (not a periodic SSDP/heartbeat update to
// an already-known one, which is handled in the branch above).
{
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceDiscovered, ctx);
}
} }
update_local_machine(*obj); update_local_machine(*obj);
} }
@@ -984,8 +1001,14 @@ namespace Slic3r
} }
void DeviceManager::OnSelectedMachineChanged(const std::string& /*pre_dev_id*/, void DeviceManager::OnSelectedMachineChanged(const std::string& /*pre_dev_id*/,
const std::string& /*new_dev_id*/) const std::string& new_dev_id)
{ {
{
LifecycleEventContext ctx;
ctx.name = new_dev_id; // empty string is a valid deselection
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceSelected, ctx);
}
if (MachineObject* obj_ = get_selected_machine()) { if (MachineObject* obj_ = get_selected_machine()) {
GUI::wxGetApp().sidebar().update_sync_status(obj_); GUI::wxGetApp().sidebar().update_sync_status(obj_);
if(m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription) if(m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription)
+27 -1
View File
@@ -5,6 +5,7 @@
#include "libslic3r/Time.hpp" #include "libslic3r/Time.hpp"
#include "libslic3r/Thread.hpp" #include "libslic3r/Thread.hpp"
#include "slic3r/Utils/NetworkAgent.hpp" #include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "GuiColor.hpp" #include "GuiColor.hpp"
@@ -2626,7 +2627,15 @@ void MachineObject::reset()
void MachineObject::set_print_state(std::string status) void MachineObject::set_print_state(std::string status)
{ {
const bool changed = (print_status != status);
print_status = status; print_status = status;
if (changed) {
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = print_status;
fire_lifecycle_event(LifecycleEvent::PrintStateChanged, ctx);
}
} }
int MachineObject::connect(bool use_openssl) int MachineObject::connect(bool use_openssl)
@@ -2648,6 +2657,10 @@ int MachineObject::connect(bool use_openssl)
int MachineObject::disconnect() int MachineObject::disconnect()
{ {
if (m_agent) { if (m_agent) {
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx);
return m_agent->disconnect_printer(); return m_agent->disconnect_printer();
} }
return -1; return -1;
@@ -2678,8 +2691,16 @@ bool MachineObject::is_connecting()
void MachineObject::set_online_state(bool on_off) void MachineObject::set_online_state(bool on_off)
{ {
const bool changed = (m_is_online != on_off);
m_is_online = on_off; m_is_online = on_off;
if (!on_off) m_active_state = NotActive; if (!on_off) m_active_state = NotActive;
if (changed) {
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = on_off ? "online" : "offline";
fire_lifecycle_event(LifecycleEvent::DeviceOnlineChanged, ctx);
}
} }
bool MachineObject::is_info_ready(bool check_version) const bool MachineObject::is_info_ready(bool check_version) const
@@ -4601,8 +4622,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
try { try {
if (j.contains("event")) { if (j.contains("event")) {
if (j["event"].contains("event")) { if (j["event"].contains("event")) {
if (j["event"]["event"].get<std::string>() == "client.disconnected") if (j["event"]["event"].get<std::string>() == "client.disconnected") {
set_online_state(false); set_online_state(false);
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx);
}
else if (j["event"]["event"].get<std::string>() == "client.connected") else if (j["event"]["event"].get<std::string>() == "client.connected")
set_online_state(true); set_online_state(true);
} }
+28 -3
View File
@@ -42,6 +42,7 @@
#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp" #include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp"
#include "slic3r/Utils/UndoRedo.hpp" #include "slic3r/Utils/UndoRedo.hpp"
#include "slic3r/Utils/MacDarkMode.hpp" #include "slic3r/Utils/MacDarkMode.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include <slic3r/GUI/GUI_Utils.hpp> #include <slic3r/GUI/GUI_Utils.hpp>
@@ -5208,8 +5209,16 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
//BBS: nofity object list to update //BBS: nofity object list to update
wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items(); wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items();
if (object_moved) if (object_moved) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "moved";
if (done.size() == 1)
ctx.name = m_model->objects[done.begin()->first]->name;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED)); post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED));
}
// BBS: support wipe-tower for multi-plates // BBS: support wipe-tower for multi-plates
for (int plate_id = 0; plate_id < wipe_tower_origins.size(); plate_id++) { for (int plate_id = 0; plate_id < wipe_tower_origins.size(); plate_id++) {
@@ -5330,8 +5339,16 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type)
//BBS: nofity object list to update //BBS: nofity object list to update
wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items(); wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items();
if (!done.empty()) if (!done.empty()) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "rotated";
if (done.size() == 1)
ctx.name = m_model->objects[done.begin()->first]->name;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED)); post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED));
}
m_dirty = true; m_dirty = true;
} }
@@ -5422,8 +5439,16 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type)
//BBS: notify object info update //BBS: notify object info update
wxGetApp().plater()->show_object_info(); wxGetApp().plater()->show_object_info();
if (!done.empty()) if (!done.empty()) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "scaled";
if (done.size() == 1)
ctx.name = m_model->objects[done.begin()->first]->name;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED)); post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED));
}
m_dirty = true; m_dirty = true;
} }
+22
View File
@@ -2179,6 +2179,11 @@ void GUI_App::init_networking_callbacks()
obj->command_get_access_code(); obj->command_get_access_code();
if (m_agent) if (m_agent)
m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer()); m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer());
LifecycleEventContext ctx;
ctx.name = obj->get_dev_id();
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceConnected, ctx);
} }
}); });
}); });
@@ -2217,6 +2222,11 @@ void GUI_App::init_networking_callbacks()
obj->command_get_version(); obj->command_get_version();
event.SetInt(0); event.SetInt(0);
event.SetString(obj->get_dev_id()); event.SetString(obj->get_dev_id());
LifecycleEventContext ctx;
ctx.name = obj->get_dev_id();
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceConnected, ctx);
} else if (state == ConnectStatus::ConnectStatusFailed) { } else if (state == ConnectStatus::ConnectStatusFailed) {
// Orca: only update status if same device id // Orca: only update status if same device id
if (m_device_manager->selected_machine != dev_id) return; if (m_device_manager->selected_machine != dev_id) return;
@@ -2232,10 +2242,22 @@ void GUI_App::init_networking_callbacks()
wxGetApp().show_dialog(text); wxGetApp().show_dialog(text);
} }
event.SetInt(-1); event.SetInt(-1);
{
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx);
}
} else if (state == ConnectStatus::ConnectStatusLost) { } else if (state == ConnectStatus::ConnectStatusLost) {
m_device_manager->set_selected_machine(""); m_device_manager->set_selected_machine("");
event.SetInt(-1); event.SetInt(-1);
BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = lost"; BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = lost";
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx);
} else { } else {
event.SetInt(-1); event.SetInt(-1);
BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = " << state; BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = " << state;
+39 -1
View File
@@ -1,5 +1,6 @@
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "GUI_ObjectList.hpp" #include "GUI_ObjectList.hpp"
#include "GUI_Factories.hpp" #include "GUI_Factories.hpp"
//#include "GUI_ObjectLayers.hpp" //#include "GUI_ObjectLayers.hpp"
@@ -9,6 +10,7 @@
#include "BitmapComboBox.hpp" #include "BitmapComboBox.hpp"
#include "MainFrame.hpp" #include "MainFrame.hpp"
#include "slic3r/Utils/UndoRedo.hpp" #include "slic3r/Utils/UndoRedo.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "OptionsGroup.hpp" #include "OptionsGroup.hpp"
#include "Tab.hpp" #include "Tab.hpp"
@@ -1199,17 +1201,39 @@ void ObjectList::update_name_in_model(const wxDataViewItem& item) const
if (m_objects_model->GetItemType(item) & itObject) { if (m_objects_model->GetItemType(item) & itObject) {
std::string name = m_objects_model->GetName(item).ToUTF8().data(); std::string name = m_objects_model->GetName(item).ToUTF8().data();
if (obj->name != name) { if (obj->name != name) {
const std::string previous_name = obj->name;
obj->name = name; obj->name = name;
// if object has just one volume, rename this volume too // if object has just one volume, rename this volume too
if (obj->volumes.size() == 1) if (obj->volumes.size() == 1)
obj->volumes[0]->name = obj->name; obj->volumes[0]->name = obj->name;
Slic3r::save_object_mesh(*obj); Slic3r::save_object_mesh(*obj);
LifecycleEventContext ctx;
ctx.name = name;
ctx.previous_name = previous_name;
ctx.id = std::to_string(obj->id().id);
ctx.index = obj_idx;
ctx.source = "object";
fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx);
} }
return; return;
} }
if (volume_id < 0) return; if (volume_id < 0) return;
obj->volumes[volume_id]->name = m_objects_model->GetName(item).ToUTF8().data(); std::string name = m_objects_model->GetName(item).ToUTF8().data();
if (obj->volumes[volume_id]->name == name)
return;
const std::string previous_name = obj->volumes[volume_id]->name;
obj->volumes[volume_id]->name = name;
LifecycleEventContext ctx;
ctx.name = name;
ctx.previous_name = previous_name;
ctx.id = std::to_string(obj->volumes[volume_id]->id().id);
ctx.index = volume_id;
ctx.source = "volume";
fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx);
} }
void ObjectList::update_name_in_list(int obj_idx, int vol_idx) const void ObjectList::update_name_in_list(int obj_idx, int vol_idx) const
@@ -3548,7 +3572,14 @@ void ObjectList::delete_all_connectors_for_object(int obj_idx)
obj->delete_connectors(); obj->delete_connectors();
if (obj->volumes.empty() || !obj->has_solid_mesh()) { if (obj->volumes.empty() || !obj->has_solid_mesh()) {
const std::string deleted_obj_name = obj->name;
model.delete_object(idx); model.delete_object(idx);
{
Slic3r::LifecycleEventContext ctx;
ctx.name = deleted_obj_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
}
m_objects_model->Delete(m_objects_model->GetItemById(idx)); m_objects_model->Delete(m_objects_model->GetItemById(idx));
continue; continue;
} }
@@ -4117,6 +4148,13 @@ void ObjectList::add_object_to_list(size_t obj_idx, bool call_selection_changed,
const auto item = m_objects_model->AddObject(model_object, warning_bitmap, model_object->is_cut()); const auto item = m_objects_model->AddObject(model_object, warning_bitmap, model_object->is_cut());
Expand(m_objects_model->GetParent(item)); Expand(m_objects_model->GetParent(item));
{
Slic3r::LifecycleEventContext ctx;
ctx.name = model_object->name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectAdded, ctx);
}
if (!do_info_update) if (!do_info_update)
return; return;
+5 -6
View File
@@ -15,7 +15,6 @@
#endif #endif
#include <imgui/imgui_internal.h> #include <imgui/imgui_internal.h>
#define L(s) Slic3r::GUI::I18N::translate((s)).c_str()
#define UL(s) Slic3r::GUI::I18N::translate_utf8((s)).c_str() #define UL(s) Slic3r::GUI::I18N::translate_utf8((s)).c_str()
namespace Slic3r { namespace Slic3r {
@@ -374,15 +373,15 @@ void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit)
}; };
if (m_sp.is_pocket && has_sel) { if (m_sp.is_pocket && has_sel) {
if (btn(L("Pocket (Cut)"), ok)) apply_pocket(); if (btn(_u8L("Pocket (Cut)").c_str(), ok)) apply_pocket();
} else if (is_revolve) { } else if (is_revolve) {
if (btn(L("Revolve"), ok)) apply_revolve(); if (btn(_u8L("Revolve").c_str(), ok)) apply_revolve();
} else { } else {
if (btn(L("Extrude"), ok)) apply_extrude(); if (btn(_u8L("Extrude").c_str(), ok)) apply_extrude();
} }
if (ImGui::Button(L("Clear All"), {-1,0})) clear_all(); if (ImGui::Button(_u8L("Clear All").c_str(), {-1,0})) clear_all();
if (ImGui::Button(L("Close"), {-1,0})) m_parent.reset_all_gizmos(); if (ImGui::Button(_u8L("Close").c_str(), {-1,0})) m_parent.reset_all_gizmos();
GizmoImguiEnd(); GizmoImguiEnd();
ImGuiWrapper::pop_toolbar_style(); ImGuiWrapper::pop_toolbar_style();
+8
View File
@@ -13,6 +13,7 @@
#include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/NotificationManager.hpp"
#include "slic3r/GUI/format.hpp" #include "slic3r/GUI/format.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "libnest2d/common.hpp" #include "libnest2d/common.hpp"
@@ -698,6 +699,13 @@ void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) {
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":arrange m_unprintable: name: %4%, bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale<double>(ap.translation(X)) % unscale<double>(ap.translation(Y)) % ap.name; BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":arrange m_unprintable: name: %4%, bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale<double>(ap.translation(X)) % unscale<double>(ap.translation(Y)) % ap.name;
} }
{
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "arranged";
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
}
m_plater->update(); m_plater->update();
// BBS // BBS
//wxGetApp().obj_manipul()->set_dirty(); //wxGetApp().obj_manipul()->set_dirty();
+8
View File
@@ -6,6 +6,7 @@
#include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "libnest2d/common.hpp" #include "libnest2d/common.hpp"
#include <numeric> #include <numeric>
@@ -347,6 +348,13 @@ void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr)
m_plater->arrange(); m_plater->arrange();
} }
m_plater->update(); m_plater->update();
{
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "arranged";
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
}
} }
m_plater->mark_plate_toolbar_image_dirty(); m_plater->mark_plate_toolbar_image_dirty();
+7
View File
@@ -5,6 +5,7 @@
#include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/NotificationManager.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
@@ -254,6 +255,12 @@ void OrientJob::finalize(bool canceled, std::exception_ptr &eptr)
mesh.apply(); mesh.apply();
} }
if (!m_selected.empty()) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "auto_oriented";
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
}
m_plater->update(); m_plater->update();
+22
View File
@@ -1,4 +1,5 @@
#include "PrintJob.hpp" #include "PrintJob.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "libslic3r/MTUtils.hpp" #include "libslic3r/MTUtils.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
@@ -133,6 +134,13 @@ wxString PrintJob::get_http_error_msg(unsigned int status, std::string body)
void PrintJob::process(Ctl &ctl) void PrintJob::process(Ctl &ctl)
{ {
LifecycleEventContext start_ctx;
start_ctx.name = m_project_name;
start_ctx.device_id = m_dev_id;
start_ctx.source = "print_job";
fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx);
m_lifecycle_started = true;
/* display info */ /* display info */
std::string msg; std::string msg;
int curr_percent = 10; int curr_percent = 10;
@@ -686,6 +694,7 @@ void PrintJob::process(Ctl &ctl)
} }
wxQueueEvent(m_plater, evt); wxQueueEvent(m_plater, evt);
m_job_finished = true; m_job_finished = true;
m_lifecycle_success = true;
} }
} }
@@ -698,6 +707,19 @@ void PrintJob::finalize(bool canceled, std::exception_ptr &eptr) {
eptr = std::current_exception(); eptr = std::current_exception();
} }
if (m_lifecycle_started && !m_lifecycle_finished) {
LifecycleEventContext finish_ctx;
finish_ctx.name = m_project_name;
finish_ctx.device_id = m_dev_id;
finish_ctx.source = "print_job";
finish_ctx.code = canceled ? LifecycleEvtCode::Warn :
(eptr || !m_lifecycle_success ? LifecycleEvtCode::Error : LifecycleEvtCode::Ok);
finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" :
(m_lifecycle_success ? "" : "failed"));
fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx);
m_lifecycle_finished = true;
}
if (canceled || eptr) if (canceled || eptr)
return; return;
} }
+3
View File
@@ -43,6 +43,9 @@ class PrintJob : public Job
std::function<void()> m_success_fun{nullptr}; std::function<void()> m_success_fun{nullptr};
std::string m_dev_id; std::string m_dev_id;
bool m_job_finished{ false }; bool m_job_finished{ false };
bool m_lifecycle_started{ false };
bool m_lifecycle_finished{ false };
bool m_lifecycle_success{ false };
int m_print_job_completed_id = 0; int m_print_job_completed_id = 0;
wxString m_completed_evt_data; wxString m_completed_evt_data;
std::function<void()> m_enter_ip_address_fun_fail{ nullptr }; std::function<void()> m_enter_ip_address_fun_fail{ nullptr };
+21
View File
@@ -1,4 +1,5 @@
#include "SendJob.hpp" #include "SendJob.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/I18N.hpp"
#include "slic3r/Utils/NetworkAgent.hpp" #include "slic3r/Utils/NetworkAgent.hpp"
#include "libslic3r/MTUtils.hpp" #include "libslic3r/MTUtils.hpp"
@@ -148,6 +149,13 @@ void SendJob::process(Ctl &ctl)
} }
} }
LifecycleEventContext start_ctx;
start_ctx.name = m_project_name;
start_ctx.device_id = m_dev_id;
start_ctx.source = "send_job";
fire_lifecycle_event(LifecycleEvent::SendJobStarted, start_ctx);
m_lifecycle_started = true;
int total_plate_num = m_plater->get_partplate_list().get_plate_count(); int total_plate_num = m_plater->get_partplate_list().get_plate_count();
PartPlate* plate = m_plater->get_partplate_list().get_plate(job_data.plate_idx); PartPlate* plate = m_plater->get_partplate_list().get_plate(job_data.plate_idx);
@@ -424,6 +432,19 @@ void SendJob::finalize(bool canceled, std::exception_ptr &eptr)
eptr = std::current_exception(); eptr = std::current_exception();
} }
if (m_lifecycle_started && !m_lifecycle_finished) {
LifecycleEventContext finish_ctx;
finish_ctx.name = m_project_name;
finish_ctx.device_id = m_dev_id;
finish_ctx.source = "send_job";
finish_ctx.code = canceled ? LifecycleEvtCode::Warn :
(eptr ? LifecycleEvtCode::Error : (m_job_finished ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error));
finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" :
(m_job_finished ? "" : "failed"));
fire_lifecycle_event(LifecycleEvent::SendJobFinished, finish_ctx);
m_lifecycle_finished = true;
}
if (canceled || eptr) if (canceled || eptr)
return; return;
} }
+2
View File
@@ -22,6 +22,8 @@ class SendJob : public Job
PrintPrepareData job_data; PrintPrepareData job_data;
std::string m_dev_id; std::string m_dev_id;
bool m_job_finished{ false }; bool m_job_finished{ false };
bool m_lifecycle_started{ false };
bool m_lifecycle_finished{ false };
int m_print_job_completed_id = 0; int m_print_job_completed_id = 0;
bool m_is_check_mode{false}; bool m_is_check_mode{false};
bool m_check_and_continue{false}; bool m_check_and_continue{false};
+34 -1
View File
@@ -29,6 +29,7 @@
#include "libslic3r/Tesselate.hpp" #include "libslic3r/Tesselate.hpp"
#include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/GCode/ThumbnailData.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
@@ -2636,11 +2637,20 @@ void PartPlate::set_plate_name(const std::string& name)
if (boost::equals(m_name, name)) if (boost::equals(m_name, name))
return; return;
const std::string previous_name = m_name;
m_name = name; m_name = name;
if (m_print != nullptr) if (m_print != nullptr)
m_print->set_plate_name(name); m_print->set_plate_name(name);
invalidate_plate_name_texture(); invalidate_plate_name_texture();
if (m_plater != nullptr && !m_plater->is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = name;
ctx.previous_name = previous_name;
ctx.index = m_plate_index;
fire_lifecycle_event(LifecycleEvent::PlateRenamed, ctx);
}
} }
//get the print's object, result and index //get the print's object, result and index
@@ -4751,9 +4761,16 @@ int PartPlateList::create_plate(bool adjust_position)
if (m_plater) { if (m_plater) {
// In GUI mode // In GUI mode
wxGetApp().obj_list()->on_plate_added(plate); wxGetApp().obj_list()->on_plate_added(plate);
} }
if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = plate->get_plate_name();
ctx.index = new_index;
fire_lifecycle_event(LifecycleEvent::PlateCreated, ctx);
}
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":created a new plate %1%") % new_index; BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":created a new plate %1%") % new_index;
return new_index; return new_index;
} }
@@ -4852,6 +4869,7 @@ int PartPlateList::delete_plate(int index)
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate %1%, has an invalid index %2%") % index % plate->get_index(); BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate %1%, has an invalid index %2%") % index % plate->get_index();
return -1; return -1;
} }
const std::string plate_name = plate->get_plate_name();
if (m_plater) { if (m_plater) {
// In GUI mode // In GUI mode
@@ -4934,6 +4952,13 @@ int PartPlateList::delete_plate(int index)
delete plate; delete plate;
if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = plate_name;
ctx.index = index;
fire_lifecycle_event(LifecycleEvent::PlateDeleted, ctx);
}
// FIX: context of BackgroundSliceProcess and gcode preview need to be updated before ObjectList::reload_all_plates(). // FIX: context of BackgroundSliceProcess and gcode preview need to be updated before ObjectList::reload_all_plates().
#if 0 #if 0
if (m_plater != nullptr) { if (m_plater != nullptr) {
@@ -5042,6 +5067,7 @@ int PartPlateList::select_plate(int index)
if (m_plate_list.empty() || index >= m_plate_list.size()) { if (m_plate_list.empty() || index >= m_plate_list.size()) {
return -1; return -1;
} }
const int previous_index = m_current_plate;
// BBS: erase unnecessary snapshot // BBS: erase unnecessary snapshot
if (get_curr_plate_index() != index && m_intialized) { if (get_curr_plate_index() != index && m_intialized) {
@@ -5068,6 +5094,13 @@ int PartPlateList::select_plate(int index)
//wxQueueEvent(m_plater, new SimpleEvent(EVT_GLCANVAS_PLATE_SELECT)); //wxQueueEvent(m_plater, new SimpleEvent(EVT_GLCANVAS_PLATE_SELECT));
} }
if (previous_index != index && m_intialized && m_plater != nullptr && !m_plater->is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = m_plate_list[index]->get_plate_name();
ctx.index = index;
fire_lifecycle_event(LifecycleEvent::PlateSelected, ctx);
}
return 0; return 0;
} }
+105 -13
View File
@@ -57,6 +57,7 @@
#include <wx/aui/aui.h> #include <wx/aui/aui.h>
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "libslic3r/LifecycleEvents.hpp"
#include "libslic3r/Format/STL.hpp" #include "libslic3r/Format/STL.hpp"
#include "libslic3r/Format/DRC.hpp" #include "libslic3r/Format/DRC.hpp"
#include "libslic3r/Format/STEP.hpp" #include "libslic3r/Format/STEP.hpp"
@@ -10123,7 +10124,14 @@ void Plater::priv::remove(size_t obj_idx)
view3D->enable_layers_editing(false); view3D->enable_layers_editing(false);
m_worker.cancel_all(); m_worker.cancel_all();
std::string obj_name = (obj_idx < model.objects.size()) ? model.objects[obj_idx]->name : std::to_string(obj_idx);
model.delete_object(obj_idx); model.delete_object(obj_idx);
{
Slic3r::LifecycleEventContext ctx;
ctx.name = obj_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
}
//BBS: notify partplate the instance removed //BBS: notify partplate the instance removed
partplate_list.notify_instance_removed(obj_idx, -1); partplate_list.notify_instance_removed(obj_idx, -1);
update(); update();
@@ -10156,7 +10164,14 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia
if (obj->is_cut()) if (obj->is_cut())
sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx); sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx);
std::string obj_name = obj->name;
model.delete_object(obj_idx); model.delete_object(obj_idx);
{
Slic3r::LifecycleEventContext ctx;
ctx.name = obj_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
}
//BBS: notify partplate the instance removed //BBS: notify partplate the instance removed
partplate_list.notify_instance_removed(obj_idx, -1); partplate_list.notify_instance_removed(obj_idx, -1);
@@ -10202,10 +10217,22 @@ void Plater::priv::delete_all_objects_from_model()
void Plater::priv::reset(bool apply_presets_change) void Plater::priv::reset(bool apply_presets_change)
{ {
// TakeSnapshot below and load_current_presets() further down each re-evaluate the
// aggregate dirty flag against a baseline that hasn't been reset yet, so they can toggle
// is_dirty() back and forth several times before it settles; coalesce those into one event.
ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(dirty_state);
Plater::TakeSnapshot snapshot(q, _u8L("Reset Project"), UndoRedo::SnapshotType::ProjectSeparator); Plater::TakeSnapshot snapshot(q, _u8L("Reset Project"), UndoRedo::SnapshotType::ProjectSeparator);
clear_warnings(); clear_warnings();
const std::string closed_project_name = into_u8(get_project_filename());
if (!closed_project_name.empty() || !model.objects.empty()) {
Slic3r::LifecycleEventContext ctx;
ctx.name = closed_project_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectClosed, ctx);
}
// A new project must not inherit the previous project's published selection (Feature A/B). // A new project must not inherit the previous project's published selection (Feature A/B).
m_has_pending_published = false; m_has_pending_published = false;
m_pending_published_keys.clear(); m_pending_published_keys.clear();
@@ -12633,11 +12660,15 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
notification_manager->set_slicing_progress_export_possible(); notification_manager->set_slicing_progress_export_possible();
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again. // Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
const std::string lifecycle_job_name = this->background_process.fff_print() ?
this->background_process.fff_print()->output_filename() : std::string();
this->background_process.reset_export(); this->background_process.reset_export();
// This bool stops showing export finished notification even when process_completed_with_error is false // This bool stops showing export finished notification even when process_completed_with_error is false
bool has_error = false; bool has_error = false;
std::string lifecycle_error_msg;
if (evt.error()) { if (evt.error()) {
auto message = evt.format_error_message(); auto message = evt.format_error_message();
lifecycle_error_msg = message.first;
if (evt.critical_error()) { if (evt.critical_error()) {
if (q->m_tracking_popup_menu) { if (q->m_tracking_popup_menu) {
// We don't want to pop-up a message box when tracking a pop-up menu. // We don't want to pop-up a message box when tracking a pop-up menu.
@@ -12675,6 +12706,14 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
is_finished = true; is_finished = true;
} }
{
Slic3r::LifecycleEventContext ctx;
ctx.name = lifecycle_job_name;
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
}
//BBS: set the current plater's slice result to valid //BBS: set the current plater's slice result to valid
if (!this->background_process.empty()) if (!this->background_process.empty())
this->background_process.get_current_plate()->update_slice_result_valid_state(evt.success()); this->background_process.get_current_plate()->update_slice_result_valid_state(evt.success());
@@ -15217,21 +15256,33 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_
//get_partplate_list().reinit(); //get_partplate_list().reinit();
//get_partplate_list().update_slice_context_to_current_plate(p->background_process); //get_partplate_list().update_slice_context_to_current_plate(p->background_process);
//p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); //p->preview->update_gcode_result(p->partplate_list.get_current_slice_result());
reset(transfer_preset_changes); if (!silent) {
reset_project_dirty_after_save(); Slic3r::LifecycleEventContext ctx;
reset_project_dirty_initial_presets(); ctx.code = Slic3r::LifecycleEvtCode::Ok;
wxGetApp().update_saved_preset_from_current_preset(); Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::NewProject, ctx);
update_project_dirty_from_presets(); }
{
// Same rationale as in Plater::priv::reset(): the whole reset + preset-reload +
// baseline-reset sequence below settles into its final dirty state only once it
// completes, so hold notifications until then to avoid firing on transient flips.
ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(p->dirty_state);
//reset project reset(transfer_preset_changes);
p->project.reset(); reset_project_dirty_after_save();
//set project name reset_project_dirty_initial_presets();
if (project_name.empty()) wxGetApp().update_saved_preset_from_current_preset();
p->set_project_name(_L("Untitled")); update_project_dirty_from_presets();
else
p->set_project_name(project_name);
Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator); //reset project
p->project.reset();
//set project name
if (project_name.empty())
p->set_project_name(_L("Untitled"));
else
p->set_project_name(project_name);
Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator);
}
Model m; Model m;
model().load_from(m); // new id avoid same path name model().load_from(m); // new id avoid same path name
@@ -15349,6 +15400,13 @@ void Plater::load_project(wxString const& filename2,
p->set_project_name(_L("Untitled")); p->set_project_name(_L("Untitled"));
} }
{
Slic3r::LifecycleEventContext ctx;
ctx.name = into_u8(load_restore ? originfile : filename);
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectOpened, ctx);
}
} else { } else {
if (using_exported_file()) { if (using_exported_file()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename;
@@ -15420,11 +15478,23 @@ int Plater::save_project(bool saveAs)
if (full_pathnames) { if (full_pathnames) {
save_strategy = save_strategy | SaveStrategy::FullPathSources; save_strategy = save_strategy | SaveStrategy::FullPathSources;
} }
{
Slic3r::LifecycleEventContext ctx;
ctx.name = into_u8(filename);
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectBeforeSave, ctx);
}
if (export_3mf(into_path(filename), save_strategy) < 0) { if (export_3mf(into_path(filename), save_strategy) < 0) {
MessageDialog(this, _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs have the project file open."), MessageDialog(this, _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs have the project file open."),
_L("Save project"), wxOK | wxICON_WARNING).ShowModal(); _L("Save project"), wxOK | wxICON_WARNING).ShowModal();
return wxID_CANCEL; return wxID_CANCEL;
} }
{
Slic3r::LifecycleEventContext ctx;
ctx.name = into_u8(filename);
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectAfterSave, ctx);
}
Slic3r::remove_backup(model(), false); Slic3r::remove_backup(model(), false);
@@ -20744,6 +20814,14 @@ void Plater::changed_object(ModelObject &object){
// Check outside bed // Check outside bed
get_current_canvas3D()->requires_check_outside_state(); get_current_canvas3D()->requires_check_outside_state();
if (!is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = object.name;
ctx.id = std::to_string(object.id().id);
ctx.source = "geometry";
fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx);
}
} }
void Plater::changed_object(int obj_idx) void Plater::changed_object(int obj_idx)
@@ -20780,6 +20858,20 @@ void Plater::changed_objects(const std::vector<size_t>& object_idxs)
// update print // update print
this->p->schedule_background_process(); this->p->schedule_background_process();
if (!is_loading_project()) {
for (size_t obj_idx : object_idxs) {
if (obj_idx >= p->model.objects.size() || p->model.objects[obj_idx] == nullptr)
continue;
LifecycleEventContext ctx;
ctx.name = p->model.objects[obj_idx]->name;
ctx.id = std::to_string(p->model.objects[obj_idx]->id().id);
ctx.index = static_cast<int>(obj_idx);
ctx.source = "geometry";
fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx);
}
}
} }
void Plater::schedule_background_process(bool schedule/* = true*/) void Plater::schedule_background_process(bool schedule/* = true*/)
+46 -1
View File
@@ -6,6 +6,7 @@
#include "MainFrame.hpp" #include "MainFrame.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include "Plater.hpp" #include "Plater.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
@@ -17,13 +18,23 @@ namespace GUI {
void ProjectDirtyStateManager::update_from_undo_redo_stack(bool dirty) void ProjectDirtyStateManager::update_from_undo_redo_stack(bool dirty)
{ {
const bool was_dirty = is_dirty();
m_plater_dirty = dirty; m_plater_dirty = dirty;
notify_dirty_change(was_dirty, "undo_redo");
if (const Plater *plater = wxGetApp().plater(); plater && wxGetApp().initialized()) if (const Plater *plater = wxGetApp().plater(); plater && wxGetApp().initialized())
wxGetApp().mainframe->update_title(); wxGetApp().mainframe->update_title();
} }
void ProjectDirtyStateManager::set_plater_dirty(bool is_dirty)
{
const bool was_dirty = this->is_dirty();
m_plater_dirty = is_dirty;
notify_dirty_change(was_dirty, "plater");
}
void ProjectDirtyStateManager::update_from_presets() void ProjectDirtyStateManager::update_from_presets()
{ {
const bool was_dirty = is_dirty();
m_presets_dirty = false; m_presets_dirty = false;
// check switching of the presets only for exist/loaded project, but not for new // check switching of the presets only for exist/loaded project, but not for new
GUI_App &app = wxGetApp(); GUI_App &app = wxGetApp();
@@ -45,18 +56,53 @@ void ProjectDirtyStateManager::update_from_presets()
} }
m_presets_dirty |= app.has_unsaved_preset_changes(); m_presets_dirty |= app.has_unsaved_preset_changes();
m_project_config_dirty = m_initial_project_config != app.preset_bundle->project_config; m_project_config_dirty = m_initial_project_config != app.preset_bundle->project_config;
notify_dirty_change(was_dirty, "presets");
app.mainframe->update_title(); app.mainframe->update_title();
} }
void ProjectDirtyStateManager::reset_after_save() void ProjectDirtyStateManager::reset_after_save()
{ {
const bool was_dirty = is_dirty();
this->reset_initial_presets(); this->reset_initial_presets();
m_plater_dirty = false; m_plater_dirty = false;
m_presets_dirty = false; m_presets_dirty = false;
m_project_config_dirty = false; m_project_config_dirty = false;
notify_dirty_change(was_dirty, "save");
wxGetApp().mainframe->update_title(); wxGetApp().mainframe->update_title();
} }
void ProjectDirtyStateManager::notify_dirty_change(bool was_dirty, const char *source)
{
if (m_suppress_depth > 0)
return;
const bool dirty = is_dirty();
if (was_dirty == dirty)
return;
LifecycleEventContext ctx;
ctx.code = LifecycleEvtCode::Ok;
ctx.dirty = dirty;
ctx.source = source;
fire_lifecycle_event(LifecycleEvent::ProjectDirtyChanged, ctx);
}
void ProjectDirtyStateManager::begin_suppress_notifications()
{
if (m_suppress_depth == 0)
m_suppress_entry_dirty = is_dirty();
++m_suppress_depth;
}
void ProjectDirtyStateManager::end_suppress_notifications()
{
assert(m_suppress_depth > 0);
if (m_suppress_depth > 0)
--m_suppress_depth;
if (m_suppress_depth == 0)
notify_dirty_change(m_suppress_entry_dirty, "batch");
}
void ProjectDirtyStateManager::reset_initial_presets() void ProjectDirtyStateManager::reset_initial_presets()
{ {
m_initial_presets.fill(std::string{}); m_initial_presets.fill(std::string{});
@@ -168,4 +214,3 @@ void ProjectDirtyStateManager::render_debug_window() const
} // namespace GUI } // namespace GUI
} // namespace Slic3r } // namespace Slic3r
+22 -1
View File
@@ -14,21 +14,42 @@ public:
void reset_after_save(); void reset_after_save();
void reset_initial_presets(); void reset_initial_presets();
void set_plater_dirty(bool is_dirty) { m_plater_dirty = is_dirty; } void set_plater_dirty(bool is_dirty);
bool is_dirty() const { return m_plater_dirty || m_project_config_dirty || m_presets_dirty; } bool is_dirty() const { return m_plater_dirty || m_project_config_dirty || m_presets_dirty; }
bool is_presets_dirty() const { return m_presets_dirty; } bool is_presets_dirty() const { return m_presets_dirty; }
// RAII guard coalescing dirty-state updates: while any guard is alive, ProjectDirtyChanged
// notifications are held back; when the outermost guard is destroyed, at most one
// notification fires, reflecting only the net change across the whole guarded scope.
class NotificationSuppressor
{
public:
explicit NotificationSuppressor(ProjectDirtyStateManager &owner) : m_owner(owner) { m_owner.begin_suppress_notifications(); }
~NotificationSuppressor() { m_owner.end_suppress_notifications(); }
NotificationSuppressor(const NotificationSuppressor &) = delete;
NotificationSuppressor &operator=(const NotificationSuppressor &) = delete;
private:
ProjectDirtyStateManager &m_owner;
};
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW #if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
void render_debug_window() const; void render_debug_window() const;
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW #endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
private: private:
void notify_dirty_change(bool was_dirty, const char *source);
void begin_suppress_notifications();
void end_suppress_notifications();
// Does the Undo / Redo stack indicate the project is dirty? // Does the Undo / Redo stack indicate the project is dirty?
bool m_plater_dirty { false }; bool m_plater_dirty { false };
// Do the presets indicate the project is dirty? // Do the presets indicate the project is dirty?
bool m_presets_dirty { false }; bool m_presets_dirty { false };
// Is the project config dirty? // Is the project config dirty?
bool m_project_config_dirty { false }; bool m_project_config_dirty { false };
// NotificationSuppressor nesting depth and the dirty state observed when the outermost guard began.
int m_suppress_depth { 0 };
bool m_suppress_entry_dirty { false };
// Keeps track of preset names selected at the time of last project save. // Keeps track of preset names selected at the time of last project save.
std::array<std::string, Preset::TYPE_COUNT> m_initial_presets; std::array<std::string, Preset::TYPE_COUNT> m_initial_presets;
DynamicPrintConfig m_initial_project_config; DynamicPrintConfig m_initial_project_config;
+15
View File
@@ -38,6 +38,7 @@
#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "slic3r/Utils/PresetUpdater.hpp" #include "slic3r/Utils/PresetUpdater.hpp"
#include "slic3r/plugin/PluginConfig.hpp" #include "slic3r/plugin/PluginConfig.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "Plater.hpp" #include "Plater.hpp"
#include "MainFrame.hpp" #include "MainFrame.hpp"
#include "format.hpp" #include "format.hpp"
@@ -6918,6 +6919,20 @@ bool Tab::select_preset(
} }
load_current_preset(); load_current_preset();
{
Slic3r::LifecycleEventContext ctx;
ctx.name = preset_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
switch (m_type) {
case Preset::TYPE_PRINT: ctx.msg = "print"; break;
case Preset::TYPE_SLA_PRINT: ctx.msg = "sla_print"; break;
case Preset::TYPE_FILAMENT: ctx.msg = "filament"; break;
case Preset::TYPE_SLA_MATERIAL: ctx.msg = "sla_material"; break;
case Preset::TYPE_PRINTER: ctx.msg = "printer"; break;
default: break;
}
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::PresetSelected, ctx);
}
if (delete_third_printer) { if (delete_third_printer) {
wxGetApp().CallAfter([filament_presets, process_presets]() { wxGetApp().CallAfter([filament_presets, process_presets]() {
+18
View File
@@ -1,6 +1,7 @@
#include "TaskManager.hpp" #include "TaskManager.hpp"
#include "libslic3r/Thread.hpp" #include "libslic3r/Thread.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
#include "MainFrame.hpp" #include "MainFrame.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
@@ -210,6 +211,13 @@ int TaskManager::schedule(TaskStateInfo* task)
assert(task->state() == TaskState::TS_PENDING); assert(task->state() == TaskState::TS_PENDING);
task->set_state(TaskState::TS_SENDING); task->set_state(TaskState::TS_SENDING);
LifecycleEventContext start_ctx;
start_ctx.name = task->params().project_name;
start_ctx.device_id = task->params().dev_id;
start_ctx.job_id = std::to_string(task->task_info_id);
start_ctx.source = "task_manager";
fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx);
BOOST_LOG_TRIVIAL(trace) << "task_manager: schedule a task to dev_id = " << task->params().dev_id; BOOST_LOG_TRIVIAL(trace) << "task_manager: schedule a task to dev_id = " << task->params().dev_id;
boost::thread* new_sending_thread = new boost::thread(); boost::thread* new_sending_thread = new boost::thread();
*new_sending_thread = Slic3r::create_thread( *new_sending_thread = Slic3r::create_thread(
@@ -237,6 +245,16 @@ int TaskManager::schedule(TaskStateInfo* task)
task->set_state(TaskState::TS_SEND_CANCELED); task->set_state(TaskState::TS_SEND_CANCELED);
} }
} }
LifecycleEventContext finish_ctx;
finish_ctx.name = task->params().project_name;
finish_ctx.device_id = task->params().dev_id;
finish_ctx.job_id = std::to_string(task->task_info_id);
finish_ctx.source = "task_manager";
finish_ctx.code = result == 0 ? LifecycleEvtCode::Ok :
(task->is_canceled() ? LifecycleEvtCode::Warn : LifecycleEvtCode::Error);
finish_ctx.msg = result == 0 ? "" : (task->is_canceled() ? "cancelled" : "failed");
fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx);
/* remove from sending task list */ /* remove from sending task list */
m_scedule_mutex.lock(); m_scedule_mutex.lock();
+18
View File
@@ -23,6 +23,7 @@
#include "CrealityPrint.hpp" #include "CrealityPrint.hpp"
#include "../GUI/PrintHostDialogs.hpp" #include "../GUI/PrintHostDialogs.hpp"
#include "../GUI/MainFrame.hpp" #include "../GUI/MainFrame.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "Obico.hpp" #include "Obico.hpp"
#include "Flashforge.hpp" #include "Flashforge.hpp"
#include "SimplyPrint.hpp" #include "SimplyPrint.hpp"
@@ -358,12 +359,29 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job)
{ {
emit_progress(0); // Indicate the upload is starting emit_progress(0); // Indicate the upload is starting
// Captured before upload_data is moved into upload() below.
const std::string upload_filename = the_job.upload_data.source_path.filename().string();
{
LifecycleEventContext ctx;
ctx.name = upload_filename;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::UploadStarted, ctx);
}
bool success = the_job.printhost->upload(std::move(the_job.upload_data), bool success = the_job.printhost->upload(std::move(the_job.upload_data),
[this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); }, [this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); },
[this](wxString error) { this->error_fn(std::move(error)); }, [this](wxString error) { this->error_fn(std::move(error)); },
[this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); } [this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); }
); );
{
LifecycleEventContext ctx;
ctx.name = upload_filename;
ctx.code = success ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error;
fire_lifecycle_event(LifecycleEvent::UploadFinished, ctx);
}
if (success) { if (success) {
emit_progress(100); emit_progress(100);
if (the_job.switch_to_device_tab) { if (the_job.switch_to_device_tab) {
+13
View File
@@ -7,6 +7,7 @@
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "libslic3r/Exception.hpp" #include "libslic3r/Exception.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "libslic3r/Print.hpp" #include "libslic3r/Print.hpp"
#include "libslic3r_version.h" #include "libslic3r_version.h"
@@ -46,6 +47,16 @@ void install_capability_resolver()
}); });
} }
// Global libslic3r-side seam (Slic3r::fire_lifecycle_event, in libslic3r/LifecycleEvents.hpp):
// broadcasts to every loaded, enabled capability regardless of type, unlike the SlicingPipeline
// hook below which only targets picker-selected SlicingPipeline capabilities.
void install_lifecycle_event_hook()
{
set_lifecycle_hook_fn([](LifecycleEvent event, const LifecycleEventContext& ctx) {
PluginManager::instance().dispatch_lifecycle_event(event, ctx);
});
}
// Print::process() fires this hook at each pipeline seam on the slicing worker // Print::process() fires this hook at each pipeline seam on the slicing worker
// thread; here we run the picker-selected SlicingPipeline capabilities. Per // thread; here we run the picker-selected SlicingPipeline capabilities. Per
// capability we acquire the GIL, honor cancellation, and convert a plugin // capability we acquire the GIL, honor cancellation, and convert a plugin
@@ -122,12 +133,14 @@ void install()
{ {
install_capability_resolver(); install_capability_resolver();
install_slicing_pipeline_hook(); install_slicing_pipeline_hook();
install_lifecycle_event_hook();
} }
void uninstall() void uninstall()
{ {
ConfigBase::set_resolve_capability_fn(nullptr); ConfigBase::set_resolve_capability_fn(nullptr);
Print::set_slicing_pipeline_hook_fn(nullptr); Print::set_slicing_pipeline_hook_fn(nullptr);
set_lifecycle_hook_fn(nullptr);
} }
} // namespace Slic3r::plugin_hooks } // namespace Slic3r::plugin_hooks
+23
View File
@@ -1,5 +1,6 @@
#include "PluginManager.hpp" #include "PluginManager.hpp"
#include <exception>
#include <libslic3r/Utils.hpp> #include <libslic3r/Utils.hpp>
#include <memory> #include <memory>
#include <pybind11/embed.h> #include <pybind11/embed.h>
@@ -2042,6 +2043,8 @@ bool PluginManager::delete_and_unsubscribe_cloud_plugin(const std::string& plugi
return false; return false;
} }
unload_plugin(plugin_key);
if (!m_cloud_service.request_cloud_unsubscribe(descriptor, error)) { if (!m_cloud_service.request_cloud_unsubscribe(descriptor, error)) {
set_plugin_error(plugin_key, error); set_plugin_error(plugin_key, error);
return false; return false;
@@ -2087,4 +2090,24 @@ ExecutionResult PluginManager::run_script_capability(const std::string& plugin_k
return result; return result;
} }
void PluginManager::dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx) {
for (const auto& cap : get_plugin_capabilities()) {
if (!cap || !cap->is_enabled()) continue;
try {
cap->on_lifecycle_event(evt, ctx);
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name()
<< "' on_lifecycle_event(" << lifecycle_event_to_string(evt) << ") threw: " << ex.what()
<< " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code)
<< ", msg='" << ctx.msg << "']";
} catch (...) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name()
<< "' on_lifecycle_event(" << lifecycle_event_to_string(evt)
<< ") threw a non-standard exception"
<< " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code)
<< ", msg='" << ctx.msg << "']";
}
}
}
} // namespace Slic3r } // namespace Slic3r
+3 -1
View File
@@ -8,6 +8,7 @@
#include <condition_variable> #include <condition_variable>
#include <functional> #include <functional>
#include <libslic3r/Config.hpp> #include <libslic3r/Config.hpp>
#include <libslic3r/LifecycleEvents.hpp>
#include <map> #include <map>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
@@ -20,7 +21,6 @@
#include <pybind11/embed.h> #include <pybind11/embed.h>
#include "CloudPluginService.hpp" #include "CloudPluginService.hpp"
#include "PluginFsUtils.hpp"
#include "PluginDescriptor.hpp" #include "PluginDescriptor.hpp"
#include "PluginLoader.hpp" #include "PluginLoader.hpp"
#include "PluginConfig.hpp" #include "PluginConfig.hpp"
@@ -210,6 +210,8 @@ public:
ExecutionResult run_script_capability(const std::string& plugin_key, const std::string& capability_name, std::string& error); ExecutionResult run_script_capability(const std::string& plugin_key, const std::string& capability_name, std::string& error);
void dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx);
private: private:
PluginManager() = default; PluginManager() = default;
PluginManager(const PluginManager&) = delete; PluginManager(const PluginManager&) = delete;
+5
View File
@@ -132,6 +132,11 @@ public:
{ {
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled); ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
} }
void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) override
{
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_lifecycle_event, event, ctx);
}
}; };
class PyPluginInterfaceTrampoline : public PyPluginCommonTrampoline<PluginCapabilityInterface> class PyPluginInterfaceTrampoline : public PyPluginCommonTrampoline<PluginCapabilityInterface>
+59
View File
@@ -377,6 +377,62 @@ void bind_python_api(pybind11::module_& m)
.value("FatalError", PluginResult::FatalError) .value("FatalError", PluginResult::FatalError)
.export_values(); .export_values();
py::enum_<LifecycleEvent>(m, "LifecycleEvent", "Application lifecycle moment passed to on_lifecycle_event")
.value("NewProject", LifecycleEvent::NewProject)
.value("ProjectOpened", LifecycleEvent::ProjectOpened)
.value("ProjectBeforeSave", LifecycleEvent::ProjectBeforeSave)
.value("ProjectAfterSave", LifecycleEvent::ProjectAfterSave)
.value("ProjectClosed", LifecycleEvent::ProjectClosed)
.value("ProjectDirtyChanged", LifecycleEvent::ProjectDirtyChanged)
.value("SliceStarted", LifecycleEvent::SliceStarted)
.value("SliceGeometryFinished", LifecycleEvent::SliceGeometryFinished)
.value("GCodeExportStarted", LifecycleEvent::GCodeExportStarted)
.value("GCodeExportFinished", LifecycleEvent::GCodeExportFinished)
.value("SlicingJobComplete", LifecycleEvent::SlicingJobComplete)
.value("ObjectAdded", LifecycleEvent::ObjectAdded)
.value("ObjectDeleted", LifecycleEvent::ObjectDeleted)
.value("ObjectTransformed", LifecycleEvent::ObjectTransformed)
.value("ObjectChanged", LifecycleEvent::ObjectChanged)
.value("ObjectRenamed", LifecycleEvent::ObjectRenamed)
.value("PlateCreated", LifecycleEvent::PlateCreated)
.value("PlateDeleted", LifecycleEvent::PlateDeleted)
.value("PlateSelected", LifecycleEvent::PlateSelected)
.value("PlateRenamed", LifecycleEvent::PlateRenamed)
.value("PresetSelected", LifecycleEvent::PresetSelected)
.value("PresetSaved", LifecycleEvent::PresetSaved)
.value("PrintStateChanged", LifecycleEvent::PrintStateChanged)
.value("DeviceOnlineChanged", LifecycleEvent::DeviceOnlineChanged)
.value("DeviceDiscovered", LifecycleEvent::DeviceDiscovered)
.value("DeviceSelected", LifecycleEvent::DeviceSelected)
.value("DeviceConnected", LifecycleEvent::DeviceConnected)
.value("DeviceDisconnected", LifecycleEvent::DeviceDisconnected)
.value("UploadStarted", LifecycleEvent::UploadStarted)
.value("UploadFinished", LifecycleEvent::UploadFinished)
.value("PrintJobStarted", LifecycleEvent::PrintJobStarted)
.value("PrintJobFinished", LifecycleEvent::PrintJobFinished)
.value("SendJobStarted", LifecycleEvent::SendJobStarted)
.value("SendJobFinished", LifecycleEvent::SendJobFinished)
.export_values();
py::enum_<LifecycleEvtCode>(m, "LifecycleEvtCode", "Outcome code accompanying a LifecycleEventContext")
.value("Ok", LifecycleEvtCode::Ok)
.value("Error", LifecycleEvtCode::Error)
.value("Warn", LifecycleEvtCode::Warn)
.export_values();
py::class_<LifecycleEventContext>(m, "LifecycleEventContext", "Payload accompanying a LifecycleEvent")
.def(py::init<>())
.def_readonly("name", &LifecycleEventContext::name)
.def_readonly("code", &LifecycleEventContext::code)
.def_readonly("msg", &LifecycleEventContext::msg)
.def_readonly("id", &LifecycleEventContext::id)
.def_readonly("previous_name", &LifecycleEventContext::previous_name)
.def_readonly("device_id", &LifecycleEventContext::device_id)
.def_readonly("job_id", &LifecycleEventContext::job_id)
.def_readonly("source", &LifecycleEventContext::source)
.def_readonly("index", &LifecycleEventContext::index)
.def_readonly("dirty", &LifecycleEventContext::dirty);
py::class_<PluginContext>(m, "PluginContext", "Context shared with plugin entry points") py::class_<PluginContext>(m, "PluginContext", "Context shared with plugin entry points")
.def(py::init<>()) .def(py::init<>())
.def_readwrite("orca_version", &PluginContext::orca_version); .def_readwrite("orca_version", &PluginContext::orca_version);
@@ -401,6 +457,9 @@ void bind_python_api(pybind11::module_& m)
.def("get_type", &PluginCapabilityInterface::get_type) .def("get_type", &PluginCapabilityInterface::get_type)
.def("on_load", &PluginCapabilityInterface::on_load) .def("on_load", &PluginCapabilityInterface::on_load)
.def("on_unload", &PluginCapabilityInterface::on_unload) .def("on_unload", &PluginCapabilityInterface::on_unload)
.def("on_lifecycle_event", &PluginCapabilityInterface::on_lifecycle_event,
"Override to react to an application lifecycle moment (LifecycleEvent) and its\n"
"LifecycleEventContext payload. Available on every capability type.")
.def("has_config_ui", &PluginCapabilityInterface::has_config_ui, .def("has_config_ui", &PluginCapabilityInterface::has_config_ui,
"Override to return True to replace the host's default JSON editor with your own HTML\n" "Override to return True to replace the host's default JSON editor with your own HTML\n"
"UI, returned by get_config_ui(). Every capability is configurable and appears in the\n" "UI, returned by get_config_ui(). Every capability is configurable and appears in the\n"
@@ -10,6 +10,8 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <pybind11/embed.h> #include <pybind11/embed.h>
#include <libslic3r/LifecycleEvents.hpp>
namespace Slic3r { namespace Slic3r {
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown }; enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
@@ -167,6 +169,8 @@ public:
virtual void on_unload() {} virtual void on_unload() {}
virtual void on_cancelled() {} virtual void on_cancelled() {}
virtual void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) { (void) event; (void) ctx; }
// ── C++-only host state, never exposed to Python. Set by the loader at materialization. ── // ── C++-only host state, never exposed to Python. Set by the loader at materialization. ──
// //
// The capability owns its own identity and enable flag: they are read once under the GIL, live // The capability owns its own identity and enable flag: they are read once under the GIL, live