mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-10-04 15:06:33 +08:00
Merge remote-tracking branch 'origin/main' into zyyx_profile_addition
This commit is contained in:
commit
5fba3cd161
13
.github/ISSUE_TEMPLATE/SlicingCrash.yaml
vendored
13
.github/ISSUE_TEMPLATE/SlicingCrash.yaml
vendored
@ -4,12 +4,11 @@ labels: ["Type: Bug", "Status: Triage", "Slicing Error :collision:"]
|
|||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
### 💥 Slicing Crash Analysis Tool 💥
|
### ✨Try our improved Cura 5.7✨
|
||||||
We are taking steps to analyze an increase in reported crashes more systematically. We'll need some help with that. 😇
|
Before filling out the report below, we want you to try the latest Cura 5.7.
|
||||||
Before filling out the report below, we want you to try a special Cura 5.7 Alpha.
|
This version of Cura has become significantly more reliable and has an updated slicing engine that will automatically send a report to the Cura Team for analysis.
|
||||||
This version of Cura has an updated slicing engine that will automatically send a report to the Cura Team for analysis.
|
#### [You can find the downloads here](https://github.com/Ultimaker/Cura/releases/latest) ####
|
||||||
#### [You can find the downloads here](https://github.com/Ultimaker/Cura/discussions/18080) ####
|
|
||||||
If you still encounter a crash you are still welcome to report the issue so we can use your model as a test case, you can find instructions on how to do that below.
|
If you still encounter a crash you are still welcome to report the issue so we can use your model as a test case, you can find instructions on how to do that below.
|
||||||
|
|
||||||
### Project File
|
### Project File
|
||||||
@ -36,7 +35,7 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
We work hard on improving our slicing crashes. Our most recent release is 5.6.0.
|
We work hard on improving our slicing crashes. Our most recent release is 5.7.1.
|
||||||
If you are not on the latest version of Cura, [you can download it here](https://github.com/Ultimaker/Cura/releases/latest)
|
If you are not on the latest version of Cura, [you can download it here](https://github.com/Ultimaker/Cura/releases/latest)
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
|
40
.github/workflows/conan-package-resources.yml
vendored
Normal file
40
.github/workflows/conan-package-resources.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
name: conan-package-resources
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/conan-package-resources.yml'
|
||||||
|
- 'resources/definitions/**'
|
||||||
|
- 'resources/extruders/**'
|
||||||
|
- 'resources/images/**'
|
||||||
|
- 'resources/intent/**'
|
||||||
|
- 'resources/meshes/**'
|
||||||
|
- 'resources/quality/**'
|
||||||
|
- 'resources/variants/**'
|
||||||
|
- 'resources/conanfile.py'
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
- 'CURA-*'
|
||||||
|
- 'PP-*'
|
||||||
|
- 'NP-*'
|
||||||
|
- '[0-9].[0-9]*'
|
||||||
|
- '[0-9].[0-9][0-9]*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
|
||||||
|
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
conan-recipe-version:
|
||||||
|
uses: ultimaker/cura-workflows/.github/workflows/conan-recipe-version.yml@main
|
||||||
|
with:
|
||||||
|
project_name: cura_resources
|
||||||
|
|
||||||
|
conan-package-export:
|
||||||
|
needs: [ conan-recipe-version ]
|
||||||
|
uses: ultimaker/cura-workflows/.github/workflows/conan-recipe-export.yml@main
|
||||||
|
with:
|
||||||
|
recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }}
|
||||||
|
recipe_id_latest: ${{ needs.conan-recipe-version.outputs.recipe_id_latest }}
|
||||||
|
conan_recipe_root: "./resources/"
|
||||||
|
secrets: inherit
|
13
.github/workflows/conan-package.yml
vendored
13
.github/workflows/conan-package.yml
vendored
@ -4,12 +4,20 @@ on:
|
|||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- 'plugins/**'
|
- 'plugins/**'
|
||||||
- 'resources/**'
|
|
||||||
- 'cura/**'
|
- 'cura/**'
|
||||||
|
- 'resources/bundled_packages/**'
|
||||||
|
- 'resources/i18n/**'
|
||||||
|
- 'resources/qml/**'
|
||||||
|
- 'resources/setting_visibility/**'
|
||||||
|
- 'resources/shaders/**'
|
||||||
|
- 'resources/texts/**'
|
||||||
|
- 'resources/themes/**'
|
||||||
|
- 'resources/public_key.pem'
|
||||||
|
- 'resources/README_resources.txt'
|
||||||
- 'icons/**'
|
- 'icons/**'
|
||||||
- 'tests/**'
|
- 'tests/**'
|
||||||
- 'packaging/**'
|
- 'packaging/**'
|
||||||
- '.github/workflows/conan-*.yml'
|
- '.github/workflows/conan-package.yml'
|
||||||
- '.github/workflows/notify.yml'
|
- '.github/workflows/notify.yml'
|
||||||
- '.github/workflows/requirements-runner.txt'
|
- '.github/workflows/requirements-runner.txt'
|
||||||
- 'requirements*.txt'
|
- 'requirements*.txt'
|
||||||
@ -20,6 +28,7 @@ on:
|
|||||||
- 'main'
|
- 'main'
|
||||||
- 'CURA-*'
|
- 'CURA-*'
|
||||||
- 'PP-*'
|
- 'PP-*'
|
||||||
|
- 'NP-*'
|
||||||
- '[0-9].[0-9]*'
|
- '[0-9].[0-9]*'
|
||||||
- '[0-9].[0-9][0-9]*'
|
- '[0-9].[0-9][0-9]*'
|
||||||
|
|
||||||
|
43
.github/workflows/installers.yml
vendored
43
.github/workflows/installers.yml
vendored
@ -30,6 +30,29 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
cura_conan_version:
|
||||||
|
default: 'cura/latest@ultimaker/testing'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
conan_args:
|
||||||
|
default: ''
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
enterprise:
|
||||||
|
default: false
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
staging:
|
||||||
|
default: false
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
nightly:
|
||||||
|
default: false
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
schedule:
|
schedule:
|
||||||
# Daily at 4:15 CET (main-branch) and 5:15 CET (release-branch)
|
# Daily at 4:15 CET (main-branch) and 5:15 CET (release-branch)
|
||||||
- cron: '15 3 * * *'
|
- cron: '15 3 * * *'
|
||||||
@ -70,7 +93,7 @@ jobs:
|
|||||||
enterprise: ${{ github.event.inputs.enterprise == 'true' }}
|
enterprise: ${{ github.event.inputs.enterprise == 'true' }}
|
||||||
staging: ${{ github.event.inputs.staging == 'true' }}
|
staging: ${{ github.event.inputs.staging == 'true' }}
|
||||||
architecture: X64
|
architecture: X64
|
||||||
operating_system: ubuntu-22.04
|
operating_system: self-hosted-Ubuntu22-X64
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
macos-installer:
|
macos-installer:
|
||||||
@ -109,7 +132,7 @@ jobs:
|
|||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Download the run info
|
- name: Download the run info
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux-run-info
|
name: linux-run-info
|
||||||
|
|
||||||
@ -151,13 +174,13 @@ jobs:
|
|||||||
f.writelines(f"NIGHTLY_TIME={nightly_creation_time}\n")
|
f.writelines(f"NIGHTLY_TIME={nightly_creation_time}\n")
|
||||||
|
|
||||||
- name: Download linux installer jobs artifacts
|
- name: Download linux installer jobs artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.filename.outputs.LINUX }}-AppImage
|
name: ${{ steps.filename.outputs.LINUX }}-AppImage
|
||||||
path: installers
|
path: installers
|
||||||
|
|
||||||
- name: Download linux installer jobs asc artifacts
|
- name: Download linux installer jobs asc artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.filename.outputs.LINUX }}-asc
|
name: ${{ steps.filename.outputs.LINUX }}-asc
|
||||||
path: installers
|
path: installers
|
||||||
@ -175,13 +198,13 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Download win msi installer jobs artifacts
|
- name: Download win msi installer jobs artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.filename.outputs.WIN_MSI }}-msi
|
name: ${{ steps.filename.outputs.WIN_MSI }}-msi
|
||||||
path: installers
|
path: installers
|
||||||
|
|
||||||
- name: Download win exe installer jobs artifacts
|
- name: Download win exe installer jobs artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.filename.outputs.WIN_EXE }}-exe
|
name: ${{ steps.filename.outputs.WIN_EXE }}-exe
|
||||||
path: installers
|
path: installers
|
||||||
@ -199,13 +222,13 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Download MacOS (X64) dmg installer jobs artifacts
|
- name: Download MacOS (X64) dmg installer jobs artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.filename.outputs.MAC_X64_DMG }}-dmg
|
name: ${{ steps.filename.outputs.MAC_X64_DMG }}-dmg
|
||||||
path: installers
|
path: installers
|
||||||
|
|
||||||
- name: Download MacOS (X64) pkg installer jobs artifacts
|
- name: Download MacOS (X64) pkg installer jobs artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.filename.outputs.MAC_X64_PKG }}-pkg
|
name: ${{ steps.filename.outputs.MAC_X64_PKG }}-pkg
|
||||||
path: installers
|
path: installers
|
||||||
@ -223,13 +246,13 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Download MacOS (ARM-64) dmg installer jobs artifacts
|
- name: Download MacOS (ARM-64) dmg installer jobs artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.filename.outputs.MAC_ARM_DMG }}-dmg
|
name: ${{ steps.filename.outputs.MAC_ARM_DMG }}-dmg
|
||||||
path: installers
|
path: installers
|
||||||
|
|
||||||
- name: Download MacOS (ARM-64) pkg installer jobs artifacts
|
- name: Download MacOS (ARM-64) pkg installer jobs artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.filename.outputs.MAC_ARM_PKG }}-pkg
|
name: ${{ steps.filename.outputs.MAC_ARM_PKG }}-pkg
|
||||||
path: installers
|
path: installers
|
||||||
|
5
.github/workflows/linux.yml
vendored
5
.github/workflows/linux.yml
vendored
@ -34,10 +34,11 @@ on:
|
|||||||
operating_system:
|
operating_system:
|
||||||
description: 'OS'
|
description: 'OS'
|
||||||
required: true
|
required: true
|
||||||
default: 'ubuntu-22.04'
|
default: 'self-hosted-Ubuntu22-X64'
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- ubuntu-22.04
|
- ubuntu-22.04
|
||||||
|
- self-hosted-Ubuntu22-X64
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
linux-installer:
|
linux-installer:
|
||||||
@ -49,4 +50,4 @@ jobs:
|
|||||||
staging: ${{ inputs.staging }}
|
staging: ${{ inputs.staging }}
|
||||||
architecture: ${{ inputs.architecture }}
|
architecture: ${{ inputs.architecture }}
|
||||||
operating_system: ${{ inputs.operating_system }}
|
operating_system: ${{ inputs.operating_system }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
3
.github/workflows/macos.yml
vendored
3
.github/workflows/macos.yml
vendored
@ -40,7 +40,6 @@ on:
|
|||||||
options:
|
options:
|
||||||
- self-hosted-X64
|
- self-hosted-X64
|
||||||
- self-hosted-ARM64
|
- self-hosted-ARM64
|
||||||
- macos-11
|
|
||||||
- macos-12
|
- macos-12
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -53,4 +52,4 @@ jobs:
|
|||||||
staging: ${{ inputs.staging }}
|
staging: ${{ inputs.staging }}
|
||||||
architecture: ${{ inputs.architecture }}
|
architecture: ${{ inputs.architecture }}
|
||||||
operating_system: ${{ inputs.operating_system }}
|
operating_system: ${{ inputs.operating_system }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
10
.github/workflows/printer-linter-pr-diagnose.yml
vendored
10
.github/workflows/printer-linter-pr-diagnose.yml
vendored
@ -5,6 +5,9 @@ on:
|
|||||||
path:
|
path:
|
||||||
- "resources/**"
|
- "resources/**"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
printer-linter-diagnose:
|
printer-linter-diagnose:
|
||||||
name: Printer linter PR diagnose
|
name: Printer linter PR diagnose
|
||||||
@ -18,6 +21,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: technote-space/get-diff-action@v6
|
- uses: technote-space/get-diff-action@v6
|
||||||
with:
|
with:
|
||||||
|
DIFF_FILTER: AMRCD
|
||||||
PATTERNS: |
|
PATTERNS: |
|
||||||
resources/+(extruders|definitions)/*.def.json
|
resources/+(extruders|definitions)/*.def.json
|
||||||
resources/+(intent|quality|variants)/**/*.inst.cfg
|
resources/+(intent|quality|variants)/**/*.inst.cfg
|
||||||
@ -41,11 +45,15 @@ jobs:
|
|||||||
if: env.GIT_DIFF && !env.MATCHED_FILES
|
if: env.GIT_DIFF && !env.MATCHED_FILES
|
||||||
run: python printer-linter/src/terminal.py --diagnose --report printer-linter-result/fixes.yml ${{ env.GIT_DIFF_FILTERED }}
|
run: python printer-linter/src/terminal.py --diagnose --report printer-linter-result/fixes.yml ${{ env.GIT_DIFF_FILTERED }}
|
||||||
|
|
||||||
|
- name: Check Deleted Files(s)
|
||||||
|
if: env.GIT_DIFF
|
||||||
|
run: python printer-linter/src/terminal.py --deleted --report printer-linter-result/comment.md ${{ env.GIT_DIFF_FILTERED }}
|
||||||
|
|
||||||
- name: Save PR metadata
|
- name: Save PR metadata
|
||||||
run: |
|
run: |
|
||||||
echo ${{ github.event.number }} > printer-linter-result/pr-id.txt
|
echo ${{ github.event.number }} > printer-linter-result/pr-id.txt
|
||||||
echo ${{ github.event.pull_request.head.repo.full_name }} > printer-linter-result/pr-head-repo.txt
|
echo ${{ github.event.pull_request.head.repo.full_name }} > printer-linter-result/pr-head-repo.txt
|
||||||
echo ${{ github.event.pull_request.head.ref }} > printer-linter-result/pr-head-ref.txt
|
echo ${{ github.event.pull_request.head.sha }} > printer-linter-result/pr-head-sha.txt
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
|
120
.github/workflows/printer-linter-pr-post.yml
vendored
120
.github/workflows/printer-linter-pr-post.yml
vendored
@ -6,76 +6,106 @@ on:
|
|||||||
types: [completed]
|
types: [completed]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
clang-tidy-results:
|
printer-linter-result:
|
||||||
# Trigger the job only if the previous (insecure) workflow completed successfully
|
# Trigger the job only if the previous (insecure) workflow completed successfully
|
||||||
if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }}
|
if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Download analysis results
|
- name: Download analysis results
|
||||||
uses: actions/github-script@v3.1.0
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
let artifacts = await github.actions.listWorkflowRunArtifacts({
|
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
run_id: ${{github.event.workflow_run.id }},
|
run_id: ${{github.event.workflow_run.id }},
|
||||||
});
|
});
|
||||||
let matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
const matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||||
return artifact.name == "printer-linter-result"
|
return artifact.name == "printer-linter-result"
|
||||||
})[0];
|
})[0];
|
||||||
let download = await github.actions.downloadArtifact({
|
const download = await github.rest.actions.downloadArtifact({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
artifact_id: matchArtifact.id,
|
artifact_id: matchArtifact.id,
|
||||||
archive_format: "zip",
|
archive_format: "zip",
|
||||||
});
|
});
|
||||||
let fs = require("fs");
|
const fs = require("fs");
|
||||||
fs.writeFileSync("${{github.workspace}}/printer-linter-result.zip", Buffer.from(download.data));
|
fs.writeFileSync("${{ github.workspace }}/printer-linter-result.zip", Buffer.from(download.data));
|
||||||
|
|
||||||
- name: Set environment variables
|
|
||||||
run: |
|
|
||||||
mkdir printer-linter-result
|
|
||||||
unzip printer-linter-result.zip -d printer-linter-result
|
|
||||||
echo "pr_id=$(cat printer-linter-result/pr-id.txt)" >> $GITHUB_ENV
|
|
||||||
echo "pr_head_repo=$(cat printer-linter-result/pr-head-repo.txt)" >> $GITHUB_ENV
|
|
||||||
echo "pr_head_ref=$(cat printer-linter-result/pr-head-ref.txt)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
repository: ${{ env.pr_head_repo }}
|
|
||||||
ref: ${{ env.pr_head_ref }}
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Redownload analysis results
|
|
||||||
uses: actions/github-script@v3.1.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
let artifacts = await github.actions.listWorkflowRunArtifacts({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
run_id: ${{github.event.workflow_run.id }},
|
|
||||||
});
|
|
||||||
let matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
|
||||||
return artifact.name == "printer-linter-result"
|
|
||||||
})[0];
|
|
||||||
let download = await github.actions.downloadArtifact({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
artifact_id: matchArtifact.id,
|
|
||||||
archive_format: "zip",
|
|
||||||
});
|
|
||||||
let fs = require("fs");
|
|
||||||
fs.writeFileSync("${{github.workspace}}/printer-linter-result.zip", Buffer.from(download.data));
|
|
||||||
|
|
||||||
- name: Extract analysis results
|
- name: Extract analysis results
|
||||||
run: |
|
run: |
|
||||||
mkdir printer-linter-result
|
mkdir printer-linter-result
|
||||||
unzip printer-linter-result.zip -d printer-linter-result
|
unzip -j printer-linter-result.zip -d printer-linter-result
|
||||||
|
|
||||||
|
- name: Set PR details environment variables
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const assert = require("node:assert").strict;
|
||||||
|
const fs = require("fs");
|
||||||
|
function exportVar(varName, fileName, regEx) {
|
||||||
|
const val = fs.readFileSync("${{ github.workspace }}/printer-linter-result/" + fileName, {
|
||||||
|
encoding: "ascii"
|
||||||
|
}).trimEnd();
|
||||||
|
assert.ok(regEx.test(val), "Invalid value format for " + varName);
|
||||||
|
core.exportVariable(varName, val);
|
||||||
|
}
|
||||||
|
exportVar("PR_ID", "pr-id.txt", /^[0-9]+$/);
|
||||||
|
exportVar("PR_HEAD_REPO", "pr-head-repo.txt", /^[-./0-9A-Z_a-z]+$/);
|
||||||
|
exportVar("PR_HEAD_SHA", "pr-head-sha.txt", /^[0-9A-Fa-f]+$/);
|
||||||
|
fs.access("${{ github.workspace }}/printer-linter-result/comment.md", fs.constants.F_OK, (err) => {
|
||||||
|
if (err) {
|
||||||
|
core.exportVariable("commentFileExists", "false");
|
||||||
|
} else {
|
||||||
|
core.exportVariable("commentFileExists", "true");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: ${{ env.PR_HEAD_REPO }}
|
||||||
|
ref: ${{ env.PR_HEAD_SHA }}
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Redownload analysis results
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: ${{github.event.workflow_run.id }},
|
||||||
|
});
|
||||||
|
const matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||||
|
return artifact.name == "printer-linter-result"
|
||||||
|
})[0];
|
||||||
|
const download = await github.rest.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: matchArtifact.id,
|
||||||
|
archive_format: "zip",
|
||||||
|
});
|
||||||
|
const fs = require("fs");
|
||||||
|
fs.writeFileSync("${{ github.workspace }}/printer-linter-result.zip", Buffer.from(download.data));
|
||||||
|
|
||||||
|
- name: Extract analysis results
|
||||||
|
run: |
|
||||||
|
mkdir printer-linter-result
|
||||||
|
unzip -j printer-linter-result.zip -d printer-linter-result
|
||||||
|
|
||||||
|
- name: Run PR Comments
|
||||||
|
if: env.commentFileExists == 'true'
|
||||||
|
uses: peter-evans/create-or-update-comment@v4
|
||||||
|
with:
|
||||||
|
issue-number: ${{ env.PR_ID }}
|
||||||
|
body-path: 'printer-linter-result/comment.md'
|
||||||
|
|
||||||
- name: Run clang-tidy-pr-comments action
|
- name: Run clang-tidy-pr-comments action
|
||||||
uses: platisd/clang-tidy-pr-comments@bc0bb7da034a8317d54e7fe1e819159002f4cc40
|
uses: platisd/clang-tidy-pr-comments@v1
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
clang_tidy_fixes: printer-linter-result/fixes.yml
|
clang_tidy_fixes: printer-linter-result/fixes.yml
|
||||||
pull_request_id: ${{ env.pr_id }}
|
pull_request_id: ${{ env.PR_ID }}
|
||||||
request_changes: true
|
request_changes: true
|
||||||
|
32
.github/workflows/release-process_feature-freeze.yml
vendored
Normal file
32
.github/workflows/release-process_feature-freeze.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
name: Feature Freeze
|
||||||
|
run-name: Feature freeze Cura ${{ inputs.cura_version }} by @${{ github.actor }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
cura_version:
|
||||||
|
description: 'Cura version major and minor, e.g. 5.7'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
parse-version:
|
||||||
|
name: Parse input version string
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
package_version: ${{ steps.version_parser.outputs.major }}.${{ steps.version_parser.outputs.minor }}.0-alpha.1
|
||||||
|
steps:
|
||||||
|
- name: Parse version string
|
||||||
|
id: version_parser
|
||||||
|
uses: booxmedialtd/ws-action-parse-semver@v1.4.7
|
||||||
|
with:
|
||||||
|
input_string: ${{ inputs.cura_version }}.0
|
||||||
|
|
||||||
|
feature-freeze:
|
||||||
|
name: Process feature freeze
|
||||||
|
uses: Ultimaker/Cura-workflows/.github/workflows/cura-set-packages-versions.yml@main
|
||||||
|
needs: [parse-version]
|
||||||
|
with:
|
||||||
|
cura_version: ${{ needs.parse-version.outputs.package_version }}
|
||||||
|
create_feature_branch: true
|
||||||
|
secrets: inherit
|
187
.github/workflows/release-process_release-candidate.yml
vendored
Normal file
187
.github/workflows/release-process_release-candidate.yml
vendored
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
name: Prepare Release Candidate
|
||||||
|
run-name: Release Candidate for Cura ${{ inputs.cura_version }} by @${{ github.actor }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
cura_version:
|
||||||
|
description: 'Cura version number, e.g. 5.7.0, 5.7.2 or 5.8.0-beta.2'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
parse-version:
|
||||||
|
name: Parse input version string
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version_major: ${{ steps.version_parser.outputs.major }}
|
||||||
|
version_minor: ${{ steps.version_parser.outputs.minor }}
|
||||||
|
version_patch: ${{ steps.version_parser.outputs.patch }}
|
||||||
|
branch_name: ${{ steps.version_parser.outputs.major }}.${{ steps.version_parser.outputs.minor }}
|
||||||
|
steps:
|
||||||
|
- name: Parse version string
|
||||||
|
id: version_parser
|
||||||
|
uses: booxmedialtd/ws-action-parse-semver@v1.4.7
|
||||||
|
with:
|
||||||
|
input_string: ${{ inputs.cura_version }}
|
||||||
|
|
||||||
|
freeze-packages-versions:
|
||||||
|
name: Freeze packges versions
|
||||||
|
uses: Ultimaker/Cura-workflows/.github/workflows/cura-set-packages-versions.yml@main
|
||||||
|
needs: [parse-version]
|
||||||
|
with:
|
||||||
|
cura_version: ${{ inputs.cura_version }}
|
||||||
|
create_feature_branch: false
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
find-rc-tag:
|
||||||
|
name: Find RC tag name
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [freeze-packages-versions]
|
||||||
|
outputs:
|
||||||
|
tag_name: ${{ steps.find-available-tag-name.outputs.tag_name }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-tags: true
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Find available tag name
|
||||||
|
id: find-available-tag-name
|
||||||
|
run: |
|
||||||
|
VERSION=${{ inputs.cura_version }}
|
||||||
|
|
||||||
|
RC_INDEX=0
|
||||||
|
while
|
||||||
|
RC_INDEX=$((RC_INDEX+1))
|
||||||
|
TAG_NAME="$VERSION-RC$RC_INDEX"
|
||||||
|
[[ $(git tag -l "$TAG_NAME") ]]
|
||||||
|
do true; done
|
||||||
|
|
||||||
|
echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
create-tags:
|
||||||
|
name: Create tags
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [parse-version, find-rc-tag]
|
||||||
|
outputs:
|
||||||
|
main_commit: ${{ steps.export-main-commit.outputs.main_commit }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
repository: [Cura, Uranium, CuraEngine, cura-binary-data, fdm_materials]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: Ultimaker/${{ matrix.repository }}
|
||||||
|
ref: ${{ needs.parse-version.outputs.branch_name }}
|
||||||
|
token: ${{ secrets.CURA_AUTORELEASE_PAT }}
|
||||||
|
|
||||||
|
- name: Create RC tag
|
||||||
|
run: |
|
||||||
|
git tag ${{ needs.find-rc-tag.outputs.tag_name }}
|
||||||
|
git push origin tag ${{ needs.find-rc-tag.outputs.tag_name }}
|
||||||
|
|
||||||
|
- name: Create or update release tag
|
||||||
|
run: |
|
||||||
|
git tag -f ${{ inputs.cura_version }}
|
||||||
|
git push -f origin tag ${{ inputs.cura_version }}
|
||||||
|
|
||||||
|
- name: Export Cura tagged commit
|
||||||
|
id: export-main-commit
|
||||||
|
if: ${{ matrix.repository == 'Cura' }}
|
||||||
|
run: |
|
||||||
|
echo "main_commit=`git rev-parse HEAD`" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
|
||||||
|
create-dependencies-packages:
|
||||||
|
name: Create conan packages for dependencies
|
||||||
|
uses: ultimaker/cura-workflows/.github/workflows/conan-package-release.yml@main
|
||||||
|
needs: [parse-version, freeze-packages-versions]
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
repository: [Cura, Uranium, CuraEngine, cura-binary-data, fdm_materials]
|
||||||
|
include:
|
||||||
|
- conan_recipe_root: "."
|
||||||
|
- repository: Cura
|
||||||
|
conan_recipe_root: "resources"
|
||||||
|
with:
|
||||||
|
repository: ${{ matrix.repository }}
|
||||||
|
ref_name: ${{ needs.parse-version.outputs.branch_name }}
|
||||||
|
version: ${{ inputs.cura_version }}
|
||||||
|
conan_release: true
|
||||||
|
conan_user_channel: ultimaker/stable
|
||||||
|
conan_internal: false
|
||||||
|
conan_latest: true
|
||||||
|
conan_recipe_root: ${{ matrix.conan_recipe_root }}
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
create-cura-package:
|
||||||
|
name: Create conan package for Cura
|
||||||
|
uses: ultimaker/cura-workflows/.github/workflows/conan-package-release.yml@main
|
||||||
|
needs: [parse-version, create-dependencies-packages]
|
||||||
|
with:
|
||||||
|
repository: Cura
|
||||||
|
ref_name: ${{ needs.parse-version.outputs.branch_name }}
|
||||||
|
version: ${{ inputs.cura_version }}
|
||||||
|
conan_release: true
|
||||||
|
conan_user_channel: ultimaker/stable
|
||||||
|
conan_internal: false
|
||||||
|
conan_latest: true
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
create-installers:
|
||||||
|
name: Create installers
|
||||||
|
uses: ./.github/workflows/installers.yml
|
||||||
|
needs: [parse-version, create-cura-package]
|
||||||
|
with:
|
||||||
|
cura_conan_version: cura/${{ inputs.cura_version }}@/
|
||||||
|
enterprise: false
|
||||||
|
staging: false
|
||||||
|
nightly: false
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
create-release-draft:
|
||||||
|
name: Create the release draft
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [create-installers, parse-version, create-tags]
|
||||||
|
steps:
|
||||||
|
- name: Checkout Cura repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.parse-version.outputs.branch_name }}
|
||||||
|
|
||||||
|
- name: Extract changelog
|
||||||
|
run: python ./scripts/extract_changelog.py --version ${{ needs.parse-version.outputs.version_major }}.${{ needs.parse-version.outputs.version_minor }}.${{ needs.parse-version.outputs.version_patch }} --changelog ./resources/texts/change_log.txt > formatted_changelog.txt
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: notpeelz/action-gh-create-release@v5.0.1
|
||||||
|
with:
|
||||||
|
target: ${{ needs.create-tags.outputs.main_commit }}
|
||||||
|
tag: ${{ inputs.cura_version }}
|
||||||
|
strategy: replace
|
||||||
|
title: UltiMaker Cura ${{ inputs.cura_version }}
|
||||||
|
draft: true
|
||||||
|
body-source: file
|
||||||
|
body: formatted_changelog.txt
|
||||||
|
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4.1.7
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
working-directory: artifacts
|
||||||
|
run: |
|
||||||
|
gh release upload ${{ inputs.cura_version }} UltiMaker-Cura-${{ inputs.cura_version }}-linux-X64.AppImage --clobber
|
||||||
|
gh release upload ${{ inputs.cura_version }} UltiMaker-Cura-${{ inputs.cura_version }}-linux-X64.AppImage.asc --clobber
|
||||||
|
gh release upload ${{ inputs.cura_version }} UltiMaker-Cura-${{ inputs.cura_version }}-macos-ARM64.dmg --clobber
|
||||||
|
gh release upload ${{ inputs.cura_version }} UltiMaker-Cura-${{ inputs.cura_version }}-macos-ARM64.pkg --clobber
|
||||||
|
gh release upload ${{ inputs.cura_version }} UltiMaker-Cura-${{ inputs.cura_version }}-macos-X64.dmg --clobber
|
||||||
|
gh release upload ${{ inputs.cura_version }} UltiMaker-Cura-${{ inputs.cura_version }}-macos-X64.pkg --clobber
|
||||||
|
gh release upload ${{ inputs.cura_version }} UltiMaker-Cura-${{ inputs.cura_version }}-win64-X64.exe --clobber
|
||||||
|
gh release upload ${{ inputs.cura_version }} UltiMaker-Cura-${{ inputs.cura_version }}-win64-X64.msi --clobber
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
43
.github/workflows/security_badge.yml
vendored
43
.github/workflows/security_badge.yml
vendored
@ -1,5 +1,5 @@
|
|||||||
# NOTE: Best to keep all of these remarks in, they might prove useful in the future.
|
# NOTE: Best to keep all of these remarks in, they might prove useful in the future.
|
||||||
# This is basically just the standard one that is sugested on 'new workflow'.
|
# This is basically just the standard one that is suggested on 'new workflow'.
|
||||||
|
|
||||||
name: Scorecard supply-chain security
|
name: Scorecard supply-chain security
|
||||||
on:
|
on:
|
||||||
@ -21,51 +21,42 @@ jobs:
|
|||||||
name: Scorecard analysis
|
name: Scorecard analysis
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
# Needed to upload the results to code-scanning dashboard.
|
# Needed for Code scanning upload
|
||||||
security-events: write
|
security-events: write
|
||||||
# Needed to publish results and get a badge (see publish_results below).
|
# Needed for GitHub OIDC token if publish_results is true
|
||||||
id-token: write
|
id-token: write
|
||||||
# Uncomment the permissions below if installing in a private repository.
|
|
||||||
# contents: read
|
|
||||||
# actions: read
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: "Run analysis"
|
- name: "Run analysis"
|
||||||
uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2
|
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
|
||||||
with:
|
with:
|
||||||
results_file: results.sarif
|
results_file: results.sarif
|
||||||
results_format: sarif
|
results_format: sarif
|
||||||
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
|
# Scorecard team runs a weekly scan of public GitHub repos,
|
||||||
# - you want to enable the Branch-Protection check on a *public* repository, or
|
# see https://github.com/ossf/scorecard#public-data.
|
||||||
# - you are installing Scorecard on a *private* repository
|
# Setting `publish_results: true` helps us scale by leveraging your workflow to
|
||||||
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
|
# extract the results instead of relying on our own infrastructure to run scans.
|
||||||
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
# And it's free for you!
|
||||||
|
|
||||||
# Public repositories:
|
|
||||||
# - Publish results to OpenSSF REST API for easy access by consumers
|
|
||||||
# - Allows the repository to include the Scorecard badge.
|
|
||||||
# - See https://github.com/ossf/scorecard-action#publishing-results.
|
|
||||||
# For private repositories:
|
|
||||||
# - `publish_results` will always be set to `false`, regardless
|
|
||||||
# of the value entered here.
|
|
||||||
publish_results: true
|
publish_results: true
|
||||||
|
|
||||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
# Upload the results as artifacts (optional). Commenting out will disable
|
||||||
# format to the repository Actions tab.
|
# uploads of run results in SARIF format to the repository Actions tab.
|
||||||
|
# https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts
|
||||||
- name: "Upload artifact"
|
- name: "Upload artifact"
|
||||||
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: SARIF file
|
name: SARIF file
|
||||||
path: results.sarif
|
path: results.sarif
|
||||||
retention-days: 5
|
retention-days: 5
|
||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard (optional).
|
||||||
|
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4
|
uses: github/codeql-action/upload-sarif@83a02f7883b12e0e4e1a146174f5e2292a01e601 # v2.16.4
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
2
.github/workflows/unit-test.yml
vendored
2
.github/workflows/unit-test.yml
vendored
@ -55,7 +55,7 @@ jobs:
|
|||||||
needs: [ conan-recipe-version ]
|
needs: [ conan-recipe-version ]
|
||||||
with:
|
with:
|
||||||
recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }}
|
recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }}
|
||||||
conan_extra_args: '-g VirtualPythonEnv -o cura:devtools=True -c tools.build:skip_test=False'
|
conan_extra_args: '-g VirtualPythonEnv -o cura:devtools=True -c tools.build:skip_test=False --options "*:enable_sentry=False"'
|
||||||
unit_test_cmd: 'pytest --junitxml=junit_cura.xml'
|
unit_test_cmd: 'pytest --junitxml=junit_cura.xml'
|
||||||
unit_test_dir: 'tests'
|
unit_test_dir: 'tests'
|
||||||
conan_generator_dir: './venv/bin'
|
conan_generator_dir: './venv/bin'
|
||||||
|
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@ -50,4 +50,4 @@ jobs:
|
|||||||
staging: ${{ inputs.staging }}
|
staging: ${{ inputs.staging }}
|
||||||
architecture: ${{ inputs.architecture }}
|
architecture: ${{ inputs.architecture }}
|
||||||
operating_system: ${{ inputs.operating_system }}
|
operating_system: ${{ inputs.operating_system }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
@ -2,7 +2,12 @@ checks:
|
|||||||
diagnostic-mesh-file-extension: true
|
diagnostic-mesh-file-extension: true
|
||||||
diagnostic-mesh-file-size: true
|
diagnostic-mesh-file-size: true
|
||||||
diagnostic-definition-redundant-override: true
|
diagnostic-definition-redundant-override: true
|
||||||
|
diagnostic-definition-experimental-setting: true
|
||||||
diagnostic-resources-macos-app-directory-name: true
|
diagnostic-resources-macos-app-directory-name: true
|
||||||
|
diagnostic-incorrect-formula: true
|
||||||
|
diagnostic-resource-file-deleted: true
|
||||||
|
diagnostic-material-temperature-defined: false
|
||||||
|
diagnostic-long-profile-names: true
|
||||||
fixes:
|
fixes:
|
||||||
diagnostic-definition-redundant-override: true
|
diagnostic-definition-redundant-override: true
|
||||||
format:
|
format:
|
||||||
|
@ -26,7 +26,9 @@
|
|||||||
*With hundreds of settings & community-managed print profiles,* <br>
|
*With hundreds of settings & community-managed print profiles,* <br>
|
||||||
*Ultimaker Cura is sure to lead your next project to a success.*
|
*Ultimaker Cura is sure to lead your next project to a success.*
|
||||||
|
|
||||||
<br>
|
**Contribute Printer Profiles?** -- Please [look here](https://github.com/Ultimaker/Cura/wiki/Adding-new-machine-profiles-to-Cura) first. <br>
|
||||||
|
**Contribute Translations?** -- Please [look here](https://github.com/Ultimaker/Cura/wiki/Translating-Cura) first.
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
[![Button Building]][Building]
|
[![Button Building]][Building]
|
||||||
|
@ -55,7 +55,8 @@ exe = EXE(
|
|||||||
target_arch={{ target_arch }},
|
target_arch={{ target_arch }},
|
||||||
codesign_identity=os.getenv('CODESIGN_IDENTITY', None),
|
codesign_identity=os.getenv('CODESIGN_IDENTITY', None),
|
||||||
entitlements_file={{ entitlements_file }},
|
entitlements_file={{ entitlements_file }},
|
||||||
icon={{ icon }}
|
icon={{ icon }},
|
||||||
|
contents_directory='.'
|
||||||
)
|
)
|
||||||
|
|
||||||
coll = COLLECT(
|
coll = COLLECT(
|
||||||
@ -70,188 +71,7 @@ coll = COLLECT(
|
|||||||
)
|
)
|
||||||
|
|
||||||
{% if macos == true %}
|
{% if macos == true %}
|
||||||
# PyInstaller seems to copy everything in the resource folder for the MacOS, this causes issues with codesigning and notarizing
|
app = BUNDLE(
|
||||||
# The folder structure should adhere to the one specified in Table 2-5
|
|
||||||
# https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1
|
|
||||||
# The class below is basically ducktyping the BUNDLE class of PyInstaller and using our own `assemble` method for more fine-grain and specific
|
|
||||||
# control. Some code of the method below is copied from:
|
|
||||||
# https://github.com/pyinstaller/pyinstaller/blob/22d1d2a5378228744cc95f14904dae1664df32c4/PyInstaller/building/osx.py#L115
|
|
||||||
#-----------------------------------------------------------------------------
|
|
||||||
# Copyright (c) 2005-2022, PyInstaller Development Team.
|
|
||||||
#
|
|
||||||
# Distributed under the terms of the GNU General Public License (version 2
|
|
||||||
# or later) with exception for distributing the bootloader.
|
|
||||||
#
|
|
||||||
# The full license is in the file COPYING.txt, distributed with this software.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
|
|
||||||
#-----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
import plistlib
|
|
||||||
import shutil
|
|
||||||
import PyInstaller.utils.osx as osxutils
|
|
||||||
from pathlib import Path
|
|
||||||
from PyInstaller.building.osx import BUNDLE
|
|
||||||
from PyInstaller.building.utils import (_check_path_overlap, _rmtree, add_suffix_to_extension, checkCache)
|
|
||||||
from PyInstaller.building.datastruct import logger
|
|
||||||
from PyInstaller.building.icon import normalize_icon_type
|
|
||||||
|
|
||||||
|
|
||||||
class UMBUNDLE(BUNDLE):
|
|
||||||
def assemble(self):
|
|
||||||
from PyInstaller.config import CONF
|
|
||||||
|
|
||||||
if _check_path_overlap(self.name) and os.path.isdir(self.name):
|
|
||||||
_rmtree(self.name)
|
|
||||||
logger.info("Building BUNDLE %s", self.tocbasename)
|
|
||||||
|
|
||||||
# Create a minimal Mac bundle structure.
|
|
||||||
macos_path = Path(self.name, "Contents", "MacOS")
|
|
||||||
resources_path = Path(self.name, "Contents", "Resources")
|
|
||||||
frameworks_path = Path(self.name, "Contents", "Frameworks")
|
|
||||||
os.makedirs(macos_path)
|
|
||||||
os.makedirs(resources_path)
|
|
||||||
os.makedirs(frameworks_path)
|
|
||||||
|
|
||||||
# Makes sure the icon exists and attempts to convert to the proper format if applicable
|
|
||||||
self.icon = normalize_icon_type(self.icon, ("icns",), "icns", CONF["workpath"])
|
|
||||||
|
|
||||||
# Ensure icon path is absolute
|
|
||||||
self.icon = os.path.abspath(self.icon)
|
|
||||||
|
|
||||||
# Copy icns icon to Resources directory.
|
|
||||||
shutil.copy(self.icon, os.path.join(self.name, 'Contents', 'Resources'))
|
|
||||||
|
|
||||||
# Key/values for a minimal Info.plist file
|
|
||||||
info_plist_dict = {
|
|
||||||
"CFBundleDisplayName": self.appname,
|
|
||||||
"CFBundleName": self.appname,
|
|
||||||
|
|
||||||
# Required by 'codesign' utility.
|
|
||||||
# The value for CFBundleIdentifier is used as the default unique name of your program for Code Signing
|
|
||||||
# purposes. It even identifies the APP for access to restricted OS X areas like Keychain.
|
|
||||||
#
|
|
||||||
# The identifier used for signing must be globally unique. The usual form for this identifier is a
|
|
||||||
# hierarchical name in reverse DNS notation, starting with the toplevel domain, followed by the company
|
|
||||||
# name, followed by the department within the company, and ending with the product name. Usually in the
|
|
||||||
# form: com.mycompany.department.appname
|
|
||||||
# CLI option --osx-bundle-identifier sets this value.
|
|
||||||
"CFBundleIdentifier": self.bundle_identifier,
|
|
||||||
"CFBundleExecutable": os.path.basename(self.exename),
|
|
||||||
"CFBundleIconFile": os.path.basename(self.icon),
|
|
||||||
"CFBundleInfoDictionaryVersion": "6.0",
|
|
||||||
"CFBundlePackageType": "APPL",
|
|
||||||
"CFBundleVersionString": self.version,
|
|
||||||
"CFBundleShortVersionString": self.version,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set some default values. But they still can be overwritten by the user.
|
|
||||||
if self.console:
|
|
||||||
# Setting EXE console=True implies LSBackgroundOnly=True.
|
|
||||||
info_plist_dict['LSBackgroundOnly'] = True
|
|
||||||
else:
|
|
||||||
# Let's use high resolution by default.
|
|
||||||
info_plist_dict['NSHighResolutionCapable'] = True
|
|
||||||
|
|
||||||
# Merge info_plist settings from spec file
|
|
||||||
if isinstance(self.info_plist, dict) and self.info_plist:
|
|
||||||
info_plist_dict.update(self.info_plist)
|
|
||||||
|
|
||||||
plist_filename = os.path.join(self.name, "Contents", "Info.plist")
|
|
||||||
with open(plist_filename, "wb") as plist_fh:
|
|
||||||
plistlib.dump(info_plist_dict, plist_fh)
|
|
||||||
|
|
||||||
links = []
|
|
||||||
_QT_BASE_PATH = {'PySide2', 'PySide6', 'PyQt5', 'PyQt6', 'PySide6'}
|
|
||||||
for inm, fnm, typ in self.toc:
|
|
||||||
# Adjust name for extensions, if applicable
|
|
||||||
inm, fnm, typ = add_suffix_to_extension(inm, fnm, typ)
|
|
||||||
inm = Path(inm)
|
|
||||||
fnm = Path(fnm)
|
|
||||||
# Copy files from cache. This ensures that are used files with relative paths to dynamic library
|
|
||||||
# dependencies (@executable_path)
|
|
||||||
if typ in ('EXTENSION', 'BINARY') or (typ == 'DATA' and inm.suffix == '.so'):
|
|
||||||
if any(['.' in p for p in inm.parent.parts]):
|
|
||||||
inm = Path(inm.name)
|
|
||||||
fnm = Path(checkCache(
|
|
||||||
str(fnm),
|
|
||||||
strip = self.strip,
|
|
||||||
upx = self.upx,
|
|
||||||
upx_exclude = self.upx_exclude,
|
|
||||||
dist_nm = str(inm),
|
|
||||||
target_arch = self.target_arch,
|
|
||||||
codesign_identity = self.codesign_identity,
|
|
||||||
entitlements_file = self.entitlements_file,
|
|
||||||
strict_arch_validation = (typ == 'EXTENSION'),
|
|
||||||
))
|
|
||||||
frame_dst = frameworks_path.joinpath(inm)
|
|
||||||
if not frame_dst.exists():
|
|
||||||
if frame_dst.is_dir():
|
|
||||||
os.makedirs(frame_dst, exist_ok = True)
|
|
||||||
else:
|
|
||||||
os.makedirs(frame_dst.parent, exist_ok = True)
|
|
||||||
shutil.copy(fnm, frame_dst, follow_symlinks = True)
|
|
||||||
macos_dst = macos_path.joinpath(inm)
|
|
||||||
if not macos_dst.exists():
|
|
||||||
if macos_dst.is_dir():
|
|
||||||
os.makedirs(macos_dst, exist_ok = True)
|
|
||||||
else:
|
|
||||||
os.makedirs(macos_dst.parent, exist_ok = True)
|
|
||||||
|
|
||||||
# Create relative symlink to the framework
|
|
||||||
symlink_to = Path(*[".." for p in macos_dst.relative_to(macos_path).parts], "Frameworks").joinpath(
|
|
||||||
frame_dst.relative_to(frameworks_path))
|
|
||||||
try:
|
|
||||||
macos_dst.symlink_to(symlink_to)
|
|
||||||
except FileExistsError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if typ == 'DATA':
|
|
||||||
if any(['.' in p for p in inm.parent.parts]) or inm.suffix == '.so':
|
|
||||||
# Skip info dist egg and some not needed folders in tcl and tk, since they all contain dots in their files
|
|
||||||
logger.warning(f"Skipping DATA file {inm}")
|
|
||||||
continue
|
|
||||||
res_dst = resources_path.joinpath(inm)
|
|
||||||
if not res_dst.exists():
|
|
||||||
if res_dst.is_dir():
|
|
||||||
os.makedirs(res_dst, exist_ok = True)
|
|
||||||
else:
|
|
||||||
os.makedirs(res_dst.parent, exist_ok = True)
|
|
||||||
shutil.copy(fnm, res_dst, follow_symlinks = True)
|
|
||||||
macos_dst = macos_path.joinpath(inm)
|
|
||||||
if not macos_dst.exists():
|
|
||||||
if macos_dst.is_dir():
|
|
||||||
os.makedirs(macos_dst, exist_ok = True)
|
|
||||||
else:
|
|
||||||
os.makedirs(macos_dst.parent, exist_ok = True)
|
|
||||||
|
|
||||||
# Create relative symlink to the resource
|
|
||||||
symlink_to = Path(*[".." for p in macos_dst.relative_to(macos_path).parts], "Resources").joinpath(
|
|
||||||
res_dst.relative_to(resources_path))
|
|
||||||
try:
|
|
||||||
macos_dst.symlink_to(symlink_to)
|
|
||||||
except FileExistsError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
macos_dst = macos_path.joinpath(inm)
|
|
||||||
if not macos_dst.exists():
|
|
||||||
if macos_dst.is_dir():
|
|
||||||
os.makedirs(macos_dst, exist_ok = True)
|
|
||||||
else:
|
|
||||||
os.makedirs(macos_dst.parent, exist_ok = True)
|
|
||||||
shutil.copy(fnm, macos_dst, follow_symlinks = True)
|
|
||||||
|
|
||||||
# Sign the bundle
|
|
||||||
logger.info('Signing the BUNDLE...')
|
|
||||||
try:
|
|
||||||
osxutils.sign_binary(self.name, self.codesign_identity, self.entitlements_file, deep = True)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error while signing the bundle: {e}")
|
|
||||||
logger.warning("You will need to sign the bundle manually!")
|
|
||||||
|
|
||||||
logger.info(f"Building BUNDLE {self.tocbasename} completed successfully.")
|
|
||||||
|
|
||||||
app = UMBUNDLE(
|
|
||||||
coll,
|
coll,
|
||||||
name='{{ display_name }}.app',
|
name='{{ display_name }}.app',
|
||||||
icon={{ icon }},
|
icon={{ icon }},
|
||||||
@ -271,9 +91,10 @@ app = UMBUNDLE(
|
|||||||
'CFBundleURLSchemes': ['cura', 'slicer'],
|
'CFBundleURLSchemes': ['cura', 'slicer'],
|
||||||
}],
|
}],
|
||||||
'CFBundleDocumentTypes': [{
|
'CFBundleDocumentTypes': [{
|
||||||
'CFBundleTypeRole': 'Viewer',
|
'CFBundleTypeRole': 'Viewer',
|
||||||
'CFBundleTypeExtensions': ['*'],
|
'CFBundleTypeExtensions': ['stl', 'obj', '3mf', 'gcode', 'ufp'],
|
||||||
'CFBundleTypeName': 'Model Files',
|
'CFBundleTypeName': 'Model Files',
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
){% endif %}
|
)
|
||||||
|
{% endif %}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
version: "5.7.0-alpha.1"
|
version: "5.9.0-alpha.0"
|
||||||
requirements:
|
requirements:
|
||||||
|
- "cura_resources/(latest)@ultimaker/testing"
|
||||||
- "uranium/(latest)@ultimaker/testing"
|
- "uranium/(latest)@ultimaker/testing"
|
||||||
- "curaengine/(latest)@ultimaker/testing"
|
- "curaengine/(latest)@ultimaker/testing"
|
||||||
- "cura_binary_data/(latest)@ultimaker/testing"
|
- "cura_binary_data/(latest)@ultimaker/testing"
|
||||||
- "fdm_materials/(latest)@ultimaker/testing"
|
- "fdm_materials/(latest)@ultimaker/testing"
|
||||||
- "curaengine_plugin_gradual_flow/0.1.0-beta.2"
|
- "dulcificum/0.2.1"
|
||||||
- "dulcificum/latest@ultimaker/testing"
|
|
||||||
- "pysavitar/5.3.0"
|
- "pysavitar/5.3.0"
|
||||||
- "pynest2d/5.3.0"
|
- "pynest2d/5.3.0"
|
||||||
- "curaengine_grpc_definitions/(latest)@ultimaker/testing"
|
- "native_cad_plugin/2.0.0"
|
||||||
requirements_internal:
|
requirements_internal:
|
||||||
- "fdm_materials/(latest)@internal/testing"
|
- "fdm_materials/(latest)@internal/testing"
|
||||||
- "cura_private_data/(latest)@internal/testing"
|
- "cura_private_data/(latest)@internal/testing"
|
||||||
@ -33,18 +33,22 @@ pyinstaller:
|
|||||||
package: "cura"
|
package: "cura"
|
||||||
src: "plugins"
|
src: "plugins"
|
||||||
dst: "share/cura/plugins"
|
dst: "share/cura/plugins"
|
||||||
curaengine_gradual_flow_plugin:
|
native_cad_plugin:
|
||||||
package: "curaengine_plugin_gradual_flow"
|
package: "native_cad_plugin"
|
||||||
src: "res/plugins/CuraEngineGradualFlow"
|
src: "res/plugins/NativeCADplugin"
|
||||||
dst: "share/cura/plugins/CuraEngineGradualFlow"
|
dst: "share/cura/plugins/NativeCADplugin"
|
||||||
curaengine_gradual_flow_plugin_bundled:
|
native_cad_plugin_bundled:
|
||||||
package: "curaengine_plugin_gradual_flow"
|
package: "native_cad_plugin"
|
||||||
src: "res/bundled_packages"
|
src: "res/bundled_packages"
|
||||||
dst: "share/cura/resources/bundled_packages"
|
dst: "share/cura/resources/bundled_packages"
|
||||||
cura_resources:
|
cura_resources:
|
||||||
package: "cura"
|
package: "cura"
|
||||||
src: "resources"
|
src: "resources"
|
||||||
dst: "share/cura/resources"
|
dst: "share/cura/resources"
|
||||||
|
cura_shared_resources:
|
||||||
|
package: "cura_resources"
|
||||||
|
src: "res"
|
||||||
|
dst: "share/cura/resources"
|
||||||
cura_private_data:
|
cura_private_data:
|
||||||
package: "cura_private_data"
|
package: "cura_private_data"
|
||||||
src: "res"
|
src: "res"
|
||||||
@ -92,11 +96,6 @@ pyinstaller:
|
|||||||
src: "bin"
|
src: "bin"
|
||||||
dst: "."
|
dst: "."
|
||||||
binary: "CuraEngine"
|
binary: "CuraEngine"
|
||||||
curaengine_gradual_flow_plugin_service:
|
|
||||||
package: "curaengine_plugin_gradual_flow"
|
|
||||||
src: "bin"
|
|
||||||
dst: "."
|
|
||||||
binary: "curaengine_plugin_gradual_flow"
|
|
||||||
hiddenimports:
|
hiddenimports:
|
||||||
- "pySavitar"
|
- "pySavitar"
|
||||||
- "pyArcus"
|
- "pyArcus"
|
||||||
@ -118,7 +117,6 @@ pyinstaller:
|
|||||||
- "sqlite3"
|
- "sqlite3"
|
||||||
- "trimesh"
|
- "trimesh"
|
||||||
- "win32ctypes"
|
- "win32ctypes"
|
||||||
- "PyQt6"
|
|
||||||
- "PyQt6.QtNetwork"
|
- "PyQt6.QtNetwork"
|
||||||
- "PyQt6.sip"
|
- "PyQt6.sip"
|
||||||
- "stl"
|
- "stl"
|
||||||
@ -160,6 +158,10 @@ pycharm_targets:
|
|||||||
module_name: Cura
|
module_name: Cura
|
||||||
name: pytest in TestGCodeListDecorator.py
|
name: pytest in TestGCodeListDecorator.py
|
||||||
script_name: tests/TestGCodeListDecorator.py
|
script_name: tests/TestGCodeListDecorator.py
|
||||||
|
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
|
||||||
|
module_name: Cura
|
||||||
|
name: pytest in TestHitChecker.py
|
||||||
|
script_name: tests/TestHitChecker.py
|
||||||
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
|
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
|
||||||
module_name: Cura
|
module_name: Cura
|
||||||
name: pytest in TestIntentManager.py
|
name: pytest in TestIntentManager.py
|
||||||
@ -188,6 +190,10 @@ pycharm_targets:
|
|||||||
module_name: Cura
|
module_name: Cura
|
||||||
name: pytest in TestPrintInformation.py
|
name: pytest in TestPrintInformation.py
|
||||||
script_name: tests/TestPrintInformation.py
|
script_name: tests/TestPrintInformation.py
|
||||||
|
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
|
||||||
|
module_name: Cura
|
||||||
|
name: pytest in TestPrintOrderManager.py
|
||||||
|
script_name: tests/TestPrintOrderManager.py
|
||||||
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
|
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
|
||||||
module_name: Cura
|
module_name: Cura
|
||||||
name: pytest in TestProfileRequirements.py
|
name: pytest in TestProfileRequirements.py
|
||||||
|
53
conanfile.py
53
conanfile.py
@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
from io import StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
@ -150,6 +151,7 @@ class CuraConan(ConanFile):
|
|||||||
return "None"
|
return "None"
|
||||||
|
|
||||||
def _conan_installs(self):
|
def _conan_installs(self):
|
||||||
|
self.output.info("Collecting conan installs")
|
||||||
conan_installs = {}
|
conan_installs = {}
|
||||||
|
|
||||||
# list of conan installs
|
# list of conan installs
|
||||||
@ -161,13 +163,22 @@ class CuraConan(ConanFile):
|
|||||||
return conan_installs
|
return conan_installs
|
||||||
|
|
||||||
def _python_installs(self):
|
def _python_installs(self):
|
||||||
|
self.output.info("Collecting python installs")
|
||||||
python_installs = {}
|
python_installs = {}
|
||||||
|
|
||||||
# list of python installs
|
# list of python installs
|
||||||
python_ins_cmd = f"python -c \"import pkg_resources; print(';'.join([(s.key+','+ s.version) for s in pkg_resources.working_set]))\""
|
run_env = VirtualRunEnv(self)
|
||||||
from six import StringIO
|
env = run_env.environment()
|
||||||
|
env.prepend_path("PYTHONPATH", str(self._site_packages.as_posix()))
|
||||||
|
venv_vars = env.vars(self, scope = "run")
|
||||||
|
|
||||||
|
outer = '"' if self.settings.os == "Windows" else "'"
|
||||||
|
inner = "'" if self.settings.os == "Windows" else '"'
|
||||||
buffer = StringIO()
|
buffer = StringIO()
|
||||||
self.run(python_ins_cmd, run_environment= True, env = "conanrun", output=buffer)
|
with venv_vars.apply():
|
||||||
|
self.run(f"""python -c {outer}import pkg_resources; print({inner};{inner}.join([(s.key+{inner},{inner}+ s.version) for s in pkg_resources.working_set])){outer}""",
|
||||||
|
env = "conanrun",
|
||||||
|
output = buffer)
|
||||||
|
|
||||||
packages = str(buffer.getvalue()).split("-----------------\n")
|
packages = str(buffer.getvalue()).split("-----------------\n")
|
||||||
packages = packages[1].strip('\r\n').split(";")
|
packages = packages[1].strip('\r\n').split(";")
|
||||||
@ -220,6 +231,8 @@ class CuraConan(ConanFile):
|
|||||||
else:
|
else:
|
||||||
src_path = os.path.join(self.source_folder, data["src"])
|
src_path = os.path.join(self.source_folder, data["src"])
|
||||||
else:
|
else:
|
||||||
|
if data["package"] not in self.deps_cpp_info.deps:
|
||||||
|
continue
|
||||||
src_path = os.path.join(self.deps_cpp_info[data["package"]].rootpath, data["src"])
|
src_path = os.path.join(self.deps_cpp_info[data["package"]].rootpath, data["src"])
|
||||||
elif "root" in data: # get the paths relative from the install folder
|
elif "root" in data: # get the paths relative from the install folder
|
||||||
src_path = os.path.join(self.install_folder, data["root"], data["src"])
|
src_path = os.path.join(self.install_folder, data["root"], data["src"])
|
||||||
@ -316,7 +329,6 @@ class CuraConan(ConanFile):
|
|||||||
self.options["cpython"].shared = True
|
self.options["cpython"].shared = True
|
||||||
self.options["boost"].header_only = True
|
self.options["boost"].header_only = True
|
||||||
if self.settings.os == "Linux":
|
if self.settings.os == "Linux":
|
||||||
self.options["curaengine_grpc_definitions"].shared = True
|
|
||||||
self.options["openssl"].shared = True
|
self.options["openssl"].shared = True
|
||||||
if self.conf.get("user.curaengine:sentry_url", "", check_type=str) != "":
|
if self.conf.get("user.curaengine:sentry_url", "", check_type=str) != "":
|
||||||
self.options["curaengine"].enable_sentry = True
|
self.options["curaengine"].enable_sentry = True
|
||||||
@ -332,6 +344,8 @@ class CuraConan(ConanFile):
|
|||||||
for req in self.conan_data["requirements"]:
|
for req in self.conan_data["requirements"]:
|
||||||
if self._internal and "fdm_materials" in req:
|
if self._internal and "fdm_materials" in req:
|
||||||
continue
|
continue
|
||||||
|
if not self._enterprise and "native_cad_plugin" in req:
|
||||||
|
continue
|
||||||
self.requires(req)
|
self.requires(req)
|
||||||
if self._internal:
|
if self._internal:
|
||||||
for req in self.conan_data["requirements_internal"]:
|
for req in self.conan_data["requirements_internal"]:
|
||||||
@ -339,6 +353,7 @@ class CuraConan(ConanFile):
|
|||||||
self.requires("cpython/3.10.4@ultimaker/stable")
|
self.requires("cpython/3.10.4@ultimaker/stable")
|
||||||
self.requires("clipper/6.4.2@ultimaker/stable")
|
self.requires("clipper/6.4.2@ultimaker/stable")
|
||||||
self.requires("openssl/3.2.0")
|
self.requires("openssl/3.2.0")
|
||||||
|
self.requires("protobuf/3.21.12")
|
||||||
self.requires("boost/1.82.0")
|
self.requires("boost/1.82.0")
|
||||||
self.requires("spdlog/1.12.0")
|
self.requires("spdlog/1.12.0")
|
||||||
self.requires("fmt/10.1.1")
|
self.requires("fmt/10.1.1")
|
||||||
@ -375,11 +390,11 @@ class CuraConan(ConanFile):
|
|||||||
copy(self, "CuraEngine", curaengine.bindirs[0], self.source_folder, keep_path = False)
|
copy(self, "CuraEngine", curaengine.bindirs[0], self.source_folder, keep_path = False)
|
||||||
|
|
||||||
# Copy the external plugins that we want to bundle with Cura
|
# Copy the external plugins that we want to bundle with Cura
|
||||||
rmdir(self,str(self.source_path.joinpath("plugins", "CuraEngineGradualFlow")))
|
if self._enterprise:
|
||||||
curaengine_plugin_gradual_flow = self.dependencies["curaengine_plugin_gradual_flow"].cpp_info
|
rmdir(self, str(self.source_path.joinpath("plugins", "NativeCADplugin")))
|
||||||
copy(self, "*", curaengine_plugin_gradual_flow.resdirs[0], str(self.source_path.joinpath("plugins", "CuraEngineGradualFlow")), keep_path = True)
|
native_cad_plugin = self.dependencies["native_cad_plugin"].cpp_info
|
||||||
copy(self, "*", curaengine_plugin_gradual_flow.bindirs[0], self.source_folder, keep_path = False)
|
copy(self, "*", native_cad_plugin.resdirs[0], str(self.source_path.joinpath("plugins", "NativeCADplugin")), keep_path = True)
|
||||||
copy(self, "bundled_*.json", curaengine_plugin_gradual_flow.resdirs[1], str(self.source_path.joinpath("resources", "bundled_packages")), keep_path = False)
|
copy(self, "bundled_*.json", native_cad_plugin.resdirs[1], str(self.source_path.joinpath("resources", "bundled_packages")), keep_path = False)
|
||||||
|
|
||||||
# Copy resources of cura_binary_data
|
# Copy resources of cura_binary_data
|
||||||
cura_binary_data = self.dependencies["cura_binary_data"].cpp_info
|
cura_binary_data = self.dependencies["cura_binary_data"].cpp_info
|
||||||
@ -446,6 +461,12 @@ class CuraConan(ConanFile):
|
|||||||
copy(self, "*", os.path.join(self.package_folder, self.cpp_info.resdirs[0]), str(self._share_dir.joinpath("cura", "resources")), keep_path = True)
|
copy(self, "*", os.path.join(self.package_folder, self.cpp_info.resdirs[0]), str(self._share_dir.joinpath("cura", "resources")), keep_path = True)
|
||||||
copy(self, "*", os.path.join(self.package_folder, self.cpp_info.resdirs[1]), str(self._share_dir.joinpath("cura", "plugins")), keep_path = True)
|
copy(self, "*", os.path.join(self.package_folder, self.cpp_info.resdirs[1]), str(self._share_dir.joinpath("cura", "plugins")), keep_path = True)
|
||||||
|
|
||||||
|
# Copy the cura_resources resources from the package
|
||||||
|
rm(self, "conanfile.py", os.path.join(self.package_folder, self.cpp.package.resdirs[0]))
|
||||||
|
cura_resources = self.dependencies["cura_resources"].cpp_info
|
||||||
|
for res_dir in cura_resources.resdirs:
|
||||||
|
copy(self, "*", res_dir, str(self._share_dir.joinpath("cura", "resources", Path(res_dir).name)), keep_path = True)
|
||||||
|
|
||||||
# Copy resources of Uranium (keep folder structure)
|
# Copy resources of Uranium (keep folder structure)
|
||||||
uranium = self.dependencies["uranium"].cpp_info
|
uranium = self.dependencies["uranium"].cpp_info
|
||||||
copy(self, "*", uranium.resdirs[0], str(self._share_dir.joinpath("uranium", "resources")), keep_path = True)
|
copy(self, "*", uranium.resdirs[0], str(self._share_dir.joinpath("uranium", "resources")), keep_path = True)
|
||||||
@ -490,13 +511,15 @@ echo "CURA_APP_NAME={{ cura_app_name }}" >> ${{ env_prefix }}GITHUB_ENV
|
|||||||
copy(self, "requirement*.txt", src = self.source_folder, dst = os.path.join(self.package_folder, self.cpp.package.resdirs[-1]))
|
copy(self, "requirement*.txt", src = self.source_folder, dst = os.path.join(self.package_folder, self.cpp.package.resdirs[-1]))
|
||||||
copy(self, "*", src = os.path.join(self.source_folder, "packaging"), dst = os.path.join(self.package_folder, self.cpp.package.resdirs[2]))
|
copy(self, "*", src = os.path.join(self.source_folder, "packaging"), dst = os.path.join(self.package_folder, self.cpp.package.resdirs[2]))
|
||||||
|
|
||||||
# Remove the CuraEngineGradualFlow plugin from the package
|
|
||||||
rmdir(self, os.path.join(self.package_folder, self.cpp.package.resdirs[1], "CuraEngineGradualFlow"))
|
|
||||||
rm(self, "bundled_*.json", os.path.join(self.package_folder, self.cpp.package.resdirs[0], "bundled_packages"), recursive = False)
|
|
||||||
|
|
||||||
# Remove the fdm_materials from the package
|
# Remove the fdm_materials from the package
|
||||||
rmdir(self, os.path.join(self.package_folder, self.cpp.package.resdirs[0], "materials"))
|
rmdir(self, os.path.join(self.package_folder, self.cpp.package.resdirs[0], "materials"))
|
||||||
|
|
||||||
|
# Remove the cura_resources resources from the package
|
||||||
|
rm(self, "conanfile.py", os.path.join(self.package_folder, self.cpp.package.resdirs[0]))
|
||||||
|
cura_resources = self.dependencies["cura_resources"].cpp_info
|
||||||
|
for res_dir in cura_resources.resdirs:
|
||||||
|
rmdir(self, os.path.join(self.package_folder, self.cpp.package.resdirs[0], Path(res_dir).name))
|
||||||
|
|
||||||
def package_info(self):
|
def package_info(self):
|
||||||
self.user_info.pip_requirements = "requirements.txt"
|
self.user_info.pip_requirements = "requirements.txt"
|
||||||
self.user_info.pip_requirements_git = "requirements-ultimaker.txt"
|
self.user_info.pip_requirements_git = "requirements-ultimaker.txt"
|
||||||
@ -504,10 +527,14 @@ echo "CURA_APP_NAME={{ cura_app_name }}" >> ${{ env_prefix }}GITHUB_ENV
|
|||||||
|
|
||||||
if self.in_local_cache:
|
if self.in_local_cache:
|
||||||
self.runenv_info.append_path("PYTHONPATH", os.path.join(self.package_folder, "site-packages"))
|
self.runenv_info.append_path("PYTHONPATH", os.path.join(self.package_folder, "site-packages"))
|
||||||
|
self.env_info.PYTHONPATH.append(os.path.join(self.package_folder, "site-packages"))
|
||||||
self.runenv_info.append_path("PYTHONPATH", os.path.join(self.package_folder, "plugins"))
|
self.runenv_info.append_path("PYTHONPATH", os.path.join(self.package_folder, "plugins"))
|
||||||
|
self.env_info.PYTHONPATH.append(os.path.join(self.package_folder, "plugins"))
|
||||||
else:
|
else:
|
||||||
self.runenv_info.append_path("PYTHONPATH", self.source_folder)
|
self.runenv_info.append_path("PYTHONPATH", self.source_folder)
|
||||||
|
self.env_info.PYTHONPATH.append(self.source_folder)
|
||||||
self.runenv_info.append_path("PYTHONPATH", os.path.join(self.source_folder, "plugins"))
|
self.runenv_info.append_path("PYTHONPATH", os.path.join(self.source_folder, "plugins"))
|
||||||
|
self.env_info.PYTHONPATH.append(os.path.join(self.source_folder, "plugins"))
|
||||||
|
|
||||||
def package_id(self):
|
def package_id(self):
|
||||||
self.info.clear()
|
self.info.clear()
|
||||||
|
@ -115,15 +115,15 @@ class Account(QObject):
|
|||||||
self._update_timer.setSingleShot(True)
|
self._update_timer.setSingleShot(True)
|
||||||
self._update_timer.timeout.connect(self.sync)
|
self._update_timer.timeout.connect(self.sync)
|
||||||
|
|
||||||
self._sync_services: Dict[str, int] = {}
|
|
||||||
"""contains entries "service_name" : SyncState"""
|
"""contains entries "service_name" : SyncState"""
|
||||||
self.syncRequested.connect(self._updatePermissions)
|
self._sync_services: Dict[str, int] = {}
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
self._authorization_service.initialize(self._application.getPreferences())
|
self._authorization_service.initialize(self._application.getPreferences())
|
||||||
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
|
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
|
||||||
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
|
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
|
||||||
self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
|
self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
|
||||||
|
self._authorization_service.accessTokenChanged.connect(self._updatePermissions)
|
||||||
self._authorization_service.loadAuthDataFromPreferences()
|
self._authorization_service.loadAuthDataFromPreferences()
|
||||||
|
|
||||||
@pyqtProperty(int, notify=syncStateChanged)
|
@pyqtProperty(int, notify=syncStateChanged)
|
||||||
@ -190,6 +190,20 @@ class Account(QObject):
|
|||||||
def isLoggedIn(self) -> bool:
|
def isLoggedIn(self) -> bool:
|
||||||
return self._logged_in
|
return self._logged_in
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def stopSyncing(self) -> None:
|
||||||
|
Logger.debug(f"Stopping sync of cloud printers")
|
||||||
|
self._setManualSyncEnabled(True)
|
||||||
|
if self._update_timer.isActive():
|
||||||
|
self._update_timer.stop()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def startSyncing(self) -> None:
|
||||||
|
Logger.debug(f"Starting sync of cloud printers")
|
||||||
|
self._setManualSyncEnabled(False)
|
||||||
|
if not self._update_timer.isActive():
|
||||||
|
self._update_timer.start()
|
||||||
|
|
||||||
def _onLoginStateChanged(self, logged_in: bool = False, error_message: Optional[str] = None) -> None:
|
def _onLoginStateChanged(self, logged_in: bool = False, error_message: Optional[str] = None) -> None:
|
||||||
if error_message:
|
if error_message:
|
||||||
if self._error_message:
|
if self._error_message:
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from dataclasses import asdict
|
||||||
|
|
||||||
|
from typing import cast, Dict, TYPE_CHECKING
|
||||||
|
|
||||||
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
@ -47,3 +53,57 @@ class Settings:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
return self.application.getSidebarCustomMenuItems()
|
return self.application.getSidebarCustomMenuItems()
|
||||||
|
|
||||||
|
def getSliceMetadata(self) -> Dict[str, Dict[str, Dict[str, str]]]:
|
||||||
|
"""Get all changed settings and all settings. For each extruder and the global stack"""
|
||||||
|
print_information = self.application.getPrintInformation()
|
||||||
|
machine_manager = self.application.getMachineManager()
|
||||||
|
settings = {
|
||||||
|
"material": {
|
||||||
|
"length": print_information.materialLengths,
|
||||||
|
"weight": print_information.materialWeights,
|
||||||
|
"cost": print_information.materialCosts,
|
||||||
|
},
|
||||||
|
"global": {
|
||||||
|
"changes": {},
|
||||||
|
"all_settings": {},
|
||||||
|
},
|
||||||
|
"quality": asdict(machine_manager.activeQualityDisplayNameMap()),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _retrieveValue(container: InstanceContainer, setting_: str):
|
||||||
|
value_ = container.getProperty(setting_, "value")
|
||||||
|
for _ in range(0, 1024): # Prevent possibly endless loop by not using a limit.
|
||||||
|
if not isinstance(value_, SettingFunction):
|
||||||
|
return value_ # Success!
|
||||||
|
value_ = value_(container)
|
||||||
|
return 0 # Fallback value after breaking possibly endless loop.
|
||||||
|
|
||||||
|
global_stack = cast(GlobalStack, self.application.getGlobalContainerStack())
|
||||||
|
|
||||||
|
# Add global user or quality changes
|
||||||
|
global_flattened_changes = InstanceContainer.createMergedInstanceContainer(global_stack.userChanges, global_stack.qualityChanges)
|
||||||
|
for setting in global_flattened_changes.getAllKeys():
|
||||||
|
settings["global"]["changes"][setting] = _retrieveValue(global_flattened_changes, setting)
|
||||||
|
|
||||||
|
# Get global all settings values without user or quality changes
|
||||||
|
for setting in global_stack.getAllKeys():
|
||||||
|
settings["global"]["all_settings"][setting] = _retrieveValue(global_stack, setting)
|
||||||
|
|
||||||
|
for i, extruder in enumerate(global_stack.extruderList):
|
||||||
|
# Add extruder fields to settings dictionary
|
||||||
|
settings[f"extruder_{i}"] = {
|
||||||
|
"changes": {},
|
||||||
|
"all_settings": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add extruder user or quality changes
|
||||||
|
extruder_flattened_changes = InstanceContainer.createMergedInstanceContainer(extruder.userChanges, extruder.qualityChanges)
|
||||||
|
for setting in extruder_flattened_changes.getAllKeys():
|
||||||
|
settings[f"extruder_{i}"]["changes"][setting] = _retrieveValue(extruder_flattened_changes, setting)
|
||||||
|
|
||||||
|
# Get extruder all settings values without user or quality changes
|
||||||
|
for setting in extruder.getAllKeys():
|
||||||
|
settings[f"extruder_{i}"]["all_settings"][setting] = _retrieveValue(extruder, setting)
|
||||||
|
|
||||||
|
return settings
|
||||||
|
@ -14,7 +14,7 @@ DEFAULT_CURA_LATEST_URL = "https://software.ultimaker.com/latest.json"
|
|||||||
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
|
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
|
||||||
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
|
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
|
||||||
# CuraVersion.py.in template.
|
# CuraVersion.py.in template.
|
||||||
CuraSDKVersion = "8.6.0"
|
CuraSDKVersion = "8.8.0"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from cura.CuraVersion import CuraLatestURL
|
from cura.CuraVersion import CuraLatestURL
|
||||||
|
@ -18,8 +18,8 @@ class BackendPlugin(AdditionalSettingDefinitionsAppender, PluginObject):
|
|||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
settings_catalog = i18nCatalog("fdmprinter.def.json")
|
settings_catalog = i18nCatalog("fdmprinter.def.json")
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, catalog_i18n = settings_catalog) -> None:
|
||||||
super().__init__(self.settings_catalog)
|
super().__init__(catalog_i18n)
|
||||||
self.__port: int = 0
|
self.__port: int = 0
|
||||||
self._plugin_address: str = "127.0.0.1"
|
self._plugin_address: str = "127.0.0.1"
|
||||||
self._plugin_command: Optional[List[str]] = None
|
self._plugin_command: Optional[List[str]] = None
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# Copyright (c) 2023 UltiMaker
|
# Copyright (c) 2023 UltiMaker
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import List, cast
|
from typing import List, cast
|
||||||
|
|
||||||
from PyQt6.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty
|
from PyQt6.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty
|
||||||
@ -33,7 +32,6 @@ from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOper
|
|||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
|
||||||
|
|
||||||
class CuraActions(QObject):
|
class CuraActions(QObject):
|
||||||
def __init__(self, parent: QObject = None) -> None:
|
def __init__(self, parent: QObject = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -273,7 +271,11 @@ class CuraActions(QObject):
|
|||||||
# deselect currently selected nodes, and select the new nodes
|
# deselect currently selected nodes, and select the new nodes
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
Selection.remove(node)
|
Selection.remove(node)
|
||||||
|
|
||||||
|
numberOfFixedNodes = len(fixed_nodes)
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
|
numberOfFixedNodes += 1
|
||||||
|
node.printOrder = numberOfFixedNodes
|
||||||
Selection.add(node)
|
Selection.add(node)
|
||||||
|
|
||||||
def _openUrl(self, url: QUrl) -> None:
|
def _openUrl(self, url: QUrl) -> None:
|
||||||
|
@ -33,6 +33,7 @@ from UM.Message import Message
|
|||||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||||
from UM.Operations.GroupedOperation import GroupedOperation
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
from UM.Operations.SetTransformOperation import SetTransformOperation
|
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||||
|
from UM.OutputDevice.ProjectOutputDevice import ProjectOutputDevice
|
||||||
from UM.Platform import Platform
|
from UM.Platform import Platform
|
||||||
from UM.PluginError import PluginNotFoundError
|
from UM.PluginError import PluginNotFoundError
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
@ -104,7 +105,8 @@ from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
|
|||||||
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
|
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
|
||||||
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
||||||
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
|
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
|
||||||
from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation
|
from cura.UI import CuraSplashScreen, PrintInformation
|
||||||
|
from cura.UI.MachineActionManager import MachineActionManager
|
||||||
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
|
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
|
||||||
from cura.UI.MachineSettingsManager import MachineSettingsManager
|
from cura.UI.MachineSettingsManager import MachineSettingsManager
|
||||||
from cura.UI.ObjectsModel import ObjectsModel
|
from cura.UI.ObjectsModel import ObjectsModel
|
||||||
@ -125,6 +127,7 @@ from .Machines.Models.CompatibleMachineModel import CompatibleMachineModel
|
|||||||
from .Machines.Models.MachineListModel import MachineListModel
|
from .Machines.Models.MachineListModel import MachineListModel
|
||||||
from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel
|
from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel
|
||||||
from .Machines.Models.IntentSelectionModel import IntentSelectionModel
|
from .Machines.Models.IntentSelectionModel import IntentSelectionModel
|
||||||
|
from .PrintOrderManager import PrintOrderManager
|
||||||
from .SingleInstance import SingleInstance
|
from .SingleInstance import SingleInstance
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -136,7 +139,7 @@ class CuraApplication(QtApplication):
|
|||||||
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
||||||
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
||||||
# changes of the settings.
|
# changes of the settings.
|
||||||
SettingVersion = 22
|
SettingVersion = 23
|
||||||
|
|
||||||
Created = False
|
Created = False
|
||||||
|
|
||||||
@ -179,6 +182,7 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
# Variables set from CLI
|
# Variables set from CLI
|
||||||
self._files_to_open = []
|
self._files_to_open = []
|
||||||
|
self._urls_to_open = []
|
||||||
self._use_single_instance = False
|
self._use_single_instance = False
|
||||||
|
|
||||||
self._single_instance = None
|
self._single_instance = None
|
||||||
@ -186,7 +190,7 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions]
|
self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions]
|
||||||
|
|
||||||
self._machine_action_manager = None # type: Optional[MachineActionManager.MachineActionManager]
|
self._machine_action_manager: Optional[MachineActionManager] = None
|
||||||
|
|
||||||
self.empty_container = None # type: EmptyInstanceContainer
|
self.empty_container = None # type: EmptyInstanceContainer
|
||||||
self.empty_definition_changes_container = None # type: EmptyInstanceContainer
|
self.empty_definition_changes_container = None # type: EmptyInstanceContainer
|
||||||
@ -202,6 +206,7 @@ class CuraApplication(QtApplication):
|
|||||||
self._container_manager = None
|
self._container_manager = None
|
||||||
|
|
||||||
self._object_manager = None
|
self._object_manager = None
|
||||||
|
self._print_order_manager = None
|
||||||
self._extruders_model = None
|
self._extruders_model = None
|
||||||
self._extruders_model_with_optional = None
|
self._extruders_model_with_optional = None
|
||||||
self._build_plate_model = None
|
self._build_plate_model = None
|
||||||
@ -333,7 +338,7 @@ class CuraApplication(QtApplication):
|
|||||||
for filename in self._cli_args.file:
|
for filename in self._cli_args.file:
|
||||||
url = QUrl(filename)
|
url = QUrl(filename)
|
||||||
if url.scheme() in self._supported_url_schemes:
|
if url.scheme() in self._supported_url_schemes:
|
||||||
self._open_url_queue.append(url)
|
self._urls_to_open.append(url)
|
||||||
else:
|
else:
|
||||||
self._files_to_open.append(os.path.abspath(filename))
|
self._files_to_open.append(os.path.abspath(filename))
|
||||||
|
|
||||||
@ -352,11 +357,11 @@ class CuraApplication(QtApplication):
|
|||||||
self.__addAllEmptyContainers()
|
self.__addAllEmptyContainers()
|
||||||
self.__setLatestResouceVersionsForVersionUpgrade()
|
self.__setLatestResouceVersionsForVersionUpgrade()
|
||||||
|
|
||||||
self._machine_action_manager = MachineActionManager.MachineActionManager(self)
|
self._machine_action_manager = MachineActionManager(self)
|
||||||
self._machine_action_manager.initialize()
|
self._machine_action_manager.initialize()
|
||||||
|
|
||||||
def __sendCommandToSingleInstance(self):
|
def __sendCommandToSingleInstance(self):
|
||||||
self._single_instance = SingleInstance(self, self._files_to_open)
|
self._single_instance = SingleInstance(self, self._files_to_open, self._urls_to_open)
|
||||||
|
|
||||||
# If we use single instance, try to connect to the single instance server, send commands, and then exit.
|
# If we use single instance, try to connect to the single instance server, send commands, and then exit.
|
||||||
# If we cannot find an existing single instance server, this is the only instance, so just keep going.
|
# If we cannot find an existing single instance server, this is the only instance, so just keep going.
|
||||||
@ -373,9 +378,15 @@ class CuraApplication(QtApplication):
|
|||||||
Resources.addExpectedDirNameInData(dir_name)
|
Resources.addExpectedDirNameInData(dir_name)
|
||||||
|
|
||||||
app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable)))
|
app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable)))
|
||||||
Resources.addSecureSearchPath(os.path.join(app_root, "share", "cura", "resources"))
|
|
||||||
|
|
||||||
Resources.addSecureSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
|
if platform.system() == "Darwin":
|
||||||
|
Resources.addSecureSearchPath(os.path.join(app_root, "Resources", "share", "cura", "resources"))
|
||||||
|
Resources.addSecureSearchPath(
|
||||||
|
os.path.join(self._app_install_dir, "Resources", "share", "cura", "resources"))
|
||||||
|
else:
|
||||||
|
Resources.addSecureSearchPath(os.path.join(app_root, "share", "cura", "resources"))
|
||||||
|
Resources.addSecureSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
|
||||||
|
|
||||||
if not hasattr(sys, "frozen"):
|
if not hasattr(sys, "frozen"):
|
||||||
cura_data_root = os.environ.get('CURA_DATA_ROOT', None)
|
cura_data_root = os.environ.get('CURA_DATA_ROOT', None)
|
||||||
if cura_data_root:
|
if cura_data_root:
|
||||||
@ -607,6 +618,7 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
preferences.addPreference("view/invert_zoom", False)
|
preferences.addPreference("view/invert_zoom", False)
|
||||||
preferences.addPreference("view/filter_current_build_plate", False)
|
preferences.addPreference("view/filter_current_build_plate", False)
|
||||||
|
preferences.addPreference("view/navigation_style", "cura")
|
||||||
preferences.addPreference("cura/sidebar_collapsed", False)
|
preferences.addPreference("cura/sidebar_collapsed", False)
|
||||||
|
|
||||||
preferences.addPreference("cura/favorite_materials", "")
|
preferences.addPreference("cura/favorite_materials", "")
|
||||||
@ -899,6 +911,7 @@ class CuraApplication(QtApplication):
|
|||||||
# initialize info objects
|
# initialize info objects
|
||||||
self._print_information = PrintInformation.PrintInformation(self)
|
self._print_information = PrintInformation.PrintInformation(self)
|
||||||
self._cura_actions = CuraActions.CuraActions(self)
|
self._cura_actions = CuraActions.CuraActions(self)
|
||||||
|
self._print_order_manager = PrintOrderManager(self.getObjectsModel().getNodes)
|
||||||
self.processEvents()
|
self.processEvents()
|
||||||
# Initialize setting visibility presets model.
|
# Initialize setting visibility presets model.
|
||||||
self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self.getPreferences(), parent = self)
|
self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self.getPreferences(), parent = self)
|
||||||
@ -956,6 +969,8 @@ class CuraApplication(QtApplication):
|
|||||||
self.callLater(self._openFile, file_name)
|
self.callLater(self._openFile, file_name)
|
||||||
for file_name in self._open_file_queue: # Open all the files that were queued up while plug-ins were loading.
|
for file_name in self._open_file_queue: # Open all the files that were queued up while plug-ins were loading.
|
||||||
self.callLater(self._openFile, file_name)
|
self.callLater(self._openFile, file_name)
|
||||||
|
for url in self._urls_to_open:
|
||||||
|
self.callLater(self._openUrl, url)
|
||||||
for url in self._open_url_queue:
|
for url in self._open_url_queue:
|
||||||
self.callLater(self._openUrl, url)
|
self.callLater(self._openUrl, url)
|
||||||
|
|
||||||
@ -979,6 +994,7 @@ class CuraApplication(QtApplication):
|
|||||||
t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis])
|
t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis])
|
||||||
|
|
||||||
Selection.selectionChanged.connect(self.onSelectionChanged)
|
Selection.selectionChanged.connect(self.onSelectionChanged)
|
||||||
|
self._print_order_manager.printOrderChanged.connect(self._onPrintOrderChanged)
|
||||||
|
|
||||||
# Set default background color for scene
|
# Set default background color for scene
|
||||||
self.getRenderer().setBackgroundColor(QColor(245, 245, 245))
|
self.getRenderer().setBackgroundColor(QColor(245, 245, 245))
|
||||||
@ -1068,6 +1084,10 @@ class CuraApplication(QtApplication):
|
|||||||
def getTextManager(self, *args) -> "TextManager":
|
def getTextManager(self, *args) -> "TextManager":
|
||||||
return self._text_manager
|
return self._text_manager
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def setWorkplaceDropToBuildplate(self):
|
||||||
|
return self._physics.setAppAllModelDropDown()
|
||||||
|
|
||||||
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
|
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
|
||||||
if self._cura_formula_functions is None:
|
if self._cura_formula_functions is None:
|
||||||
self._cura_formula_functions = CuraFormulaFunctions(self)
|
self._cura_formula_functions = CuraFormulaFunctions(self)
|
||||||
@ -1094,6 +1114,10 @@ class CuraApplication(QtApplication):
|
|||||||
self._object_manager = ObjectsModel(self)
|
self._object_manager = ObjectsModel(self)
|
||||||
return self._object_manager
|
return self._object_manager
|
||||||
|
|
||||||
|
@pyqtSlot(str, result = "QVariantList")
|
||||||
|
def getSupportedActionMachineList(self, definition_id: str) -> List["MachineAction"]:
|
||||||
|
return self._machine_action_manager.getSupportedActions(self._machine_manager.getDefinitionByMachineId(definition_id))
|
||||||
|
|
||||||
@pyqtSlot(result = QObject)
|
@pyqtSlot(result = QObject)
|
||||||
def getExtrudersModel(self, *args) -> "ExtrudersModel":
|
def getExtrudersModel(self, *args) -> "ExtrudersModel":
|
||||||
if self._extruders_model is None:
|
if self._extruders_model is None:
|
||||||
@ -1119,6 +1143,16 @@ class CuraApplication(QtApplication):
|
|||||||
self._build_plate_model = BuildPlateModel(self)
|
self._build_plate_model = BuildPlateModel(self)
|
||||||
return self._build_plate_model
|
return self._build_plate_model
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def exportUcp(self):
|
||||||
|
writer = self.getMeshFileHandler().getWriter("3MFWriter")
|
||||||
|
|
||||||
|
if writer is None:
|
||||||
|
Logger.warning("3mf writer is not enabled")
|
||||||
|
return
|
||||||
|
|
||||||
|
writer.exportUcp()
|
||||||
|
|
||||||
def getCuraSceneController(self, *args) -> CuraSceneController:
|
def getCuraSceneController(self, *args) -> CuraSceneController:
|
||||||
if self._cura_scene_controller is None:
|
if self._cura_scene_controller is None:
|
||||||
self._cura_scene_controller = CuraSceneController.createCuraSceneController()
|
self._cura_scene_controller = CuraSceneController.createCuraSceneController()
|
||||||
@ -1129,18 +1163,16 @@ class CuraApplication(QtApplication):
|
|||||||
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
|
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
|
||||||
return self._setting_inheritance_manager
|
return self._setting_inheritance_manager
|
||||||
|
|
||||||
def getMachineActionManager(self, *args: Any) -> MachineActionManager.MachineActionManager:
|
@pyqtSlot(result = QObject)
|
||||||
|
def getMachineActionManager(self, *args: Any) -> MachineActionManager:
|
||||||
"""Get the machine action manager
|
"""Get the machine action manager
|
||||||
|
|
||||||
We ignore any *args given to this, as we also register the machine manager as qml singleton.
|
We ignore any *args given to this, as we also register the machine manager as qml singleton.
|
||||||
It wants to give this function an engine and script engine, but we don't care about that.
|
It wants to give this function an engine and script engine, but we don't care about that.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return cast(MachineActionManager.MachineActionManager, self._machine_action_manager)
|
return self._machine_action_manager
|
||||||
|
|
||||||
@pyqtSlot(result = QObject)
|
|
||||||
def getMachineActionManagerQml(self)-> MachineActionManager.MachineActionManager:
|
|
||||||
return cast(QObject, self._machine_action_manager)
|
|
||||||
|
|
||||||
@pyqtSlot(result = QObject)
|
@pyqtSlot(result = QObject)
|
||||||
def getMaterialManagementModel(self) -> MaterialManagementModel:
|
def getMaterialManagementModel(self) -> MaterialManagementModel:
|
||||||
@ -1186,6 +1218,8 @@ class CuraApplication(QtApplication):
|
|||||||
# Once we're at this point, everything should have been flushed already (past OnExitCallbackManager).
|
# Once we're at this point, everything should have been flushed already (past OnExitCallbackManager).
|
||||||
# It's more difficult to call sys.exit(0): That requires that it happens as the result of a pyqtSignal-emit.
|
# It's more difficult to call sys.exit(0): That requires that it happens as the result of a pyqtSignal-emit.
|
||||||
# (See https://doc.qt.io/qt-6/qcoreapplication.html#quit)
|
# (See https://doc.qt.io/qt-6/qcoreapplication.html#quit)
|
||||||
|
# WARNING: With this in place you CAN NOT use cProfile. You will need to replace the next line with pass
|
||||||
|
# for it to work!
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
return super().event(event)
|
return super().event(event)
|
||||||
@ -1250,6 +1284,7 @@ class CuraApplication(QtApplication):
|
|||||||
self.processEvents()
|
self.processEvents()
|
||||||
engine.rootContext().setContextProperty("Printer", self)
|
engine.rootContext().setContextProperty("Printer", self)
|
||||||
engine.rootContext().setContextProperty("CuraApplication", self)
|
engine.rootContext().setContextProperty("CuraApplication", self)
|
||||||
|
engine.rootContext().setContextProperty("PrintOrderManager", self._print_order_manager)
|
||||||
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
||||||
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
|
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
|
||||||
engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
|
engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
|
||||||
@ -1264,7 +1299,7 @@ class CuraApplication(QtApplication):
|
|||||||
qmlRegisterSingletonType(IntentManager, "Cura", 1, 6, self.getIntentManager, "IntentManager")
|
qmlRegisterSingletonType(IntentManager, "Cura", 1, 6, self.getIntentManager, "IntentManager")
|
||||||
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, self.getSettingInheritanceManager, "SettingInheritanceManager")
|
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, self.getSettingInheritanceManager, "SettingInheritanceManager")
|
||||||
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, self.getSimpleModeSettingsManagerWrapper, "SimpleModeSettingsManager")
|
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, self.getSimpleModeSettingsManagerWrapper, "SimpleModeSettingsManager")
|
||||||
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, self.getMachineActionManagerWrapper, "MachineActionManager")
|
qmlRegisterSingletonType(MachineActionManager, "Cura", 1, 0, self.getMachineActionManagerWrapper, "MachineActionManager")
|
||||||
|
|
||||||
self.processEvents()
|
self.processEvents()
|
||||||
qmlRegisterType(NetworkingUtil, "Cura", 1, 5, "NetworkingUtil")
|
qmlRegisterType(NetworkingUtil, "Cura", 1, 5, "NetworkingUtil")
|
||||||
@ -1425,7 +1460,11 @@ class CuraApplication(QtApplication):
|
|||||||
self._scene_bounding_box = scene_bounding_box
|
self._scene_bounding_box = scene_bounding_box
|
||||||
self.sceneBoundingBoxChanged.emit()
|
self.sceneBoundingBoxChanged.emit()
|
||||||
|
|
||||||
self._platform_activity = True if count > 0 else False
|
if count > 0:
|
||||||
|
self._platform_activity = True
|
||||||
|
else:
|
||||||
|
ProjectOutputDevice.setLastOutputName(None)
|
||||||
|
self._platform_activity = False
|
||||||
self.activityChanged.emit()
|
self.activityChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@ -1745,8 +1784,12 @@ class CuraApplication(QtApplication):
|
|||||||
Selection.remove(node)
|
Selection.remove(node)
|
||||||
Selection.add(group_node)
|
Selection.add(group_node)
|
||||||
|
|
||||||
|
all_nodes = self.getObjectsModel().getNodes()
|
||||||
|
PrintOrderManager.updatePrintOrdersAfterGroupOperation(all_nodes, group_node, selected_nodes)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def ungroupSelected(self) -> None:
|
def ungroupSelected(self) -> None:
|
||||||
|
all_nodes = self.getObjectsModel().getNodes()
|
||||||
selected_objects = Selection.getAllSelectedObjects().copy()
|
selected_objects = Selection.getAllSelectedObjects().copy()
|
||||||
for node in selected_objects:
|
for node in selected_objects:
|
||||||
if node.callDecoration("isGroup"):
|
if node.callDecoration("isGroup"):
|
||||||
@ -1754,21 +1797,30 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
group_parent = node.getParent()
|
group_parent = node.getParent()
|
||||||
children = node.getChildren().copy()
|
children = node.getChildren().copy()
|
||||||
for child in children:
|
|
||||||
# Ungroup only 1 level deep
|
|
||||||
if child.getParent() != node:
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
# Ungroup only 1 level deep
|
||||||
|
children_to_ungroup = list(filter(lambda child: child.getParent() == node, children))
|
||||||
|
for child in children_to_ungroup:
|
||||||
# Set the parent of the children to the parent of the group-node
|
# Set the parent of the children to the parent of the group-node
|
||||||
op.addOperation(SetParentOperation(child, group_parent))
|
op.addOperation(SetParentOperation(child, group_parent))
|
||||||
|
|
||||||
# Add all individual nodes to the selection
|
# Add all individual nodes to the selection
|
||||||
Selection.add(child)
|
Selection.add(child)
|
||||||
|
|
||||||
|
PrintOrderManager.updatePrintOrdersAfterUngroupOperation(all_nodes, node, children_to_ungroup)
|
||||||
op.push()
|
op.push()
|
||||||
# Note: The group removes itself from the scene once all its children have left it,
|
# Note: The group removes itself from the scene once all its children have left it,
|
||||||
# see GroupDecorator._onChildrenChanged
|
# see GroupDecorator._onChildrenChanged
|
||||||
|
|
||||||
|
def _onPrintOrderChanged(self) -> None:
|
||||||
|
# update object list
|
||||||
|
scene = self.getController().getScene()
|
||||||
|
scene.sceneChanged.emit(scene.getRoot())
|
||||||
|
|
||||||
|
# reset if already was sliced
|
||||||
|
Application.getInstance().getBackend().needsSlicing()
|
||||||
|
Application.getInstance().getBackend().tickle()
|
||||||
|
|
||||||
def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
|
def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
|
||||||
if self._is_headless:
|
if self._is_headless:
|
||||||
return None
|
return None
|
||||||
@ -1932,6 +1984,17 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
openProjectFile = pyqtSignal(QUrl, bool, arguments = ["project_file", "add_to_recent_files"]) # Emitted when a project file is about to open.
|
openProjectFile = pyqtSignal(QUrl, bool, arguments = ["project_file", "add_to_recent_files"]) # Emitted when a project file is about to open.
|
||||||
|
|
||||||
|
@pyqtSlot(QUrl, bool)
|
||||||
|
def readLocalUcpFile(self, file: QUrl, add_to_recent_files: bool = True):
|
||||||
|
|
||||||
|
file_name = QUrl(file).toLocalFile()
|
||||||
|
workspace_reader = self.getWorkspaceFileHandler()
|
||||||
|
if workspace_reader is None:
|
||||||
|
Logger.warning(f"Workspace reader not found, cannot read file {file_name}.")
|
||||||
|
return
|
||||||
|
|
||||||
|
workspace_reader.readLocalFile(file, add_to_recent_files)
|
||||||
|
|
||||||
@pyqtSlot(QUrl, str, bool)
|
@pyqtSlot(QUrl, str, bool)
|
||||||
@pyqtSlot(QUrl, str)
|
@pyqtSlot(QUrl, str)
|
||||||
@pyqtSlot(QUrl)
|
@pyqtSlot(QUrl)
|
||||||
@ -2137,6 +2200,12 @@ class CuraApplication(QtApplication):
|
|||||||
def addNonSliceableExtension(self, extension):
|
def addNonSliceableExtension(self, extension):
|
||||||
self._non_sliceable_extensions.append(extension)
|
self._non_sliceable_extensions.append(extension)
|
||||||
|
|
||||||
|
@pyqtSlot(str, result = bool)
|
||||||
|
def isProjectUcp(self, file_url) -> bool:
|
||||||
|
file_path = QUrl(file_url).toLocalFile()
|
||||||
|
workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path)
|
||||||
|
return workspace_reader.getIsProjectUcp()
|
||||||
|
|
||||||
@pyqtSlot(str, result=bool)
|
@pyqtSlot(str, result=bool)
|
||||||
def checkIsValidProjectFile(self, file_url):
|
def checkIsValidProjectFile(self, file_url):
|
||||||
"""Checks if the given file URL is a valid project file. """
|
"""Checks if the given file URL is a valid project file. """
|
||||||
@ -2146,6 +2215,8 @@ class CuraApplication(QtApplication):
|
|||||||
if workspace_reader is None:
|
if workspace_reader is None:
|
||||||
return False # non-project files won't get a reader
|
return False # non-project files won't get a reader
|
||||||
try:
|
try:
|
||||||
|
if workspace_reader.getPluginId() == "3MFReader":
|
||||||
|
workspace_reader.clearOpenAsUcp()
|
||||||
result = workspace_reader.preRead(file_path, show_dialog=False)
|
result = workspace_reader.preRead(file_path, show_dialog=False)
|
||||||
return result == WorkspaceReader.PreReadResult.accepted
|
return result == WorkspaceReader.PreReadResult.accepted
|
||||||
except:
|
except:
|
||||||
|
88
cura/HitChecker.py
Normal file
88
cura/HitChecker.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
from typing import List, Dict
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
|
|
||||||
|
|
||||||
|
class HitChecker:
|
||||||
|
"""Checks if nodes can be printed without causing any collisions and interference"""
|
||||||
|
|
||||||
|
def __init__(self, nodes: List[CuraSceneNode]) -> None:
|
||||||
|
self._hit_map = self._buildHitMap(nodes)
|
||||||
|
|
||||||
|
def anyTwoNodesBlockEachOther(self, nodes: List[CuraSceneNode]) -> bool:
|
||||||
|
"""Returns True if any 2 nodes block each other"""
|
||||||
|
for a in nodes:
|
||||||
|
for b in nodes:
|
||||||
|
if self._hit_map[a][b] and self._hit_map[b][a]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def canPrintBefore(self, node: CuraSceneNode, other_nodes: List[CuraSceneNode]) -> bool:
|
||||||
|
"""Returns True if node doesn't block other_nodes and can be printed before them"""
|
||||||
|
no_hits = all(not self._hit_map[node][other_node] for other_node in other_nodes)
|
||||||
|
return no_hits
|
||||||
|
|
||||||
|
def canPrintAfter(self, node: CuraSceneNode, other_nodes: List[CuraSceneNode]) -> bool:
|
||||||
|
"""Returns True if node doesn't hit other nodes and can be printed after them"""
|
||||||
|
no_hits = all(not self._hit_map[other_node][node] for other_node in other_nodes)
|
||||||
|
return no_hits
|
||||||
|
|
||||||
|
def calculateScore(self, a: CuraSceneNode, b: CuraSceneNode) -> int:
|
||||||
|
"""Calculate score simply sums the number of other objects it 'blocks'
|
||||||
|
|
||||||
|
:param a: node
|
||||||
|
:param b: node
|
||||||
|
:return: sum of the number of other objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
score_a = sum(self._hit_map[a].values())
|
||||||
|
score_b = sum(self._hit_map[b].values())
|
||||||
|
return score_a - score_b
|
||||||
|
|
||||||
|
def canPrintNodesInProvidedOrder(self, ordered_nodes: List[CuraSceneNode]) -> bool:
|
||||||
|
"""Returns True If nodes don't have any hits in provided order"""
|
||||||
|
for node_index, node in enumerate(ordered_nodes):
|
||||||
|
nodes_before = ordered_nodes[:node_index - 1] if node_index - 1 >= 0 else []
|
||||||
|
nodes_after = ordered_nodes[node_index + 1:] if node_index + 1 < len(ordered_nodes) else []
|
||||||
|
if not self.canPrintBefore(node, nodes_after) or not self.canPrintAfter(node, nodes_before):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _buildHitMap(nodes: List[CuraSceneNode]) -> Dict[CuraSceneNode, CuraSceneNode]:
|
||||||
|
"""Pre-computes all hits between all objects
|
||||||
|
|
||||||
|
:nodes: nodes that need to be checked for collisions
|
||||||
|
:return: dictionary where hit_map[node1][node2] is False if there node1 can be printed before node2
|
||||||
|
"""
|
||||||
|
hit_map = {j: {i: HitChecker._checkHit(j, i) for i in nodes} for j in nodes}
|
||||||
|
return hit_map
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _checkHit(a: CuraSceneNode, b: CuraSceneNode) -> bool:
|
||||||
|
"""Checks if a can be printed before b
|
||||||
|
|
||||||
|
:param a: node
|
||||||
|
:param b: node
|
||||||
|
:return: False if a can be printed before b
|
||||||
|
"""
|
||||||
|
|
||||||
|
if a == b:
|
||||||
|
return False
|
||||||
|
|
||||||
|
a_hit_hull = a.callDecoration("getConvexHullBoundary")
|
||||||
|
b_hit_hull = b.callDecoration("getConvexHullHeadFull")
|
||||||
|
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
|
||||||
|
|
||||||
|
if overlap:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Adhesion areas must never overlap, regardless of printing order
|
||||||
|
# This would cause over-extrusion
|
||||||
|
a_hit_hull = a.callDecoration("getAdhesionArea")
|
||||||
|
b_hit_hull = b.callDecoration("getAdhesionArea")
|
||||||
|
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
|
||||||
|
|
||||||
|
if overlap:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
@ -49,7 +49,7 @@ class MachineErrorChecker(QObject):
|
|||||||
|
|
||||||
self._keys_to_check = set() # type: Set[str]
|
self._keys_to_check = set() # type: Set[str]
|
||||||
|
|
||||||
self._num_keys_to_check_per_update = 10
|
self._num_keys_to_check_per_update = 1
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
self._error_check_timer.timeout.connect(self._rescheduleCheck)
|
self._error_check_timer.timeout.connect(self._rescheduleCheck)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2024 UltiMaker
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from UM.Decorators import deprecated
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
from UM.Util import parseBool
|
from UM.Util import parseBool
|
||||||
@ -168,13 +169,18 @@ class MachineNode(ContainerNode):
|
|||||||
|
|
||||||
return self.global_qualities.get(self.preferred_quality_type, next(iter(self.global_qualities.values())))
|
return self.global_qualities.get(self.preferred_quality_type, next(iter(self.global_qualities.values())))
|
||||||
|
|
||||||
def isExcludedMaterial(self, material: MaterialNode) -> bool:
|
def isExcludedMaterialBaseFile(self, material_base_file: str) -> bool:
|
||||||
"""Returns whether the material should be excluded from the list of materials."""
|
"""Returns whether the material should be excluded from the list of materials."""
|
||||||
for exclude_material in self.exclude_materials:
|
for exclude_material in self.exclude_materials:
|
||||||
if exclude_material in material["id"]:
|
if exclude_material in material_base_file:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@deprecated("Use isExcludedMaterialBaseFile instead.", since = "5.9.0")
|
||||||
|
def isExcludedMaterial(self, material: MaterialNode) -> bool:
|
||||||
|
"""Returns whether the material should be excluded from the list of materials."""
|
||||||
|
return self.isExcludedMaterialBaseFile(material.base_file)
|
||||||
|
|
||||||
@UM.FlameProfiler.profile
|
@UM.FlameProfiler.profile
|
||||||
def _loadAll(self) -> None:
|
def _loadAll(self) -> None:
|
||||||
"""(Re)loads all variants under this printer."""
|
"""(Re)loads all variants under this printer."""
|
||||||
|
@ -21,18 +21,25 @@ class MaterialNode(ContainerNode):
|
|||||||
Its subcontainers are quality profiles.
|
Its subcontainers are quality profiles.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, container_id: str, variant: "VariantNode") -> None:
|
def __init__(self, container_id: str, variant: "VariantNode", *, container: ContainerInterface = None) -> None:
|
||||||
super().__init__(container_id)
|
super().__init__(container_id)
|
||||||
self.variant = variant
|
self.variant = variant
|
||||||
self.qualities = {} # type: Dict[str, QualityNode] # Mapping container IDs to quality profiles.
|
self.qualities = {} # type: Dict[str, QualityNode] # Mapping container IDs to quality profiles.
|
||||||
self.materialChanged = Signal() # Triggered when the material is removed or its metadata is updated.
|
self.materialChanged = Signal() # Triggered when the material is removed or its metadata is updated.
|
||||||
|
|
||||||
container_registry = ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
my_metadata = container_registry.findContainersMetadata(id = container_id)[0]
|
|
||||||
self.base_file = my_metadata["base_file"]
|
if container is not None:
|
||||||
self.material_type = my_metadata["material"]
|
self.base_file = container.getMetaDataEntry("base_file")
|
||||||
self.brand = my_metadata["brand"]
|
self.material_type = container.getMetaDataEntry("material")
|
||||||
self.guid = my_metadata["GUID"]
|
self.brand = container.getMetaDataEntry("brand")
|
||||||
|
self.guid = container.getMetaDataEntry("GUID")
|
||||||
|
else:
|
||||||
|
my_metadata = container_registry.findContainersMetadata(id = container_id)[0]
|
||||||
|
self.base_file = my_metadata["base_file"]
|
||||||
|
self.material_type = my_metadata["material"]
|
||||||
|
self.brand = my_metadata["brand"]
|
||||||
|
self.guid = my_metadata["GUID"]
|
||||||
self._loadAll()
|
self._loadAll()
|
||||||
container_registry.containerRemoved.connect(self._onRemoved)
|
container_registry.containerRemoved.connect(self._onRemoved)
|
||||||
container_registry.containerMetaDataChanged.connect(self._onMetadataChanged)
|
container_registry.containerMetaDataChanged.connect(self._onMetadataChanged)
|
||||||
|
@ -54,10 +54,7 @@ class ActiveIntentQualitiesModel(ListModel):
|
|||||||
self._updateDelayed()
|
self._updateDelayed()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
active_extruder_stack = cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeStack
|
self._intent_category = IntentManager.getInstance().currentIntentCategory
|
||||||
if active_extruder_stack:
|
|
||||||
self._intent_category = active_extruder_stack.intent.getMetaDataEntry("intent_category", "")
|
|
||||||
|
|
||||||
new_items: List[Dict[str, Any]] = []
|
new_items: List[Dict[str, Any]] = []
|
||||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
|
@ -24,6 +24,10 @@ intent_translations["quick"] = {
|
|||||||
"name": catalog.i18nc("@label", "Draft"),
|
"name": catalog.i18nc("@label", "Draft"),
|
||||||
"description": catalog.i18nc("@text", "The draft profile is designed to print initial prototypes and concept validation with the intent of significant print time reduction.")
|
"description": catalog.i18nc("@text", "The draft profile is designed to print initial prototypes and concept validation with the intent of significant print time reduction.")
|
||||||
}
|
}
|
||||||
|
intent_translations["annealing"] = {
|
||||||
|
"name": catalog.i18nc("@label", "Annealing"),
|
||||||
|
"description": catalog.i18nc("@text", "The annealing profile requires post-processing in an oven after the print is finished. This profile retains the dimensional accuracy of the printed part after annealing and improves strength, stiffness, and thermal resistance.")
|
||||||
|
}
|
||||||
intent_translations["solid"] = {
|
intent_translations["solid"] = {
|
||||||
"name": catalog.i18nc("@label", "Solid"),
|
"name": catalog.i18nc("@label", "Solid"),
|
||||||
"description": catalog.i18nc("@text",
|
"description": catalog.i18nc("@text",
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
# online cloud connected printers are represented within this ListModel. Additional information such as the number of
|
# online cloud connected printers are represented within this ListModel. Additional information such as the number of
|
||||||
# connected printers for each printer type is gathered.
|
# connected printers for each printer type is gathered.
|
||||||
|
|
||||||
from typing import Optional, List, cast
|
from typing import Optional, List, cast, Dict, Any
|
||||||
|
|
||||||
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||||
|
|
||||||
@ -30,10 +30,10 @@ class MachineListModel(ListModel):
|
|||||||
ComponentTypeRole = Qt.ItemDataRole.UserRole + 8
|
ComponentTypeRole = Qt.ItemDataRole.UserRole + 8
|
||||||
IsNetworkedMachineRole = Qt.ItemDataRole.UserRole + 9
|
IsNetworkedMachineRole = Qt.ItemDataRole.UserRole + 9
|
||||||
|
|
||||||
def __init__(self, parent: Optional[QObject] = None, machines_filter: List[GlobalStack] = None, listenToChanges: bool = True) -> None:
|
def __init__(self, parent: Optional[QObject] = None, machines_filter: List[GlobalStack] = None, listenToChanges: bool = True, showCloudPrinters: bool = False) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._show_cloud_printers = False
|
self._show_cloud_printers = showCloudPrinters
|
||||||
self._machines_filter = machines_filter
|
self._machines_filter = machines_filter
|
||||||
|
|
||||||
self._catalog = i18nCatalog("cura")
|
self._catalog = i18nCatalog("cura")
|
||||||
@ -159,3 +159,8 @@ class MachineListModel(ListModel):
|
|||||||
"machineCount": machine_count,
|
"machineCount": machine_count,
|
||||||
"catergory": "connected" if is_online else "other",
|
"catergory": "connected" if is_online else "other",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def getItems(self) -> Dict[str, Any]:
|
||||||
|
if self.count > 0:
|
||||||
|
return self.items
|
||||||
|
return {}
|
@ -60,7 +60,7 @@ class VariantNode(ContainerNode):
|
|||||||
materials = list(materials_per_base_file.values())
|
materials = list(materials_per_base_file.values())
|
||||||
|
|
||||||
# Filter materials based on the exclude_materials property.
|
# Filter materials based on the exclude_materials property.
|
||||||
filtered_materials = [material for material in materials if not self.machine.isExcludedMaterial(material)]
|
filtered_materials = [material for material in materials if not self.machine.isExcludedMaterialBaseFile(material["id"])]
|
||||||
|
|
||||||
for material in filtered_materials:
|
for material in filtered_materials:
|
||||||
base_file = material["base_file"]
|
base_file = material["base_file"]
|
||||||
@ -127,7 +127,7 @@ class VariantNode(ContainerNode):
|
|||||||
material_definition = container.getMetaDataEntry("definition")
|
material_definition = container.getMetaDataEntry("definition")
|
||||||
|
|
||||||
base_file = container.getMetaDataEntry("base_file")
|
base_file = container.getMetaDataEntry("base_file")
|
||||||
if base_file in self.machine.exclude_materials:
|
if self.machine.isExcludedMaterialBaseFile(base_file):
|
||||||
return # Material is forbidden for this printer.
|
return # Material is forbidden for this printer.
|
||||||
if base_file not in self.materials: # Completely new base file. Always better than not having a file as long as it matches our set-up.
|
if base_file not in self.materials: # Completely new base file. Always better than not having a file as long as it matches our set-up.
|
||||||
if material_definition != "fdmprinter" and material_definition != self.machine.container_id:
|
if material_definition != "fdmprinter" and material_definition != self.machine.container_id:
|
||||||
@ -148,7 +148,7 @@ class VariantNode(ContainerNode):
|
|||||||
|
|
||||||
if "empty_material" in self.materials:
|
if "empty_material" in self.materials:
|
||||||
del self.materials["empty_material"]
|
del self.materials["empty_material"]
|
||||||
self.materials[base_file] = MaterialNode(container.getId(), variant = self)
|
self.materials[base_file] = MaterialNode(container.getId(), variant = self, container = container)
|
||||||
self.materials[base_file].materialChanged.connect(self.materialsChanged)
|
self.materials[base_file].materialChanged.connect(self.materialsChanged)
|
||||||
self.materialsChanged.emit(self.materials[base_file])
|
self.materialsChanged.emit(self.materials[base_file])
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To downlo
|
|||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
|
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
REQUEST_TIMEOUT = 5 # Seconds
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationHelpers:
|
class AuthorizationHelpers:
|
||||||
@ -53,7 +54,8 @@ class AuthorizationHelpers:
|
|||||||
data = urllib.parse.urlencode(data).encode("UTF-8"),
|
data = urllib.parse.urlencode(data).encode("UTF-8"),
|
||||||
headers_dict = headers,
|
headers_dict = headers,
|
||||||
callback = lambda response: self.parseTokenResponse(response, callback),
|
callback = lambda response: self.parseTokenResponse(response, callback),
|
||||||
error_callback = lambda response, _: self.parseTokenResponse(response, callback)
|
error_callback = lambda response, _: self.parseTokenResponse(response, callback),
|
||||||
|
timeout = REQUEST_TIMEOUT
|
||||||
)
|
)
|
||||||
|
|
||||||
def getAccessTokenUsingRefreshToken(self, refresh_token: str, callback: Callable[[AuthenticationResponse], None]) -> None:
|
def getAccessTokenUsingRefreshToken(self, refresh_token: str, callback: Callable[[AuthenticationResponse], None]) -> None:
|
||||||
@ -77,7 +79,9 @@ class AuthorizationHelpers:
|
|||||||
data = urllib.parse.urlencode(data).encode("UTF-8"),
|
data = urllib.parse.urlencode(data).encode("UTF-8"),
|
||||||
headers_dict = headers,
|
headers_dict = headers,
|
||||||
callback = lambda response: self.parseTokenResponse(response, callback),
|
callback = lambda response: self.parseTokenResponse(response, callback),
|
||||||
error_callback = lambda response, _: self.parseTokenResponse(response, callback)
|
error_callback = lambda response, _: self.parseTokenResponse(response, callback),
|
||||||
|
urgent = True,
|
||||||
|
timeout = REQUEST_TIMEOUT
|
||||||
)
|
)
|
||||||
|
|
||||||
def parseTokenResponse(self, token_response: QNetworkReply, callback: Callable[[AuthenticationResponse], None]) -> None:
|
def parseTokenResponse(self, token_response: QNetworkReply, callback: Callable[[AuthenticationResponse], None]) -> None:
|
||||||
@ -92,7 +96,7 @@ class AuthorizationHelpers:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if token_response.error() != QNetworkReply.NetworkError.NoError:
|
if token_response.error() != QNetworkReply.NetworkError.NoError:
|
||||||
callback(AuthenticationResponse(success = False, err_message = token_data["error_description"]))
|
callback(AuthenticationResponse(success = False, err_message = token_data.get("error_description", "an unknown server error occurred")))
|
||||||
return
|
return
|
||||||
|
|
||||||
callback(AuthenticationResponse(success = True,
|
callback(AuthenticationResponse(success = True,
|
||||||
@ -122,7 +126,8 @@ class AuthorizationHelpers:
|
|||||||
check_token_url,
|
check_token_url,
|
||||||
headers_dict = headers,
|
headers_dict = headers,
|
||||||
callback = lambda reply: self._parseUserProfile(reply, success_callback, failed_callback),
|
callback = lambda reply: self._parseUserProfile(reply, success_callback, failed_callback),
|
||||||
error_callback = lambda _, _2: failed_callback() if failed_callback is not None else None
|
error_callback = lambda _, _2: failed_callback() if failed_callback is not None else None,
|
||||||
|
timeout = REQUEST_TIMEOUT
|
||||||
)
|
)
|
||||||
|
|
||||||
def _parseUserProfile(self, reply: QNetworkReply, success_callback: Optional[Callable[[UserProfile], None]], failed_callback: Optional[Callable[[], None]] = None) -> None:
|
def _parseUserProfile(self, reply: QNetworkReply, success_callback: Optional[Callable[[UserProfile], None]], failed_callback: Optional[Callable[[], None]] = None) -> None:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2021 Ultimaker B.V.
|
# Copyright (c) 2024 UltiMaker
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@ -6,13 +6,14 @@ from datetime import datetime, timedelta
|
|||||||
from typing import Callable, Dict, Optional, TYPE_CHECKING, Union
|
from typing import Callable, Dict, Optional, TYPE_CHECKING, Union
|
||||||
from urllib.parse import urlencode, quote_plus
|
from urllib.parse import urlencode, quote_plus
|
||||||
|
|
||||||
from PyQt6.QtCore import QUrl
|
from PyQt6.QtCore import QUrl, QTimer
|
||||||
from PyQt6.QtGui import QDesktopServices
|
from PyQt6.QtGui import QDesktopServices
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To download log-in tokens.
|
||||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
|
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
|
||||||
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
|
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
|
||||||
from cura.OAuth2.Models import AuthenticationResponse, BaseModel
|
from cura.OAuth2.Models import AuthenticationResponse, BaseModel
|
||||||
@ -25,6 +26,8 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
MYCLOUD_LOGOFF_URL = "https://account.ultimaker.com/logoff?utm_source=cura&utm_medium=software&utm_campaign=change-account-before-adding-printers"
|
MYCLOUD_LOGOFF_URL = "https://account.ultimaker.com/logoff?utm_source=cura&utm_medium=software&utm_campaign=change-account-before-adding-printers"
|
||||||
|
|
||||||
|
REFRESH_TOKEN_MAX_RETRIES = 15
|
||||||
|
REFRESH_TOKEN_RETRY_INTERVAL = 1000
|
||||||
|
|
||||||
class AuthorizationService:
|
class AuthorizationService:
|
||||||
"""The authorization service is responsible for handling the login flow, storing user credentials and providing
|
"""The authorization service is responsible for handling the login flow, storing user credentials and providing
|
||||||
@ -57,6 +60,12 @@ class AuthorizationService:
|
|||||||
|
|
||||||
self.onAuthStateChanged.connect(self._authChanged)
|
self.onAuthStateChanged.connect(self._authChanged)
|
||||||
|
|
||||||
|
self._refresh_token_retries = 0
|
||||||
|
self._refresh_token_retry_timer = QTimer()
|
||||||
|
self._refresh_token_retry_timer.setInterval(REFRESH_TOKEN_RETRY_INTERVAL)
|
||||||
|
self._refresh_token_retry_timer.setSingleShot(True)
|
||||||
|
self._refresh_token_retry_timer.timeout.connect(self.refreshAccessToken)
|
||||||
|
|
||||||
def _authChanged(self, logged_in):
|
def _authChanged(self, logged_in):
|
||||||
if logged_in and self._unable_to_get_data_message is not None:
|
if logged_in and self._unable_to_get_data_message is not None:
|
||||||
self._unable_to_get_data_message.hide()
|
self._unable_to_get_data_message.hide()
|
||||||
@ -167,16 +176,29 @@ class AuthorizationService:
|
|||||||
return
|
return
|
||||||
|
|
||||||
def process_auth_data(response: AuthenticationResponse) -> None:
|
def process_auth_data(response: AuthenticationResponse) -> None:
|
||||||
|
self._currently_refreshing_token = False
|
||||||
|
|
||||||
if response.success:
|
if response.success:
|
||||||
|
self._refresh_token_retries = 0
|
||||||
self._storeAuthData(response)
|
self._storeAuthData(response)
|
||||||
|
HttpRequestManager.getInstance().setDelayRequests(False)
|
||||||
self.onAuthStateChanged.emit(logged_in = True)
|
self.onAuthStateChanged.emit(logged_in = True)
|
||||||
else:
|
else:
|
||||||
Logger.warning("Failed to get a new access token from the server.")
|
if self._refresh_token_retries >= REFRESH_TOKEN_MAX_RETRIES:
|
||||||
self.onAuthStateChanged.emit(logged_in = False)
|
self._refresh_token_retries = 0
|
||||||
|
Logger.warning("Failed to get a new access token from the server, giving up.")
|
||||||
|
HttpRequestManager.getInstance().setDelayRequests(False)
|
||||||
|
self.onAuthStateChanged.emit(logged_in = False)
|
||||||
|
else:
|
||||||
|
# Retry a bit later, network may be offline right now and will hopefully be back soon
|
||||||
|
Logger.warning("Failed to get a new access token from the server, retrying later.")
|
||||||
|
self._refresh_token_retries += 1
|
||||||
|
self._refresh_token_retry_timer.start()
|
||||||
|
|
||||||
if self._currently_refreshing_token:
|
if self._currently_refreshing_token:
|
||||||
Logger.debug("Was already busy refreshing token. Do not start a new request.")
|
Logger.debug("Was already busy refreshing token. Do not start a new request.")
|
||||||
return
|
return
|
||||||
|
HttpRequestManager.getInstance().setDelayRequests(True)
|
||||||
self._currently_refreshing_token = True
|
self._currently_refreshing_token = True
|
||||||
self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token, process_auth_data)
|
self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token, process_auth_data)
|
||||||
|
|
||||||
@ -283,7 +305,8 @@ class AuthorizationService:
|
|||||||
message_type = Message.MessageType.ERROR)
|
message_type = Message.MessageType.ERROR)
|
||||||
Logger.warning("Unable to get user profile using auth data from preferences.")
|
Logger.warning("Unable to get user profile using auth data from preferences.")
|
||||||
self._unable_to_get_data_message.show()
|
self._unable_to_get_data_message.show()
|
||||||
self.getUserProfile(callback)
|
if self._get_user_profile:
|
||||||
|
self.getUserProfile(callback)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
Logger.logException("w", "Could not load auth data from preferences")
|
Logger.logException("w", "Could not load auth data from preferences")
|
||||||
|
|
||||||
|
@ -7,6 +7,11 @@ from UM.Scene.Iterator import Iterator
|
|||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from functools import cmp_to_key
|
from functools import cmp_to_key
|
||||||
|
|
||||||
|
from cura.HitChecker import HitChecker
|
||||||
|
from cura.PrintOrderManager import PrintOrderManager
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
|
|
||||||
|
|
||||||
class OneAtATimeIterator(Iterator.Iterator):
|
class OneAtATimeIterator(Iterator.Iterator):
|
||||||
"""Iterator that returns a list of nodes in the order that they need to be printed
|
"""Iterator that returns a list of nodes in the order that they need to be printed
|
||||||
|
|
||||||
@ -16,8 +21,6 @@ class OneAtATimeIterator(Iterator.Iterator):
|
|||||||
|
|
||||||
def __init__(self, scene_node) -> None:
|
def __init__(self, scene_node) -> None:
|
||||||
super().__init__(scene_node) # Call super to make multiple inheritance work.
|
super().__init__(scene_node) # Call super to make multiple inheritance work.
|
||||||
self._hit_map = [[]] # type: List[List[bool]] # For each node, which other nodes this hits. A grid of booleans on which nodes hit which.
|
|
||||||
self._original_node_list = [] # type: List[SceneNode] # The nodes that need to be checked for collisions.
|
|
||||||
|
|
||||||
def _fillStack(self) -> None:
|
def _fillStack(self) -> None:
|
||||||
"""Fills the ``_node_stack`` with a list of scene nodes that need to be printed in order. """
|
"""Fills the ``_node_stack`` with a list of scene nodes that need to be printed in order. """
|
||||||
@ -38,104 +41,50 @@ class OneAtATimeIterator(Iterator.Iterator):
|
|||||||
self._node_stack = node_list[:]
|
self._node_stack = node_list[:]
|
||||||
return
|
return
|
||||||
|
|
||||||
# Copy the list
|
hit_checker = HitChecker(node_list)
|
||||||
self._original_node_list = node_list[:]
|
|
||||||
|
|
||||||
# Initialise the hit map (pre-compute all hits between all objects)
|
if PrintOrderManager.isUserDefinedPrintOrderEnabled():
|
||||||
self._hit_map = [[self._checkHit(i, j) for i in node_list] for j in node_list]
|
self._node_stack = self._getNodesOrderedByUser(hit_checker, node_list)
|
||||||
|
else:
|
||||||
|
self._node_stack = self._getNodesOrderedAutomatically(hit_checker, node_list)
|
||||||
|
|
||||||
# Check if we have to files that block each other. If this is the case, there is no solution!
|
# update print orders so that user can try to arrange the nodes automatically first
|
||||||
for a in range(0, len(node_list)):
|
# and if result is not satisfactory he/she can switch to manual mode and change it
|
||||||
for b in range(0, len(node_list)):
|
for index, node in enumerate(self._node_stack):
|
||||||
if a != b and self._hit_map[a][b] and self._hit_map[b][a]:
|
node.printOrder = index + 1
|
||||||
return
|
|
||||||
|
@staticmethod
|
||||||
|
def _getNodesOrderedByUser(hit_checker: HitChecker, node_list: List[CuraSceneNode]) -> List[CuraSceneNode]:
|
||||||
|
nodes_ordered_by_user = sorted(node_list, key=lambda n: n.printOrder)
|
||||||
|
if hit_checker.canPrintNodesInProvidedOrder(nodes_ordered_by_user):
|
||||||
|
return nodes_ordered_by_user
|
||||||
|
return [] # No solution
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _getNodesOrderedAutomatically(hit_checker: HitChecker, node_list: List[CuraSceneNode]) -> List[CuraSceneNode]:
|
||||||
|
# Check if we have two files that block each other. If this is the case, there is no solution!
|
||||||
|
if hit_checker.anyTwoNodesBlockEachOther(node_list):
|
||||||
|
return [] # No solution
|
||||||
|
|
||||||
# Sort the original list so that items that block the most other objects are at the beginning.
|
# Sort the original list so that items that block the most other objects are at the beginning.
|
||||||
# This does not decrease the worst case running time, but should improve it in most cases.
|
# This does not decrease the worst case running time, but should improve it in most cases.
|
||||||
sorted(node_list, key = cmp_to_key(self._calculateScore))
|
node_list = sorted(node_list, key = cmp_to_key(hit_checker.calculateScore))
|
||||||
|
|
||||||
todo_node_list = [_ObjectOrder([], node_list)]
|
todo_node_list = [_ObjectOrder([], node_list)]
|
||||||
while len(todo_node_list) > 0:
|
while len(todo_node_list) > 0:
|
||||||
current = todo_node_list.pop()
|
current = todo_node_list.pop()
|
||||||
for node in current.todo:
|
for node in current.todo:
|
||||||
# Check if the object can be placed with what we have and still allows for a solution in the future
|
# Check if the object can be placed with what we have and still allows for a solution in the future
|
||||||
if not self._checkHitMultiple(node, current.order) and not self._checkBlockMultiple(node, current.todo):
|
if hit_checker.canPrintAfter(node, current.order) and hit_checker.canPrintBefore(node, current.todo):
|
||||||
# We found a possible result. Create new todo & order list.
|
# We found a possible result. Create new todo & order list.
|
||||||
new_todo_list = current.todo[:]
|
new_todo_list = current.todo[:]
|
||||||
new_todo_list.remove(node)
|
new_todo_list.remove(node)
|
||||||
new_order = current.order[:] + [node]
|
new_order = current.order[:] + [node]
|
||||||
if len(new_todo_list) == 0:
|
if len(new_todo_list) == 0:
|
||||||
# We have no more nodes to check, so quit looking.
|
# We have no more nodes to check, so quit looking.
|
||||||
self._node_stack = new_order
|
return new_order # Solution found!
|
||||||
return
|
|
||||||
todo_node_list.append(_ObjectOrder(new_order, new_todo_list))
|
todo_node_list.append(_ObjectOrder(new_order, new_todo_list))
|
||||||
self._node_stack = [] #No result found!
|
return [] # No result found!
|
||||||
|
|
||||||
|
|
||||||
# Check if first object can be printed before the provided list (using the hit map)
|
|
||||||
def _checkHitMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool:
|
|
||||||
node_index = self._original_node_list.index(node)
|
|
||||||
for other_node in other_nodes:
|
|
||||||
other_node_index = self._original_node_list.index(other_node)
|
|
||||||
if self._hit_map[node_index][other_node_index]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _checkBlockMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool:
|
|
||||||
"""Check for a node whether it hits any of the other nodes.
|
|
||||||
|
|
||||||
:param node: The node to check whether it collides with the other nodes.
|
|
||||||
:param other_nodes: The nodes to check for collisions.
|
|
||||||
:return: returns collision between nodes
|
|
||||||
"""
|
|
||||||
|
|
||||||
node_index = self._original_node_list.index(node)
|
|
||||||
for other_node in other_nodes:
|
|
||||||
other_node_index = self._original_node_list.index(other_node)
|
|
||||||
if self._hit_map[other_node_index][node_index] and node_index != other_node_index:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _calculateScore(self, a: SceneNode, b: SceneNode) -> int:
|
|
||||||
"""Calculate score simply sums the number of other objects it 'blocks'
|
|
||||||
|
|
||||||
:param a: node
|
|
||||||
:param b: node
|
|
||||||
:return: sum of the number of other objects
|
|
||||||
"""
|
|
||||||
|
|
||||||
score_a = sum(self._hit_map[self._original_node_list.index(a)])
|
|
||||||
score_b = sum(self._hit_map[self._original_node_list.index(b)])
|
|
||||||
return score_a - score_b
|
|
||||||
|
|
||||||
def _checkHit(self, a: SceneNode, b: SceneNode) -> bool:
|
|
||||||
"""Checks if a can be printed before b
|
|
||||||
|
|
||||||
:param a: node
|
|
||||||
:param b: node
|
|
||||||
:return: true if a can be printed before b
|
|
||||||
"""
|
|
||||||
|
|
||||||
if a == b:
|
|
||||||
return False
|
|
||||||
|
|
||||||
a_hit_hull = a.callDecoration("getConvexHullBoundary")
|
|
||||||
b_hit_hull = b.callDecoration("getConvexHullHeadFull")
|
|
||||||
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
|
|
||||||
|
|
||||||
if overlap:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Adhesion areas must never overlap, regardless of printing order
|
|
||||||
# This would cause over-extrusion
|
|
||||||
a_hit_hull = a.callDecoration("getAdhesionArea")
|
|
||||||
b_hit_hull = b.callDecoration("getAdhesionArea")
|
|
||||||
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
|
|
||||||
|
|
||||||
if overlap:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class _ObjectOrder:
|
class _ObjectOrder:
|
||||||
|
@ -39,6 +39,11 @@ class PlatformPhysics:
|
|||||||
|
|
||||||
Application.getInstance().getPreferences().addPreference("physics/automatic_push_free", False)
|
Application.getInstance().getPreferences().addPreference("physics/automatic_push_free", False)
|
||||||
Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True)
|
Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True)
|
||||||
|
self._app_all_model_drop = False
|
||||||
|
|
||||||
|
def setAppAllModelDropDown(self):
|
||||||
|
self._app_all_model_drop = True
|
||||||
|
self._onChangeTimerFinished()
|
||||||
|
|
||||||
def _onSceneChanged(self, source):
|
def _onSceneChanged(self, source):
|
||||||
if not source.callDecoration("isSliceable"):
|
if not source.callDecoration("isSliceable"):
|
||||||
@ -80,9 +85,9 @@ class PlatformPhysics:
|
|||||||
# Move it downwards if bottom is above platform
|
# Move it downwards if bottom is above platform
|
||||||
move_vector = Vector()
|
move_vector = Vector()
|
||||||
|
|
||||||
if node.getSetting(SceneNodeSettings.AutoDropDown, app_automatic_drop_down) and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down
|
if (node.getSetting(SceneNodeSettings.AutoDropDown, app_automatic_drop_down) or self._app_all_model_drop) and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled():
|
||||||
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
|
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
|
||||||
move_vector = move_vector.set(y = -bbox.bottom + z_offset)
|
move_vector = move_vector.set(y=-bbox.bottom + z_offset)
|
||||||
|
|
||||||
# If there is no convex hull for the node, start calculating it and continue.
|
# If there is no convex hull for the node, start calculating it and continue.
|
||||||
if not node.getDecorator(ConvexHullDecorator) and not node.callDecoration("isNonPrintingMesh") and node.callDecoration("getLayerData") is None:
|
if not node.getDecorator(ConvexHullDecorator) and not node.callDecoration("isNonPrintingMesh") and node.callDecoration("getLayerData") is None:
|
||||||
@ -168,6 +173,8 @@ class PlatformPhysics:
|
|||||||
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
|
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
|
||||||
op.push()
|
op.push()
|
||||||
|
|
||||||
|
# setting this drop to model same as app_automatic_drop_down
|
||||||
|
self._app_all_model_drop = False
|
||||||
# After moving, we have to evaluate the boundary checks for nodes
|
# After moving, we have to evaluate the boundary checks for nodes
|
||||||
build_volume.updateNodeBoundaryCheck()
|
build_volume.updateNodeBoundaryCheck()
|
||||||
|
|
||||||
|
174
cura/PrintOrderManager.py
Normal file
174
cura/PrintOrderManager.py
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
from typing import List, Callable, Optional, Any
|
||||||
|
|
||||||
|
from PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Scene.Selection import Selection
|
||||||
|
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
|
|
||||||
|
|
||||||
|
class PrintOrderManager(QObject):
|
||||||
|
"""Allows to order the object list to set the print sequence manually"""
|
||||||
|
|
||||||
|
def __init__(self, get_nodes: Callable[[], List[CuraSceneNode]]) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._get_nodes = get_nodes
|
||||||
|
self._configureEvents()
|
||||||
|
|
||||||
|
_settingsChanged = pyqtSignal()
|
||||||
|
_uiActionsOutdated = pyqtSignal()
|
||||||
|
printOrderChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def swapSelectedAndPreviousNodes(self) -> None:
|
||||||
|
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
|
||||||
|
self._swapPrintOrders(selected_node, previous_node)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def swapSelectedAndNextNodes(self) -> None:
|
||||||
|
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
|
||||||
|
self._swapPrintOrders(selected_node, next_node)
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=_uiActionsOutdated)
|
||||||
|
def previousNodeName(self) -> str:
|
||||||
|
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
|
||||||
|
return self._getNodeName(previous_node)
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=_uiActionsOutdated)
|
||||||
|
def nextNodeName(self) -> str:
|
||||||
|
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
|
||||||
|
return self._getNodeName(next_node)
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=_uiActionsOutdated)
|
||||||
|
def shouldEnablePrintBeforeAction(self) -> bool:
|
||||||
|
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
|
||||||
|
can_swap_with_previous_node = selected_node is not None and previous_node is not None
|
||||||
|
return can_swap_with_previous_node
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=_uiActionsOutdated)
|
||||||
|
def shouldEnablePrintAfterAction(self) -> bool:
|
||||||
|
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
|
||||||
|
can_swap_with_next_node = selected_node is not None and next_node is not None
|
||||||
|
return can_swap_with_next_node
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=_settingsChanged)
|
||||||
|
def shouldShowEditPrintOrderActions(self) -> bool:
|
||||||
|
return PrintOrderManager.isUserDefinedPrintOrderEnabled()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def isUserDefinedPrintOrderEnabled() -> bool:
|
||||||
|
stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
is_enabled = stack and \
|
||||||
|
stack.getProperty("print_sequence", "value") == "one_at_a_time" and \
|
||||||
|
stack.getProperty("user_defined_print_order_enabled", "value")
|
||||||
|
return bool(is_enabled)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def initializePrintOrders(nodes: List[CuraSceneNode]) -> None:
|
||||||
|
"""Just created (loaded from file) nodes have print order 0.
|
||||||
|
|
||||||
|
This method initializes print orders with max value to put nodes at the end of object list"""
|
||||||
|
max_print_order = max(map(lambda n: n.printOrder, nodes), default=0)
|
||||||
|
for node in nodes:
|
||||||
|
if node.printOrder == 0:
|
||||||
|
max_print_order += 1
|
||||||
|
node.printOrder = max_print_order
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def updatePrintOrdersAfterGroupOperation(
|
||||||
|
all_nodes: List[CuraSceneNode],
|
||||||
|
group_node: CuraSceneNode,
|
||||||
|
grouped_nodes: List[CuraSceneNode]
|
||||||
|
) -> None:
|
||||||
|
group_node.printOrder = min(map(lambda n: n.printOrder, grouped_nodes))
|
||||||
|
|
||||||
|
all_nodes.append(group_node)
|
||||||
|
for node in grouped_nodes:
|
||||||
|
all_nodes.remove(node)
|
||||||
|
|
||||||
|
# reassign print orders so there won't be gaps like 1 2 5 6 7
|
||||||
|
sorted_nodes = sorted(all_nodes, key=lambda n: n.printOrder)
|
||||||
|
for i, node in enumerate(sorted_nodes):
|
||||||
|
node.printOrder = i + 1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def updatePrintOrdersAfterUngroupOperation(
|
||||||
|
all_nodes: List[CuraSceneNode],
|
||||||
|
group_node: CuraSceneNode,
|
||||||
|
ungrouped_nodes: List[CuraSceneNode]
|
||||||
|
) -> None:
|
||||||
|
all_nodes.remove(group_node)
|
||||||
|
nodes_to_update_print_order = filter(lambda n: n.printOrder > group_node.printOrder, all_nodes)
|
||||||
|
for node in nodes_to_update_print_order:
|
||||||
|
node.printOrder += len(ungrouped_nodes) - 1
|
||||||
|
|
||||||
|
for i, child in enumerate(ungrouped_nodes):
|
||||||
|
child.printOrder = group_node.printOrder + i
|
||||||
|
all_nodes.append(child)
|
||||||
|
|
||||||
|
def _swapPrintOrders(self, node1: CuraSceneNode, node2: CuraSceneNode) -> None:
|
||||||
|
if node1 and node2:
|
||||||
|
node1.printOrder, node2.printOrder = node2.printOrder, node1.printOrder # swap print orders
|
||||||
|
self.printOrderChanged.emit() # update object list first
|
||||||
|
self._uiActionsOutdated.emit() # then update UI actions
|
||||||
|
|
||||||
|
def _getSelectedAndNeighborNodes(self
|
||||||
|
) -> (Optional[CuraSceneNode], Optional[CuraSceneNode], Optional[CuraSceneNode]):
|
||||||
|
nodes = self._get_nodes()
|
||||||
|
ordered_nodes = sorted(nodes, key=lambda n: n.printOrder)
|
||||||
|
for i, node in enumerate(ordered_nodes, 1):
|
||||||
|
node.printOrder = i
|
||||||
|
|
||||||
|
selected_node = PrintOrderManager._getSingleSelectedNode()
|
||||||
|
if selected_node and selected_node in ordered_nodes:
|
||||||
|
selected_node_index = ordered_nodes.index(selected_node)
|
||||||
|
else:
|
||||||
|
selected_node_index = None
|
||||||
|
|
||||||
|
if selected_node_index is not None and selected_node_index - 1 >= 0:
|
||||||
|
previous_node = ordered_nodes[selected_node_index - 1]
|
||||||
|
else:
|
||||||
|
previous_node = None
|
||||||
|
|
||||||
|
if selected_node_index is not None and selected_node_index + 1 < len(ordered_nodes):
|
||||||
|
next_node = ordered_nodes[selected_node_index + 1]
|
||||||
|
else:
|
||||||
|
next_node = None
|
||||||
|
|
||||||
|
return selected_node, previous_node, next_node
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _getNodeName(node: CuraSceneNode, max_length: int = 30) -> str:
|
||||||
|
node_name = node.getName() if node else ""
|
||||||
|
truncated_node_name = node_name[:max_length]
|
||||||
|
return truncated_node_name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _getSingleSelectedNode() -> Optional[CuraSceneNode]:
|
||||||
|
if len(Selection.getAllSelectedObjects()) == 1:
|
||||||
|
selected_node = Selection.getSelectedObject(0)
|
||||||
|
return selected_node
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _configureEvents(self) -> None:
|
||||||
|
Selection.selectionChanged.connect(self._onSelectionChanged)
|
||||||
|
self._global_stack = None
|
||||||
|
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||||
|
self._onGlobalStackChanged()
|
||||||
|
|
||||||
|
def _onGlobalStackChanged(self) -> None:
|
||||||
|
if self._global_stack:
|
||||||
|
self._global_stack.propertyChanged.disconnect(self._onSettingsChanged)
|
||||||
|
self._global_stack.containersChanged.disconnect(self._onSettingsChanged)
|
||||||
|
|
||||||
|
self._global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
|
if self._global_stack:
|
||||||
|
self._global_stack.propertyChanged.connect(self._onSettingsChanged)
|
||||||
|
self._global_stack.containersChanged.connect(self._onSettingsChanged)
|
||||||
|
|
||||||
|
def _onSettingsChanged(self, *args: Any) -> None:
|
||||||
|
self._settingsChanged.emit()
|
||||||
|
|
||||||
|
def _onSelectionChanged(self) -> None:
|
||||||
|
self._uiActionsOutdated.emit()
|
106
cura/PrinterOutput/FormatMaps.py
Normal file
106
cura/PrinterOutput/FormatMaps.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# Copyright (c) 2024 UltiMaker
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Resources import Resources
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
class FormatMaps:
|
||||||
|
|
||||||
|
# A map from the printer-type in their native file-formats to the internal name we use.
|
||||||
|
PRINTER_TYPE_NAME = {
|
||||||
|
"fire_e": "ultimaker_method",
|
||||||
|
"lava_f": "ultimaker_methodx",
|
||||||
|
"magma_10": "ultimaker_methodxl",
|
||||||
|
"sketch": "ultimaker_sketch"
|
||||||
|
}
|
||||||
|
|
||||||
|
# A map from the extruder-name in their native file-formats to the internal name we use.
|
||||||
|
EXTRUDER_NAME_MAP = {
|
||||||
|
"mk14_hot": "1XA",
|
||||||
|
"mk14_hot_s": "2XA",
|
||||||
|
"mk14_c": "1C",
|
||||||
|
"mk14": "1A",
|
||||||
|
"mk14_s": "2A",
|
||||||
|
"mk14_e": "LABS"
|
||||||
|
}
|
||||||
|
|
||||||
|
# A map from the material-name in their native file-formats to some info, including the internal name we use.
|
||||||
|
MATERIAL_MAP = {
|
||||||
|
"abs": {"name": "ABS", "guid": "2780b345-577b-4a24-a2c5-12e6aad3e690"},
|
||||||
|
"abs-cf10": {"name": "ABS-CF", "guid": "495a0ce5-9daf-4a16-b7b2-06856d82394d"},
|
||||||
|
"abs-wss1": {"name": "ABS-R", "guid": "88c8919c-6a09-471a-b7b6-e801263d862d"},
|
||||||
|
"asa": {"name": "ASA", "guid": "f79bc612-21eb-482e-ad6c-87d75bdde066"},
|
||||||
|
"nylon12-cf": {"name": "Nylon 12 CF", "guid": "3c6f2877-71cc-4760-84e6-4b89ab243e3b"},
|
||||||
|
"nylon": {"name": "Nylon", "guid": "283d439a-3490-4481-920c-c51d8cdecf9c"},
|
||||||
|
"pc": {"name": "PC", "guid": "62414577-94d1-490d-b1e4-7ef3ec40db02"},
|
||||||
|
"petg": {"name": "PETG", "guid": "69386c85-5b6c-421a-bec5-aeb1fb33f060"},
|
||||||
|
"pla": {"name": "PLA", "guid": "abb9c58e-1f56-48d1-bd8f-055fde3a5b56"},
|
||||||
|
"pva": {"name": "PVA", "guid": "add51ef2-86eb-4c39-afd5-5586564f0715"},
|
||||||
|
"wss1": {"name": "RapidRinse", "guid": "a140ef8f-4f26-4e73-abe0-cfc29d6d1024"},
|
||||||
|
"sr30": {"name": "SR-30", "guid": "77873465-83a9-4283-bc44-4e542b8eb3eb"},
|
||||||
|
"bvoh": {"name": "BVOH", "guid": "923e604c-8432-4b09-96aa-9bbbd42207f4"},
|
||||||
|
"cpe": {"name": "CPE", "guid": "da1872c1-b991-4795-80ad-bdac0f131726"},
|
||||||
|
"hips": {"name": "HIPS", "guid": "a468d86a-220c-47eb-99a5-bbb47e514eb0"},
|
||||||
|
"tpu": {"name": "TPU 95A", "guid": "19baa6a9-94ff-478b-b4a1-8157b74358d2"},
|
||||||
|
"im-pla": {"name": "Tough", "guid": "de031137-a8ca-4a72-bd1b-17bb964033ad"}
|
||||||
|
}
|
||||||
|
|
||||||
|
__inverse_printer_name: Optional[Dict[str, str]] = None
|
||||||
|
__inverse_extruder_type: Optional[Dict[str, str]] = None
|
||||||
|
__inverse_material_map: Optional[Dict[str, str]] = None
|
||||||
|
__product_to_id_map: Optional[Dict[str, List[str]]] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getInversePrinterNameMap(cls) -> Dict[str, str]:
|
||||||
|
"""Returns the inverse of the printer name map, that is, from the internal name to the name used in output."""
|
||||||
|
if cls.__inverse_printer_name is not None:
|
||||||
|
return cls.__inverse_printer_name
|
||||||
|
cls.__inverse_printer_name = {}
|
||||||
|
for key, value in cls.PRINTER_TYPE_NAME.items():
|
||||||
|
cls.__inverse_printer_name[value] = key
|
||||||
|
return cls.__inverse_printer_name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getInverseExtruderTypeMap(cls) -> Dict[str, str]:
|
||||||
|
"""Returns the inverse of the extruder type map, that is, from the internal name to the name used in output."""
|
||||||
|
if cls.__inverse_extruder_type is not None:
|
||||||
|
return cls.__inverse_extruder_type
|
||||||
|
cls.__inverse_extruder_type = {}
|
||||||
|
for key, value in cls.EXTRUDER_NAME_MAP.items():
|
||||||
|
cls.__inverse_extruder_type[value] = key
|
||||||
|
return cls.__inverse_extruder_type
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getInverseMaterialMap(cls) -> Dict[str, str]:
|
||||||
|
"""Returns the inverse of the material map, that is, from the internal name to the name used in output.
|
||||||
|
|
||||||
|
Note that this drops the extra info saved in the non-inverse material map, use that if you need it.
|
||||||
|
"""
|
||||||
|
if cls.__inverse_material_map is not None:
|
||||||
|
return cls.__inverse_material_map
|
||||||
|
cls.__inverse_material_map = {}
|
||||||
|
for key, value in cls.MATERIAL_MAP.items():
|
||||||
|
cls.__inverse_material_map[value["name"]] = key
|
||||||
|
return cls.__inverse_material_map
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getProductIdMap(cls) -> Dict[str, List[str]]:
|
||||||
|
"""Gets a mapping from product names (for example, in the XML files) to their definition IDs.
|
||||||
|
|
||||||
|
This loads the mapping from a file.
|
||||||
|
"""
|
||||||
|
if cls.__product_to_id_map is not None:
|
||||||
|
return cls.__product_to_id_map
|
||||||
|
|
||||||
|
product_to_id_file = Resources.getPath(Resources.Texts, "product_to_id.json")
|
||||||
|
with open(product_to_id_file, encoding = "utf-8") as f:
|
||||||
|
contents = ""
|
||||||
|
for line in f:
|
||||||
|
contents += line if "#" not in line else "".join([line.replace("#", str(n)) for n in range(1, 12)])
|
||||||
|
cls.__product_to_id_map = json.loads(contents)
|
||||||
|
cls.__product_to_id_map = {key: [value] for key, value in cls.__product_to_id_map.items()}
|
||||||
|
#This also loads "Ultimaker S5" -> "ultimaker_s5" even though that is not strictly necessary with the default to change spaces into underscores.
|
||||||
|
#However it is not always loaded with that default; this mapping is also used in serialize() without that default.
|
||||||
|
return cls.__product_to_id_map
|
@ -1,9 +1,10 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2024 UltiMaker
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PyQt6.QtCore import pyqtProperty, QObject, pyqtSignal
|
from PyQt6.QtCore import pyqtProperty, QObject, pyqtSignal
|
||||||
|
|
||||||
|
from cura.PrinterOutput.FormatMaps import FormatMaps
|
||||||
from .MaterialOutputModel import MaterialOutputModel
|
from .MaterialOutputModel import MaterialOutputModel
|
||||||
|
|
||||||
|
|
||||||
@ -45,15 +46,8 @@ class ExtruderConfigurationModel(QObject):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def applyNameMappingHotend(hotendId) -> str:
|
def applyNameMappingHotend(hotendId) -> str:
|
||||||
_EXTRUDER_NAME_MAP = {
|
if hotendId in FormatMaps.EXTRUDER_NAME_MAP:
|
||||||
"mk14_hot":"1XA",
|
return FormatMaps.EXTRUDER_NAME_MAP[hotendId]
|
||||||
"mk14_hot_s":"2XA",
|
|
||||||
"mk14_c":"1C",
|
|
||||||
"mk14":"1A",
|
|
||||||
"mk14_s":"2A"
|
|
||||||
}
|
|
||||||
if hotendId in _EXTRUDER_NAME_MAP:
|
|
||||||
return _EXTRUDER_NAME_MAP[hotendId]
|
|
||||||
return hotendId
|
return hotendId
|
||||||
|
|
||||||
@pyqtProperty(str, fset = setHotendID, notify = extruderConfigurationChanged)
|
@pyqtProperty(str, fset = setHotendID, notify = extruderConfigurationChanged)
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2024 UltiMaker
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PyQt6.QtCore import pyqtProperty, QObject
|
from PyQt6.QtCore import pyqtProperty, QObject
|
||||||
|
from cura.PrinterOutput.FormatMaps import FormatMaps
|
||||||
|
|
||||||
|
|
||||||
class MaterialOutputModel(QObject):
|
class MaterialOutputModel(QObject):
|
||||||
def __init__(self, guid: Optional[str], type: str, color: str, brand: str, name: str, parent = None) -> None:
|
def __init__(self, guid: Optional[str], type: str, color: str, brand: str, name: str, parent = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
name, guid = MaterialOutputModel.getMaterialFromDefinition(guid,type, brand, name)
|
name, guid = MaterialOutputModel.getMaterialFromDefinition(guid, type, brand, name)
|
||||||
self._guid =guid
|
self._guid = guid
|
||||||
self._type = type
|
self._type = type
|
||||||
self._color = color
|
self._color = color
|
||||||
self._brand = brand
|
self._brand = brand
|
||||||
@ -23,29 +24,9 @@ class MaterialOutputModel(QObject):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getMaterialFromDefinition(guid, type, brand, name):
|
def getMaterialFromDefinition(guid, type, brand, name):
|
||||||
|
if guid is None and brand != "empty" and type in FormatMaps.MATERIAL_MAP:
|
||||||
_MATERIAL_MAP = { "abs" :{"name" :"abs_175" ,"guid": "2780b345-577b-4a24-a2c5-12e6aad3e690"},
|
name = FormatMaps.MATERIAL_MAP[type]["name"]
|
||||||
"abs-wss1" :{"name" :"absr_175" ,"guid": "88c8919c-6a09-471a-b7b6-e801263d862d"},
|
guid = FormatMaps.MATERIAL_MAP[type]["guid"]
|
||||||
"asa" :{"name" :"asa_175" ,"guid": "416eead4-0d8e-4f0b-8bfc-a91a519befa5"},
|
|
||||||
"nylon-cf" :{"name" :"cffpa_175" ,"guid": "85bbae0e-938d-46fb-989f-c9b3689dc4f0"},
|
|
||||||
"nylon" :{"name" :"nylon_175" ,"guid": "283d439a-3490-4481-920c-c51d8cdecf9c"},
|
|
||||||
"pc" :{"name" :"pc_175" ,"guid": "62414577-94d1-490d-b1e4-7ef3ec40db02"},
|
|
||||||
"petg" :{"name" :"petg_175" ,"guid": "69386c85-5b6c-421a-bec5-aeb1fb33f060"},
|
|
||||||
"pla" :{"name" :"pla_175" ,"guid": "0ff92885-617b-4144-a03c-9989872454bc"},
|
|
||||||
"pva" :{"name" :"pva_175" ,"guid": "a4255da2-cb2a-4042-be49-4a83957a2f9a"},
|
|
||||||
"wss1" :{"name" :"rapidrinse_175","guid": "a140ef8f-4f26-4e73-abe0-cfc29d6d1024"},
|
|
||||||
"sr30" :{"name" :"sr30_175" ,"guid": "77873465-83a9-4283-bc44-4e542b8eb3eb"},
|
|
||||||
"im-pla" :{"name" :"tough_pla_175" ,"guid": "96fca5d9-0371-4516-9e96-8e8182677f3c"},
|
|
||||||
"bvoh" :{"name" :"bvoh_175" ,"guid": "923e604c-8432-4b09-96aa-9bbbd42207f4"},
|
|
||||||
"cpe" :{"name" :"cpe_175" ,"guid": "da1872c1-b991-4795-80ad-bdac0f131726"},
|
|
||||||
"hips" :{"name" :"hips_175" ,"guid": "a468d86a-220c-47eb-99a5-bbb47e514eb0"},
|
|
||||||
"tpu" :{"name" :"tpu_175" ,"guid": "19baa6a9-94ff-478b-b4a1-8157b74358d2"}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if guid is None and brand != "empty" and type in _MATERIAL_MAP:
|
|
||||||
name = _MATERIAL_MAP[type]["name"]
|
|
||||||
guid = _MATERIAL_MAP[type]["guid"]
|
|
||||||
return name, guid
|
return name, guid
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2021 Ultimaker B.V.
|
# Copyright (c) 2024 UltiMaker
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
||||||
@ -6,6 +6,7 @@ from UM.Logger import Logger
|
|||||||
from UM.Scene.SceneNode import SceneNode #For typing.
|
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||||
from cura.API import Account
|
from cura.API import Account
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
from cura.PrinterOutput.FormatMaps import FormatMaps
|
||||||
|
|
||||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
|
||||||
|
|
||||||
@ -419,13 +420,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def applyPrinterTypeMapping(printer_type):
|
def applyPrinterTypeMapping(printer_type):
|
||||||
_PRINTER_TYPE_NAME = {
|
if printer_type in FormatMaps.PRINTER_TYPE_NAME:
|
||||||
"fire_e": "ultimaker_method",
|
return FormatMaps.PRINTER_TYPE_NAME[printer_type]
|
||||||
"lava_f": "ultimaker_methodx",
|
|
||||||
"magma_10": "ultimaker_methodxl"
|
|
||||||
}
|
|
||||||
if printer_type in _PRINTER_TYPE_NAME:
|
|
||||||
return _PRINTER_TYPE_NAME[printer_type]
|
|
||||||
return printer_type
|
return printer_type
|
||||||
|
|
||||||
@pyqtProperty(str, constant = True)
|
@pyqtProperty(str, constant = True)
|
||||||
|
@ -11,6 +11,7 @@ from UM.Scene.SceneNode import SceneNode
|
|||||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator # To cast the deepcopy of every decorator back to SceneNodeDecorator.
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator # To cast the deepcopy of every decorator back to SceneNodeDecorator.
|
||||||
|
|
||||||
import cura.CuraApplication # To get the build plate.
|
import cura.CuraApplication # To get the build plate.
|
||||||
|
from UM.Scene.SceneNodeSettings import SceneNodeSettings
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack # For typing.
|
from cura.Settings.ExtruderStack import ExtruderStack # For typing.
|
||||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings.
|
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings.
|
||||||
|
|
||||||
@ -25,13 +26,26 @@ class CuraSceneNode(SceneNode):
|
|||||||
if not no_setting_override:
|
if not no_setting_override:
|
||||||
self.addDecorator(SettingOverrideDecorator()) # Now we always have a getActiveExtruderPosition, unless explicitly disabled
|
self.addDecorator(SettingOverrideDecorator()) # Now we always have a getActiveExtruderPosition, unless explicitly disabled
|
||||||
self._outside_buildarea = False
|
self._outside_buildarea = False
|
||||||
|
self._print_order = 0
|
||||||
|
|
||||||
def setOutsideBuildArea(self, new_value: bool) -> None:
|
def setOutsideBuildArea(self, new_value: bool) -> None:
|
||||||
self._outside_buildarea = new_value
|
self._outside_buildarea = new_value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def printOrder(self):
|
||||||
|
return self._print_order
|
||||||
|
|
||||||
|
@printOrder.setter
|
||||||
|
def printOrder(self, new_value):
|
||||||
|
self._print_order = new_value
|
||||||
|
|
||||||
def isOutsideBuildArea(self) -> bool:
|
def isOutsideBuildArea(self) -> bool:
|
||||||
return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0
|
return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def isDropDownEnabled(self) ->bool:
|
||||||
|
return self.getSetting(SceneNodeSettings.AutoDropDown, Application.getInstance().getPreferences().getValue("physics/automatic_drop_down"))
|
||||||
|
|
||||||
def isVisible(self) -> bool:
|
def isVisible(self) -> bool:
|
||||||
return super().isVisible() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
return super().isVisible() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||||
|
|
||||||
@ -157,3 +171,6 @@ class CuraSceneNode(SceneNode):
|
|||||||
|
|
||||||
def transformChanged(self) -> None:
|
def transformChanged(self) -> None:
|
||||||
self._transformChanged()
|
self._transformChanged()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "{print_order}. {name}".format(print_order = self._print_order, name = self.getName())
|
||||||
|
@ -83,6 +83,15 @@ class GlobalStack(CuraContainerStack):
|
|||||||
"""
|
"""
|
||||||
return self.getMetaDataEntry("supports_material_export", False)
|
return self.getMetaDataEntry("supports_material_export", False)
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", constant = True)
|
||||||
|
def getOutputFileFormats(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Which output formats the printer supports.
|
||||||
|
:return: A list of strings with MIME-types.
|
||||||
|
"""
|
||||||
|
all_formats_str = self.getMetaDataEntry("file_formats", "")
|
||||||
|
return all_formats_str.split(";")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def getLoadingPriority(cls) -> int:
|
def getLoadingPriority(cls) -> int:
|
||||||
return 2
|
return 2
|
||||||
|
@ -145,10 +145,24 @@ class IntentManager(QObject):
|
|||||||
@pyqtProperty(str, notify = intentCategoryChanged)
|
@pyqtProperty(str, notify = intentCategoryChanged)
|
||||||
def currentIntentCategory(self) -> str:
|
def currentIntentCategory(self) -> str:
|
||||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||||
active_extruder_stack = application.getMachineManager().activeStack
|
global_stack = application.getGlobalContainerStack()
|
||||||
if active_extruder_stack is None:
|
|
||||||
return ""
|
active_intent = "default"
|
||||||
return active_extruder_stack.intent.getMetaDataEntry("intent_category", "")
|
if global_stack is None:
|
||||||
|
return active_intent
|
||||||
|
|
||||||
|
# Loop over all active extruders and check if they have an intent that isn't default.
|
||||||
|
# The logic behind this is that support materials (for instance, PVA) don't have intents, but they should be
|
||||||
|
# combinable with all other intents. So if one extruder has "engineering" as an intent and the other has
|
||||||
|
# "default" the 'dominant' intent is "engineering"
|
||||||
|
for extruder_stack in global_stack.extruderList:
|
||||||
|
if not extruder_stack.isEnabled: # Ignore disabled stacks
|
||||||
|
continue
|
||||||
|
extruder_intent = extruder_stack.intent.getMetaDataEntry("intent_category", "")
|
||||||
|
if extruder_intent != "default":
|
||||||
|
active_intent = extruder_intent
|
||||||
|
|
||||||
|
return active_intent
|
||||||
|
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str)
|
||||||
def selectIntent(self, intent_category: str, quality_type: str) -> None:
|
def selectIntent(self, intent_category: str, quality_type: str) -> None:
|
||||||
|
@ -847,6 +847,24 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = currentConfigurationChanged)
|
||||||
|
def variantCoreUsableForFactor4(self) -> bool:
|
||||||
|
"""The selected core is usable if it is in second extruder of Factor4
|
||||||
|
"""
|
||||||
|
result = True
|
||||||
|
if not self._global_container_stack:
|
||||||
|
return result
|
||||||
|
if self.activeMachine.definition.id != "ultimaker_factor4":
|
||||||
|
return result
|
||||||
|
|
||||||
|
for extruder_container in self._global_container_stack.extruderList:
|
||||||
|
if extruder_container.definition.id.startswith("ultimaker_factor4_extruder_right"):
|
||||||
|
if extruder_container.material == empty_material_container:
|
||||||
|
return True
|
||||||
|
if extruder_container.variant.id.startswith("ultimaker_factor4_bb"):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
@pyqtSlot(str, result = str)
|
@pyqtSlot(str, result = str)
|
||||||
def getDefinitionByMachineId(self, machine_id: str) -> Optional[str]:
|
def getDefinitionByMachineId(self, machine_id: str) -> Optional[str]:
|
||||||
"""Get the Definition ID of a machine (specified by ID)
|
"""Get the Definition ID of a machine (specified by ID)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import List, Optional, TYPE_CHECKING
|
from typing import List, Optional, Set, TYPE_CHECKING
|
||||||
|
|
||||||
from PyQt6.QtCore import QObject, QTimer, pyqtProperty, pyqtSignal
|
from PyQt6.QtCore import QObject, QTimer, pyqtProperty, pyqtSignal
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
@ -168,37 +168,26 @@ class SettingInheritanceManager(QObject):
|
|||||||
def settingsWithInheritanceWarning(self) -> List[str]:
|
def settingsWithInheritanceWarning(self) -> List[str]:
|
||||||
return self._settings_with_inheritance_warning
|
return self._settings_with_inheritance_warning
|
||||||
|
|
||||||
def _settingIsOverwritingInheritance(self, key: str, stack: ContainerStack = None) -> bool:
|
def _userSettingIsOverwritingInheritance(self, key: str, stack: ContainerStack, all_keys: Set[str] = set()) -> bool:
|
||||||
"""Check if a setting has an inheritance function that is overwritten"""
|
"""Check if a setting known as having a User state has an inheritance function that is overwritten"""
|
||||||
|
|
||||||
has_setting_function = False
|
has_setting_function = False
|
||||||
if not stack:
|
|
||||||
stack = self._active_container_stack
|
|
||||||
if not stack: # No active container stack yet!
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self._active_container_stack is None:
|
|
||||||
return False
|
|
||||||
all_keys = self._active_container_stack.getAllKeys()
|
|
||||||
|
|
||||||
containers = [] # type: List[ContainerInterface]
|
containers = [] # type: List[ContainerInterface]
|
||||||
|
|
||||||
has_user_state = stack.getProperty(key, "state") == InstanceState.User
|
|
||||||
"""Check if the setting has a user state. If not, it is never overwritten."""
|
|
||||||
|
|
||||||
if not has_user_state:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If a setting is not enabled, don't label it as overwritten (It's never visible anyway).
|
# If a setting is not enabled, don't label it as overwritten (It's never visible anyway).
|
||||||
if not stack.getProperty(key, "enabled"):
|
if not stack.getProperty(key, "enabled"):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
user_container = stack.getTop()
|
user_container = stack.getTop()
|
||||||
"""Also check if the top container is not a setting function (this happens if the inheritance is restored)."""
|
# Also check if the top container is not a setting function (this happens if the inheritance is restored).
|
||||||
|
|
||||||
if user_container and isinstance(user_container.getProperty(key, "value"), SettingFunction):
|
if user_container and isinstance(user_container.getProperty(key, "value"), SettingFunction):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not all_keys:
|
||||||
|
all_keys = self._active_container_stack.getAllKeys()
|
||||||
|
|
||||||
## Mash all containers for all the stacks together.
|
## Mash all containers for all the stacks together.
|
||||||
while stack:
|
while stack:
|
||||||
containers.extend(stack.getContainers())
|
containers.extend(stack.getContainers())
|
||||||
@ -229,17 +218,35 @@ class SettingInheritanceManager(QObject):
|
|||||||
break # There is a setting function somewhere, stop looking deeper.
|
break # There is a setting function somewhere, stop looking deeper.
|
||||||
return has_setting_function and has_non_function_value
|
return has_setting_function and has_non_function_value
|
||||||
|
|
||||||
|
def _settingIsOverwritingInheritance(self, key: str, stack: ContainerStack = None) -> bool:
|
||||||
|
"""Check if a setting has an inheritance function that is overwritten"""
|
||||||
|
|
||||||
|
if not stack:
|
||||||
|
stack = self._active_container_stack
|
||||||
|
if not stack: # No active container stack yet!
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._active_container_stack is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
has_user_state = stack.getProperty(key, "state") == InstanceState.User
|
||||||
|
|
||||||
|
if not has_user_state:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self._userSettingIsOverwritingInheritance(key, stack)
|
||||||
|
|
||||||
def _update(self) -> None:
|
def _update(self) -> None:
|
||||||
self._settings_with_inheritance_warning = [] # Reset previous data.
|
self._settings_with_inheritance_warning = [] # Reset previous data.
|
||||||
|
|
||||||
# Make sure that the GlobalStack is not None. sometimes the globalContainerChanged signal gets here late.
|
# Make sure that the GlobalStack is not None. sometimes the globalContainerChanged signal gets here late.
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None or self._active_container_stack is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check all setting keys that we know of and see if they are overridden.
|
# Check all user setting keys that we know of and see if they are overridden.
|
||||||
for setting_key in self._global_container_stack.getAllKeys():
|
all_keys = self._active_container_stack.getAllKeys()
|
||||||
override = self._settingIsOverwritingInheritance(setting_key)
|
for setting_key in self._active_container_stack.getAllKeysWithUserState():
|
||||||
if override:
|
if self._userSettingIsOverwritingInheritance(setting_key, self._active_container_stack, all_keys):
|
||||||
self._settings_with_inheritance_warning.append(setting_key)
|
self._settings_with_inheritance_warning.append(setting_key)
|
||||||
|
|
||||||
# Check all the categories if any of their children have their inheritance overwritten.
|
# Check all the categories if any of their children have their inheritance overwritten.
|
||||||
|
@ -5,16 +5,18 @@ import json
|
|||||||
import os
|
import os
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from PyQt6.QtCore import QUrl
|
||||||
from PyQt6.QtNetwork import QLocalServer, QLocalSocket
|
from PyQt6.QtNetwork import QLocalServer, QLocalSocket
|
||||||
|
|
||||||
from UM.Qt.QtApplication import QtApplication #For typing.
|
from UM.Qt.QtApplication import QtApplication # For typing.
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
|
||||||
class SingleInstance:
|
class SingleInstance:
|
||||||
def __init__(self, application: QtApplication, files_to_open: Optional[List[str]]) -> None:
|
def __init__(self, application: QtApplication, files_to_open: Optional[List[str]], url_to_open: Optional[List[str]]) -> None:
|
||||||
self._application = application
|
self._application = application
|
||||||
self._files_to_open = files_to_open
|
self._files_to_open = files_to_open
|
||||||
|
self._url_to_open = url_to_open
|
||||||
|
|
||||||
self._single_instance_server = None
|
self._single_instance_server = None
|
||||||
|
|
||||||
@ -33,7 +35,7 @@ class SingleInstance:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# We only send the files that need to be opened.
|
# We only send the files that need to be opened.
|
||||||
if not self._files_to_open:
|
if not self._files_to_open and not self._url_to_open:
|
||||||
Logger.log("i", "No file need to be opened, do nothing.")
|
Logger.log("i", "No file need to be opened, do nothing.")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -55,8 +57,12 @@ class SingleInstance:
|
|||||||
payload = {"command": "open", "filePath": os.path.abspath(filename)}
|
payload = {"command": "open", "filePath": os.path.abspath(filename)}
|
||||||
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
|
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
|
||||||
|
|
||||||
|
for url in self._url_to_open:
|
||||||
|
payload = {"command": "open-url", "urlPath": url.toString()}
|
||||||
|
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding="ascii"))
|
||||||
|
|
||||||
payload = {"command": "close-connection"}
|
payload = {"command": "close-connection"}
|
||||||
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
|
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding="ascii"))
|
||||||
|
|
||||||
single_instance_socket.flush()
|
single_instance_socket.flush()
|
||||||
single_instance_socket.waitForDisconnected()
|
single_instance_socket.waitForDisconnected()
|
||||||
@ -72,7 +78,7 @@ class SingleInstance:
|
|||||||
|
|
||||||
def _onClientConnected(self) -> None:
|
def _onClientConnected(self) -> None:
|
||||||
Logger.log("i", "New connection received on our single-instance server")
|
Logger.log("i", "New connection received on our single-instance server")
|
||||||
connection = None #type: Optional[QLocalSocket]
|
connection = None # type: Optional[QLocalSocket]
|
||||||
if self._single_instance_server:
|
if self._single_instance_server:
|
||||||
connection = self._single_instance_server.nextPendingConnection()
|
connection = self._single_instance_server.nextPendingConnection()
|
||||||
|
|
||||||
@ -81,7 +87,7 @@ class SingleInstance:
|
|||||||
|
|
||||||
def __readCommands(self, connection: QLocalSocket) -> None:
|
def __readCommands(self, connection: QLocalSocket) -> None:
|
||||||
line = connection.readLine()
|
line = connection.readLine()
|
||||||
while len(line) != 0: # There is also a .canReadLine()
|
while len(line) != 0: # There is also a .canReadLine()
|
||||||
try:
|
try:
|
||||||
payload = json.loads(str(line, encoding = "ascii").strip())
|
payload = json.loads(str(line, encoding = "ascii").strip())
|
||||||
command = payload["command"]
|
command = payload["command"]
|
||||||
@ -94,13 +100,19 @@ class SingleInstance:
|
|||||||
elif command == "open":
|
elif command == "open":
|
||||||
self._application.callLater(lambda f = payload["filePath"]: self._application._openFile(f))
|
self._application.callLater(lambda f = payload["filePath"]: self._application._openFile(f))
|
||||||
|
|
||||||
|
#command: Load a url link in Cura
|
||||||
|
elif command == "open-url":
|
||||||
|
url = QUrl(payload["urlPath"])
|
||||||
|
self._application.callLater(lambda: self._application._openUrl(url))
|
||||||
|
|
||||||
|
|
||||||
# Command: Activate the window and bring it to the top.
|
# Command: Activate the window and bring it to the top.
|
||||||
elif command == "focus":
|
elif command == "focus":
|
||||||
# Operating systems these days prevent windows from moving around by themselves.
|
# Operating systems these days prevent windows from moving around by themselves.
|
||||||
# 'alert' or flashing the icon in the taskbar is the best thing we do now.
|
# 'alert' or flashing the icon in the taskbar is the best thing we do now.
|
||||||
main_window = self._application.getMainWindow()
|
main_window = self._application.getMainWindow()
|
||||||
if main_window is not None:
|
if main_window is not None:
|
||||||
self._application.callLater(lambda: main_window.alert(0)) # type: ignore # I don't know why MyPy complains here
|
self._application.callLater(lambda: main_window.alert(0)) # type: ignore # I don't know why MyPy complains here
|
||||||
|
|
||||||
# Command: Close the socket connection. We're done.
|
# Command: Close the socket connection. We're done.
|
||||||
elif command == "close-connection":
|
elif command == "close-connection":
|
||||||
|
@ -21,23 +21,31 @@ from UM.Scene.SceneNode import SceneNode
|
|||||||
from UM.Qt.QtRenderer import QtRenderer
|
from UM.Qt.QtRenderer import QtRenderer
|
||||||
|
|
||||||
class Snapshot:
|
class Snapshot:
|
||||||
|
|
||||||
|
DEFAULT_WIDTH_HEIGHT = 300
|
||||||
|
MAX_RENDER_DISTANCE = 10000
|
||||||
|
BOUND_BOX_FACTOR = 1.75
|
||||||
|
CAMERA_FOVY = 30
|
||||||
|
ATTEMPTS_FOR_SNAPSHOT = 10
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getImageBoundaries(image: QImage):
|
def getNonZeroPixels(image: QImage):
|
||||||
# Look at the resulting image to get a good crop.
|
|
||||||
# Get the pixels as byte array
|
|
||||||
pixel_array = image.bits().asarray(image.sizeInBytes())
|
pixel_array = image.bits().asarray(image.sizeInBytes())
|
||||||
width, height = image.width(), image.height()
|
width, height = image.width(), image.height()
|
||||||
# Convert to numpy array, assume it's 32 bit (it should always be)
|
|
||||||
pixels = numpy.frombuffer(pixel_array, dtype=numpy.uint8).reshape([height, width, 4])
|
pixels = numpy.frombuffer(pixel_array, dtype=numpy.uint8).reshape([height, width, 4])
|
||||||
# Find indices of non zero pixels
|
# Find indices of non zero pixels
|
||||||
nonzero_pixels = numpy.nonzero(pixels)
|
return numpy.nonzero(pixels)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getImageBoundaries(image: QImage):
|
||||||
|
nonzero_pixels = Snapshot.getNonZeroPixels(image)
|
||||||
min_y, min_x, min_a_ = numpy.amin(nonzero_pixels, axis=1) # type: ignore
|
min_y, min_x, min_a_ = numpy.amin(nonzero_pixels, axis=1) # type: ignore
|
||||||
max_y, max_x, max_a_ = numpy.amax(nonzero_pixels, axis=1) # type: ignore
|
max_y, max_x, max_a_ = numpy.amax(nonzero_pixels, axis=1) # type: ignore
|
||||||
|
|
||||||
return min_x, max_x, min_y, max_y
|
return min_x, max_x, min_y, max_y
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def isometricSnapshot(width: int = 300, height: int = 300, *, node: Optional[SceneNode] = None) -> Optional[QImage]:
|
def isometricSnapshot(width: int = DEFAULT_WIDTH_HEIGHT, height: int = DEFAULT_WIDTH_HEIGHT, *, node: Optional[SceneNode] = None) -> Optional[QImage]:
|
||||||
"""
|
"""
|
||||||
Create an isometric snapshot of the scene.
|
Create an isometric snapshot of the scene.
|
||||||
|
|
||||||
@ -92,8 +100,8 @@ class Snapshot:
|
|||||||
camera_width / 2,
|
camera_width / 2,
|
||||||
-camera_height / 2,
|
-camera_height / 2,
|
||||||
camera_height / 2,
|
camera_height / 2,
|
||||||
-10000,
|
-Snapshot.MAX_RENDER_DISTANCE,
|
||||||
10000
|
Snapshot.MAX_RENDER_DISTANCE
|
||||||
)
|
)
|
||||||
camera.setPerspective(False)
|
camera.setPerspective(False)
|
||||||
camera.setProjectionMatrix(ortho_matrix)
|
camera.setProjectionMatrix(ortho_matrix)
|
||||||
@ -112,22 +120,25 @@ class Snapshot:
|
|||||||
|
|
||||||
return render_pass.getOutput()
|
return render_pass.getOutput()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def isNodeRenderable(node):
|
||||||
|
return not getattr(node, "_outside_buildarea", False) and node.callDecoration(
|
||||||
|
"isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration(
|
||||||
|
"isNonThumbnailVisibleMesh")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def nodeBounds(root_node: SceneNode) -> Optional[AxisAlignedBox]:
|
def nodeBounds(root_node: SceneNode) -> Optional[AxisAlignedBox]:
|
||||||
axis_aligned_box = None
|
axis_aligned_box = None
|
||||||
for node in DepthFirstIterator(root_node):
|
for node in DepthFirstIterator(root_node):
|
||||||
if not getattr(node, "_outside_buildarea", False):
|
if Snapshot.isNodeRenderable(node):
|
||||||
if node.callDecoration(
|
if axis_aligned_box is None:
|
||||||
"isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration(
|
axis_aligned_box = node.getBoundingBox()
|
||||||
"isNonThumbnailVisibleMesh"):
|
else:
|
||||||
if axis_aligned_box is None:
|
axis_aligned_box = axis_aligned_box + node.getBoundingBox()
|
||||||
axis_aligned_box = node.getBoundingBox()
|
|
||||||
else:
|
|
||||||
axis_aligned_box = axis_aligned_box + node.getBoundingBox()
|
|
||||||
return axis_aligned_box
|
return axis_aligned_box
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def snapshot(width = 300, height = 300):
|
def snapshot(width = DEFAULT_WIDTH_HEIGHT, height = DEFAULT_WIDTH_HEIGHT, number_of_attempts = ATTEMPTS_FOR_SNAPSHOT):
|
||||||
"""Return a QImage of the scene
|
"""Return a QImage of the scene
|
||||||
|
|
||||||
Uses PreviewPass that leaves out some elements Aspect ratio assumes a square
|
Uses PreviewPass that leaves out some elements Aspect ratio assumes a square
|
||||||
@ -163,13 +174,13 @@ class Snapshot:
|
|||||||
looking_from_offset = Vector(-1, 1, 2)
|
looking_from_offset = Vector(-1, 1, 2)
|
||||||
if size > 0:
|
if size > 0:
|
||||||
# determine the watch distance depending on the size
|
# determine the watch distance depending on the size
|
||||||
looking_from_offset = looking_from_offset * size * 1.75
|
looking_from_offset = looking_from_offset * size * Snapshot.BOUND_BOX_FACTOR
|
||||||
camera.setPosition(look_at + looking_from_offset)
|
camera.setPosition(look_at + looking_from_offset)
|
||||||
camera.lookAt(look_at)
|
camera.lookAt(look_at)
|
||||||
|
|
||||||
satisfied = False
|
satisfied = False
|
||||||
size = None
|
size = None
|
||||||
fovy = 30
|
fovy = Snapshot.CAMERA_FOVY
|
||||||
|
|
||||||
while not satisfied:
|
while not satisfied:
|
||||||
if size is not None:
|
if size is not None:
|
||||||
@ -184,9 +195,14 @@ class Snapshot:
|
|||||||
pixel_output = preview_pass.getOutput()
|
pixel_output = preview_pass.getOutput()
|
||||||
try:
|
try:
|
||||||
min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries(pixel_output)
|
min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries(pixel_output)
|
||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError) as e:
|
||||||
Logger.logException("w", "Failed to crop the snapshot!")
|
if number_of_attempts == 0:
|
||||||
return None
|
Logger.warning( f"Failed to crop the snapshot even after {Snapshot.ATTEMPTS_FOR_SNAPSHOT} attempts!")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
number_of_attempts = number_of_attempts - 1
|
||||||
|
Logger.info("Trying to get the snapshot again.")
|
||||||
|
return Snapshot.snapshot(width, height, number_of_attempts)
|
||||||
|
|
||||||
size = max((max_x - min_x) / render_width, (max_y - min_y) / render_height)
|
size = max((max_x - min_x) / render_width, (max_y - min_y) / render_height)
|
||||||
if size > 0.5 or satisfied:
|
if size > 0.5 or satisfied:
|
||||||
|
@ -14,6 +14,9 @@ from UM.Scene.SceneNode import SceneNode
|
|||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
|
||||||
|
from cura.PrintOrderManager import PrintOrderManager
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
@ -76,6 +79,9 @@ class ObjectsModel(ListModel):
|
|||||||
self._build_plate_number = nr
|
self._build_plate_number = nr
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
|
def getNodes(self) -> List[CuraSceneNode]:
|
||||||
|
return list(map(lambda n: n["node"], self.items))
|
||||||
|
|
||||||
def _updateSceneDelayed(self, source) -> None:
|
def _updateSceneDelayed(self, source) -> None:
|
||||||
if not isinstance(source, Camera):
|
if not isinstance(source, Camera):
|
||||||
self._update_timer.start()
|
self._update_timer.start()
|
||||||
@ -175,6 +181,10 @@ class ObjectsModel(ListModel):
|
|||||||
|
|
||||||
all_nodes = self._renameNodes(name_to_node_info_dict)
|
all_nodes = self._renameNodes(name_to_node_info_dict)
|
||||||
|
|
||||||
|
user_defined_print_order_enabled = PrintOrderManager.isUserDefinedPrintOrderEnabled()
|
||||||
|
if user_defined_print_order_enabled:
|
||||||
|
PrintOrderManager.initializePrintOrders(all_nodes)
|
||||||
|
|
||||||
for node in all_nodes:
|
for node in all_nodes:
|
||||||
if hasattr(node, "isOutsideBuildArea"):
|
if hasattr(node, "isOutsideBuildArea"):
|
||||||
is_outside_build_area = node.isOutsideBuildArea() # type: ignore
|
is_outside_build_area = node.isOutsideBuildArea() # type: ignore
|
||||||
@ -223,8 +233,13 @@ class ObjectsModel(ListModel):
|
|||||||
# for anti overhang meshes and groups the extruder nr is irrelevant
|
# for anti overhang meshes and groups the extruder nr is irrelevant
|
||||||
extruder_number = -1
|
extruder_number = -1
|
||||||
|
|
||||||
|
if not user_defined_print_order_enabled:
|
||||||
|
name = node.getName()
|
||||||
|
else:
|
||||||
|
name = "{print_order}. {name}".format(print_order = node.printOrder, name = node.getName())
|
||||||
|
|
||||||
nodes.append({
|
nodes.append({
|
||||||
"name": node.getName(),
|
"name": name,
|
||||||
"selected": Selection.isSelected(node),
|
"selected": Selection.isSelected(node),
|
||||||
"outside_build_area": is_outside_build_area,
|
"outside_build_area": is_outside_build_area,
|
||||||
"buildplate_number": node_build_plate_number,
|
"buildplate_number": node_build_plate_number,
|
||||||
@ -234,5 +249,5 @@ class ObjectsModel(ListModel):
|
|||||||
"node": node
|
"node": node
|
||||||
})
|
})
|
||||||
|
|
||||||
nodes = sorted(nodes, key=lambda n: n["name"])
|
nodes = sorted(nodes, key=lambda n: n["name"] if not user_defined_print_order_enabled else n["node"].printOrder)
|
||||||
self.setItems(nodes)
|
self.setItems(nodes)
|
||||||
|
62
plugins/3MFReader/SpecificSettingsModel.py
Normal file
62
plugins/3MFReader/SpecificSettingsModel.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# Copyright (c) 2024 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal
|
||||||
|
|
||||||
|
from UM import i18nCatalog
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Settings.SettingDefinition import SettingDefinition
|
||||||
|
from UM.Qt.ListModel import ListModel
|
||||||
|
|
||||||
|
|
||||||
|
class SpecificSettingsModel(ListModel):
|
||||||
|
CategoryRole = Qt.ItemDataRole.UserRole + 1
|
||||||
|
LabelRole = Qt.ItemDataRole.UserRole + 2
|
||||||
|
ValueRole = Qt.ItemDataRole.UserRole + 3
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent = parent)
|
||||||
|
self.addRoleName(self.CategoryRole, "category")
|
||||||
|
self.addRoleName(self.LabelRole, "label")
|
||||||
|
self.addRoleName(self.ValueRole, "value")
|
||||||
|
|
||||||
|
self._settings_catalog = i18nCatalog("fdmprinter.def.json")
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
modelChanged = pyqtSignal()
|
||||||
|
|
||||||
|
|
||||||
|
def addSettingsFromStack(self, stack, category, settings):
|
||||||
|
for setting, value in settings.items():
|
||||||
|
unit = stack.getProperty(setting, "unit")
|
||||||
|
|
||||||
|
setting_type = stack.getProperty(setting, "type")
|
||||||
|
if setting_type is not None:
|
||||||
|
# This is not very good looking, but will do for now
|
||||||
|
value = str(SettingDefinition.settingValueToString(setting_type, value))
|
||||||
|
if unit:
|
||||||
|
value += " " + str(unit)
|
||||||
|
if setting_type == "enum":
|
||||||
|
options = stack.getProperty(setting, "options")
|
||||||
|
value_msgctxt = f"{str(setting)} option {str(value)}"
|
||||||
|
value_msgid = options[stack.getProperty(setting, "value")]
|
||||||
|
value = self._settings_catalog.i18nc(value_msgctxt, value_msgid)
|
||||||
|
else:
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
label_msgctxt = f"{str(setting)} label"
|
||||||
|
label_msgid = stack.getProperty(setting, "label")
|
||||||
|
label = self._settings_catalog.i18nc(label_msgctxt, label_msgid)
|
||||||
|
|
||||||
|
self.appendItem({
|
||||||
|
"category": category,
|
||||||
|
"label": label,
|
||||||
|
"value": value
|
||||||
|
})
|
||||||
|
self.modelChanged.emit()
|
||||||
|
|
||||||
|
def _update(self):
|
||||||
|
Logger.debug(f"Updating {self.__class__.__name__}")
|
||||||
|
self.setItems([])
|
||||||
|
self.modelChanged.emit()
|
||||||
|
return
|
@ -16,6 +16,8 @@ from UM.Mesh.MeshReader import MeshReader
|
|||||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||||
from UM.Scene.GroupDecorator import GroupDecorator
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
from UM.Scene.SceneNode import SceneNode # For typing.
|
from UM.Scene.SceneNode import SceneNode # For typing.
|
||||||
|
from UM.Scene.SceneNodeSettings import SceneNodeSettings
|
||||||
|
from UM.Util import parseBool
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Machines.ContainerTree import ContainerTree
|
from cura.Machines.ContainerTree import ContainerTree
|
||||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||||
@ -41,7 +43,7 @@ class ThreeMFReader(MeshReader):
|
|||||||
|
|
||||||
MimeTypeDatabase.addMimeType(
|
MimeTypeDatabase.addMimeType(
|
||||||
MimeType(
|
MimeType(
|
||||||
name = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
name="application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||||
comment="3MF",
|
comment="3MF",
|
||||||
suffixes=["3mf"]
|
suffixes=["3mf"]
|
||||||
)
|
)
|
||||||
@ -177,6 +179,12 @@ class ThreeMFReader(MeshReader):
|
|||||||
else:
|
else:
|
||||||
Logger.log("w", "Unable to find extruder in position %s", setting_value)
|
Logger.log("w", "Unable to find extruder in position %s", setting_value)
|
||||||
continue
|
continue
|
||||||
|
if key == "print_order":
|
||||||
|
um_node.printOrder = int(setting_value)
|
||||||
|
continue
|
||||||
|
if key =="drop_to_buildplate":
|
||||||
|
um_node.setSetting(SceneNodeSettings.AutoDropDown, parseBool(setting_value))
|
||||||
|
continue
|
||||||
if key in known_setting_keys:
|
if key in known_setting_keys:
|
||||||
setting_container.setProperty(key, "value", setting_value)
|
setting_container.setProperty(key, "value", setting_value)
|
||||||
else:
|
else:
|
||||||
|
@ -5,10 +5,13 @@ from configparser import ConfigParser
|
|||||||
import zipfile
|
import zipfile
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from typing import cast, Dict, List, Optional, Tuple, Any, Set
|
from typing import cast, Dict, List, Optional, Tuple, Any, Set
|
||||||
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
from UM.Util import parseBool
|
from UM.Util import parseBool
|
||||||
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
@ -57,6 +60,7 @@ _ignored_machine_network_metadata: Set[str] = {
|
|||||||
"is_abstract_machine"
|
"is_abstract_machine"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
USER_SETTINGS_PATH = "Cura/user-settings.json"
|
||||||
|
|
||||||
class ContainerInfo:
|
class ContainerInfo:
|
||||||
def __init__(self, file_name: Optional[str], serialized: Optional[str], parser: Optional[ConfigParser]) -> None:
|
def __init__(self, file_name: Optional[str], serialized: Optional[str], parser: Optional[ConfigParser]) -> None:
|
||||||
@ -115,6 +119,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
self._supported_extensions = [".3mf"]
|
self._supported_extensions = [".3mf"]
|
||||||
self._dialog = WorkspaceDialog()
|
self._dialog = WorkspaceDialog()
|
||||||
self._3mf_mesh_reader = None
|
self._3mf_mesh_reader = None
|
||||||
|
self._is_ucp = None
|
||||||
self._container_registry = ContainerRegistry.getInstance()
|
self._container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
# suffixes registered with the MimeTypes don't start with a dot '.'
|
# suffixes registered with the MimeTypes don't start with a dot '.'
|
||||||
@ -141,10 +146,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
self._old_new_materials: Dict[str, str] = {}
|
self._old_new_materials: Dict[str, str] = {}
|
||||||
self._machine_info = None
|
self._machine_info = None
|
||||||
|
|
||||||
|
self._user_settings: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
def _clearState(self):
|
def _clearState(self):
|
||||||
self._id_mapping = {}
|
self._id_mapping = {}
|
||||||
self._old_new_materials = {}
|
self._old_new_materials = {}
|
||||||
self._machine_info = None
|
self._machine_info = None
|
||||||
|
self._user_settings = {}
|
||||||
|
|
||||||
|
def clearOpenAsUcp(self):
|
||||||
|
self._is_ucp = None
|
||||||
|
|
||||||
def getNewId(self, old_id: str):
|
def getNewId(self, old_id: str):
|
||||||
"""Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
|
"""Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
|
||||||
@ -200,6 +211,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
|
|
||||||
return global_stack_file_list[0], extruder_stack_file_list
|
return global_stack_file_list[0], extruder_stack_file_list
|
||||||
|
|
||||||
|
def _isProjectUcp(self, file_name) -> bool:
|
||||||
|
if self._is_ucp == None:
|
||||||
|
archive = zipfile.ZipFile(file_name, "r")
|
||||||
|
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
|
||||||
|
self._is_ucp =True if USER_SETTINGS_PATH in cura_file_names else False
|
||||||
|
|
||||||
|
def getIsProjectUcp(self) -> bool:
|
||||||
|
return self._is_ucp
|
||||||
|
|
||||||
|
|
||||||
def preRead(self, file_name, show_dialog=True, *args, **kwargs):
|
def preRead(self, file_name, show_dialog=True, *args, **kwargs):
|
||||||
"""Read some info so we can make decisions
|
"""Read some info so we can make decisions
|
||||||
|
|
||||||
@ -208,7 +229,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
we don't want to show a dialog.
|
we don't want to show a dialog.
|
||||||
"""
|
"""
|
||||||
self._clearState()
|
self._clearState()
|
||||||
|
self._isProjectUcp(file_name)
|
||||||
self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
|
self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
|
||||||
if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted:
|
if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted:
|
||||||
pass
|
pass
|
||||||
@ -228,11 +249,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
self._resolve_strategies = {k: None for k in resolve_strategy_keys}
|
self._resolve_strategies = {k: None for k in resolve_strategy_keys}
|
||||||
containers_found_dict = {k: False for k in resolve_strategy_keys}
|
containers_found_dict = {k: False for k in resolve_strategy_keys}
|
||||||
|
|
||||||
|
# Check whether the file is a UCP, which changes some import options
|
||||||
|
is_ucp = USER_SETTINGS_PATH in cura_file_names
|
||||||
|
|
||||||
#
|
#
|
||||||
# Read definition containers
|
# Read definition containers
|
||||||
#
|
#
|
||||||
machine_definition_id = None
|
machine_definition_id = None
|
||||||
updatable_machines = []
|
updatable_machines = None if self._is_ucp else []
|
||||||
machine_definition_container_count = 0
|
machine_definition_container_count = 0
|
||||||
extruder_definition_container_count = 0
|
extruder_definition_container_count = 0
|
||||||
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
||||||
@ -250,7 +274,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
if definition_container_type == "machine":
|
if definition_container_type == "machine":
|
||||||
machine_definition_id = container_id
|
machine_definition_id = container_id
|
||||||
machine_definition_containers = self._container_registry.findDefinitionContainers(id = machine_definition_id)
|
machine_definition_containers = self._container_registry.findDefinitionContainers(id = machine_definition_id)
|
||||||
if machine_definition_containers:
|
if machine_definition_containers and updatable_machines is not None:
|
||||||
updatable_machines = [machine for machine in self._container_registry.findContainerStacks(type = "machine") if machine.definition == machine_definition_containers[0]]
|
updatable_machines = [machine for machine in self._container_registry.findContainerStacks(type = "machine") if machine.definition == machine_definition_containers[0]]
|
||||||
machine_type = definition_container["name"]
|
machine_type = definition_container["name"]
|
||||||
variant_type_name = definition_container.get("variants_name", variant_type_name)
|
variant_type_name = definition_container.get("variants_name", variant_type_name)
|
||||||
@ -597,6 +621,39 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
package_metadata = self._parse_packages_metadata(archive)
|
package_metadata = self._parse_packages_metadata(archive)
|
||||||
missing_package_metadata = self._filter_missing_package_metadata(package_metadata)
|
missing_package_metadata = self._filter_missing_package_metadata(package_metadata)
|
||||||
|
|
||||||
|
# Load the user specifically exported settings
|
||||||
|
self._dialog.exportedSettingModel.clear()
|
||||||
|
self._dialog.setCurrentMachineName("")
|
||||||
|
if self._is_ucp:
|
||||||
|
try:
|
||||||
|
self._user_settings = json.loads(archive.open("Cura/user-settings.json").read().decode("utf-8"))
|
||||||
|
any_extruder_stack = ExtruderManager.getInstance().getExtruderStack(0)
|
||||||
|
actual_global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
|
self._dialog.setCurrentMachineName(actual_global_stack.id)
|
||||||
|
|
||||||
|
for stack_name, settings in self._user_settings.items():
|
||||||
|
if stack_name == 'global':
|
||||||
|
self._dialog.exportedSettingModel.addSettingsFromStack(actual_global_stack, i18n_catalog.i18nc("@label", "Global"), settings)
|
||||||
|
else:
|
||||||
|
extruder_match = re.fullmatch('extruder_([0-9]+)', stack_name)
|
||||||
|
if extruder_match is not None:
|
||||||
|
extruder_nr = int(extruder_match.group(1))
|
||||||
|
self._dialog.exportedSettingModel.addSettingsFromStack(any_extruder_stack,
|
||||||
|
i18n_catalog.i18nc("@label",
|
||||||
|
"Extruder {0}", extruder_nr + 1),
|
||||||
|
settings)
|
||||||
|
except KeyError as e:
|
||||||
|
# If there is no user settings file, it's not a UCP, so notify user of failure.
|
||||||
|
Logger.log("w", "File %s is not a valid UCP.", file_name)
|
||||||
|
message = Message(
|
||||||
|
i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
|
||||||
|
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.",
|
||||||
|
file_name, str(e)),
|
||||||
|
title=i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
|
||||||
|
message_type=Message.MessageType.ERROR)
|
||||||
|
message.show()
|
||||||
|
return WorkspaceReader.PreReadResult.failed
|
||||||
|
|
||||||
# Show the dialog, informing the user what is about to happen.
|
# Show the dialog, informing the user what is about to happen.
|
||||||
self._dialog.setMachineConflict(machine_conflict)
|
self._dialog.setMachineConflict(machine_conflict)
|
||||||
self._dialog.setIsPrinterGroup(is_printer_group)
|
self._dialog.setIsPrinterGroup(is_printer_group)
|
||||||
@ -617,12 +674,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
self._dialog.setVariantType(variant_type_name)
|
self._dialog.setVariantType(variant_type_name)
|
||||||
self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
|
self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
|
||||||
self._dialog.setMissingPackagesMetadata(missing_package_metadata)
|
self._dialog.setMissingPackagesMetadata(missing_package_metadata)
|
||||||
|
self._dialog.setAllowCreatemachine(not self._is_ucp)
|
||||||
|
self._dialog.setIsUcp(self._is_ucp)
|
||||||
self._dialog.show()
|
self._dialog.show()
|
||||||
|
|
||||||
|
|
||||||
# Choosing the initially selected printer in MachineSelector
|
# Choosing the initially selected printer in MachineSelector
|
||||||
is_networked_machine = False
|
is_networked_machine = False
|
||||||
is_abstract_machine = False
|
is_abstract_machine = False
|
||||||
if global_stack and isinstance(global_stack, GlobalStack):
|
if global_stack and isinstance(global_stack, GlobalStack) and not self._is_ucp:
|
||||||
# The machine included in the project file exists locally already, no need to change selected printers.
|
# The machine included in the project file exists locally already, no need to change selected printers.
|
||||||
is_networked_machine = global_stack.hasNetworkedConnection()
|
is_networked_machine = global_stack.hasNetworkedConnection()
|
||||||
is_abstract_machine = parseBool(existing_global_stack.getMetaDataEntry("is_abstract_machine", False))
|
is_abstract_machine = parseBool(existing_global_stack.getMetaDataEntry("is_abstract_machine", False))
|
||||||
@ -631,7 +691,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
elif self._dialog.updatableMachinesModel.count > 0:
|
elif self._dialog.updatableMachinesModel.count > 0:
|
||||||
# The machine included in the project file does not exist. There is another machine of the same type.
|
# The machine included in the project file does not exist. There is another machine of the same type.
|
||||||
# This will always default to an abstract machine first.
|
# This will always default to an abstract machine first.
|
||||||
machine = self._dialog.updatableMachinesModel.getItem(0)
|
machine = self._dialog.updatableMachinesModel.getItem(self._dialog.currentMachinePositionIndex)
|
||||||
machine_name = machine["name"]
|
machine_name = machine["name"]
|
||||||
is_networked_machine = machine["isNetworked"]
|
is_networked_machine = machine["isNetworked"]
|
||||||
is_abstract_machine = machine["isAbstractMachine"]
|
is_abstract_machine = machine["isAbstractMachine"]
|
||||||
@ -648,6 +708,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
self._dialog.setIsNetworkedMachine(is_networked_machine)
|
self._dialog.setIsNetworkedMachine(is_networked_machine)
|
||||||
self._dialog.setIsAbstractMachine(is_abstract_machine)
|
self._dialog.setIsAbstractMachine(is_abstract_machine)
|
||||||
self._dialog.setMachineName(machine_name)
|
self._dialog.setMachineName(machine_name)
|
||||||
|
self._dialog.updateCompatibleMachine()
|
||||||
|
|
||||||
# Block until the dialog is closed.
|
# Block until the dialog is closed.
|
||||||
self._dialog.waitForClose()
|
self._dialog.waitForClose()
|
||||||
@ -669,7 +730,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
if key not in containers_found_dict or strategy is not None:
|
if key not in containers_found_dict or strategy is not None:
|
||||||
continue
|
continue
|
||||||
self._resolve_strategies[key] = "override" if containers_found_dict[key] else "new"
|
self._resolve_strategies[key] = "override" if containers_found_dict[key] else "new"
|
||||||
|
|
||||||
return WorkspaceReader.PreReadResult.accepted
|
return WorkspaceReader.PreReadResult.accepted
|
||||||
|
|
||||||
@call_on_qt_thread
|
@call_on_qt_thread
|
||||||
@ -690,16 +750,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
except EnvironmentError as e:
|
except EnvironmentError as e:
|
||||||
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
|
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
|
||||||
"Project file <filename>{0}</filename> is suddenly inaccessible: <message>{1}</message>.", file_name, str(e)),
|
"Project file <filename>{0}</filename> is suddenly inaccessible: <message>{1}</message>.", file_name, str(e)),
|
||||||
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
|
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
|
||||||
message_type = Message.MessageType.ERROR)
|
message_type = Message.MessageType.ERROR)
|
||||||
message.show()
|
message.show()
|
||||||
self.setWorkspaceName("")
|
self.setWorkspaceName("")
|
||||||
return [], {}
|
return [], {}
|
||||||
except zipfile.BadZipFile as e:
|
except zipfile.BadZipFile as e:
|
||||||
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
|
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
|
||||||
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.", file_name, str(e)),
|
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.", file_name, str(e)),
|
||||||
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
|
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
|
||||||
message_type = Message.MessageType.ERROR)
|
message_type = Message.MessageType.ERROR)
|
||||||
message.show()
|
message.show()
|
||||||
self.setWorkspaceName("")
|
self.setWorkspaceName("")
|
||||||
return [], {}
|
return [], {}
|
||||||
@ -761,9 +821,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
# Find the machine which will be overridden
|
# Find the machine which will be overridden
|
||||||
global_stacks = self._container_registry.findContainerStacks(id = self._dialog.getMachineToOverride(), type = "machine")
|
global_stacks = self._container_registry.findContainerStacks(id = self._dialog.getMachineToOverride(), type = "machine")
|
||||||
if not global_stacks:
|
if not global_stacks:
|
||||||
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!",
|
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!",
|
||||||
"Project file <filename>{0}</filename> is made using profiles that are unknown to this version of UltiMaker Cura.", file_name),
|
"Project file <filename>{0}</filename> is made using profiles that are unknown to this version of UltiMaker Cura.", file_name),
|
||||||
message_type = Message.MessageType.ERROR)
|
message_type = Message.MessageType.ERROR)
|
||||||
message.show()
|
message.show()
|
||||||
self.setWorkspaceName("")
|
self.setWorkspaceName("")
|
||||||
return [], {}
|
return [], {}
|
||||||
@ -777,84 +837,86 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
for stack in extruder_stacks:
|
for stack in extruder_stacks:
|
||||||
stack.setNextStack(global_stack, connect_signals = False)
|
stack.setNextStack(global_stack, connect_signals = False)
|
||||||
|
|
||||||
Logger.log("d", "Workspace loading is checking definitions...")
|
if not self._is_ucp:
|
||||||
# Get all the definition files & check if they exist. If not, add them.
|
Logger.log("d", "Workspace loading is checking definitions...")
|
||||||
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
# Get all the definition files & check if they exist. If not, add them.
|
||||||
for definition_container_file in definition_container_files:
|
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
||||||
container_id = self._stripFileToId(definition_container_file)
|
for definition_container_file in definition_container_files:
|
||||||
|
container_id = self._stripFileToId(definition_container_file)
|
||||||
|
|
||||||
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
|
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
|
||||||
if not definitions:
|
if not definitions:
|
||||||
definition_container = DefinitionContainer(container_id)
|
definition_container = DefinitionContainer(container_id)
|
||||||
try:
|
|
||||||
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
|
|
||||||
file_name = definition_container_file)
|
|
||||||
except ContainerFormatError:
|
|
||||||
# We cannot just skip the definition file because everything else later will just break if the
|
|
||||||
# machine definition cannot be found.
|
|
||||||
Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
|
|
||||||
definition_container_file, file_name)
|
|
||||||
definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
|
|
||||||
self._container_registry.addContainer(definition_container)
|
|
||||||
Job.yieldThread()
|
|
||||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
|
||||||
|
|
||||||
Logger.log("d", "Workspace loading is checking materials...")
|
|
||||||
# Get all the material files and check if they exist. If not, add them.
|
|
||||||
xml_material_profile = self._getXmlProfileClass()
|
|
||||||
if self._material_container_suffix is None:
|
|
||||||
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
|
|
||||||
if xml_material_profile:
|
|
||||||
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
|
|
||||||
for material_container_file in material_container_files:
|
|
||||||
to_deserialize_material = False
|
|
||||||
container_id = self._stripFileToId(material_container_file)
|
|
||||||
need_new_name = False
|
|
||||||
materials = self._container_registry.findInstanceContainers(id = container_id)
|
|
||||||
|
|
||||||
if not materials:
|
|
||||||
# No material found, deserialize this material later and add it
|
|
||||||
to_deserialize_material = True
|
|
||||||
else:
|
|
||||||
material_container = materials[0]
|
|
||||||
old_material_root_id = material_container.getMetaDataEntry("base_file")
|
|
||||||
if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only.
|
|
||||||
to_deserialize_material = True
|
|
||||||
|
|
||||||
if self._resolve_strategies["material"] == "override":
|
|
||||||
# Remove the old materials and then deserialize the one from the project
|
|
||||||
root_material_id = material_container.getMetaDataEntry("base_file")
|
|
||||||
application.getContainerRegistry().removeContainer(root_material_id)
|
|
||||||
elif self._resolve_strategies["material"] == "new":
|
|
||||||
# Note that we *must* deserialize it with a new ID, as multiple containers will be
|
|
||||||
# auto created & added.
|
|
||||||
container_id = self.getNewId(container_id)
|
|
||||||
self._old_new_materials[old_material_root_id] = container_id
|
|
||||||
need_new_name = True
|
|
||||||
|
|
||||||
if to_deserialize_material:
|
|
||||||
material_container = xml_material_profile(container_id)
|
|
||||||
try:
|
try:
|
||||||
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
|
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
|
||||||
file_name = container_id + "." + self._material_container_suffix)
|
file_name = definition_container_file)
|
||||||
except ContainerFormatError:
|
except ContainerFormatError:
|
||||||
Logger.logException("e", "Failed to deserialize material file %s in project file %s",
|
# We cannot just skip the definition file because everything else later will just break if the
|
||||||
material_container_file, file_name)
|
# machine definition cannot be found.
|
||||||
continue
|
Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
|
||||||
if need_new_name:
|
definition_container_file, file_name)
|
||||||
new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName())
|
definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
|
||||||
material_container.setName(new_name)
|
self._container_registry.addContainer(definition_container)
|
||||||
material_container.setDirty(True)
|
|
||||||
self._container_registry.addContainer(material_container)
|
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||||
|
|
||||||
if global_stack:
|
Logger.log("d", "Workspace loading is checking materials...")
|
||||||
# Handle quality changes if any
|
# Get all the material files and check if they exist. If not, add them.
|
||||||
self._processQualityChanges(global_stack)
|
xml_material_profile = self._getXmlProfileClass()
|
||||||
|
if self._material_container_suffix is None:
|
||||||
|
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
|
||||||
|
if xml_material_profile:
|
||||||
|
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
|
||||||
|
for material_container_file in material_container_files:
|
||||||
|
to_deserialize_material = False
|
||||||
|
container_id = self._stripFileToId(material_container_file)
|
||||||
|
need_new_name = False
|
||||||
|
materials = self._container_registry.findInstanceContainers(id = container_id)
|
||||||
|
|
||||||
# Prepare the machine
|
if not materials:
|
||||||
self._applyChangesToMachine(global_stack, extruder_stack_dict)
|
# No material found, deserialize this material later and add it
|
||||||
|
to_deserialize_material = True
|
||||||
|
else:
|
||||||
|
material_container = materials[0]
|
||||||
|
old_material_root_id = material_container.getMetaDataEntry("base_file")
|
||||||
|
if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only.
|
||||||
|
to_deserialize_material = True
|
||||||
|
|
||||||
|
if self._resolve_strategies["material"] == "override":
|
||||||
|
# Remove the old materials and then deserialize the one from the project
|
||||||
|
root_material_id = material_container.getMetaDataEntry("base_file")
|
||||||
|
application.getContainerRegistry().removeContainer(root_material_id)
|
||||||
|
elif self._resolve_strategies["material"] == "new":
|
||||||
|
# Note that we *must* deserialize it with a new ID, as multiple containers will be
|
||||||
|
# auto created & added.
|
||||||
|
container_id = self.getNewId(container_id)
|
||||||
|
self._old_new_materials[old_material_root_id] = container_id
|
||||||
|
need_new_name = True
|
||||||
|
|
||||||
|
if to_deserialize_material:
|
||||||
|
material_container = xml_material_profile(container_id)
|
||||||
|
try:
|
||||||
|
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
|
||||||
|
file_name = container_id + "." + self._material_container_suffix)
|
||||||
|
except ContainerFormatError:
|
||||||
|
Logger.logException("e", "Failed to deserialize material file %s in project file %s",
|
||||||
|
material_container_file, file_name)
|
||||||
|
continue
|
||||||
|
if need_new_name:
|
||||||
|
new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName())
|
||||||
|
material_container.setName(new_name)
|
||||||
|
material_container.setDirty(True)
|
||||||
|
self._container_registry.addContainer(material_container)
|
||||||
|
Job.yieldThread()
|
||||||
|
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||||
|
|
||||||
|
if global_stack:
|
||||||
|
if not self._is_ucp:
|
||||||
|
# Handle quality changes if any
|
||||||
|
self._processQualityChanges(global_stack)
|
||||||
|
|
||||||
|
# Prepare the machine
|
||||||
|
self._applyChangesToMachine(global_stack, extruder_stack_dict)
|
||||||
|
|
||||||
Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
|
Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
|
||||||
# Actually change the active machine.
|
# Actually change the active machine.
|
||||||
@ -864,16 +926,40 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
# function is running on the main thread (Qt thread), although those "changed" signals have been emitted, but
|
# function is running on the main thread (Qt thread), although those "changed" signals have been emitted, but
|
||||||
# they won't take effect until this function is done.
|
# they won't take effect until this function is done.
|
||||||
# To solve this, we schedule _updateActiveMachine() for later so it will have the latest data.
|
# To solve this, we schedule _updateActiveMachine() for later so it will have the latest data.
|
||||||
|
|
||||||
self._updateActiveMachine(global_stack)
|
self._updateActiveMachine(global_stack)
|
||||||
|
|
||||||
|
if self._is_ucp:
|
||||||
|
# Now we have switched, apply the user settings
|
||||||
|
self._applyUserSettings(global_stack, extruder_stack_dict, self._user_settings)
|
||||||
|
|
||||||
# Load all the nodes / mesh data of the workspace
|
# Load all the nodes / mesh data of the workspace
|
||||||
nodes = self._3mf_mesh_reader.read(file_name)
|
nodes = self._3mf_mesh_reader.read(file_name)
|
||||||
if nodes is None:
|
if nodes is None:
|
||||||
nodes = []
|
nodes = []
|
||||||
|
|
||||||
|
if self._is_ucp:
|
||||||
|
# We might be on a different printer than the one this project was made on.
|
||||||
|
# The offset to the printers' center isn't saved; instead, try to just fit everything on the buildplate.
|
||||||
|
full_extents = None
|
||||||
|
for node in nodes:
|
||||||
|
extents = node.getMeshData().getExtents() if node.getMeshData() else None
|
||||||
|
if extents is not None:
|
||||||
|
pos = node.getPosition()
|
||||||
|
node_box = AxisAlignedBox(extents.minimum + pos, extents.maximum + pos)
|
||||||
|
if full_extents is None:
|
||||||
|
full_extents = node_box
|
||||||
|
else:
|
||||||
|
full_extents = full_extents + node_box
|
||||||
|
if full_extents and full_extents.isValid():
|
||||||
|
for node in nodes:
|
||||||
|
pos = node.getPosition()
|
||||||
|
node.setPosition(Vector(pos.x - full_extents.center.x, pos.y, pos.z - full_extents.center.z))
|
||||||
|
|
||||||
base_file_name = os.path.basename(file_name)
|
base_file_name = os.path.basename(file_name)
|
||||||
self.setWorkspaceName(base_file_name)
|
self.setWorkspaceName(base_file_name)
|
||||||
|
|
||||||
|
self._is_ucp = None
|
||||||
return nodes, self._loadMetadata(file_name)
|
return nodes, self._loadMetadata(file_name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -1159,7 +1245,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
node = machine_node.variants.get(machine_node.preferred_variant_name, next(iter(machine_node.variants.values())))
|
node = machine_node.variants.get(machine_node.preferred_variant_name, next(iter(machine_node.variants.values())))
|
||||||
else:
|
else:
|
||||||
variant_name = extruder_info.variant_info.parser["general"]["name"]
|
variant_name = extruder_info.variant_info.parser["general"]["name"]
|
||||||
node = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants[variant_name]
|
node = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants.get(variant_name, next(iter(machine_node.variants.values())))
|
||||||
extruder_stack.variant = node.container
|
extruder_stack.variant = node.container
|
||||||
|
|
||||||
def _applyMaterials(self, global_stack, extruder_stack_dict):
|
def _applyMaterials(self, global_stack, extruder_stack_dict):
|
||||||
@ -1174,24 +1260,50 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
root_material_id = extruder_info.root_material_id
|
root_material_id = extruder_info.root_material_id
|
||||||
root_material_id = self._old_new_materials.get(root_material_id, root_material_id)
|
root_material_id = self._old_new_materials.get(root_material_id, root_material_id)
|
||||||
|
|
||||||
material_node = machine_node.variants[extruder_stack.variant.getName()].materials[root_material_id]
|
available_materials = machine_node.variants[extruder_stack.variant.getName()].materials
|
||||||
|
if root_material_id not in available_materials:
|
||||||
|
continue
|
||||||
|
material_node = available_materials[root_material_id]
|
||||||
extruder_stack.material = material_node.container
|
extruder_stack.material = material_node.container
|
||||||
|
|
||||||
def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
|
def _clearMachineSettings(self, global_stack, extruder_stack_dict):
|
||||||
# Clear all first
|
|
||||||
self._clearStack(global_stack)
|
self._clearStack(global_stack)
|
||||||
for extruder_stack in extruder_stack_dict.values():
|
for extruder_stack in extruder_stack_dict.values():
|
||||||
self._clearStack(extruder_stack)
|
self._clearStack(extruder_stack)
|
||||||
|
|
||||||
|
self._quality_changes_to_apply = None
|
||||||
|
self._quality_type_to_apply = None
|
||||||
|
self._intent_category_to_apply = None
|
||||||
|
self._user_settings_to_apply = None
|
||||||
|
|
||||||
|
def _applyUserSettings(self, global_stack, extruder_stack_dict, user_settings):
|
||||||
|
for stack_name, settings in user_settings.items():
|
||||||
|
if stack_name == 'global':
|
||||||
|
ThreeMFWorkspaceReader._applyUserSettingsOnStack(global_stack, settings)
|
||||||
|
else:
|
||||||
|
extruder_match = re.fullmatch('extruder_([0-9]+)', stack_name)
|
||||||
|
if extruder_match is not None:
|
||||||
|
extruder_nr = extruder_match.group(1)
|
||||||
|
if extruder_nr in extruder_stack_dict:
|
||||||
|
ThreeMFWorkspaceReader._applyUserSettingsOnStack(extruder_stack_dict[extruder_nr], settings)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _applyUserSettingsOnStack(stack, user_settings):
|
||||||
|
user_settings_container = stack.userChanges
|
||||||
|
|
||||||
|
for setting_to_import, setting_value in user_settings.items():
|
||||||
|
user_settings_container.setProperty(setting_to_import, 'value', setting_value)
|
||||||
|
|
||||||
|
def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
|
||||||
|
# Clear all first
|
||||||
|
self._clearMachineSettings(global_stack, extruder_stack_dict)
|
||||||
|
|
||||||
self._applyDefinitionChanges(global_stack, extruder_stack_dict)
|
self._applyDefinitionChanges(global_stack, extruder_stack_dict)
|
||||||
self._applyUserChanges(global_stack, extruder_stack_dict)
|
self._applyUserChanges(global_stack, extruder_stack_dict)
|
||||||
self._applyVariants(global_stack, extruder_stack_dict)
|
self._applyVariants(global_stack, extruder_stack_dict)
|
||||||
self._applyMaterials(global_stack, extruder_stack_dict)
|
self._applyMaterials(global_stack, extruder_stack_dict)
|
||||||
|
|
||||||
# prepare the quality to select
|
# prepare the quality to select
|
||||||
self._quality_changes_to_apply = None
|
|
||||||
self._quality_type_to_apply = None
|
|
||||||
self._intent_category_to_apply = None
|
|
||||||
if self._machine_info.quality_changes_info is not None:
|
if self._machine_info.quality_changes_info is not None:
|
||||||
self._quality_changes_to_apply = self._machine_info.quality_changes_info.name
|
self._quality_changes_to_apply = self._machine_info.quality_changes_info.name
|
||||||
else:
|
else:
|
||||||
@ -1229,39 +1341,40 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
machine_manager.setActiveMachine(global_stack.getId())
|
machine_manager.setActiveMachine(global_stack.getId())
|
||||||
|
|
||||||
# Set metadata fields that are missing from the global stack
|
# Set metadata fields that are missing from the global stack
|
||||||
for key, value in self._machine_info.metadata_dict.items():
|
if not self._is_ucp:
|
||||||
if key not in global_stack.getMetaData() and key not in _ignored_machine_network_metadata:
|
for key, value in self._machine_info.metadata_dict.items():
|
||||||
global_stack.setMetaDataEntry(key, value)
|
if key not in global_stack.getMetaData() and key not in _ignored_machine_network_metadata:
|
||||||
|
global_stack.setMetaDataEntry(key, value)
|
||||||
|
|
||||||
if self._quality_changes_to_apply:
|
if self._quality_changes_to_apply !=None:
|
||||||
quality_changes_group_list = container_tree.getCurrentQualityChangesGroups()
|
quality_changes_group_list = container_tree.getCurrentQualityChangesGroups()
|
||||||
quality_changes_group = next((qcg for qcg in quality_changes_group_list if qcg.name == self._quality_changes_to_apply), None)
|
quality_changes_group = next((qcg for qcg in quality_changes_group_list if qcg.name == self._quality_changes_to_apply), None)
|
||||||
if not quality_changes_group:
|
if not quality_changes_group:
|
||||||
Logger.log("e", "Could not find quality_changes [%s]", self._quality_changes_to_apply)
|
Logger.log("e", "Could not find quality_changes [%s]", self._quality_changes_to_apply)
|
||||||
return
|
return
|
||||||
machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True)
|
machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True)
|
||||||
else:
|
|
||||||
self._quality_type_to_apply = self._quality_type_to_apply.lower() if self._quality_type_to_apply else None
|
|
||||||
quality_group_dict = container_tree.getCurrentQualityGroups()
|
|
||||||
if self._quality_type_to_apply in quality_group_dict:
|
|
||||||
quality_group = quality_group_dict[self._quality_type_to_apply]
|
|
||||||
else:
|
else:
|
||||||
Logger.log("i", "Could not find quality type [%s], switch to default", self._quality_type_to_apply)
|
self._quality_type_to_apply = self._quality_type_to_apply.lower() if self._quality_type_to_apply else None
|
||||||
preferred_quality_type = global_stack.getMetaDataEntry("preferred_quality_type")
|
quality_group_dict = container_tree.getCurrentQualityGroups()
|
||||||
quality_group = quality_group_dict.get(preferred_quality_type)
|
if self._quality_type_to_apply in quality_group_dict:
|
||||||
if quality_group is None:
|
quality_group = quality_group_dict[self._quality_type_to_apply]
|
||||||
Logger.log("e", "Could not get preferred quality type [%s]", preferred_quality_type)
|
|
||||||
|
|
||||||
if quality_group is not None:
|
|
||||||
machine_manager.setQualityGroup(quality_group, no_dialog = True)
|
|
||||||
|
|
||||||
# Also apply intent if available
|
|
||||||
available_intent_category_list = IntentManager.getInstance().currentAvailableIntentCategories()
|
|
||||||
if self._intent_category_to_apply is not None and self._intent_category_to_apply in available_intent_category_list:
|
|
||||||
machine_manager.setIntentByCategory(self._intent_category_to_apply)
|
|
||||||
else:
|
else:
|
||||||
# if no intent is provided, reset to the default (balanced) intent
|
Logger.log("i", "Could not find quality type [%s], switch to default", self._quality_type_to_apply)
|
||||||
machine_manager.resetIntents()
|
preferred_quality_type = global_stack.getMetaDataEntry("preferred_quality_type")
|
||||||
|
quality_group = quality_group_dict.get(preferred_quality_type)
|
||||||
|
if quality_group is None:
|
||||||
|
Logger.log("e", "Could not get preferred quality type [%s]", preferred_quality_type)
|
||||||
|
|
||||||
|
if quality_group is not None:
|
||||||
|
machine_manager.setQualityGroup(quality_group, no_dialog = True)
|
||||||
|
|
||||||
|
# Also apply intent if available
|
||||||
|
available_intent_category_list = IntentManager.getInstance().currentAvailableIntentCategories()
|
||||||
|
if self._intent_category_to_apply is not None and self._intent_category_to_apply in available_intent_category_list:
|
||||||
|
machine_manager.setIntentByCategory(self._intent_category_to_apply)
|
||||||
|
else:
|
||||||
|
# if no intent is provided, reset to the default (balanced) intent
|
||||||
|
machine_manager.resetIntents()
|
||||||
# Notify everything/one that is to notify about changes.
|
# Notify everything/one that is to notify about changes.
|
||||||
global_stack.containersChanged.emit(global_stack.getTop())
|
global_stack.containersChanged.emit(global_stack.getTop())
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ import time
|
|||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
from .SpecificSettingsModel import SpecificSettingsModel
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
@ -61,16 +63,23 @@ class WorkspaceDialog(QObject):
|
|||||||
self._machine_name = ""
|
self._machine_name = ""
|
||||||
self._machine_type = ""
|
self._machine_type = ""
|
||||||
self._variant_type = ""
|
self._variant_type = ""
|
||||||
|
self._current_machine_name = ""
|
||||||
self._material_labels = []
|
self._material_labels = []
|
||||||
self._extruders = []
|
self._extruders = []
|
||||||
self._objects_on_plate = False
|
self._objects_on_plate = False
|
||||||
self._is_printer_group = False
|
self._is_printer_group = False
|
||||||
self._updatable_machines_model = MachineListModel(self, listenToChanges=False)
|
self._updatable_machines_model = MachineListModel(self, listenToChanges = False, showCloudPrinters = True)
|
||||||
self._missing_package_metadata: List[Dict[str, str]] = []
|
self._missing_package_metadata: List[Dict[str, str]] = []
|
||||||
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
|
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
|
||||||
self._install_missing_package_dialog: Optional[QObject] = None
|
self._install_missing_package_dialog: Optional[QObject] = None
|
||||||
self._is_abstract_machine = False
|
self._is_abstract_machine = False
|
||||||
self._is_networked_machine = False
|
self._is_networked_machine = False
|
||||||
|
self._is_compatible_machine = False
|
||||||
|
self._allow_create_machine = True
|
||||||
|
self._exported_settings_model = SpecificSettingsModel()
|
||||||
|
self._exported_settings_model.modelChanged.connect(self.exportedSettingModelChanged.emit)
|
||||||
|
self._current_machine_pos_index = 0
|
||||||
|
self._is_ucp = False
|
||||||
|
|
||||||
machineConflictChanged = pyqtSignal()
|
machineConflictChanged = pyqtSignal()
|
||||||
qualityChangesConflictChanged = pyqtSignal()
|
qualityChangesConflictChanged = pyqtSignal()
|
||||||
@ -94,6 +103,9 @@ class WorkspaceDialog(QObject):
|
|||||||
extrudersChanged = pyqtSignal()
|
extrudersChanged = pyqtSignal()
|
||||||
isPrinterGroupChanged = pyqtSignal()
|
isPrinterGroupChanged = pyqtSignal()
|
||||||
missingPackagesChanged = pyqtSignal()
|
missingPackagesChanged = pyqtSignal()
|
||||||
|
isCompatibleMachineChanged = pyqtSignal()
|
||||||
|
isUcpChanged = pyqtSignal()
|
||||||
|
exportedSettingModelChanged = pyqtSignal()
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = isPrinterGroupChanged)
|
@pyqtProperty(bool, notify = isPrinterGroupChanged)
|
||||||
def isPrinterGroup(self) -> bool:
|
def isPrinterGroup(self) -> bool:
|
||||||
@ -166,8 +178,30 @@ class WorkspaceDialog(QObject):
|
|||||||
self._machine_name = machine_name
|
self._machine_name = machine_name
|
||||||
self.machineNameChanged.emit()
|
self.machineNameChanged.emit()
|
||||||
|
|
||||||
|
def setCurrentMachineName(self, machine: str) -> None:
|
||||||
|
self._current_machine_name = machine
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = machineNameChanged)
|
||||||
|
def currentMachineName(self) -> str:
|
||||||
|
return self._current_machine_name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getIndexOfCurrentMachine(list_of_dicts, key, value, defaultIndex):
|
||||||
|
for i, d in enumerate(list_of_dicts):
|
||||||
|
if d.get(key) == value: # found the dictionary
|
||||||
|
return i
|
||||||
|
return defaultIndex
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = machineNameChanged)
|
||||||
|
def currentMachinePositionIndex(self):
|
||||||
|
return self._current_machine_pos_index
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = updatableMachinesChanged)
|
@pyqtProperty(QObject, notify = updatableMachinesChanged)
|
||||||
def updatableMachinesModel(self) -> MachineListModel:
|
def updatableMachinesModel(self) -> MachineListModel:
|
||||||
|
if self._current_machine_name != "":
|
||||||
|
self._current_machine_pos_index = self.getIndexOfCurrentMachine(self._updatable_machines_model.getItems(), "id", self._current_machine_name, defaultIndex = 0)
|
||||||
|
else:
|
||||||
|
self._current_machine_pos_index = 0
|
||||||
return cast(MachineListModel, self._updatable_machines_model)
|
return cast(MachineListModel, self._updatable_machines_model)
|
||||||
|
|
||||||
def setUpdatableMachines(self, updatable_machines: List[GlobalStack]) -> None:
|
def setUpdatableMachines(self, updatable_machines: List[GlobalStack]) -> None:
|
||||||
@ -292,7 +326,49 @@ class WorkspaceDialog(QObject):
|
|||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def setMachineToOverride(self, machine_name: str) -> None:
|
def setMachineToOverride(self, machine_name: str) -> None:
|
||||||
self._override_machine = machine_name
|
self._override_machine = machine_name
|
||||||
|
self.updateCompatibleMachine()
|
||||||
|
|
||||||
|
def updateCompatibleMachine(self):
|
||||||
|
registry = ContainerRegistry.getInstance()
|
||||||
|
containers_expected = registry.findDefinitionContainers(name=self._machine_type)
|
||||||
|
containers_selected = registry.findContainerStacks(id=self._override_machine)
|
||||||
|
if len(containers_expected) == 1 and len(containers_selected) == 1:
|
||||||
|
new_compatible_machine = (containers_expected[0] == containers_selected[0].definition)
|
||||||
|
if new_compatible_machine != self._is_compatible_machine:
|
||||||
|
self._is_compatible_machine = new_compatible_machine
|
||||||
|
self.isCompatibleMachineChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = isCompatibleMachineChanged)
|
||||||
|
def isCompatibleMachine(self) -> bool:
|
||||||
|
return self._is_compatible_machine
|
||||||
|
|
||||||
|
def setIsUcp(self, isUcp: bool) -> None:
|
||||||
|
if isUcp != self._is_ucp:
|
||||||
|
self._is_ucp = isUcp
|
||||||
|
self.isUcpChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=isUcpChanged)
|
||||||
|
def isUcp(self):
|
||||||
|
return self._is_ucp
|
||||||
|
|
||||||
|
def setAllowCreatemachine(self, allow_create_machine):
|
||||||
|
self._allow_create_machine = allow_create_machine
|
||||||
|
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def allowCreateMachine(self):
|
||||||
|
return self._allow_create_machine
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, notify=exportedSettingModelChanged)
|
||||||
|
def exportedSettingModel(self):
|
||||||
|
return self._exported_settings_model
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify=exportedSettingModelChanged)
|
||||||
|
def exportedSettingModelItems(self):
|
||||||
|
return self._exported_settings_model.items
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify=exportedSettingModelChanged)
|
||||||
|
def exportedSettingModelRowCount(self):
|
||||||
|
return self._exported_settings_model.rowCount()
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def closeBackend(self) -> None:
|
def closeBackend(self) -> None:
|
||||||
"""Close the backend: otherwise one could end up with "Slicing..."""
|
"""Close the backend: otherwise one could end up with "Slicing..."""
|
||||||
|
@ -6,13 +6,13 @@ import QtQuick.Controls 2.3
|
|||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import UM 1.5 as UM
|
import UM 1.6 as UM
|
||||||
import Cura 1.1 as Cura
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
UM.Dialog
|
UM.Dialog
|
||||||
{
|
{
|
||||||
id: workspaceDialog
|
id: workspaceDialog
|
||||||
title: catalog.i18nc("@title:window", "Open Project")
|
title: manager.isUcp? catalog.i18nc("@title:window Don't translate 'Universal Cura Project'", "Open Universal Cura Project (UCP)"): catalog.i18nc("@title:window", "Open Project")
|
||||||
|
|
||||||
margin: UM.Theme.getSize("default_margin").width
|
margin: UM.Theme.getSize("default_margin").width
|
||||||
minimumWidth: UM.Theme.getSize("modal_window_minimum").width
|
minimumWidth: UM.Theme.getSize("modal_window_minimum").width
|
||||||
@ -24,16 +24,34 @@ UM.Dialog
|
|||||||
{
|
{
|
||||||
height: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height
|
height: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height
|
||||||
color: UM.Theme.getColor("main_background")
|
color: UM.Theme.getColor("main_background")
|
||||||
|
ColumnLayout
|
||||||
UM.Label
|
|
||||||
{
|
{
|
||||||
id: titleLabel
|
id: headerColumn
|
||||||
text: catalog.i18nc("@action:title", "Summary - Cura Project")
|
|
||||||
font: UM.Theme.getFont("large")
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||||
anchors.leftMargin: UM.Theme.getSize("default_margin").height
|
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||||
|
anchors.rightMargin: anchors.leftMargin
|
||||||
|
RowLayout
|
||||||
|
{
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
id: titleLabel
|
||||||
|
text: manager.isUcp? catalog.i18nc("@action:title Don't translate 'Universal Cura Project'", "Summary - Open Universal Cura Project (UCP)"): catalog.i18nc("@action:title", "Summary - Cura Project")
|
||||||
|
font: UM.Theme.getFont("large")
|
||||||
|
}
|
||||||
|
Cura.TertiaryButton
|
||||||
|
{
|
||||||
|
id: learnMoreButton
|
||||||
|
visible: manager.isUcp
|
||||||
|
text: catalog.i18nc("@button", "Learn more")
|
||||||
|
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||||
|
isIconOnRightSide: true
|
||||||
|
onClicked: Qt.openUrlExternally("https://support.ultimaker.com/s/article/000002979")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +114,7 @@ UM.Dialog
|
|||||||
WorkspaceRow
|
WorkspaceRow
|
||||||
{
|
{
|
||||||
leftLabelText: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name")
|
leftLabelText: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name")
|
||||||
rightLabelText: manager.machineName == catalog.i18nc("@button", "Create new") ? "" : manager.machineName
|
rightLabelText: manager.isUcp? manager.machineType: manager.machineName == catalog.i18nc("@button", "Create new") ? "" : manager.machineName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,13 +138,17 @@ UM.Dialog
|
|||||||
|
|
||||||
minDropDownWidth: machineSelector.width
|
minDropDownWidth: machineSelector.width
|
||||||
|
|
||||||
buttons: [
|
Component
|
||||||
|
{
|
||||||
|
id: componentNewPrinter
|
||||||
|
|
||||||
Cura.SecondaryButton
|
Cura.SecondaryButton
|
||||||
{
|
{
|
||||||
id: createNewPrinter
|
id: createNewPrinter
|
||||||
text: catalog.i18nc("@button", "Create new")
|
text: catalog.i18nc("@button", "Create new")
|
||||||
fixedWidthMode: true
|
fixedWidthMode: true
|
||||||
width: parent.width - leftPadding * 1.5
|
width: parent.width - leftPadding * 1.5
|
||||||
|
visible: manager.allowCreateMachine
|
||||||
onClicked:
|
onClicked:
|
||||||
{
|
{
|
||||||
toggleContent()
|
toggleContent()
|
||||||
@ -136,7 +158,9 @@ UM.Dialog
|
|||||||
manager.setIsNetworkedMachine(false)
|
manager.setIsNetworkedMachine(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
|
|
||||||
|
buttons: manager.allowCreateMachine ? [componentNewPrinter.createObject()] : []
|
||||||
|
|
||||||
onSelectPrinter: function(machine)
|
onSelectPrinter: function(machine)
|
||||||
{
|
{
|
||||||
@ -152,39 +176,56 @@ UM.Dialog
|
|||||||
|
|
||||||
WorkspaceSection
|
WorkspaceSection
|
||||||
{
|
{
|
||||||
id: profileSection
|
id: ucpProfileSection
|
||||||
title: catalog.i18nc("@action:label", "Profile settings")
|
visible: manager.isUcp
|
||||||
iconSource: UM.Theme.getIcon("Sliders")
|
title: catalog.i18nc("@action:label", "Settings Loaded from UCP file")
|
||||||
|
iconSource: UM.Theme.getIcon("Settings")
|
||||||
|
|
||||||
content: Column
|
content: Column
|
||||||
{
|
{
|
||||||
id: profileSettingsValuesTable
|
id: ucpProfileSettingsValuesTable
|
||||||
spacing: UM.Theme.getSize("default_margin").height
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
leftPadding: UM.Theme.getSize("medium_button_icon").width + UM.Theme.getSize("default_margin").width
|
leftPadding: UM.Theme.getSize("medium_button_icon").width + UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
WorkspaceRow
|
WorkspaceRow
|
||||||
{
|
{
|
||||||
leftLabelText: catalog.i18nc("@action:label", "Name")
|
id: numberOfOverrides
|
||||||
rightLabelText: manager.qualityName
|
leftLabelText: catalog.i18nc("@action:label", "Settings Loaded from UCP file")
|
||||||
|
rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.exportedSettingModelRowCount).arg(manager.exportedSettingModelRowCount)
|
||||||
|
buttonText: tableViewSpecificSettings.shouldBeVisible ? catalog.i18nc("@action:button", "Hide settings") : catalog.i18nc("@action:button", "Show settings")
|
||||||
|
onButtonClicked: tableViewSpecificSettings.shouldBeVisible = !tableViewSpecificSettings.shouldBeVisible
|
||||||
}
|
}
|
||||||
|
Cura.TableView
|
||||||
WorkspaceRow
|
|
||||||
{
|
{
|
||||||
leftLabelText: catalog.i18nc("@action:label", "Intent")
|
id: tableViewSpecificSettings
|
||||||
rightLabelText: manager.intentName
|
width: parent.width - parent.leftPadding - UM.Theme.getSize("default_margin").width
|
||||||
}
|
height: UM.Theme.getSize("card").height
|
||||||
|
visible: shouldBeVisible && manager.isUcp
|
||||||
|
property bool shouldBeVisible: true
|
||||||
|
|
||||||
WorkspaceRow
|
columnHeaders:
|
||||||
{
|
[
|
||||||
leftLabelText: catalog.i18nc("@action:label", "Not in profile")
|
catalog.i18nc("@title:column", "Applies on"),
|
||||||
rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
|
catalog.i18nc("@title:column", "Setting"),
|
||||||
visible: manager.numUserSettings != 0
|
catalog.i18nc("@title:column", "Value")
|
||||||
}
|
]
|
||||||
|
|
||||||
WorkspaceRow
|
model: UM.TableModel
|
||||||
{
|
{
|
||||||
leftLabelText: catalog.i18nc("@action:label", "Derivative from")
|
id: tableModel
|
||||||
rightLabelText: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
|
headers: ["category", "label", "value"]
|
||||||
visible: manager.numSettingsOverridenByQualityChanges != 0
|
rows: manager.exportedSettingModelItems
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections
|
||||||
|
{
|
||||||
|
target: manager
|
||||||
|
function onExportedSettingModelChanged()
|
||||||
|
{
|
||||||
|
tableModel.clear()
|
||||||
|
tableModel.rows = manager.exportedSettingModelItems
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +235,7 @@ UM.Dialog
|
|||||||
id: qualityChangesResolveComboBox
|
id: qualityChangesResolveComboBox
|
||||||
model: resolveStrategiesModel
|
model: resolveStrategiesModel
|
||||||
textRole: "label"
|
textRole: "label"
|
||||||
visible: manager.qualityChangesConflict
|
visible: manager.qualityChangesConflict && !manager.isUcp
|
||||||
contentLeftPadding: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width
|
contentLeftPadding: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width
|
||||||
textFont: UM.Theme.getFont("medium")
|
textFont: UM.Theme.getFont("medium")
|
||||||
|
|
||||||
@ -220,10 +261,51 @@ UM.Dialog
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WorkspaceSection
|
||||||
|
{
|
||||||
|
id: profileSection
|
||||||
|
title: manager.isUcp? catalog.i18nc("@action:label", "Suggested Profile settings"):catalog.i18nc("@action:label", "Profile settings")
|
||||||
|
iconSource: UM.Theme.getIcon("Sliders")
|
||||||
|
content: Column
|
||||||
|
{
|
||||||
|
id: profileSettingsValuesTable
|
||||||
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
|
leftPadding: UM.Theme.getSize("medium_button_icon").width + UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
WorkspaceRow
|
||||||
|
{
|
||||||
|
leftLabelText: catalog.i18nc("@action:label", "Name")
|
||||||
|
rightLabelText: manager.qualityName
|
||||||
|
visible: manager.isCompatibleMachine
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceRow
|
||||||
|
{
|
||||||
|
leftLabelText: catalog.i18nc("@action:label", "Intent")
|
||||||
|
rightLabelText: manager.intentName
|
||||||
|
visible: manager.isCompatibleMachine
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceRow
|
||||||
|
{
|
||||||
|
leftLabelText: catalog.i18nc("@action:label", "Not in profile")
|
||||||
|
rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
|
||||||
|
visible: manager.numUserSettings != 0 && !manager.isUcp
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceRow
|
||||||
|
{
|
||||||
|
leftLabelText: catalog.i18nc("@action:label", "Derivative from")
|
||||||
|
rightLabelText: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
|
||||||
|
visible: manager.numSettingsOverridenByQualityChanges != 0 && manager.isCompatibleMachine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WorkspaceSection
|
WorkspaceSection
|
||||||
{
|
{
|
||||||
id: materialSection
|
id: materialSection
|
||||||
title: catalog.i18nc("@action:label", "Material settings")
|
title: manager.isUcp? catalog.i18nc("@action:label", "Suggested Material settings"): catalog.i18nc("@action:label", "Material settings")
|
||||||
iconSource: UM.Theme.getIcon("Spool")
|
iconSource: UM.Theme.getIcon("Spool")
|
||||||
content: Column
|
content: Column
|
||||||
{
|
{
|
||||||
@ -248,7 +330,7 @@ UM.Dialog
|
|||||||
id: materialResolveComboBox
|
id: materialResolveComboBox
|
||||||
model: resolveStrategiesModel
|
model: resolveStrategiesModel
|
||||||
textRole: "label"
|
textRole: "label"
|
||||||
visible: manager.materialConflict
|
visible: manager.materialConflict && !manager.isUcp
|
||||||
contentLeftPadding: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width
|
contentLeftPadding: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width
|
||||||
textFont: UM.Theme.getFont("medium")
|
textFont: UM.Theme.getFont("medium")
|
||||||
|
|
||||||
@ -279,6 +361,7 @@ UM.Dialog
|
|||||||
id: visibilitySection
|
id: visibilitySection
|
||||||
title: catalog.i18nc("@action:label", "Setting visibility")
|
title: catalog.i18nc("@action:label", "Setting visibility")
|
||||||
iconSource: UM.Theme.getIcon("Eye")
|
iconSource: UM.Theme.getIcon("Eye")
|
||||||
|
visible : !manager.isUcp
|
||||||
content: Column
|
content: Column
|
||||||
{
|
{
|
||||||
spacing: UM.Theme.getSize("default_margin").height
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
@ -416,12 +499,13 @@ UM.Dialog
|
|||||||
{
|
{
|
||||||
if (visible)
|
if (visible)
|
||||||
{
|
{
|
||||||
// Force relead the comboboxes
|
// Force reload the comboboxes
|
||||||
// Since this dialog is only created once the first time you open it, these comboxes need to be reloaded
|
// Since this dialog is only created once the first time you open it, these comboxes need to be reloaded
|
||||||
// each time it is shown after the first time so that the indexes will update correctly.
|
// each time it is shown after the first time so that the indexes will update correctly.
|
||||||
materialSection.reloadValues()
|
materialSection.reloadValues()
|
||||||
profileSection.reloadValues()
|
profileSection.reloadValues()
|
||||||
printerSection.reloadValues()
|
printerSection.reloadValues()
|
||||||
|
ucpProfileSection.reloadValues()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,26 +9,38 @@ import QtQuick.Window 2.2
|
|||||||
import UM 1.5 as UM
|
import UM 1.5 as UM
|
||||||
import Cura 1.1 as Cura
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
Row
|
RowLayout
|
||||||
{
|
{
|
||||||
|
id: root
|
||||||
|
|
||||||
property alias leftLabelText: leftLabel.text
|
property alias leftLabelText: leftLabel.text
|
||||||
property alias rightLabelText: rightLabel.text
|
property alias rightLabelText: rightLabel.text
|
||||||
|
property alias buttonText: button.text
|
||||||
|
signal buttonClicked
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: visible ? childrenRect.height : 0
|
|
||||||
|
|
||||||
UM.Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: leftLabel
|
id: leftLabel
|
||||||
text: catalog.i18nc("@action:label", "Type")
|
text: catalog.i18nc("@action:label", "Type")
|
||||||
width: Math.round(parent.width / 4)
|
Layout.preferredWidth: Math.round(parent.width / 4)
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
UM.Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: rightLabel
|
id: rightLabel
|
||||||
text: manager.machineType
|
text: manager.machineType
|
||||||
width: Math.round(parent.width / 3)
|
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cura.TertiaryButton
|
||||||
|
{
|
||||||
|
id: button
|
||||||
|
visible: !text.isEmpty
|
||||||
|
Layout.maximumHeight: leftLabel.implicitHeight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
onClicked: root.buttonClicked()
|
||||||
|
}
|
||||||
}
|
}
|
@ -5,7 +5,7 @@ import QtQuick 2.10
|
|||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
|
|
||||||
|
|
||||||
import UM 1.5 as UM
|
import UM 1.8 as UM
|
||||||
|
|
||||||
|
|
||||||
Item
|
Item
|
||||||
@ -80,42 +80,22 @@ Item
|
|||||||
sourceComponent: combobox
|
sourceComponent: combobox
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea
|
UM.HelpIcon
|
||||||
{
|
{
|
||||||
id: helpIconMouseArea
|
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: comboboxLabel.verticalCenter
|
anchors.verticalCenter: comboboxLabel.verticalCenter
|
||||||
width: childrenRect.width
|
color: UM.Theme.getColor("small_button_text")
|
||||||
height: childrenRect.height
|
icon: UM.Theme.getIcon("Information")
|
||||||
hoverEnabled: true
|
text: comboboxTooltipText
|
||||||
|
visible: comboboxTooltipText != ""
|
||||||
UM.ColorImage
|
|
||||||
{
|
|
||||||
width: UM.Theme.getSize("section_icon").width
|
|
||||||
height: width
|
|
||||||
|
|
||||||
visible: comboboxTooltipText != ""
|
|
||||||
source: UM.Theme.getIcon("Help")
|
|
||||||
color: UM.Theme.getColor("text")
|
|
||||||
|
|
||||||
UM.ToolTip
|
|
||||||
{
|
|
||||||
text: comboboxTooltipText
|
|
||||||
visible: helpIconMouseArea.containsMouse
|
|
||||||
targetPoint: Qt.point(parent.x + Math.round(parent.width / 2), parent.y)
|
|
||||||
x: 0
|
|
||||||
y: parent.y + parent.height + UM.Theme.getSize("default_margin").height
|
|
||||||
width: UM.Theme.getSize("tooltip").width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Loader
|
Loader
|
||||||
{
|
{
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: content.height
|
height: content.height
|
||||||
|
z: -1
|
||||||
anchors.top: sectionTitleRow.bottom
|
anchors.top: sectionTitleRow.bottom
|
||||||
sourceComponent: content
|
sourceComponent: content
|
||||||
}
|
}
|
||||||
|
48
plugins/3MFWriter/SettingExport.py
Normal file
48
plugins/3MFWriter/SettingExport.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# Copyright (c) 2024 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
|
class SettingExport(QObject):
|
||||||
|
|
||||||
|
def __init__(self, id, name, value, value_name, selectable, show):
|
||||||
|
super().__init__()
|
||||||
|
self.id = id
|
||||||
|
self._name = name
|
||||||
|
self._value = value
|
||||||
|
self._value_name = value_name
|
||||||
|
self._selected = selectable
|
||||||
|
self._selectable = selectable
|
||||||
|
self._show_in_menu = show
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def value(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def valuename(self):
|
||||||
|
return str(self._value_name)
|
||||||
|
|
||||||
|
selectedChanged = pyqtSignal(bool)
|
||||||
|
|
||||||
|
def setSelected(self, selected):
|
||||||
|
if selected != self._selected:
|
||||||
|
self._selected = selected
|
||||||
|
self.selectedChanged.emit(self._selected)
|
||||||
|
|
||||||
|
@pyqtProperty(bool, fset = setSelected, notify = selectedChanged)
|
||||||
|
def selected(self):
|
||||||
|
return self._selected
|
||||||
|
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def selectable(self):
|
||||||
|
return self._selectable
|
||||||
|
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def isVisible(self):
|
||||||
|
return self._show_in_menu
|
39
plugins/3MFWriter/SettingSelection.qml
Normal file
39
plugins/3MFWriter/SettingSelection.qml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) 2024 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.10
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
|
import UM 1.8 as UM
|
||||||
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
|
RowLayout
|
||||||
|
{
|
||||||
|
id: settingSelection
|
||||||
|
|
||||||
|
UM.CheckBox
|
||||||
|
{
|
||||||
|
text: modelData.name
|
||||||
|
Layout.preferredWidth: UM.Theme.getSize("setting").width
|
||||||
|
checked: modelData.selected
|
||||||
|
onClicked: modelData.selected = checked
|
||||||
|
tooltip: modelData.selectable ? "" :catalog.i18nc("@tooltip Don't translate 'Universal Cura Project'", "This setting may not perform well while exporting to Universal Cura Project. Users are asked to add it at their own risk.")
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: modelData.valuename
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.HelpIcon
|
||||||
|
{
|
||||||
|
UM.I18nCatalog { id: catalog; name: "cura" }
|
||||||
|
text: catalog.i18nc("@tooltip Don't translate 'Universal Cura Project'",
|
||||||
|
"This setting may not perform well while exporting to Universal Cura Project, Users are asked to add it at their own risk.")
|
||||||
|
visible: !modelData.selectable
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
56
plugins/3MFWriter/SettingsExportGroup.py
Normal file
56
plugins/3MFWriter/SettingsExportGroup.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Copyright (c) 2024 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
from PyQt6.QtCore import QObject, pyqtProperty, pyqtEnum
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsExportGroup(QObject):
|
||||||
|
|
||||||
|
@pyqtEnum
|
||||||
|
class Category(IntEnum):
|
||||||
|
Global = 0
|
||||||
|
Extruder = 1
|
||||||
|
Model = 2
|
||||||
|
|
||||||
|
def __init__(self, stack, name, category, settings, category_details = '', extruder_index = 0, extruder_color = ''):
|
||||||
|
super().__init__()
|
||||||
|
self.stack = stack
|
||||||
|
self._name = name
|
||||||
|
self._settings = settings
|
||||||
|
self._category = category
|
||||||
|
self._category_details = category_details
|
||||||
|
self._extruder_index = extruder_index
|
||||||
|
self._extruder_color = extruder_color
|
||||||
|
self._visible_settings = []
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@pyqtProperty(list, constant=True)
|
||||||
|
def settings(self):
|
||||||
|
return self._settings
|
||||||
|
|
||||||
|
@pyqtProperty(list, constant=True)
|
||||||
|
def visibleSettings(self):
|
||||||
|
if self._visible_settings == []:
|
||||||
|
self._visible_settings = list(filter(lambda item : item.isVisible, self._settings))
|
||||||
|
return self._visible_settings
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant=True)
|
||||||
|
def category(self):
|
||||||
|
return self._category
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def category_details(self):
|
||||||
|
return self._category_details
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant=True)
|
||||||
|
def extruder_index(self):
|
||||||
|
return self._extruder_index
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def extruder_color(self):
|
||||||
|
return self._extruder_color
|
150
plugins/3MFWriter/SettingsExportModel.py
Normal file
150
plugins/3MFWriter/SettingsExportModel.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# Copyright (c) 2024 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
|
from typing import Optional, cast, List, Dict, Pattern, Set
|
||||||
|
|
||||||
|
from PyQt6.QtCore import QObject, pyqtProperty
|
||||||
|
|
||||||
|
from UM import i18nCatalog
|
||||||
|
from UM.Settings.SettingDefinition import SettingDefinition
|
||||||
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
|
from .SettingsExportGroup import SettingsExportGroup
|
||||||
|
from .SettingExport import SettingExport
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsExportModel(QObject):
|
||||||
|
|
||||||
|
EXPORTABLE_SETTINGS = {'infill_sparse_density',
|
||||||
|
'adhesion_type',
|
||||||
|
'support_enable',
|
||||||
|
'infill_pattern',
|
||||||
|
'support_type',
|
||||||
|
'support_structure',
|
||||||
|
'support_angle',
|
||||||
|
'support_infill_rate',
|
||||||
|
'ironing_enabled',
|
||||||
|
'fill_outline_gaps',
|
||||||
|
'coasting_enable',
|
||||||
|
'skin_monotonic',
|
||||||
|
'z_seam_position',
|
||||||
|
'infill_before_walls',
|
||||||
|
'ironing_only_highest_layer',
|
||||||
|
'xy_offset',
|
||||||
|
'adaptive_layer_height_enabled',
|
||||||
|
'brim_gap',
|
||||||
|
'support_offset',
|
||||||
|
'brim_location',
|
||||||
|
'magic_spiralize',
|
||||||
|
'slicing_tolerance',
|
||||||
|
'outer_inset_first',
|
||||||
|
'magic_fuzzy_skin_outside_only',
|
||||||
|
'conical_overhang_enabled',
|
||||||
|
'min_infill_area',
|
||||||
|
'small_hole_max_size',
|
||||||
|
'magic_mesh_surface_mode',
|
||||||
|
'carve_multiple_volumes',
|
||||||
|
'meshfix_union_all_remove_holes',
|
||||||
|
'support_tree_rest_preference',
|
||||||
|
'small_feature_max_length',
|
||||||
|
'draft_shield_enabled',
|
||||||
|
'brim_smart_ordering',
|
||||||
|
'ooze_shield_enabled',
|
||||||
|
'bottom_skin_preshrink',
|
||||||
|
'skin_edge_support_thickness',
|
||||||
|
'alternate_carve_order',
|
||||||
|
'top_skin_preshrink',
|
||||||
|
'interlocking_enable'}
|
||||||
|
|
||||||
|
PER_MODEL_EXPORTABLE_SETTINGS_KEYS = {"anti_overhang_mesh",
|
||||||
|
"infill_mesh",
|
||||||
|
"cutting_mesh",
|
||||||
|
"support_mesh"}
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._settings_groups = []
|
||||||
|
|
||||||
|
application = CuraApplication.getInstance()
|
||||||
|
|
||||||
|
self._appendGlobalSettings(application)
|
||||||
|
self._appendExtruderSettings(application)
|
||||||
|
self._appendModelSettings(application)
|
||||||
|
|
||||||
|
def _appendGlobalSettings(self, application):
|
||||||
|
global_stack = application.getGlobalContainerStack()
|
||||||
|
self._settings_groups.append(SettingsExportGroup(
|
||||||
|
global_stack, "Global settings", SettingsExportGroup.Category.Global, self._exportSettings(global_stack)))
|
||||||
|
|
||||||
|
def _appendExtruderSettings(self, application):
|
||||||
|
extruders_stacks = ExtruderManager.getInstance().getUsedExtruderStacks()
|
||||||
|
for extruder_stack in extruders_stacks:
|
||||||
|
color = extruder_stack.material.getMetaDataEntry("color_code") if extruder_stack.material else ""
|
||||||
|
self._settings_groups.append(SettingsExportGroup(
|
||||||
|
extruder_stack, "Extruder settings", SettingsExportGroup.Category.Extruder,
|
||||||
|
self._exportSettings(extruder_stack), extruder_index=extruder_stack.position, extruder_color=color))
|
||||||
|
|
||||||
|
def _appendModelSettings(self, application):
|
||||||
|
scene = application.getController().getScene()
|
||||||
|
for scene_node in scene.getRoot().getChildren():
|
||||||
|
self._appendNodeSettings(scene_node, "Model settings", SettingsExportGroup.Category.Model)
|
||||||
|
|
||||||
|
def _appendNodeSettings(self, node, title_prefix, category):
|
||||||
|
stack = node.callDecoration("getStack")
|
||||||
|
if stack:
|
||||||
|
self._settings_groups.append(SettingsExportGroup(
|
||||||
|
stack, f"{title_prefix}", category, self._exportSettings(stack), node.getName()))
|
||||||
|
for child in node.getChildren():
|
||||||
|
self._appendNodeSettings(child, f"Children of {node.getName()}", SettingsExportGroup.Category.Model)
|
||||||
|
|
||||||
|
|
||||||
|
@pyqtProperty(list, constant=True)
|
||||||
|
def settingsGroups(self) -> List[SettingsExportGroup]:
|
||||||
|
return self._settings_groups
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _exportSettings(settings_stack):
|
||||||
|
settings_catalog = i18nCatalog("fdmprinter.def.json")
|
||||||
|
user_settings_container = settings_stack.userChanges
|
||||||
|
user_keys = user_settings_container.getAllKeys()
|
||||||
|
exportable_settings = SettingsExportModel.EXPORTABLE_SETTINGS
|
||||||
|
settings_export = []
|
||||||
|
# Check whether any of the user keys exist in PER_MODEL_EXPORTABLE_SETTINGS_KEYS
|
||||||
|
is_exportable = any(key in SettingsExportModel.PER_MODEL_EXPORTABLE_SETTINGS_KEYS for key in user_keys)
|
||||||
|
|
||||||
|
for setting_to_export in user_keys:
|
||||||
|
show_in_menu = setting_to_export not in SettingsExportModel.PER_MODEL_EXPORTABLE_SETTINGS_KEYS
|
||||||
|
label_msgtxt = f"{str(setting_to_export)} label"
|
||||||
|
label_msgid = settings_stack.getProperty(setting_to_export, "label")
|
||||||
|
label = settings_catalog.i18nc(label_msgtxt, label_msgid)
|
||||||
|
value = settings_stack.getProperty(setting_to_export, "value")
|
||||||
|
unit = settings_stack.getProperty(setting_to_export, "unit")
|
||||||
|
setting_type = settings_stack.getProperty(setting_to_export, "type")
|
||||||
|
value_name = str(SettingDefinition.settingValueToString(setting_type, value))
|
||||||
|
if unit:
|
||||||
|
value_name += " " + str(unit)
|
||||||
|
if setting_type == "enum":
|
||||||
|
options = settings_stack.getProperty(setting_to_export, "options")
|
||||||
|
value_msgctxt = f"{str(setting_to_export)} option {str(value)}"
|
||||||
|
value_msgid = options.get(value, "")
|
||||||
|
value_name = settings_catalog.i18nc(value_msgctxt, value_msgid)
|
||||||
|
|
||||||
|
if setting_type is not None:
|
||||||
|
value = f"{str(SettingDefinition.settingValueToString(setting_type, value))} {unit}"
|
||||||
|
else:
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
settings_export.append(SettingExport(setting_to_export,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
value_name,
|
||||||
|
is_exportable or setting_to_export in exportable_settings,
|
||||||
|
show_in_menu))
|
||||||
|
|
||||||
|
return settings_export
|
86
plugins/3MFWriter/SettingsSelectionGroup.qml
Normal file
86
plugins/3MFWriter/SettingsSelectionGroup.qml
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// Copyright (c) 2024 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.10
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
|
import UM 1.5 as UM
|
||||||
|
import Cura 1.1 as Cura
|
||||||
|
import ThreeMFWriter 1.0 as ThreeMFWriter
|
||||||
|
|
||||||
|
ColumnLayout
|
||||||
|
{
|
||||||
|
id: settingsGroup
|
||||||
|
spacing: UM.Theme.getSize("narrow_margin").width
|
||||||
|
|
||||||
|
RowLayout
|
||||||
|
{
|
||||||
|
id: settingsGroupTitleRow
|
||||||
|
spacing: UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: icon
|
||||||
|
height: UM.Theme.getSize("medium_button_icon").height
|
||||||
|
width: height
|
||||||
|
|
||||||
|
UM.ColorImage
|
||||||
|
{
|
||||||
|
id: settingsMainImage
|
||||||
|
anchors.fill: parent
|
||||||
|
source:
|
||||||
|
{
|
||||||
|
switch(modelData.category)
|
||||||
|
{
|
||||||
|
case ThreeMFWriter.SettingsExportGroup.Global:
|
||||||
|
return UM.Theme.getIcon("Sliders")
|
||||||
|
case ThreeMFWriter.SettingsExportGroup.Model:
|
||||||
|
return UM.Theme.getIcon("View3D")
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.ExtruderIcon
|
||||||
|
{
|
||||||
|
id: settingsExtruderIcon
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: modelData.category === ThreeMFWriter.SettingsExportGroup.Extruder
|
||||||
|
text: (modelData.extruder_index + 1).toString()
|
||||||
|
font: UM.Theme.getFont("tiny_emphasis")
|
||||||
|
materialColor: modelData.extruder_color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
id: settingsTitle
|
||||||
|
text: modelData.name + (modelData.category_details ? ' (%1)'.arg(modelData.category_details) : '')
|
||||||
|
font: UM.Theme.getFont("default_bold")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView
|
||||||
|
{
|
||||||
|
id: settingsExportList
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: contentHeight
|
||||||
|
spacing: 0
|
||||||
|
model: modelData.visibleSettings
|
||||||
|
visible: modelData.visibleSettings.length > 0
|
||||||
|
|
||||||
|
delegate: SettingSelection { }
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
UM.I18nCatalog { id: catalog; name: "cura" }
|
||||||
|
text: catalog.i18nc("@label", "No specific value has been set")
|
||||||
|
visible: modelData.visibleSettings.length === 0
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,13 @@
|
|||||||
# Copyright (c) 2020 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from threading import Lock
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
@ -13,15 +17,23 @@ from UM.Workspace.WorkspaceWriter import WorkspaceWriter
|
|||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
from cura.Utils.Threading import call_on_qt_thread
|
from .ThreeMFWriter import ThreeMFWriter
|
||||||
|
from .SettingsExportModel import SettingsExportModel
|
||||||
|
from .SettingsExportGroup import SettingsExportGroup
|
||||||
|
|
||||||
|
USER_SETTINGS_PATH = "Cura/user-settings.json"
|
||||||
|
|
||||||
|
|
||||||
class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self._ucp_model: Optional[SettingsExportModel] = None
|
||||||
|
|
||||||
@call_on_qt_thread
|
def setExportModel(self, model: SettingsExportModel) -> None:
|
||||||
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
|
if self._ucp_model != model:
|
||||||
|
self._ucp_model = model
|
||||||
|
|
||||||
|
def _write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
|
||||||
application = Application.getInstance()
|
application = Application.getInstance()
|
||||||
machine_manager = application.getMachineManager()
|
machine_manager = application.getMachineManager()
|
||||||
|
|
||||||
@ -34,20 +46,20 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||||||
|
|
||||||
global_stack = machine_manager.activeMachine
|
global_stack = machine_manager.activeMachine
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
self.setInformation(catalog.i18nc("@error", "There is no workspace yet to write. Please add a printer first."))
|
self.setInformation(
|
||||||
|
catalog.i18nc("@error", "There is no workspace yet to write. Please add a printer first."))
|
||||||
Logger.error("Tried to write a 3MF workspace before there was a global stack.")
|
Logger.error("Tried to write a 3MF workspace before there was a global stack.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
|
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
|
||||||
mesh_writer.setStoreArchive(True)
|
mesh_writer.setStoreArchive(True)
|
||||||
if not mesh_writer.write(stream, nodes, mode):
|
if not mesh_writer.write(stream, nodes, mode, self._ucp_model):
|
||||||
self.setInformation(mesh_writer.getInformation())
|
self.setInformation(mesh_writer.getInformation())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
archive = mesh_writer.getArchive()
|
archive = mesh_writer.getArchive()
|
||||||
if archive is None: # This happens if there was no mesh data to write.
|
if archive is None: # This happens if there was no mesh data to write.
|
||||||
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
archive = zipfile.ZipFile(stream, "w", compression=zipfile.ZIP_DEFLATED)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Add global container stack data to the archive.
|
# Add global container stack data to the archive.
|
||||||
@ -62,15 +74,21 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||||||
self._writeContainerToArchive(extruder_stack, archive)
|
self._writeContainerToArchive(extruder_stack, archive)
|
||||||
for container in extruder_stack.getContainers():
|
for container in extruder_stack.getContainers():
|
||||||
self._writeContainerToArchive(container, archive)
|
self._writeContainerToArchive(container, archive)
|
||||||
|
|
||||||
|
# Write user settings data
|
||||||
|
if self._ucp_model is not None:
|
||||||
|
user_settings_data = self._getUserSettings(self._ucp_model)
|
||||||
|
ThreeMFWriter._storeMetadataJson(user_settings_data, archive, USER_SETTINGS_PATH)
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
|
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
|
||||||
Logger.error("No permission to write workspace to this stream.")
|
Logger.error("No permission to write workspace to this stream.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Write preferences to archive
|
# Write preferences to archive
|
||||||
original_preferences = Application.getInstance().getPreferences() #Copy only the preferences that we use to the workspace.
|
original_preferences = Application.getInstance().getPreferences() # Copy only the preferences that we use to the workspace.
|
||||||
temp_preferences = Preferences()
|
temp_preferences = Preferences()
|
||||||
for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded", "metadata/setting_version"}:
|
for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded",
|
||||||
|
"metadata/setting_version"}:
|
||||||
temp_preferences.addPreference(preference, None)
|
temp_preferences.addPreference(preference, None)
|
||||||
temp_preferences.setValue(preference, original_preferences.getValue(preference))
|
temp_preferences.setValue(preference, original_preferences.getValue(preference))
|
||||||
preferences_string = StringIO()
|
preferences_string = StringIO()
|
||||||
@ -81,7 +99,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||||||
|
|
||||||
# Save Cura version
|
# Save Cura version
|
||||||
version_file = zipfile.ZipInfo("Cura/version.ini")
|
version_file = zipfile.ZipInfo("Cura/version.ini")
|
||||||
version_config_parser = configparser.ConfigParser(interpolation = None)
|
version_config_parser = configparser.ConfigParser(interpolation=None)
|
||||||
version_config_parser.add_section("versions")
|
version_config_parser.add_section("versions")
|
||||||
version_config_parser.set("versions", "cura_version", application.getVersion())
|
version_config_parser.set("versions", "cura_version", application.getVersion())
|
||||||
version_config_parser.set("versions", "build_type", application.getBuildType())
|
version_config_parser.set("versions", "build_type", application.getBuildType())
|
||||||
@ -101,11 +119,17 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||||||
return False
|
return False
|
||||||
except EnvironmentError as e:
|
except EnvironmentError as e:
|
||||||
self.setInformation(catalog.i18nc("@error:zip", str(e)))
|
self.setInformation(catalog.i18nc("@error:zip", str(e)))
|
||||||
Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err = str(e)))
|
Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err=str(e)))
|
||||||
return False
|
return False
|
||||||
mesh_writer.setStoreArchive(False)
|
mesh_writer.setStoreArchive(False)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
|
||||||
|
success = self._write(stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode)
|
||||||
|
self._ucp_model = None
|
||||||
|
return success
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
|
def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
|
||||||
file_name_template = "%s/plugin_metadata.json"
|
file_name_template = "%s/plugin_metadata.json"
|
||||||
@ -165,4 +189,27 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||||||
archive.writestr(file_in_archive, serialized_data)
|
archive.writestr(file_in_archive, serialized_data)
|
||||||
except (FileNotFoundError, EnvironmentError):
|
except (FileNotFoundError, EnvironmentError):
|
||||||
Logger.error("File became inaccessible while writing to it: {archive_filename}".format(archive_filename = archive.fp.name))
|
Logger.error("File became inaccessible while writing to it: {archive_filename}".format(archive_filename = archive.fp.name))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _getUserSettings(model: SettingsExportModel) -> Dict[str, Dict[str, Any]]:
|
||||||
|
user_settings = {}
|
||||||
|
|
||||||
|
for group in model.settingsGroups:
|
||||||
|
category = ''
|
||||||
|
if group.category == SettingsExportGroup.Category.Global:
|
||||||
|
category = 'global'
|
||||||
|
elif group.category == SettingsExportGroup.Category.Extruder:
|
||||||
|
category = f"extruder_{group.extruder_index}"
|
||||||
|
|
||||||
|
if len(category) > 0:
|
||||||
|
settings_values = {}
|
||||||
|
stack = group.stack
|
||||||
|
|
||||||
|
for setting in group.settings:
|
||||||
|
if setting.selected:
|
||||||
|
settings_values[setting.id] = stack.getProperty(setting.id, "value")
|
||||||
|
|
||||||
|
user_settings[category] = settings_values
|
||||||
|
|
||||||
|
return user_settings
|
@ -2,6 +2,7 @@
|
|||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import threading
|
||||||
|
|
||||||
from typing import Optional, cast, List, Dict, Pattern, Set
|
from typing import Optional, cast, List, Dict, Pattern, Set
|
||||||
|
|
||||||
@ -10,22 +11,24 @@ from UM.Math.Vector import Vector
|
|||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Math.Matrix import Matrix
|
from UM.Math.Matrix import Matrix
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.OutputDevice import OutputDeviceError
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
|
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.CuraPackageManager import CuraPackageManager
|
from cura.CuraPackageManager import CuraPackageManager
|
||||||
from cura.Settings import CuraContainerStack
|
from cura.Settings import CuraContainerStack
|
||||||
from cura.Utils.Threading import call_on_qt_thread
|
from cura.Utils.Threading import call_on_qt_thread
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from cura.Snapshot import Snapshot
|
from cura.Snapshot import Snapshot
|
||||||
|
|
||||||
from PyQt6.QtCore import QBuffer
|
from PyQt6.QtCore import Qt, QBuffer
|
||||||
|
from PyQt6.QtGui import QImage, QPainter
|
||||||
|
|
||||||
import pySavitar as Savitar
|
import pySavitar as Savitar
|
||||||
|
from .UCPDialog import UCPDialog
|
||||||
import numpy
|
import numpy
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
@ -40,6 +43,9 @@ except ImportError:
|
|||||||
import zipfile
|
import zipfile
|
||||||
import UM.Application
|
import UM.Application
|
||||||
|
|
||||||
|
from .SettingsExportModel import SettingsExportModel
|
||||||
|
from .SettingsExportGroup import SettingsExportGroup
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
@ -60,6 +66,7 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
self._unit_matrix_string = ThreeMFWriter._convertMatrixToString(Matrix())
|
self._unit_matrix_string = ThreeMFWriter._convertMatrixToString(Matrix())
|
||||||
self._archive: Optional[zipfile.ZipFile] = None
|
self._archive: Optional[zipfile.ZipFile] = None
|
||||||
self._store_archive = False
|
self._store_archive = False
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convertMatrixToString(matrix):
|
def _convertMatrixToString(matrix):
|
||||||
@ -87,7 +94,10 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
self._store_archive = store_archive
|
self._store_archive = store_archive
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convertUMNodeToSavitarNode(um_node, transformation=Matrix()):
|
def _convertUMNodeToSavitarNode(um_node,
|
||||||
|
transformation = Matrix(),
|
||||||
|
exported_settings: Optional[Dict[str, Set[str]]] = None,
|
||||||
|
center_mesh = False):
|
||||||
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
||||||
|
|
||||||
:returns: Uranium Scene node.
|
:returns: Uranium Scene node.
|
||||||
@ -102,20 +112,26 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
savitar_node = Savitar.SceneNode()
|
savitar_node = Savitar.SceneNode()
|
||||||
savitar_node.setName(um_node.getName())
|
savitar_node.setName(um_node.getName())
|
||||||
|
|
||||||
node_matrix = Matrix()
|
|
||||||
mesh_data = um_node.getMeshData()
|
mesh_data = um_node.getMeshData()
|
||||||
# compensate for original center position, if object(s) is/are not around its zero position
|
|
||||||
if mesh_data is not None:
|
|
||||||
extents = mesh_data.getExtents()
|
|
||||||
if extents is not None:
|
|
||||||
# We use a different coordinate space while writing, so flip Z and Y
|
|
||||||
center_vector = Vector(extents.center.x, extents.center.z, extents.center.y)
|
|
||||||
node_matrix.setByTranslation(center_vector)
|
|
||||||
node_matrix.multiply(um_node.getLocalTransformation())
|
|
||||||
|
|
||||||
matrix_string = ThreeMFWriter._convertMatrixToString(node_matrix.preMultiply(transformation))
|
node_matrix = um_node.getLocalTransformation()
|
||||||
|
node_matrix.preMultiply(transformation)
|
||||||
|
|
||||||
|
if center_mesh:
|
||||||
|
center_matrix = Matrix()
|
||||||
|
# compensate for original center position, if object(s) is/are not around its zero position
|
||||||
|
if mesh_data is not None:
|
||||||
|
extents = mesh_data.getExtents()
|
||||||
|
if extents is not None:
|
||||||
|
# We use a different coordinate space while writing, so flip Z and Y
|
||||||
|
center_vector = Vector(-extents.center.x, -extents.center.y, -extents.center.z)
|
||||||
|
center_matrix.setByTranslation(center_vector)
|
||||||
|
node_matrix.preMultiply(center_matrix)
|
||||||
|
|
||||||
|
matrix_string = ThreeMFWriter._convertMatrixToString(node_matrix)
|
||||||
|
|
||||||
savitar_node.setTransformation(matrix_string)
|
savitar_node.setTransformation(matrix_string)
|
||||||
|
|
||||||
if mesh_data is not None:
|
if mesh_data is not None:
|
||||||
savitar_node.getMeshData().setVerticesFromBytes(mesh_data.getVerticesAsByteArray())
|
savitar_node.getMeshData().setVerticesFromBytes(mesh_data.getVerticesAsByteArray())
|
||||||
indices_array = mesh_data.getIndicesAsByteArray()
|
indices_array = mesh_data.getIndicesAsByteArray()
|
||||||
@ -129,13 +145,26 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
if stack is not None:
|
if stack is not None:
|
||||||
changed_setting_keys = stack.getTop().getAllKeys()
|
changed_setting_keys = stack.getTop().getAllKeys()
|
||||||
|
|
||||||
# Ensure that we save the extruder used for this object in a multi-extrusion setup
|
if exported_settings is None:
|
||||||
if stack.getProperty("machine_extruder_count", "value") > 1:
|
# Ensure that we save the extruder used for this object in a multi-extrusion setup
|
||||||
changed_setting_keys.add("extruder_nr")
|
if stack.getProperty("machine_extruder_count", "value") > 1:
|
||||||
|
changed_setting_keys.add("extruder_nr")
|
||||||
|
|
||||||
# Get values for all changed settings & save them.
|
# Get values for all changed settings & save them.
|
||||||
for key in changed_setting_keys:
|
for key in changed_setting_keys:
|
||||||
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
|
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
|
||||||
|
else:
|
||||||
|
# We want to export only the specified settings
|
||||||
|
if um_node.getName() in exported_settings:
|
||||||
|
model_exported_settings = exported_settings[um_node.getName()]
|
||||||
|
|
||||||
|
# Get values for all exported settings & save them.
|
||||||
|
for key in model_exported_settings:
|
||||||
|
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
|
||||||
|
|
||||||
|
if isinstance(um_node, CuraSceneNode):
|
||||||
|
savitar_node.setSetting("cura:print_order", str(um_node.printOrder))
|
||||||
|
savitar_node.setSetting("cura:drop_to_buildplate", str(um_node.isDropDownEnabled))
|
||||||
|
|
||||||
# Store the metadata.
|
# Store the metadata.
|
||||||
for key, value in um_node.metadata.items():
|
for key, value in um_node.metadata.items():
|
||||||
@ -145,7 +174,8 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
# only save the nodes on the active build plate
|
# only save the nodes on the active build plate
|
||||||
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
|
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
|
||||||
continue
|
continue
|
||||||
savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node)
|
savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node,
|
||||||
|
exported_settings = exported_settings)
|
||||||
if savitar_child_node is not None:
|
if savitar_child_node is not None:
|
||||||
savitar_node.addChild(savitar_child_node)
|
savitar_node.addChild(savitar_child_node)
|
||||||
|
|
||||||
@ -154,7 +184,24 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
def getArchive(self):
|
def getArchive(self):
|
||||||
return self._archive
|
return self._archive
|
||||||
|
|
||||||
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool:
|
def _addLogoToThumbnail(self, primary_image, logo_name):
|
||||||
|
# Load the icon png image
|
||||||
|
icon_image = QImage(Resources.getPath(Resources.Images, logo_name))
|
||||||
|
|
||||||
|
# Resize icon_image to be 1/4 of primary_image size
|
||||||
|
new_width = int(primary_image.width() / 4)
|
||||||
|
new_height = int(primary_image.height() / 4)
|
||||||
|
icon_image = icon_image.scaled(new_width, new_height, Qt.AspectRatioMode.KeepAspectRatio)
|
||||||
|
# Create a QPainter to draw on the image
|
||||||
|
painter = QPainter(primary_image)
|
||||||
|
|
||||||
|
# Draw the icon in the top-left corner (adjust coordinates as needed)
|
||||||
|
icon_position = (10, 10)
|
||||||
|
painter.drawImage(icon_position[0], icon_position[1], icon_image)
|
||||||
|
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None) -> bool:
|
||||||
self._archive = None # Reset archive
|
self._archive = None # Reset archive
|
||||||
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
||||||
try:
|
try:
|
||||||
@ -178,6 +225,10 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
# Attempt to add a thumbnail
|
# Attempt to add a thumbnail
|
||||||
snapshot = self._createSnapshot()
|
snapshot = self._createSnapshot()
|
||||||
if snapshot:
|
if snapshot:
|
||||||
|
if export_settings_model != None:
|
||||||
|
self._addLogoToThumbnail(snapshot, "cura-share.png")
|
||||||
|
elif export_settings_model == None and self._store_archive:
|
||||||
|
self._addLogoToThumbnail(snapshot, "cura-icon.png")
|
||||||
thumbnail_buffer = QBuffer()
|
thumbnail_buffer = QBuffer()
|
||||||
thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
||||||
snapshot.save(thumbnail_buffer, "PNG")
|
snapshot.save(thumbnail_buffer, "PNG")
|
||||||
@ -232,14 +283,21 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
transformation_matrix.preMultiply(translation_matrix)
|
transformation_matrix.preMultiply(translation_matrix)
|
||||||
|
|
||||||
root_node = UM.Application.Application.getInstance().getController().getScene().getRoot()
|
root_node = UM.Application.Application.getInstance().getController().getScene().getRoot()
|
||||||
|
exported_model_settings = ThreeMFWriter._extractModelExportedSettings(export_settings_model) if export_settings_model != None else None
|
||||||
|
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
if node == root_node:
|
if node == root_node:
|
||||||
for root_child in node.getChildren():
|
for root_child in node.getChildren():
|
||||||
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child, transformation_matrix)
|
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child,
|
||||||
|
transformation_matrix,
|
||||||
|
exported_model_settings,
|
||||||
|
center_mesh = True)
|
||||||
if savitar_node:
|
if savitar_node:
|
||||||
savitar_scene.addSceneNode(savitar_node)
|
savitar_scene.addSceneNode(savitar_node)
|
||||||
else:
|
else:
|
||||||
savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix)
|
savitar_node = self._convertUMNodeToSavitarNode(node,
|
||||||
|
transformation_matrix,
|
||||||
|
exported_model_settings)
|
||||||
if savitar_node:
|
if savitar_node:
|
||||||
savitar_scene.addSceneNode(savitar_node)
|
savitar_scene.addSceneNode(savitar_node)
|
||||||
|
|
||||||
@ -375,6 +433,7 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
@call_on_qt_thread # must be called from the main thread because of OpenGL
|
@call_on_qt_thread # must be called from the main thread because of OpenGL
|
||||||
def _createSnapshot(self):
|
def _createSnapshot(self):
|
||||||
Logger.log("d", "Creating thumbnail image...")
|
Logger.log("d", "Creating thumbnail image...")
|
||||||
|
self._lock.acquire()
|
||||||
if not CuraApplication.getInstance().isVisible:
|
if not CuraApplication.getInstance().isVisible:
|
||||||
Logger.log("w", "Can't create snapshot when renderer not initialized.")
|
Logger.log("w", "Can't create snapshot when renderer not initialized.")
|
||||||
return None
|
return None
|
||||||
@ -383,6 +442,7 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
except:
|
except:
|
||||||
Logger.logException("w", "Failed to create snapshot image")
|
Logger.logException("w", "Failed to create snapshot image")
|
||||||
return None
|
return None
|
||||||
|
finally: self._lock.release()
|
||||||
|
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
@ -390,8 +450,29 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
def sceneNodesToString(scene_nodes: [SceneNode]) -> str:
|
def sceneNodesToString(scene_nodes: [SceneNode]) -> str:
|
||||||
savitar_scene = Savitar.Scene()
|
savitar_scene = Savitar.Scene()
|
||||||
for scene_node in scene_nodes:
|
for scene_node in scene_nodes:
|
||||||
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node)
|
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node, center_mesh = True)
|
||||||
savitar_scene.addSceneNode(savitar_node)
|
savitar_scene.addSceneNode(savitar_node)
|
||||||
parser = Savitar.ThreeMFParser()
|
parser = Savitar.ThreeMFParser()
|
||||||
scene_string = parser.sceneToString(savitar_scene)
|
scene_string = parser.sceneToString(savitar_scene)
|
||||||
return scene_string
|
return scene_string
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extractModelExportedSettings(model: Optional[SettingsExportModel]) -> Dict[str, Set[str]]:
|
||||||
|
extra_settings = {}
|
||||||
|
|
||||||
|
if model is not None:
|
||||||
|
for group in model.settingsGroups:
|
||||||
|
if group.category == SettingsExportGroup.Category.Model:
|
||||||
|
exported_model_settings = set()
|
||||||
|
|
||||||
|
for exported_setting in group.settings:
|
||||||
|
if exported_setting.selected:
|
||||||
|
exported_model_settings.add(exported_setting.id)
|
||||||
|
|
||||||
|
extra_settings[group.category_details] = exported_model_settings
|
||||||
|
|
||||||
|
return extra_settings
|
||||||
|
|
||||||
|
def exportUcp(self):
|
||||||
|
self._config_dialog = UCPDialog()
|
||||||
|
self._config_dialog.show()
|
||||||
|
114
plugins/3MFWriter/UCPDialog.py
Normal file
114
plugins/3MFWriter/UCPDialog.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# Copyright (c) 2024 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt6.QtCore import pyqtSignal, QObject
|
||||||
|
|
||||||
|
import UM
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
from UM.OutputDevice import OutputDeviceError
|
||||||
|
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
from .SettingsExportModel import SettingsExportModel
|
||||||
|
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
class UCPDialog(QObject):
|
||||||
|
finished = pyqtSignal(bool)
|
||||||
|
|
||||||
|
def __init__(self, parent = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
plugin_path = os.path.dirname(__file__)
|
||||||
|
dialog_path = os.path.join(plugin_path, 'UCPDialog.qml')
|
||||||
|
self._model = SettingsExportModel()
|
||||||
|
self._view = CuraApplication.getInstance().createQmlComponent(
|
||||||
|
dialog_path,
|
||||||
|
{
|
||||||
|
"manager": self,
|
||||||
|
"settingsExportModel": self._model
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self._view.accepted.connect(self._onAccepted)
|
||||||
|
self._view.rejected.connect(self._onRejected)
|
||||||
|
self._finished = False
|
||||||
|
self._accepted = False
|
||||||
|
|
||||||
|
def show(self) -> None:
|
||||||
|
self._finished = False
|
||||||
|
self._accepted = False
|
||||||
|
self._view.show()
|
||||||
|
|
||||||
|
def getModel(self) -> SettingsExportModel:
|
||||||
|
return self._model
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def notifyClosed(self):
|
||||||
|
self._onFinished()
|
||||||
|
|
||||||
|
def save3mf(self):
|
||||||
|
application = CuraApplication.getInstance()
|
||||||
|
workspace_handler = application.getInstance().getWorkspaceFileHandler()
|
||||||
|
|
||||||
|
# Set the model to the workspace writer
|
||||||
|
mesh_writer = workspace_handler.getWriter("3MFWriter")
|
||||||
|
mesh_writer.setExportModel(self._model)
|
||||||
|
|
||||||
|
# Open file dialog and write the file
|
||||||
|
device = application.getOutputDeviceManager().getOutputDevice("local_file")
|
||||||
|
nodes = [application.getController().getScene().getRoot()]
|
||||||
|
|
||||||
|
device.writeError.connect(lambda: self._onRejected())
|
||||||
|
device.writeSuccess.connect(lambda: self._onSuccess())
|
||||||
|
device.writeFinished.connect(lambda: self._onFinished())
|
||||||
|
|
||||||
|
file_name = f"UCP_{CuraApplication.getInstance().getPrintInformation().baseName}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
device.requestWrite(
|
||||||
|
nodes,
|
||||||
|
file_name,
|
||||||
|
["application/x-ucp"],
|
||||||
|
workspace_handler,
|
||||||
|
preferred_mimetype_list="application/x-ucp"
|
||||||
|
)
|
||||||
|
except OutputDeviceError.UserCanceledError:
|
||||||
|
self._onRejected()
|
||||||
|
except Exception as e:
|
||||||
|
message = Message(
|
||||||
|
i18n_catalog.i18nc("@info:error", "Unable to write to file: {0}", file_name),
|
||||||
|
title=i18n_catalog.i18nc("@info:title", "Error"),
|
||||||
|
message_type=Message.MessageType.ERROR
|
||||||
|
)
|
||||||
|
message.show()
|
||||||
|
Logger.logException("e", "Unable to write to file %s: %s", file_name, e)
|
||||||
|
self._onRejected()
|
||||||
|
|
||||||
|
def _onAccepted(self):
|
||||||
|
self.save3mf()
|
||||||
|
|
||||||
|
def _onRejected(self):
|
||||||
|
self._onFinished()
|
||||||
|
|
||||||
|
def _onSuccess(self):
|
||||||
|
self._accepted = True
|
||||||
|
self._onFinished()
|
||||||
|
|
||||||
|
def _onFinished(self):
|
||||||
|
# Make sure we don't send the finished signal twice, whatever happens
|
||||||
|
if self._finished:
|
||||||
|
return
|
||||||
|
self._finished = True
|
||||||
|
|
||||||
|
# Reset the model to the workspace writer
|
||||||
|
mesh_writer = CuraApplication.getInstance().getInstance().getWorkspaceFileHandler().getWriter("3MFWriter")
|
||||||
|
mesh_writer.setExportModel(None)
|
||||||
|
|
||||||
|
self.finished.emit(self._accepted)
|
109
plugins/3MFWriter/UCPDialog.qml
Normal file
109
plugins/3MFWriter/UCPDialog.qml
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// Copyright (c) 2024 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.10
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
|
import UM 1.5 as UM
|
||||||
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
|
UM.Dialog
|
||||||
|
{
|
||||||
|
id: exportDialog
|
||||||
|
title: catalog.i18nc("@title:window Don't translate 'Universal Cura Project'", "Export Universal Cura Project")
|
||||||
|
|
||||||
|
margin: UM.Theme.getSize("default_margin").width
|
||||||
|
minimumWidth: UM.Theme.getSize("modal_window_minimum").width
|
||||||
|
minimumHeight: UM.Theme.getSize("modal_window_minimum").height
|
||||||
|
|
||||||
|
backgroundColor: UM.Theme.getColor("detail_background")
|
||||||
|
|
||||||
|
headerComponent: Rectangle
|
||||||
|
{
|
||||||
|
height: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height
|
||||||
|
color: UM.Theme.getColor("main_background")
|
||||||
|
|
||||||
|
ColumnLayout
|
||||||
|
{
|
||||||
|
id: headerColumn
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||||
|
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||||
|
anchors.rightMargin: anchors.leftMargin
|
||||||
|
|
||||||
|
RowLayout
|
||||||
|
{
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
id: titleLabel
|
||||||
|
text: catalog.i18nc("@action:title Don't translate 'Universal Cura Project'", "Summary - Universal Cura Project")
|
||||||
|
font: UM.Theme.getFont("large")
|
||||||
|
}
|
||||||
|
Cura.TertiaryButton
|
||||||
|
{
|
||||||
|
id: learnMoreButton
|
||||||
|
text: catalog.i18nc("@button", "Learn more")
|
||||||
|
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||||
|
isIconOnRightSide: true
|
||||||
|
onClicked: Qt.openUrlExternally("https://support.ultimaker.com/s/article/000002979")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
id: descriptionLabel
|
||||||
|
text: catalog.i18nc("@action:description Don't translate 'Universal Cura Project'", "Universal Cura Project files can be printed on different 3D printers while retaining positional data and selected settings. When exported, all models present on the build plate will be included along with their current position, orientation, and scale. You can also select which per-extruder or per-model settings should be included to ensure proper printing.")
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
Layout.maximumWidth: headerColumn.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle
|
||||||
|
{
|
||||||
|
anchors.fill: parent
|
||||||
|
color: UM.Theme.getColor("main_background")
|
||||||
|
|
||||||
|
UM.I18nCatalog { id: catalog; name: "cura" }
|
||||||
|
|
||||||
|
ListView
|
||||||
|
{
|
||||||
|
id: settingsExportList
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||||
|
spacing: UM.Theme.getSize("thick_margin").height
|
||||||
|
model: settingsExportModel.settingsGroups
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ScrollBar.vertical: UM.ScrollBar { id: verticalScrollBar }
|
||||||
|
|
||||||
|
delegate: SettingsSelectionGroup { Layout.margins: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rightButtons:
|
||||||
|
[
|
||||||
|
Cura.TertiaryButton
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:button", "Cancel")
|
||||||
|
onClicked: reject()
|
||||||
|
},
|
||||||
|
Cura.PrimaryButton
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:button", "Save project")
|
||||||
|
onClicked: accept()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
buttonSpacing: UM.Theme.getSize("wide_margin").width
|
||||||
|
|
||||||
|
onClosing:
|
||||||
|
{
|
||||||
|
manager.notifyClosed()
|
||||||
|
}
|
||||||
|
}
|
@ -2,9 +2,12 @@
|
|||||||
# Uranium is released under the terms of the LGPLv3 or higher.
|
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from PyQt6.QtQml import qmlRegisterType
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
try:
|
try:
|
||||||
from . import ThreeMFWriter
|
from . import ThreeMFWriter
|
||||||
|
from .SettingsExportGroup import SettingsExportGroup
|
||||||
threemf_writer_was_imported = True
|
threemf_writer_was_imported = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
|
Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
|
||||||
@ -23,20 +26,30 @@ def getMetaData():
|
|||||||
|
|
||||||
if threemf_writer_was_imported:
|
if threemf_writer_was_imported:
|
||||||
metaData["mesh_writer"] = {
|
metaData["mesh_writer"] = {
|
||||||
"output": [{
|
"output": [
|
||||||
"extension": "3mf",
|
{
|
||||||
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
|
"extension": "3mf",
|
||||||
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
|
||||||
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
|
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||||
}]
|
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
metaData["workspace_writer"] = {
|
metaData["workspace_writer"] = {
|
||||||
"output": [{
|
"output": [
|
||||||
"extension": workspace_extension,
|
{
|
||||||
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
|
"extension": workspace_extension,
|
||||||
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
|
||||||
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||||
}]
|
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"extension": "3mf",
|
||||||
|
"description": i18n_catalog.i18nc("@item:inlistbox", "Universal Cura Project"),
|
||||||
|
"mime_type": "application/x-ucp",
|
||||||
|
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return metaData
|
return metaData
|
||||||
@ -44,6 +57,8 @@ def getMetaData():
|
|||||||
|
|
||||||
def register(app):
|
def register(app):
|
||||||
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
||||||
|
qmlRegisterType(SettingsExportGroup, "ThreeMFWriter", 1, 0, "SettingsExportGroup")
|
||||||
|
|
||||||
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),
|
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),
|
||||||
"workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()}
|
"workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()}
|
||||||
else:
|
else:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "3MF Writer",
|
"name": "3MF Writer",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for writing 3MF files.",
|
"description": "Provides support for writing 3MF and UCP files.",
|
||||||
"api": 8,
|
"api": 8,
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -76,6 +76,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
self._default_engine_location = executable_name
|
self._default_engine_location = executable_name
|
||||||
|
|
||||||
search_path = [
|
search_path = [
|
||||||
|
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..", "Resources")),
|
||||||
os.path.abspath(os.path.dirname(sys.executable)),
|
os.path.abspath(os.path.dirname(sys.executable)),
|
||||||
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "bin")),
|
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "bin")),
|
||||||
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..")),
|
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..")),
|
||||||
@ -180,7 +181,10 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
||||||
|
|
||||||
self._slicing_error_message = Message(
|
self._slicing_error_message = Message(
|
||||||
text = catalog.i18nc("@message", "Slicing failed with an unexpected error. Please consider reporting a bug on our issue tracker."),
|
text = catalog.i18nc("@message", "Oops! We encountered an unexpected error during your slicing process. "
|
||||||
|
"Rest assured, we've automatically received the crash logs for analysis, "
|
||||||
|
"if you have not disabled data sharing in your preferences. To assist us "
|
||||||
|
"further, consider sharing your project details on our issue tracker."),
|
||||||
title = catalog.i18nc("@message:title", "Slicing failed"),
|
title = catalog.i18nc("@message:title", "Slicing failed"),
|
||||||
message_type = Message.MessageType.ERROR
|
message_type = Message.MessageType.ERROR
|
||||||
)
|
)
|
||||||
@ -540,7 +544,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
|
|
||||||
if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder:
|
if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder:
|
||||||
self._error_message = Message(catalog.i18nc("@info:status",
|
self._error_message = Message(catalog.i18nc("@info:status",
|
||||||
"Unable to slice because there are objects associated with disabled Extruder %s.") % job.getMessage(),
|
"Unable to slice because there are objects associated with disabled Extruder %s.") % job.getAssociatedDisabledExtruders(),
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"),
|
title = catalog.i18nc("@info:title", "Unable to slice"),
|
||||||
message_type = Message.MessageType.WARNING)
|
message_type = Message.MessageType.WARNING)
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
|
@ -146,6 +146,7 @@ class StartSliceJob(Job):
|
|||||||
self._slice_message: Arcus.PythonMessage = slice_message
|
self._slice_message: Arcus.PythonMessage = slice_message
|
||||||
self._is_cancelled: bool = False
|
self._is_cancelled: bool = False
|
||||||
self._build_plate_number: Optional[int] = None
|
self._build_plate_number: Optional[int] = None
|
||||||
|
self._associated_disabled_extruders: Optional[str] = None
|
||||||
|
|
||||||
# cache for all setting values from all stacks (global & extruder) for the current machine
|
# cache for all setting values from all stacks (global & extruder) for the current machine
|
||||||
self._all_extruders_settings: Optional[Dict[str, Any]] = None
|
self._all_extruders_settings: Optional[Dict[str, Any]] = None
|
||||||
@ -153,6 +154,9 @@ class StartSliceJob(Job):
|
|||||||
def getSliceMessage(self) -> Arcus.PythonMessage:
|
def getSliceMessage(self) -> Arcus.PythonMessage:
|
||||||
return self._slice_message
|
return self._slice_message
|
||||||
|
|
||||||
|
def getAssociatedDisabledExtruders(self) -> Optional[str]:
|
||||||
|
return self._associated_disabled_extruders
|
||||||
|
|
||||||
def setBuildPlate(self, build_plate_number: int) -> None:
|
def setBuildPlate(self, build_plate_number: int) -> None:
|
||||||
self._build_plate_number = build_plate_number
|
self._build_plate_number = build_plate_number
|
||||||
|
|
||||||
@ -334,7 +338,7 @@ class StartSliceJob(Job):
|
|||||||
if has_model_with_disabled_extruders:
|
if has_model_with_disabled_extruders:
|
||||||
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
|
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
|
||||||
associated_disabled_extruders = {p + 1 for p in associated_disabled_extruders}
|
associated_disabled_extruders = {p + 1 for p in associated_disabled_extruders}
|
||||||
self.setMessage(", ".join(map(str, sorted(associated_disabled_extruders))))
|
self._associated_disabled_extruders = ", ".join(map(str, sorted(associated_disabled_extruders)))
|
||||||
return
|
return
|
||||||
|
|
||||||
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
|
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
|
||||||
@ -362,7 +366,12 @@ class StartSliceJob(Job):
|
|||||||
for extruder_stack in global_stack.extruderList:
|
for extruder_stack in global_stack.extruderList:
|
||||||
self._buildExtruderMessage(extruder_stack)
|
self._buildExtruderMessage(extruder_stack)
|
||||||
|
|
||||||
for plugin in CuraApplication.getInstance().getBackendPlugins():
|
backend_plugins = CuraApplication.getInstance().getBackendPlugins()
|
||||||
|
|
||||||
|
# Sort backend plugins by name. Not a very good strategy, but at least it is repeatable. This will be improved later.
|
||||||
|
backend_plugins = sorted(backend_plugins, key=lambda backend_plugin: backend_plugin.getId())
|
||||||
|
|
||||||
|
for plugin in backend_plugins:
|
||||||
if not plugin.usePlugin():
|
if not plugin.usePlugin():
|
||||||
continue
|
continue
|
||||||
for slot in plugin.getSupportedSlots():
|
for slot in plugin.getSupportedSlots():
|
||||||
@ -550,12 +559,16 @@ class StartSliceJob(Job):
|
|||||||
start_gcode = settings["machine_start_gcode"]
|
start_gcode = settings["machine_start_gcode"]
|
||||||
# Remove all the comments from the start g-code
|
# Remove all the comments from the start g-code
|
||||||
start_gcode = re.sub(r";.+?(\n|$)", "\n", start_gcode)
|
start_gcode = re.sub(r";.+?(\n|$)", "\n", start_gcode)
|
||||||
bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"]
|
|
||||||
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr}
|
if settings["material_bed_temp_prepend"]:
|
||||||
settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None
|
bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"]
|
||||||
print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature", "print_temperature"]
|
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr}
|
||||||
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr}
|
settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None
|
||||||
settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) is None
|
|
||||||
|
if settings["material_print_temp_prepend"]:
|
||||||
|
print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature", "print_temperature"]
|
||||||
|
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr}
|
||||||
|
settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) is None
|
||||||
|
|
||||||
# Replace the setting tokens in start and end g-code.
|
# Replace the setting tokens in start and end g-code.
|
||||||
# Use values from the first used extruder by default so we get the expected temperatures
|
# Use values from the first used extruder by default so we get the expected temperatures
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# Copyright (c) 2021 Ultimaker B.V.
|
# Copyright (c) 2021 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
|
||||||
from .src import DigitalFactoryFileProvider, DigitalFactoryOutputDevicePlugin, DigitalFactoryController
|
from .src import DigitalFactoryFileProvider, DigitalFactoryOutputDevicePlugin, DigitalFactoryController
|
||||||
|
|
||||||
|
|
||||||
|
@ -208,7 +208,14 @@ Item
|
|||||||
anchors.rightMargin: UM.Theme.getSize("thin_margin").height
|
anchors.rightMargin: UM.Theme.getSize("thin_margin").height
|
||||||
|
|
||||||
enabled: UM.Backend.state == UM.Backend.Done
|
enabled: UM.Backend.state == UM.Backend.Done
|
||||||
currentIndex: UM.Backend.state == UM.Backend.Done ? dfFilenameTextfield.text.startsWith("MM")? 1 : 0 : 2
|
|
||||||
|
// Pre-select the correct index, depending on the situation (see the model-property below):
|
||||||
|
// - Don't select any post-slice-file-format when the engine isn't done.
|
||||||
|
// - Choose either the S-series or the Makerbot-series of printers' format otherwise, depending on the active printer.
|
||||||
|
// This way, the user can just click 'save' without having to worry about wether or not the format is right.
|
||||||
|
property int isMakerbotFormat: Cura.MachineManager.activeMachine.getOutputFileFormats.includes("application/x-makerbot") || Cura.MachineManager.activeMachine.getOutputFileFormats.includes("application/x-makerbot-sketch")
|
||||||
|
property int isBackendDone: UM.Backend.state == UM.Backend.Done
|
||||||
|
currentIndex: isBackendDone ? (isMakerbotFormat ? 1 : 0) : 2
|
||||||
|
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
valueRole: "value"
|
valueRole: "value"
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
import re
|
|
||||||
from time import time
|
from time import time
|
||||||
from typing import List, Any, Optional, Union, Type, Tuple, Dict, cast, TypeVar, Callable
|
from typing import List, Any, Optional, Union, Type, Tuple, Dict, cast, TypeVar, Callable
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ from typing import List, Optional
|
|||||||
|
|
||||||
from PyQt6.QtCore import Qt, pyqtSignal
|
from PyQt6.QtCore import Qt, pyqtSignal
|
||||||
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse
|
from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Platform import Platform
|
|
||||||
|
|
||||||
from . import GCodeGzWriter
|
from . import GCodeGzWriter
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ from UM.Settings.InstanceContainer import InstanceContainer
|
|||||||
from cura.Machines.ContainerTree import ContainerTree
|
from cura.Machines.ContainerTree import ContainerTree
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
# Copyright (c) 2023 UltiMaker
|
# Copyright (c) 2024 UltiMaker
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from io import StringIO, BufferedIOBase
|
from io import StringIO, BufferedIOBase
|
||||||
import json
|
import json
|
||||||
from typing import cast, List, Optional, Dict
|
from typing import cast, List, Optional, Dict, Tuple
|
||||||
from zipfile import BadZipFile, ZipFile, ZIP_DEFLATED
|
from zipfile import BadZipFile, ZipFile, ZIP_DEFLATED
|
||||||
import pyDulcificum as du
|
import pyDulcificum as du
|
||||||
|
|
||||||
@ -19,6 +18,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
from cura.PrinterOutput.FormatMaps import FormatMaps
|
||||||
from cura.Snapshot import Snapshot
|
from cura.Snapshot import Snapshot
|
||||||
from cura.Utils.Threading import call_on_qt_thread
|
from cura.Utils.Threading import call_on_qt_thread
|
||||||
from cura.CuraVersion import ConanInstalls
|
from cura.CuraVersion import ConanInstalls
|
||||||
@ -39,16 +39,27 @@ class MakerbotWriter(MeshWriter):
|
|||||||
suffixes=["makerbot"]
|
suffixes=["makerbot"]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
MimeTypeDatabase.addMimeType(
|
||||||
|
MimeType(
|
||||||
|
name="application/x-makerbot-sketch",
|
||||||
|
comment="Makerbot Toolpath Package",
|
||||||
|
suffixes=["makerbot"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
_PNG_FORMATS = [
|
_PNG_FORMAT = [
|
||||||
{"prefix": "isometric_thumbnail", "width": 120, "height": 120},
|
{"prefix": "isometric_thumbnail", "width": 120, "height": 120},
|
||||||
{"prefix": "isometric_thumbnail", "width": 320, "height": 320},
|
{"prefix": "isometric_thumbnail", "width": 320, "height": 320},
|
||||||
{"prefix": "isometric_thumbnail", "width": 640, "height": 640},
|
{"prefix": "isometric_thumbnail", "width": 640, "height": 640},
|
||||||
|
{"prefix": "thumbnail", "width": 90, "height": 90},
|
||||||
|
]
|
||||||
|
|
||||||
|
_PNG_FORMAT_METHOD = [
|
||||||
{"prefix": "thumbnail", "width": 140, "height": 106},
|
{"prefix": "thumbnail", "width": 140, "height": 106},
|
||||||
{"prefix": "thumbnail", "width": 212, "height": 300},
|
{"prefix": "thumbnail", "width": 212, "height": 300},
|
||||||
{"prefix": "thumbnail", "width": 960, "height": 1460},
|
{"prefix": "thumbnail", "width": 960, "height": 1460},
|
||||||
{"prefix": "thumbnail", "width": 90, "height": 90},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
_META_VERSION = "3.0.0"
|
_META_VERSION = "3.0.0"
|
||||||
|
|
||||||
# must be called from the main thread because of OpenGL
|
# must be called from the main thread because of OpenGL
|
||||||
@ -74,6 +85,7 @@ class MakerbotWriter(MeshWriter):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode=MeshWriter.OutputMode.BinaryMode) -> bool:
|
def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode=MeshWriter.OutputMode.BinaryMode) -> bool:
|
||||||
|
metadata, file_format = self._getMeta(nodes)
|
||||||
if mode != MeshWriter.OutputMode.BinaryMode:
|
if mode != MeshWriter.OutputMode.BinaryMode:
|
||||||
Logger.log("e", "MakerbotWriter does not support text mode.")
|
Logger.log("e", "MakerbotWriter does not support text mode.")
|
||||||
self.setInformation(catalog.i18nc("@error:not supported", "MakerbotWriter does not support text mode."))
|
self.setInformation(catalog.i18nc("@error:not supported", "MakerbotWriter does not support text mode."))
|
||||||
@ -92,14 +104,20 @@ class MakerbotWriter(MeshWriter):
|
|||||||
|
|
||||||
gcode_text_io = StringIO()
|
gcode_text_io = StringIO()
|
||||||
success = gcode_writer.write(gcode_text_io, None)
|
success = gcode_writer.write(gcode_text_io, None)
|
||||||
|
filename, filedata = "", ""
|
||||||
# Writing the g-code failed. Then I can also not write the gzipped g-code.
|
# Writing the g-code failed. Then I can also not write the gzipped g-code.
|
||||||
if not success:
|
if not success:
|
||||||
self.setInformation(gcode_writer.getInformation())
|
self.setInformation(gcode_writer.getInformation())
|
||||||
return False
|
return False
|
||||||
|
match file_format:
|
||||||
json_toolpaths = du.gcode_2_miracle_jtp(gcode_text_io.getvalue())
|
case "application/x-makerbot-sketch":
|
||||||
metadata = self._getMeta(nodes)
|
filename, filedata = "print.gcode", gcode_text_io.getvalue()
|
||||||
|
self._PNG_FORMATS = self._PNG_FORMAT
|
||||||
|
case "application/x-makerbot":
|
||||||
|
filename, filedata = "print.jsontoolpath", du.gcode_2_miracle_jtp(gcode_text_io.getvalue())
|
||||||
|
self._PNG_FORMATS = self._PNG_FORMAT + self._PNG_FORMAT_METHOD
|
||||||
|
case _:
|
||||||
|
raise Exception("Unsupported Mime type")
|
||||||
|
|
||||||
png_files = []
|
png_files = []
|
||||||
for png_format in self._PNG_FORMATS:
|
for png_format in self._PNG_FORMATS:
|
||||||
@ -116,10 +134,34 @@ class MakerbotWriter(MeshWriter):
|
|||||||
try:
|
try:
|
||||||
with ZipFile(stream, "w", compression=ZIP_DEFLATED) as zip_stream:
|
with ZipFile(stream, "w", compression=ZIP_DEFLATED) as zip_stream:
|
||||||
zip_stream.writestr("meta.json", json.dumps(metadata, indent=4))
|
zip_stream.writestr("meta.json", json.dumps(metadata, indent=4))
|
||||||
zip_stream.writestr("print.jsontoolpath", json_toolpaths)
|
zip_stream.writestr(filename, filedata)
|
||||||
for png_file in png_files:
|
for png_file in png_files:
|
||||||
file, data = png_file["file"], png_file["data"]
|
file, data = png_file["file"], png_file["data"]
|
||||||
zip_stream.writestr(file, data)
|
zip_stream.writestr(file, data)
|
||||||
|
api = CuraApplication.getInstance().getCuraAPI()
|
||||||
|
metadata_json = api.interface.settings.getSliceMetadata()
|
||||||
|
|
||||||
|
# All the mapping stuff we have to do:
|
||||||
|
product_to_id_map = FormatMaps.getProductIdMap()
|
||||||
|
printer_name_map = FormatMaps.getInversePrinterNameMap()
|
||||||
|
extruder_type_map = FormatMaps.getInverseExtruderTypeMap()
|
||||||
|
material_map = FormatMaps.getInverseMaterialMap()
|
||||||
|
for key, value in metadata_json.items():
|
||||||
|
if "all_settings" in value:
|
||||||
|
if "machine_name" in value["all_settings"]:
|
||||||
|
machine_name = value["all_settings"]["machine_name"]
|
||||||
|
if machine_name in product_to_id_map:
|
||||||
|
machine_name = product_to_id_map[machine_name][0]
|
||||||
|
value["all_settings"]["machine_name"] = printer_name_map.get(machine_name, machine_name)
|
||||||
|
if "machine_nozzle_id" in value["all_settings"]:
|
||||||
|
extruder_type = value["all_settings"]["machine_nozzle_id"]
|
||||||
|
value["all_settings"]["machine_nozzle_id"] = extruder_type_map.get(extruder_type, extruder_type)
|
||||||
|
if "material_type" in value["all_settings"]:
|
||||||
|
material_type = value["all_settings"]["material_type"]
|
||||||
|
value["all_settings"]["material_type"] = material_map.get(material_type, material_type)
|
||||||
|
|
||||||
|
slice_metadata = json.dumps(metadata_json, separators=(", ", ": "), indent=4)
|
||||||
|
zip_stream.writestr("slicemetadata.json", slice_metadata)
|
||||||
except (IOError, OSError, BadZipFile) as ex:
|
except (IOError, OSError, BadZipFile) as ex:
|
||||||
Logger.log("e", f"Could not write to (.makerbot) file because: '{ex}'.")
|
Logger.log("e", f"Could not write to (.makerbot) file because: '{ex}'.")
|
||||||
self.setInformation(catalog.i18nc("@error", "MakerbotWriter could not save to the designated path."))
|
self.setInformation(catalog.i18nc("@error", "MakerbotWriter could not save to the designated path."))
|
||||||
@ -127,7 +169,7 @@ class MakerbotWriter(MeshWriter):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _getMeta(self, root_nodes: List[SceneNode]) -> Dict[str, any]:
|
def _getMeta(self, root_nodes: List[SceneNode]) -> Tuple[Dict[str, any], str]:
|
||||||
application = CuraApplication.getInstance()
|
application = CuraApplication.getInstance()
|
||||||
machine_manager = application.getMachineManager()
|
machine_manager = application.getMachineManager()
|
||||||
global_stack = machine_manager.activeMachine
|
global_stack = machine_manager.activeMachine
|
||||||
@ -143,7 +185,9 @@ class MakerbotWriter(MeshWriter):
|
|||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
|
|
||||||
meta = dict()
|
meta = dict()
|
||||||
|
# This is a bit of a "hack", the mime type should be passed through with the export writer but
|
||||||
|
# since this is not the case we get the mime type from the global stack instead
|
||||||
|
file_format = global_stack.definition.getMetaDataEntry("file_formats")
|
||||||
meta["bot_type"] = global_stack.definition.getMetaDataEntry("reference_machine_id")
|
meta["bot_type"] = global_stack.definition.getMetaDataEntry("reference_machine_id")
|
||||||
|
|
||||||
bounds: Optional[AxisAlignedBox] = None
|
bounds: Optional[AxisAlignedBox] = None
|
||||||
@ -155,7 +199,8 @@ class MakerbotWriter(MeshWriter):
|
|||||||
bounds = node_bounds
|
bounds = node_bounds
|
||||||
else:
|
else:
|
||||||
bounds = bounds + node_bounds
|
bounds = bounds + node_bounds
|
||||||
|
if file_format == "application/x-makerbot-sketch":
|
||||||
|
bounds = None
|
||||||
if bounds is not None:
|
if bounds is not None:
|
||||||
meta["bounding_box"] = {
|
meta["bounding_box"] = {
|
||||||
"x_min": bounds.left,
|
"x_min": bounds.left,
|
||||||
@ -196,7 +241,7 @@ class MakerbotWriter(MeshWriter):
|
|||||||
meta["extruder_temperature"] = materials_temps[0]
|
meta["extruder_temperature"] = materials_temps[0]
|
||||||
meta["extruder_temperatures"] = materials_temps
|
meta["extruder_temperatures"] = materials_temps
|
||||||
|
|
||||||
meta["model_counts"] = [{"count": 1, "name": node.getName()} for node in nodes]
|
meta["model_counts"] = [{"count": len(nodes), "name": "instance0"}]
|
||||||
|
|
||||||
tool_types = [extruder.variant.getMetaDataEntry("reference_extruder_id") for extruder in extruders]
|
tool_types = [extruder.variant.getMetaDataEntry("reference_extruder_id") for extruder in extruders]
|
||||||
meta["tool_type"] = tool_types[0]
|
meta["tool_type"] = tool_types[0]
|
||||||
@ -205,14 +250,13 @@ class MakerbotWriter(MeshWriter):
|
|||||||
meta["version"] = MakerbotWriter._META_VERSION
|
meta["version"] = MakerbotWriter._META_VERSION
|
||||||
|
|
||||||
meta["preferences"] = dict()
|
meta["preferences"] = dict()
|
||||||
for node in nodes:
|
bounds = application.getBuildVolume().getBoundingBox()
|
||||||
bounds = node.getBoundingBox()
|
meta["preferences"]["instance0"] = {
|
||||||
meta["preferences"][str(node.getName())] = {
|
"machineBounds": [bounds.right, bounds.back, bounds.left, bounds.front] if bounds is not None else None,
|
||||||
"machineBounds": [bounds.right, bounds.back, bounds.left, bounds.front] if bounds is not None else None,
|
"printMode": CuraApplication.getInstance().getIntentManager().currentIntentCategory,
|
||||||
"printMode": CuraApplication.getInstance().getIntentManager().currentIntentCategory,
|
}
|
||||||
}
|
|
||||||
|
|
||||||
meta["miracle_config"] = {"gaggles": {str(node.getName()): {} for node in nodes}}
|
meta["miracle_config"] = {"gaggles": {"instance0": {}}}
|
||||||
|
|
||||||
version_info = dict()
|
version_info = dict()
|
||||||
cura_engine_info = ConanInstalls.get("curaengine", {"version": "unknown", "revision": "unknown"})
|
cura_engine_info = ConanInstalls.get("curaengine", {"version": "unknown", "revision": "unknown"})
|
||||||
@ -245,7 +289,7 @@ class MakerbotWriter(MeshWriter):
|
|||||||
# platform_temperature
|
# platform_temperature
|
||||||
# total_commands
|
# total_commands
|
||||||
|
|
||||||
return meta
|
return meta, file_format
|
||||||
|
|
||||||
|
|
||||||
def meterToMillimeter(value: float) -> float:
|
def meterToMillimeter(value: float) -> float:
|
||||||
|
@ -11,14 +11,23 @@ catalog = i18nCatalog("cura")
|
|||||||
def getMetaData():
|
def getMetaData():
|
||||||
file_extension = "makerbot"
|
file_extension = "makerbot"
|
||||||
return {
|
return {
|
||||||
"mesh_writer": {
|
"mesh_writer":
|
||||||
"output": [{
|
{
|
||||||
"extension": file_extension,
|
"output": [
|
||||||
"description": catalog.i18nc("@item:inlistbox", "Makerbot Printfile"),
|
{
|
||||||
"mime_type": "application/x-makerbot",
|
"extension": file_extension,
|
||||||
"mode": MakerbotWriter.MakerbotWriter.OutputMode.BinaryMode,
|
"description": catalog.i18nc("@item:inlistbox", "Makerbot Printfile"),
|
||||||
}],
|
"mime_type": "application/x-makerbot",
|
||||||
}
|
"mode": MakerbotWriter.MakerbotWriter.OutputMode.BinaryMode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"extension": file_extension,
|
||||||
|
"description": catalog.i18nc("@item:inlistbox", "Makerbot Sketch Printfile"),
|
||||||
|
"mime_type": "application/x-makerbot-sketch",
|
||||||
|
"mode": MakerbotWriter.MakerbotWriter.OutputMode.BinaryMode,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,12 +3,10 @@
|
|||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING, Dict, List
|
from typing import Optional, TYPE_CHECKING, Dict, List
|
||||||
|
|
||||||
from .Constants import PACKAGES_URL
|
|
||||||
from .PackageModel import PackageModel
|
from .PackageModel import PackageModel
|
||||||
from .RemotePackageList import RemotePackageList
|
from .RemotePackageList import RemotePackageList
|
||||||
from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
|
from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
|
||||||
|
|
||||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API.
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from enum import Enum
|
|
||||||
from typing import Any, cast, Dict, List, Optional
|
from typing import Any, cast, Dict, List, Optional
|
||||||
|
|
||||||
from PyQt6.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot
|
from PyQt6.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot
|
||||||
@ -12,7 +11,6 @@ from cura.CuraApplication import CuraApplication
|
|||||||
from cura.CuraPackageManager import CuraPackageManager
|
from cura.CuraPackageManager import CuraPackageManager
|
||||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with.
|
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with.
|
||||||
from UM.i18n import i18nCatalog # To translate placeholder names if data is not present.
|
from UM.i18n import i18nCatalog # To translate placeholder names if data is not present.
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
@ -11,7 +11,7 @@ from UM.Settings.SettingInstance import SettingInstance
|
|||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
import UM.Settings.Models.SettingVisibilityHandler
|
import UM.Settings.Models.SettingVisibilityHandler
|
||||||
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager #To get global-inherits-stack setting values from different extruders.
|
from cura.Settings.ExtruderManager import ExtruderManager # To get global-inherits-stack setting values from different extruders.
|
||||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||||
|
|
||||||
|
|
||||||
|
@ -120,6 +120,8 @@ UM.Dialog
|
|||||||
UM.Label
|
UM.Label
|
||||||
{
|
{
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: height
|
||||||
|
elide: Text.ElideRight
|
||||||
text: manager.getScriptLabelByKey(modelData.toString())
|
text: manager.getScriptLabelByKey(modelData.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
# M163 - Set Mix Factor
|
# M163 - Set Mix Factor
|
||||||
# M164 - Save Mix - saves to T2 as a unique mix
|
# M164 - Save Mix - saves to T2 as a unique mix
|
||||||
|
|
||||||
import re #To perform the search and replace.
|
import re # To perform the search and replace.
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
|
|
||||||
class ColorMix(Script):
|
class ColorMix(Script):
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
# Description: This plugin is now an option in 'Display Info on LCD'
|
# Description: This plugin is now an option in 'Display Info on LCD'
|
||||||
|
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
|
|
||||||
class DisplayFilenameAndLayerOnLCD(Script):
|
class DisplayFilenameAndLayerOnLCD(Script):
|
||||||
|
@ -30,9 +30,6 @@
|
|||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Qt.Duration import DurationFormat
|
from UM.Qt.Duration import DurationFormat
|
||||||
import UM.Util
|
|
||||||
import configparser
|
|
||||||
from UM.Preferences import Preferences
|
|
||||||
import time
|
import time
|
||||||
import datetime
|
import datetime
|
||||||
import math
|
import math
|
||||||
|
@ -7,8 +7,6 @@
|
|||||||
|
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
|
|
||||||
import re
|
|
||||||
import datetime
|
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
|
|
||||||
class DisplayProgressOnLCD(Script):
|
class DisplayProgressOnLCD(Script):
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
|
|
||||||
from UM.Application import Application #To get the current printer's settings.
|
from UM.Application import Application # To get the current printer's settings.
|
||||||
|
|
||||||
class FilamentChange(Script):
|
class FilamentChange(Script):
|
||||||
|
|
||||||
|
@ -26,27 +26,40 @@ class InsertAtLayerChange(Script):
|
|||||||
},
|
},
|
||||||
"gcode_to_add":
|
"gcode_to_add":
|
||||||
{
|
{
|
||||||
"label": "G-code to insert.",
|
"label": "G-code to insert",
|
||||||
"description": "G-code to add before or after layer change.",
|
"description": "G-code to add before or after layer change.",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"default_value": ""
|
"default_value": ""
|
||||||
|
},
|
||||||
|
"skip_layers":
|
||||||
|
{
|
||||||
|
"label": "Skip layers",
|
||||||
|
"description": "Number of layers to skip between insertions (0 for every layer).",
|
||||||
|
"type": "int",
|
||||||
|
"default_value": 0,
|
||||||
|
"minimum_value": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
def execute(self, data):
|
def execute(self, data):
|
||||||
gcode_to_add = self.getSettingValueByKey("gcode_to_add") + "\n"
|
gcode_to_add = self.getSettingValueByKey("gcode_to_add") + "\n"
|
||||||
|
skip_layers = self.getSettingValueByKey("skip_layers")
|
||||||
|
count = 0
|
||||||
for layer in data:
|
for layer in data:
|
||||||
# Check that a layer is being printed
|
# Check that a layer is being printed
|
||||||
lines = layer.split("\n")
|
lines = layer.split("\n")
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if ";LAYER:" in line:
|
if ";LAYER:" in line:
|
||||||
index = data.index(layer)
|
index = data.index(layer)
|
||||||
if self.getSettingValueByKey("insert_location") == "before":
|
if count == 0:
|
||||||
layer = gcode_to_add + layer
|
if self.getSettingValueByKey("insert_location") == "before":
|
||||||
else:
|
layer = gcode_to_add + layer
|
||||||
layer = layer + gcode_to_add
|
else:
|
||||||
|
layer = layer + gcode_to_add
|
||||||
|
|
||||||
data[index] = layer
|
data[index] = layer
|
||||||
|
|
||||||
|
count = (count + 1) % (skip_layers + 1)
|
||||||
break
|
break
|
||||||
return data
|
return data
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
# When setting an accel limit on multi-extruder printers ALL extruders are effected.
|
# When setting an accel limit on multi-extruder printers ALL extruders are effected.
|
||||||
# This post does not distinguish between Print Accel and Travel Accel. The limit is the limit for all regardless. Example: Skin Accel = 1000 and Outer Wall accel = 500. If the limit is set to 300 then both Skin and Outer Wall will be Accel = 300.
|
# This post does not distinguish between Print Accel and Travel Accel. The limit is the limit for all regardless. Example: Skin Accel = 1000 and Outer Wall accel = 500. If the limit is set to 300 then both Skin and Outer Wall will be Accel = 300.
|
||||||
# 9/15/2023 added support for RepRap M566 command for Jerk in mm/min
|
# 9/15/2023 added support for RepRap M566 command for Jerk in mm/min
|
||||||
|
# 2/4/2024 Added a block so the script doesn't run unless Accel Control is enabled in Cura. This should keep users from increasing the Accel Limits.
|
||||||
|
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
@ -45,6 +46,10 @@ class LimitXYAccelJerk(Script):
|
|||||||
# Warn the user if the printer is multi-extruder------------------
|
# Warn the user if the printer is multi-extruder------------------
|
||||||
if ext_count > 1:
|
if ext_count > 1:
|
||||||
Message(text = "<NOTICE> 'Limit the X-Y Accel/Jerk': The post processor treats all extruders the same. If you have multiple extruders they will all be subject to the same Accel and Jerk limits imposed. If you have different Travel and Print Accel they will also be subject to the same limits. If that is not acceptable then you should not use this Post Processor.").show()
|
Message(text = "<NOTICE> 'Limit the X-Y Accel/Jerk': The post processor treats all extruders the same. If you have multiple extruders they will all be subject to the same Accel and Jerk limits imposed. If you have different Travel and Print Accel they will also be subject to the same limits. If that is not acceptable then you should not use this Post Processor.").show()
|
||||||
|
|
||||||
|
# Warn the user if Accel Control is not enabled in Cura. This keeps the script from being able to increase the Accel limits-----------
|
||||||
|
if not bool(extruder[0].getProperty("acceleration_enabled", "value")):
|
||||||
|
Message(title = "[Limit the X-Y Accel/Jerk]", text = "You must have 'Enable Acceleration Control' checked in Cura or the script will exit.").show()
|
||||||
|
|
||||||
def getSettingDataString(self):
|
def getSettingDataString(self):
|
||||||
return """{
|
return """{
|
||||||
@ -169,6 +174,13 @@ class LimitXYAccelJerk(Script):
|
|||||||
extruder = mycura.extruderList
|
extruder = mycura.extruderList
|
||||||
machine_name = str(mycura.getProperty("machine_name", "value"))
|
machine_name = str(mycura.getProperty("machine_name", "value"))
|
||||||
print_sequence = str(mycura.getProperty("print_sequence", "value"))
|
print_sequence = str(mycura.getProperty("print_sequence", "value"))
|
||||||
|
acceleration_enabled = bool(extruder[0].getProperty("acceleration_enabled", "value"))
|
||||||
|
|
||||||
|
# Exit if acceleration control is not enabled----------------
|
||||||
|
if not acceleration_enabled:
|
||||||
|
Message(title = "[Limit the X-Y Accel/Jerk]", text = "DID NOT RUN. You must have 'Enable Acceleration Control' checked in Cura.").show()
|
||||||
|
data[0] += "; [LimitXYAccelJerk] DID NOT RUN because 'Enable Acceleration Control' is not checked in Cura.\n"
|
||||||
|
return data
|
||||||
|
|
||||||
# Exit if 'one_at_a_time' is enabled-------------------------
|
# Exit if 'one_at_a_time' is enabled-------------------------
|
||||||
if print_sequence == "one_at_a_time":
|
if print_sequence == "one_at_a_time":
|
||||||
@ -183,12 +195,8 @@ class LimitXYAccelJerk(Script):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
type_of_change = str(self.getSettingValueByKey("type_of_change"))
|
type_of_change = str(self.getSettingValueByKey("type_of_change"))
|
||||||
accel_print_enabled = bool(extruder[0].getProperty("acceleration_enabled", "value"))
|
|
||||||
accel_travel_enabled = bool(extruder[0].getProperty("acceleration_travel_enabled", "value"))
|
|
||||||
accel_print = extruder[0].getProperty("acceleration_print", "value")
|
accel_print = extruder[0].getProperty("acceleration_print", "value")
|
||||||
accel_travel = extruder[0].getProperty("acceleration_travel", "value")
|
accel_travel = extruder[0].getProperty("acceleration_travel", "value")
|
||||||
jerk_print_enabled = str(extruder[0].getProperty("jerk_enabled", "value"))
|
|
||||||
jerk_travel_enabled = str(extruder[0].getProperty("jerk_travel_enabled", "value"))
|
|
||||||
jerk_print_old = extruder[0].getProperty("jerk_print", "value")
|
jerk_print_old = extruder[0].getProperty("jerk_print", "value")
|
||||||
jerk_travel_old = extruder[0].getProperty("jerk_travel", "value")
|
jerk_travel_old = extruder[0].getProperty("jerk_travel", "value")
|
||||||
if int(accel_print) >= int(accel_travel):
|
if int(accel_print) >= int(accel_travel):
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
import re
|
import re
|
||||||
from UM.Application import Application #To get the current printer's settings.
|
from UM.Application import Application # To get the current printer's settings.
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Copyright (c) 2017 Ghostkeeper
|
# Copyright (c) 2017 Ghostkeeper
|
||||||
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
|
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import re #To perform the search and replace.
|
import re # To perform the search and replace.
|
||||||
|
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from UM.Application import Application
|
|||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.FileHandler.WriteFileJob import WriteFileJob
|
from UM.FileHandler.WriteFileJob import WriteFileJob
|
||||||
from UM.FileHandler.FileWriter import FileWriter #To check against the write modes (text vs. binary).
|
from UM.FileHandler.FileWriter import FileWriter # To check against the write modes (text vs. binary).
|
||||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||||
from UM.OutputDevice import OutputDeviceError
|
from UM.OutputDevice import OutputDeviceError
|
||||||
|
@ -121,6 +121,7 @@ class SimulationPass(RenderPass):
|
|||||||
disabled_batch = RenderBatch(self._disabled_shader)
|
disabled_batch = RenderBatch(self._disabled_shader)
|
||||||
head_position = None # Indicates the current position of the print head
|
head_position = None # Indicates the current position of the print head
|
||||||
nozzle_node = None
|
nozzle_node = None
|
||||||
|
not_a_vector = Vector(math.nan, math.nan, math.nan)
|
||||||
|
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
|
|
||||||
@ -143,12 +144,17 @@ class SimulationPass(RenderPass):
|
|||||||
if self._layer_view.getCurrentLayer() > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
|
if self._layer_view.getCurrentLayer() > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
|
||||||
start = 0
|
start = 0
|
||||||
end = 0
|
end = 0
|
||||||
|
vertex_before_head = not_a_vector
|
||||||
|
vertex_after_head = not_a_vector
|
||||||
|
vertex_distance_ratio = 0.0
|
||||||
|
towards_next_vertex = 0
|
||||||
element_counts = layer_data.getElementCounts()
|
element_counts = layer_data.getElementCounts()
|
||||||
for layer in sorted(element_counts.keys()):
|
for layer in sorted(element_counts.keys()):
|
||||||
# In the current layer, we show just the indicated paths
|
# In the current layer, we show just the indicated paths
|
||||||
if layer == self._layer_view._current_layer_num:
|
if layer == self._layer_view._current_layer_num:
|
||||||
# We look for the position of the head, searching the point of the current path
|
# We look for the position of the head, searching the point of the current path
|
||||||
index = int(self._layer_view.getCurrentPath())
|
index = int(self._layer_view.getCurrentPath()) if not math.isnan(
|
||||||
|
self._layer_view.getCurrentPath()) else 0
|
||||||
for polygon in layer_data.getLayer(layer).polygons:
|
for polygon in layer_data.getLayer(layer).polygons:
|
||||||
# The size indicates all values in the two-dimension array, and the second dimension is
|
# The size indicates all values in the two-dimension array, and the second dimension is
|
||||||
# always size 3 because we have 3D points.
|
# always size 3 because we have 3D points.
|
||||||
@ -159,6 +165,8 @@ class SimulationPass(RenderPass):
|
|||||||
ratio = self._layer_view.getCurrentPath() - math.floor(self._layer_view.getCurrentPath())
|
ratio = self._layer_view.getCurrentPath() - math.floor(self._layer_view.getCurrentPath())
|
||||||
pos_a = Vector(polygon.data[index][0], polygon.data[index][1],
|
pos_a = Vector(polygon.data[index][0], polygon.data[index][1],
|
||||||
polygon.data[index][2])
|
polygon.data[index][2])
|
||||||
|
vertex_before_head = pos_a
|
||||||
|
vertex_distance_ratio = ratio
|
||||||
if ratio <= 0.0001 or index + 1 == len(polygon.data):
|
if ratio <= 0.0001 or index + 1 == len(polygon.data):
|
||||||
# in case there multiple polygons and polygon changes, the first point has the same value as the last point in the previous polygon
|
# in case there multiple polygons and polygon changes, the first point has the same value as the last point in the previous polygon
|
||||||
head_position = pos_a + node.getWorldPosition()
|
head_position = pos_a + node.getWorldPosition()
|
||||||
@ -168,6 +176,8 @@ class SimulationPass(RenderPass):
|
|||||||
polygon.data[index + 1][2])
|
polygon.data[index + 1][2])
|
||||||
vec = pos_a * (1.0 - ratio) + pos_b * ratio
|
vec = pos_a * (1.0 - ratio) + pos_b * ratio
|
||||||
head_position = vec + node.getWorldPosition()
|
head_position = vec + node.getWorldPosition()
|
||||||
|
vertex_after_head = pos_b
|
||||||
|
towards_next_vertex = 2 # Add two to the index to print the current and next vertices as an 'unfinished' line (to the nozzle).
|
||||||
break
|
break
|
||||||
break
|
break
|
||||||
if self._layer_view.getMinimumLayer() > layer:
|
if self._layer_view.getMinimumLayer() > layer:
|
||||||
@ -187,6 +197,11 @@ class SimulationPass(RenderPass):
|
|||||||
self._current_shader = self._layer_shader
|
self._current_shader = self._layer_shader
|
||||||
self._switching_layers = True
|
self._switching_layers = True
|
||||||
|
|
||||||
|
# reset 'last vertex'
|
||||||
|
self._layer_shader.setUniformValue("u_last_vertex", not_a_vector)
|
||||||
|
self._layer_shader.setUniformValue("u_next_vertex", not_a_vector)
|
||||||
|
self._layer_shader.setUniformValue("u_last_line_ratio", 1.0)
|
||||||
|
|
||||||
# The first line does not have a previous line: add a MoveCombingType in front for start detection
|
# The first line does not have a previous line: add a MoveCombingType in front for start detection
|
||||||
# this way the first start of the layer can also be drawn
|
# this way the first start of the layer can also be drawn
|
||||||
prev_line_types = numpy.concatenate([numpy.asarray([LayerPolygon.MoveCombingType], dtype = numpy.float32), layer_data._attributes["line_types"]["value"]])
|
prev_line_types = numpy.concatenate([numpy.asarray([LayerPolygon.MoveCombingType], dtype = numpy.float32), layer_data._attributes["line_types"]["value"]])
|
||||||
@ -203,6 +218,17 @@ class SimulationPass(RenderPass):
|
|||||||
current_layer_batch.addItem(node.getWorldTransformation(), layer_data)
|
current_layer_batch.addItem(node.getWorldTransformation(), layer_data)
|
||||||
current_layer_batch.render(self._scene.getActiveCamera())
|
current_layer_batch.render(self._scene.getActiveCamera())
|
||||||
|
|
||||||
|
# Last line may be partial
|
||||||
|
if vertex_after_head != not_a_vector and vertex_after_head != not_a_vector:
|
||||||
|
self._layer_shader.setUniformValue("u_last_vertex", vertex_before_head)
|
||||||
|
self._layer_shader.setUniformValue("u_next_vertex", vertex_after_head)
|
||||||
|
self._layer_shader.setUniformValue("u_last_line_ratio", vertex_distance_ratio)
|
||||||
|
last_line_start = current_layer_end
|
||||||
|
last_line_end = current_layer_end + towards_next_vertex
|
||||||
|
last_line_batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode=RenderBatch.RenderMode.Lines, range = (last_line_start, last_line_end))
|
||||||
|
last_line_batch.addItem(node.getWorldTransformation(), layer_data)
|
||||||
|
last_line_batch.render(self._scene.getActiveCamera())
|
||||||
|
|
||||||
self._old_current_layer = self._layer_view.getCurrentLayer()
|
self._old_current_layer = self._layer_view.getCurrentLayer()
|
||||||
self._old_current_path = self._layer_view.getCurrentPath()
|
self._old_current_path = self._layer_view.getCurrentPath()
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# Copyright (c) 2021 Ultimaker B.V.
|
# Copyright (c) 2021 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
import math
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
@ -216,7 +217,8 @@ class SimulationView(CuraView):
|
|||||||
Logger.warn(
|
Logger.warn(
|
||||||
f"Binary search error (out of bounds): index {i}: left value {left_value} right value {right_value} and current time is {self._current_time}")
|
f"Binary search error (out of bounds): index {i}: left value {left_value} right value {right_value} and current time is {self._current_time}")
|
||||||
|
|
||||||
fractional_value = (self._current_time - left_value) / (right_value - left_value)
|
segment_duration = right_value - left_value
|
||||||
|
fractional_value = 0.0 if segment_duration == 0.0 else (self._current_time - left_value) / segment_duration
|
||||||
|
|
||||||
self.setPath(i + fractional_value)
|
self.setPath(i + fractional_value)
|
||||||
|
|
||||||
@ -372,7 +374,10 @@ class SimulationView(CuraView):
|
|||||||
self._minimum_path_num = min(self._minimum_path_num, self._current_path_num)
|
self._minimum_path_num = min(self._minimum_path_num, self._current_path_num)
|
||||||
# update _current time when the path is changed by user
|
# update _current time when the path is changed by user
|
||||||
if self._current_path_num < self._max_paths and round(self._current_path_num)== self._current_path_num:
|
if self._current_path_num < self._max_paths and round(self._current_path_num)== self._current_path_num:
|
||||||
self._current_time = self.cumulativeLineDuration()[int(self._current_path_num)]
|
actual_path_num = int(self._current_path_num)
|
||||||
|
cumulative_line_duration = self.cumulativeLineDuration()
|
||||||
|
if actual_path_num < len(cumulative_line_duration):
|
||||||
|
self._current_time = cumulative_line_duration[actual_path_num]
|
||||||
|
|
||||||
self._startUpdateTopLayers()
|
self._startUpdateTopLayers()
|
||||||
self.currentPathNumChanged.emit()
|
self.currentPathNumChanged.emit()
|
||||||
|
@ -19,6 +19,10 @@ vertex41core =
|
|||||||
|
|
||||||
uniform highp mat4 u_normalMatrix;
|
uniform highp mat4 u_normalMatrix;
|
||||||
|
|
||||||
|
uniform vec3 u_last_vertex;
|
||||||
|
uniform vec3 u_next_vertex;
|
||||||
|
uniform float u_last_line_ratio;
|
||||||
|
|
||||||
in highp vec4 a_vertex;
|
in highp vec4 a_vertex;
|
||||||
in lowp vec4 a_color;
|
in lowp vec4 a_color;
|
||||||
in lowp vec4 a_material_color;
|
in lowp vec4 a_material_color;
|
||||||
@ -134,6 +138,10 @@ vertex41core =
|
|||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
vec4 v1_vertex = a_vertex;
|
vec4 v1_vertex = a_vertex;
|
||||||
|
if (v1_vertex.xyz == u_next_vertex)
|
||||||
|
{
|
||||||
|
v1_vertex.xyz = mix(u_last_vertex, u_next_vertex, u_last_line_ratio);
|
||||||
|
}
|
||||||
v1_vertex.y -= a_line_dim.y / 2; // half layer down
|
v1_vertex.y -= a_line_dim.y / 2; // half layer down
|
||||||
|
|
||||||
vec4 world_space_vert = u_modelMatrix * v1_vertex;
|
vec4 world_space_vert = u_modelMatrix * v1_vertex;
|
||||||
@ -348,7 +356,10 @@ geometry41core =
|
|||||||
EndPrimitive();
|
EndPrimitive();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((u_show_starts == 1) && (v_prev_line_type[0] != 1) && (v_line_type[0] == 1)) {
|
if ((u_show_starts == 1) && (
|
||||||
|
((v_prev_line_type[0] != 1) && (v_line_type[0] == 1)) ||
|
||||||
|
((v_prev_line_type[0] != 4) && (v_line_type[0] == 4))
|
||||||
|
)) {
|
||||||
float w = size_x;
|
float w = size_x;
|
||||||
float h = size_y;
|
float h = size_y;
|
||||||
|
|
||||||
@ -427,6 +438,10 @@ u_max_feedrate = 1
|
|||||||
u_min_thickness = 0
|
u_min_thickness = 0
|
||||||
u_max_thickness = 1
|
u_max_thickness = 1
|
||||||
|
|
||||||
|
u_last_vertex = [0.0, 0.0, 0.0]
|
||||||
|
u_next_vertex = [0.0, 0.0, 0.0]
|
||||||
|
u_last_line_ratio = 1.0
|
||||||
|
|
||||||
[bindings]
|
[bindings]
|
||||||
u_modelMatrix = model_matrix
|
u_modelMatrix = model_matrix
|
||||||
u_viewMatrix = view_matrix
|
u_viewMatrix = view_matrix
|
||||||
|
@ -5,7 +5,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import time
|
import time
|
||||||
from typing import cast, Optional, Set, TYPE_CHECKING
|
from typing import Optional, Set, TYPE_CHECKING
|
||||||
|
|
||||||
from PyQt6.QtCore import pyqtSlot, QObject
|
from PyQt6.QtCore import pyqtSlot, QObject
|
||||||
from PyQt6.QtNetwork import QNetworkRequest
|
from PyQt6.QtNetwork import QNetworkRequest
|
||||||
@ -264,6 +264,7 @@ class SliceInfo(QObject, Extension):
|
|||||||
|
|
||||||
# Prime tower settings
|
# Prime tower settings
|
||||||
print_settings["prime_tower_enable"] = global_stack.getProperty("prime_tower_enable", "value")
|
print_settings["prime_tower_enable"] = global_stack.getProperty("prime_tower_enable", "value")
|
||||||
|
print_settings["prime_tower_mode"] = global_stack.getProperty("prime_tower_mode", "value")
|
||||||
|
|
||||||
# Infill settings
|
# Infill settings
|
||||||
print_settings["infill_sparse_density"] = global_stack.getProperty("infill_sparse_density", "value")
|
print_settings["infill_sparse_density"] = global_stack.getProperty("infill_sparse_density", "value")
|
||||||
|
@ -16,8 +16,6 @@ from UM.Application import Application
|
|||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.Math.Color import Color
|
from UM.Math.Color import Color
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from UM.Platform import Platform
|
|
||||||
from UM.Event import Event
|
from UM.Event import Event
|
||||||
|
|
||||||
from UM.View.RenderBatch import RenderBatch
|
from UM.View.RenderBatch import RenderBatch
|
||||||
|
@ -22,9 +22,9 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
from cura.Utils.Threading import call_on_qt_thread
|
from cura.Utils.Threading import call_on_qt_thread
|
||||||
|
from cura.API import CuraAPI
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
|
||||||
@ -86,7 +86,8 @@ class UFPWriter(MeshWriter):
|
|||||||
try:
|
try:
|
||||||
archive.addContentType(extension="json", mime_type="application/json")
|
archive.addContentType(extension="json", mime_type="application/json")
|
||||||
setting_textio = StringIO()
|
setting_textio = StringIO()
|
||||||
json.dump(self._getSliceMetadata(), setting_textio, separators=(", ", ": "), indent=4)
|
api = CuraApplication.getInstance().getCuraAPI()
|
||||||
|
json.dump(api.interface.settings.getSliceMetadata(), setting_textio, separators=(", ", ": "), indent=4)
|
||||||
steam = archive.getStream(SLICE_METADATA_PATH)
|
steam = archive.getStream(SLICE_METADATA_PATH)
|
||||||
steam.write(setting_textio.getvalue().encode("UTF-8"))
|
steam.write(setting_textio.getvalue().encode("UTF-8"))
|
||||||
except EnvironmentError as e:
|
except EnvironmentError as e:
|
||||||
@ -211,57 +212,3 @@ class UFPWriter(MeshWriter):
|
|||||||
return [{"name": item.getName()}
|
return [{"name": item.getName()}
|
||||||
for item in DepthFirstIterator(node)
|
for item in DepthFirstIterator(node)
|
||||||
if item.getMeshData() is not None and not item.callDecoration("isNonPrintingMesh")]
|
if item.getMeshData() is not None and not item.callDecoration("isNonPrintingMesh")]
|
||||||
|
|
||||||
def _getSliceMetadata(self) -> Dict[str, Dict[str, Dict[str, str]]]:
|
|
||||||
"""Get all changed settings and all settings. For each extruder and the global stack"""
|
|
||||||
print_information = CuraApplication.getInstance().getPrintInformation()
|
|
||||||
machine_manager = CuraApplication.getInstance().getMachineManager()
|
|
||||||
settings = {
|
|
||||||
"material": {
|
|
||||||
"length": print_information.materialLengths,
|
|
||||||
"weight": print_information.materialWeights,
|
|
||||||
"cost": print_information.materialCosts,
|
|
||||||
},
|
|
||||||
"global": {
|
|
||||||
"changes": {},
|
|
||||||
"all_settings": {},
|
|
||||||
},
|
|
||||||
"quality": asdict(machine_manager.activeQualityDisplayNameMap()),
|
|
||||||
}
|
|
||||||
|
|
||||||
def _retrieveValue(container: InstanceContainer, setting_: str):
|
|
||||||
value_ = container.getProperty(setting_, "value")
|
|
||||||
for _ in range(0, 1024): # Prevent possibly endless loop by not using a limit.
|
|
||||||
if not isinstance(value_, SettingFunction):
|
|
||||||
return value_ # Success!
|
|
||||||
value_ = value_(container)
|
|
||||||
return 0 # Fallback value after breaking possibly endless loop.
|
|
||||||
|
|
||||||
global_stack = cast(GlobalStack, Application.getInstance().getGlobalContainerStack())
|
|
||||||
|
|
||||||
# Add global user or quality changes
|
|
||||||
global_flattened_changes = InstanceContainer.createMergedInstanceContainer(global_stack.userChanges, global_stack.qualityChanges)
|
|
||||||
for setting in global_flattened_changes.getAllKeys():
|
|
||||||
settings["global"]["changes"][setting] = _retrieveValue(global_flattened_changes, setting)
|
|
||||||
|
|
||||||
# Get global all settings values without user or quality changes
|
|
||||||
for setting in global_stack.getAllKeys():
|
|
||||||
settings["global"]["all_settings"][setting] = _retrieveValue(global_stack, setting)
|
|
||||||
|
|
||||||
for i, extruder in enumerate(global_stack.extruderList):
|
|
||||||
# Add extruder fields to settings dictionary
|
|
||||||
settings[f"extruder_{i}"] = {
|
|
||||||
"changes": {},
|
|
||||||
"all_settings": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add extruder user or quality changes
|
|
||||||
extruder_flattened_changes = InstanceContainer.createMergedInstanceContainer(extruder.userChanges, extruder.qualityChanges)
|
|
||||||
for setting in extruder_flattened_changes.getAllKeys():
|
|
||||||
settings[f"extruder_{i}"]["changes"][setting] = _retrieveValue(extruder_flattened_changes, setting)
|
|
||||||
|
|
||||||
# Get extruder all settings values without user or quality changes
|
|
||||||
for setting in extruder.getAllKeys():
|
|
||||||
settings[f"extruder_{i}"]["all_settings"][setting] = _retrieveValue(extruder, setting)
|
|
||||||
|
|
||||||
return settings
|
|
||||||
|
@ -9,8 +9,8 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
Logger.log("w", "Could not import UFPWriter; libCharon may be missing")
|
Logger.log("w", "Could not import UFPWriter; libCharon may be missing")
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog #To translate the file format description.
|
from UM.i18n import i18nCatalog # To translate the file format description.
|
||||||
from UM.Mesh.MeshWriter import MeshWriter #For the binary mode flag.
|
from UM.Mesh.MeshWriter import MeshWriter # For the binary mode flag.
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
BIN
plugins/UM3NetworkPrinting/resources/png/MakerBot Sketch.png
Normal file
BIN
plugins/UM3NetworkPrinting/resources/png/MakerBot Sketch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 185 KiB |
BIN
plugins/UM3NetworkPrinting/resources/png/Ultimaker Factor 4.png
Normal file
BIN
plugins/UM3NetworkPrinting/resources/png/Ultimaker Factor 4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 874 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user