Merge branch 'Ultimaker:main' into master

This commit is contained in:
Modix3D 2022-11-09 19:17:48 +01:00 committed by GitHub
commit c50fc235c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
1741 changed files with 196895 additions and 199133 deletions

View file

@ -1,4 +0,0 @@
.git
.github
resources/materials
CuraEngine

View file

@ -1,6 +1,6 @@
name: Bug Report
description: Create a report to help us fix issues.
labels: "Type: Bug"
labels: ["Type: Bug", "Status: Triage"]
body:
- type: markdown
attributes:
@ -14,7 +14,7 @@ body:
attributes:
label: Application Version
description: The version of Cura this issue occurs with.
placeholder: 4.9.0
placeholder: 5.0.0
validations:
required: true
- type: input

View file

@ -1,6 +1,6 @@
name: Feature Request
description: Suggest an idea for this project.
labels: "Type: New Feature"
labels: ["Type: New Feature", "Status: Triage"]
body:
- type: markdown
attributes:
@ -41,4 +41,4 @@ body:
- type: textarea
attributes:
label: Additional information & file uploads
description: You can add pictures or files to visualize your feature request in the comments below.
description: You can add pictures or files to visualize your feature request in the comments below.

View file

@ -1,13 +0,0 @@
# Configuration for probot-no-response - https://github.com/probot/no-response
# Number of days of inactivity before an Issue is closed for lack of response
daysUntilClose: 14
# Label requiring a response
responseRequiredLabel: 'Status: Needs Info'
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. With only the
information that is currently in the issue, we don't have enough information
to take action. Please reach out if you have or find the answers we need so
that we can investigate further.

View file

@ -1,21 +0,0 @@
---
name: CI
on:
push:
branches:
- master
- 'WIP**'
- '4.*'
- 'CURA-*'
pull_request:
jobs:
build:
runs-on: ubuntu-latest
container: ultimaker/cura-build-environment
steps:
- name: Checkout Cura
uses: actions/checkout@v2
- name: Build
run: docker/build.sh
- name: Test
run: docker/test.sh

View file

@ -0,0 +1,173 @@
name: Create and Upload Conan package
on:
workflow_call:
inputs:
project_name:
required: true
type: string
build_id:
required: true
type: number
recipe_id_full:
required: true
type: string
recipe_id_latest:
required: false
type: string
runs_on:
required: true
type: string
python_version:
required: true
type: string
conan_config_branch:
required: false
type: string
conan_logging_level:
required: false
type: string
conan_clean_local_cache:
required: false
type: boolean
default: false
conan_upload_community:
required: false
default: true
type: boolean
create_from_source:
required: false
default: false
type: boolean
env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
CONAN_NON_INTERACTIVE: 1
jobs:
conan-package-create:
runs-on: ${{ inputs.runs_on }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python_version }}
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements for runner
run: pip install -r .github/workflows/requirements-conan-package.txt
- name: Use Conan download cache (Bash)
if: ${{ runner.os != 'Windows' }}
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
- name: Use Conan download cache (Powershell)
if: ${{ runner.os == 'Windows' }}
run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
- name: Cache Conan local repository packages (Bash)
uses: actions/cache@v3
if: ${{ runner.os != 'Windows' }}
with:
path: |
$HOME/.conan/data
$HOME/.conan/conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-create-cache
- name: Cache Conan local repository packages (Powershell)
uses: actions/cache@v3
if: ${{ runner.os == 'Windows' }}
with:
path: |
C:\Users\runneradmin\.conan\data
C:\.conan
C:\Users\runneradmin\.conan\conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-create-cache
- name: Install MacOS system requirements
if: ${{ runner.os == 'Macos' }}
run: brew install autoconf automake ninja
- name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }}
run: |
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update
sudo apt upgrade
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
- name: Install GCC-12 on ubuntu-22.04
if: ${{ startsWith(inputs.runs_on, 'ubuntu-22.04') }}
run: |
sudo apt install g++-12 gcc-12 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
- name: Use GCC-10 on ubuntu-20.04
if: ${{ startsWith(inputs.runs_on, 'ubuntu-20.04') }}
run: |
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10
- name: Create the default Conan profile
run: conan profile new default --detect
- name: Get Conan configuration from branch
if: ${{ inputs.conan_config_branch != '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}"
- name: Get Conan configuration
if: ${{ inputs.conan_config_branch == '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git
- name: Create the Packages
if: ${{ !inputs.create_from_source }}
run: |
conan_build_info --v2 start ${{ inputs.project_name }} ${{ github.run_number }}000${{ inputs.build_id }}
conan lock create --reference ${{ inputs.recipe_id_full }} --build=missing --update
conan install ${{ inputs.recipe_id_full }} --build=missing --update --lockfile=conan.lock
conan_build_info --v2 create buildinfo.json --lockfile conan.lock --user ${{ secrets.CONAN_USER }} --password ${{ secrets.CONAN_PASS }}
conan_build_info --v2 publish buildinfo.json --url https://ultimaker.jfrog.io/artifactory --user ${{ secrets.CONAN_USER }} --password ${{ secrets.CONAN_PASS }}
conan_build_info --v2 stop
- name: Create the Packages (from source)
if: ${{ inputs.create_from_source }}
run: conan create . ${{ inputs.recipe_id_full }} --build=missing --update
- name: Remove the latest alias
if: ${{ inputs.create_from_source && inputs.recipe_id_latest != '' && runner.os == 'Linux' }}
run: |
conan remove ${{ inputs.recipe_id_latest }} -r cura -f || true
conan remove ${{ inputs.recipe_id_latest }} -r cura-ce -f || true
- name: Create the latest alias
if: ${{ inputs.create_from_source && inputs.recipe_id_latest != '' && always() }}
run: conan alias ${{ inputs.recipe_id_latest }} ${{ inputs.recipe_id_full }}
- name: Upload the Package(s)
if: always()
run: conan upload "*" -r cura --all -c
- name: Upload the Package(s) community
if: ${{ always() && inputs.conan_upload_community == true }}
run: conan upload "*" -r cura-ce -c

116
.github/workflows/conan-package.yml vendored Normal file
View file

@ -0,0 +1,116 @@
---
name: conan-package
# Exports the recipe, sources and binaries for Mac, Windows and Linux and upload these to the server such that these can
# be used downstream.
#
# It should run on pushes against main or CURA-* branches, but it will only create the binaries for main and release branches
on:
workflow_dispatch:
inputs:
create_binaries_windows:
required: true
default: false
description: 'create binaries Windows'
create_binaries_linux:
required: true
default: false
description: 'create binaries Linux'
create_binaries_macos:
required: true
default: false
description: 'create binaries Macos'
push:
paths:
- 'plugins/**'
- 'resources/**'
- 'cura/**'
- 'icons/**'
- 'tests/**'
- 'packaging/**'
- '.github/workflows/conan-*.yml'
- '.github/workflows/notify.yml'
- '.github/workflows/requirements-conan-package.txt'
- 'requirements*.txt'
- 'conanfile.py'
- 'conandata.yml'
- 'GitVersion.yml'
- '*.jinja'
branches:
- main
- 'CURA-*'
- '[1-9].[0-9]'
- '[1-9].[0-9][0-9]'
tags:
- '[1-9].[0-9].[0-9]*'
- '[1-9].[0-9].[0-9]'
- '[1-9].[0-9][0-9].[0-9]*'
permissions: {}
jobs:
conan-recipe-version:
permissions:
contents: read
uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main
with:
project_name: cura
conan-package-export:
permissions:
contents: read
needs: [ conan-recipe-version ]
uses: ultimaker/cura/.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 }}
runs_on: 'ubuntu-20.04'
python_version: '3.10.x'
conan_logging_level: 'info'
secrets: inherit
conan-package-create-linux:
permissions:
contents: read
if: ${{ (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || needs.conan-recipe-version.outputs.is_release_branch == 'true')) || (github.event_name == 'workflow_dispatch' && inputs.create_binaries_linux) }}
needs: [ conan-recipe-version, conan-package-export ]
uses: ultimaker/cura/.github/workflows/conan-package-create.yml@main
with:
project_name: cura
build_id: 1
recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }}
runs_on: 'ubuntu-20.04'
python_version: '3.10.x'
conan_logging_level: 'info'
secrets: inherit
notify-export:
if: ${{ always() }}
needs: [ conan-recipe-version, conan-package-export ]
uses: ultimaker/cura/.github/workflows/notify.yml@main
with:
success: ${{ contains(join(needs.*.result, ','), 'success') }}
success_title: "New Conan recipe exported in ${{ github.repository }}"
success_body: "Exported ${{ needs.conan-recipe-version.outputs.recipe_id_full }}"
failure_title: "Failed to export Conan Export in ${{ github.repository }}"
failure_body: "Failed to exported ${{ needs.conan-recipe-version.outputs.recipe_id_full }}"
secrets: inherit
notify-create:
if: ${{ always() && ((github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || needs.conan-recipe-version.outputs.is_release_branch == 'true')) || (github.event_name == 'workflow_dispatch' && inputs.create_binaries_linux)) }}
needs: [ conan-recipe-version, conan-package-create-linux ]
uses: ultimaker/cura/.github/workflows/notify.yml@main
with:
success: ${{ contains(join(needs.*.result, ','), 'success') }}
success_title: "New binaries created in ${{ github.repository }}"
success_body: "Created binaries for ${{ needs.conan-recipe-version.outputs.recipe_id_full }}"
failure_title: "Failed to create binaries in ${{ github.repository }}"
failure_body: "Failed to created binaries for ${{ needs.conan-recipe-version.outputs.recipe_id_full }}"
secrets: inherit

View file

@ -0,0 +1,106 @@
name: Export Conan Recipe to server
on:
workflow_call:
inputs:
recipe_id_full:
required: true
type: string
recipe_id_latest:
required: false
type: string
runs_on:
required: true
type: string
python_version:
required: true
type: string
conan_config_branch:
required: false
type: string
conan_logging_level:
required: false
type: string
conan_export_binaries:
required: false
type: boolean
conan_upload_community:
required: false
default: true
type: boolean
env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
CONAN_NON_INTERACTIVE: 1
jobs:
package-export:
runs-on: ${{ inputs.runs_on }}
steps:
- name: Checkout project
uses: actions/checkout@v3
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python_version }}
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements and Create default Conan profile
run: |
pip install -r .github/workflows/requirements-conan-package.txt
conan profile new default --detect
- name: Cache Conan local repository packages
uses: actions/cache@v3
with:
path: $HOME/.conan/data
key: ${{ runner.os }}-conan-export-cache
- name: Get Conan configuration from branch
if: ${{ inputs.conan_config_branch != '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}"
- name: Get Conan configuration
if: ${{ inputs.conan_config_branch == '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git
- name: Export the Package (binaries)
if: ${{ inputs.conan_export_binaries }}
run: conan create . ${{ inputs.recipe_id_full }} --build=missing --update
- name: Export the Package
if: ${{ !inputs.conan_export_binaries }}
run: conan export . ${{ inputs.recipe_id_full }}
- name: Remove the latest alias
if: ${{ inputs.recipe_id_latest != '' && runner.os == 'Linux' }}
run: |
conan remove ${{ inputs.recipe_id_latest }} -r cura -f || true
conan remove ${{ inputs.recipe_id_latest }} -r cura-ce -f || true
- name: Create the latest alias
if: ${{ inputs.recipe_id_latest != '' && always() }}
run: conan alias ${{ inputs.recipe_id_latest }} ${{ inputs.recipe_id_full }}
- name: Upload the Package(s)
if: always()
run: conan upload "*" -r cura --all -c
- name: Upload the Package(s) community
if: ${{ always() && inputs.conan_upload_community == true }}
run: conan upload "*" -r cura-ce -c

View file

@ -0,0 +1,196 @@
name: Get Conan Recipe Version
on:
workflow_call:
inputs:
project_name:
required: true
type: string
additional_buildmetadata:
required: false
default: ""
type: string
outputs:
recipe_id_full:
description: "The full Conan recipe id: <name>/<version>@<user>/<channel>"
value: ${{ jobs.get-semver.outputs.recipe_id_full }}
recipe_id_latest:
description: "The full Conan recipe aliased (latest) id: <name>/(latest)@<user>/<channel>"
value: ${{ jobs.get-semver.outputs.recipe_id_latest }}
recipe_semver_full:
description: "The full semver <Major>.<Minor>.<Patch>-<PreReleaseTag>+<BuildMetaData>"
value: ${{ jobs.get-semver.outputs.semver_full }}
is_release_branch:
description: "is current branch a release branch?"
value: ${{ jobs.get-semver.outputs.release_branch }}
recipe_user:
description: "The conan user"
value: ${{ jobs.get-semver.outputs.user }}
recipe_channel:
description: "The conan channel"
value: ${{ jobs.get-semver.outputs.channel }}
jobs:
get-semver:
runs-on: ubuntu-latest
outputs:
recipe_id_full: ${{ steps.get-conan-broadcast-data.outputs.recipe_id_full }}
recipe_id_latest: ${{ steps.get-conan-broadcast-data.outputs.recipe_id_latest }}
semver_full: ${{ steps.get-conan-broadcast-data.outputs.semver_full }}
is_release_branch: ${{ steps.get-conan-broadcast-data.outputs.is_release_branch }}
user: ${{ steps.get-conan-broadcast-data.outputs.user }}
channel: ${{ steps.get-conan-broadcast-data.outputs.channel }}
steps:
- name: Checkout repo
uses: actions/checkout@v3
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
- name: Checkout repo PR
uses: actions/checkout@v3
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: "3.10.x"
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements and Create default Conan profile
run: |
pip install -r .github/workflows/requirements-conan-package.txt
pip install gitpython
- id: get-conan-broadcast-data
name: Get Conan broadcast data
run: |
import subprocess
from conans import tools
from conans.errors import ConanException
from git import Repo
repo = Repo('.')
user = "${{ github.repository_owner }}".lower()
project_name = "${{ inputs.project_name }}"
event_name = "${{ github.event_name }}"
issue_number = "${{ github.ref }}".split('/')[2]
is_tag = "${{ github.ref_type }}" == "tag"
is_release_branch = False
ref_name = "${{ github.base_ref }}" if event_name == "pull_request" else "${{ github.ref_name }}"
buildmetadata = "" if "${{ inputs.additional_buildmetadata }}" == "" else "${{ inputs.additional_buildmetadata }}_"
# FIXME: for when we push a tag (such as an release)
channel = "testing"
if is_tag:
branch_version = tools.Version(ref_name)
is_release_branch = True
channel = "_"
user = "_"
actual_version = f"{branch_version}"
else:
try:
branch_version = tools.Version(repo.active_branch.name)
except ConanException:
branch_version = tools.Version('0.0.0')
if ref_name == f"{branch_version.major}.{branch_version.minor}":
channel = 'stable'
is_release_branch = True
elif ref_name in ("main", "master"):
channel = 'testing'
else:
channel = repo.active_branch.name.split("_")[0].replace("-", "_").lower()
if "pull_request" in event_name:
channel = f"pr_{issue_number}"
# %% Get the actual version
latest_branch_version = tools.Version("0.0.0")
latest_branch_tag = None
for tag in repo.git.tag(merged = True).splitlines():
if str(tag).startswith("firmware") or str(tag).startswith("master"):
continue # Quick-fix for the versioning scheme name of the embedded team in fdm_materials(_private) repo
try:
version = tools.Version(tag)
except ConanException:
continue
if version > latest_branch_version:
latest_branch_version = version
latest_branch_tag = repo.tag(tag)
if latest_branch_tag:
# %% Get the actual version
no_commits = 0
for commit in repo.iter_commits("HEAD"):
if commit == latest_branch_tag.commit:
break
no_commits += 1
latest_branch_version_prerelease = latest_branch_version.prerelease
if latest_branch_version.prerelease and not "." in latest_branch_version.prerelease:
# The prerealese did not contain a version number, default it to 1
latest_branch_version_prerelease = f"{latest_branch_version.prerelease}.1"
if event_name == "pull_request":
actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{latest_branch_version.patch}-{latest_branch_version_prerelease.lower()}+{buildmetadata}pr_{issue_number}_{no_commits}"
channel_metadata = f"{channel}_{no_commits}"
else:
if channel in ("stable", "_", ""):
channel_metadata = f"{no_commits}"
else:
channel_metadata = f"{channel}_{no_commits}"
if is_release_branch:
if latest_branch_version.prerelease == "":
# An actual full release has been created, we are working on patch
bump_up_patch = int(latest_branch_version.patch) + 1
actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{bump_up_patch}-beta.1+{buildmetadata}{channel_metadata}"
else:
# An beta release has been created we are working toward a next beta or full release
bump_up_release_tag = int(latest_branch_version.prerelease.split('.')[1]) + 1
actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{latest_branch_version.patch}-{latest_branch_version.prerelease.split('.')[0]}.{bump_up_release_tag}+{buildmetadata}{channel_metadata}"
else:
bump_up_minor = int(latest_branch_version.minor) + 1
reset_patch = 0
actual_version = f"{latest_branch_version.major}.{bump_up_minor}.{reset_patch}-alpha+{buildmetadata}{channel_metadata}"
# %% print to output
cmd_name = ["echo", f"::set-output name=name::{project_name}"]
subprocess.call(cmd_name)
cmd_version = ["echo", f"::set-output name=version::{actual_version}"]
subprocess.call(cmd_version)
cmd_channel = ["echo", f"::set-output name=channel::{channel}"]
subprocess.call(cmd_channel)
cmd_id_full= ["echo", f"::set-output name=recipe_id_full::{project_name}/{actual_version}@{user}/{channel}"]
subprocess.call(cmd_id_full)
cmd_id_latest = ["echo", f"::set-output name=recipe_id_latest::{project_name}/latest@{user}/{channel}"]
subprocess.call(cmd_id_latest)
cmd_semver_full = ["echo", f"::set-output name=semver_full::{actual_version}"]
subprocess.call(cmd_semver_full)
cmd_is_release_branch = ["echo", f"::set-output name=is_release_branch::{str(is_release_branch).lower()}"]
subprocess.call(cmd_is_release_branch)
print("::group::Conan Recipe Information")
print(f"name = {project_name}")
print(f"version = {actual_version}")
print(f"user = {user}")
print(f"channel = {channel}")
print(f"= {project_name}/{actual_version}@{user}/{channel}")
print(f"recipe_id_latest = {project_name}/latest@{user}/{channel}")
print(f"semver_full = {actual_version}")
print(f"is_release_branch = {str(is_release_branch).lower()}")
print("::endgroup::")
shell: python

269
.github/workflows/cura-installer.yml vendored Normal file
View file

@ -0,0 +1,269 @@
name: Cura Installer
run-name: ${{ inputs.cura_conan_version }} by @${{ github.actor }}
on:
workflow_dispatch:
inputs:
cura_conan_version:
description: 'Cura Conan Version'
default: 'cura/latest@ultimaker/testing'
required: true
conan_args:
description: 'Conan args: eq.: --require-override'
default: ''
required: false
conan_config:
description: 'Conan config branch to use'
default: ''
required: false
enterprise:
description: 'Build Cura as an Enterprise edition'
required: true
default: false
type: boolean
staging:
description: 'Use staging API'
required: true
default: false
type: boolean
installer:
description: 'Create the installer'
required: true
default: true
type: boolean
# Run the nightly at 3:25 UTC on working days
#FIXME: Provide the same default values as the workflow dispatch
schedule:
- cron: '25 3 * * 1-5'
env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
CONAN_NON_INTERACTIVE: 1
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
MAC_NOTARIZE_USER: ${{ secrets.MAC_NOTARIZE_USER }}
MAC_NOTARIZE_PASS: ${{ secrets.MAC_NOTARIZE_PASS }}
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_PASS: ${{ secrets.MACOS_CERT_PASS }}
MACOS_CERT_USER: ${{ secrets.MACOS_CERT_USER }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
MACOS_CERT_PASSPHRASE: ${{ secrets.MACOS_CERT_PASSPHRASE }}
CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }}
ENTERPRISE: ${{ inputs.enterprise }}
STAGING: ${{ inputs.staging }}
jobs:
cura-installer-create:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { os: macos-11, os_id: 'mac' }
- { os: windows-2022, os_id: 'win64' }
- { os: ubuntu-20.04, os_id: 'linux' }
- { os: ubuntu-22.04, os_id: 'linux-modern' }
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: '3.10.x'
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements for runner
run: pip install -r .github/workflows/requirements-conan-package.txt
- name: Use Conan download cache (Bash)
if: ${{ runner.os != 'Windows' }}
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
- name: Use Conan download cache (Powershell)
if: ${{ runner.os == 'Windows' }}
run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
- name: Cache Conan local repository packages (Bash)
uses: actions/cache@v3
if: ${{ runner.os != 'Windows' }}
with:
path: |
$HOME/.conan/data
$HOME/.conan/conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
- name: Cache Conan local repository packages (Powershell)
uses: actions/cache@v3
if: ${{ runner.os == 'Windows' }}
with:
path: |
C:\Users\runneradmin\.conan\data
C:\.conan
C:\Users\runneradmin\.conan\conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
- name: Install MacOS system requirements
if: ${{ runner.os == 'Macos' }}
run: brew install autoconf automake ninja create-dmg
- name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }}
run: |
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update
sudo apt upgrade
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
wget --no-check-certificate --quiet https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O $GITHUB_WORKSPACE/appimagetool
chmod +x $GITHUB_WORKSPACE/appimagetool
echo "APPIMAGETOOL_LOCATION=$GITHUB_WORKSPACE/appimagetool" >> $GITHUB_ENV
- name: Install GCC-12 on ubuntu-22.04
if: ${{ startsWith(inputs.runs_on, 'ubuntu-22.04') }}
run: |
sudo apt install g++-12 gcc-12 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
- name: Use GCC-10 on ubuntu-20.04
if: ${{ startsWith(inputs.runs_on, 'ubuntu-20.04') }}
run: |
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10
- name: Create the default Conan profile
run: conan profile new default --detect
- name: Configure GPG Key Linux (Bash)
if: ${{ runner.os == 'Linux' }}
run: echo -n "$GPG_PRIVATE_KEY" | base64 --decode | gpg --import
- name: Configure Macos keychain (Bash)
id: macos-keychain
if: ${{ runner.os == 'Macos' }}
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_CERT_P12 }}
p12-password: ${{ secrets.MACOS_CERT_PASSPHRASE }}
- name: Clean Conan local cache
if: ${{ inputs.conan_clean_local_cache }}
run: conan remove "*" -f
- name: Get Conan configuration from branch
if: ${{ inputs.conan_config_branch != '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}"
- name: Get Conan configuration
if: ${{ inputs.conan_config_branch == '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git
- name: Create the Packages (Bash)
if: ${{ runner.os != 'Windows' }}
run: conan install $CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$ENTERPRISE -o cura:staging=$STAGING --json "cura_inst/conan_install_info.json"
- name: Create the Packages (Powershell)
if: ${{ runner.os == 'Windows' }}
run: conan install $Env:CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$Env:ENTERPRISE -o cura:staging=$Env:STAGING --json "cura_inst/conan_install_info.json"
- name: Set Environment variables for Cura (bash)
if: ${{ runner.os != 'Windows' }}
run: |
. ./cura_inst/bin/activate_github_actions_env.sh
. ./cura_inst/bin/activate_github_actions_version_env.sh
- name: Set Environment variables for Cura (Powershell)
if: ${{ runner.os == 'Windows' }}
run: |
.\cura_inst\Scripts\activate_github_actions_env.ps1
.\cura_inst\Scripts\activate_github_actions_version_env.ps1
- name: Unlock Macos keychain (Bash)
if: ${{ runner.os == 'Macos' }}
run: security unlock -p $TEMP_KEYCHAIN_PASSWORD signing_temp.keychain
env:
TEMP_KEYCHAIN_PASSWORD: ${{ steps.macos-keychain.outputs.keychain-password }}
# FIXME: This is a workaround to ensure that we use and pack a shared library for OpenSSL 1.1.1l. We currently compile
# OpenSSL statically for CPython, but our Python Dependenies (such as PyQt6) require a shared library.
# Because Conan won't allow for building the same library with two different options (easily) we need to install it explicitly
# and do a manual copy to the VirtualEnv, such that Pyinstaller can find it.
- name: Install OpenSSL shared
run: conan install openssl/1.1.1l@_/_ --build=missing --update -o openssl:shared=True -g deploy
- name: Copy OpenSSL shared (Bash)
if: ${{ runner.os != 'Windows' }}
run: |
cp ./openssl/lib/*.so* ./cura_inst/bin/ || true
cp ./openssl/lib/*.dylib* ./cura_inst/bin/ || true
- name: Copy OpenSSL shared (Powershell)
if: ${{ runner.os == 'Windows' }}
run: |
cp openssl/bin/*.dll ./cura_inst/Scripts/
cp openssl/lib/*.lib ./cura_inst/Lib/
- name: Create the Cura dist
run: pyinstaller ./cura_inst/Ultimaker-Cura.spec
- name: Archive the artifacts (bash)
if: ${{ github.event.inputs.installer == 'false' && runner.os != 'Windows' }}
run: tar -zcf "./Ultimaker-Cura-$CURA_VERSION_FULL-${{ matrix.os_id }}.tar.gz" "./Ultimaker-Cura/"
working-directory: dist
- name: Archive the artifacts (Powershell)
if: ${{ github.event.inputs.installer == 'false' && runner.os == 'Windows' }}
run: Compress-Archive -Path ".\Ultimaker-Cura" -DestinationPath ".\Ultimaker-Cura-$Env:CURA_VERSION_FULL-${{ matrix.os_id }}.zip"
working-directory: dist
- name: Create the Windows exe installer (Powershell)
if: ${{ github.event.inputs.installer == 'true' && runner.os == 'Windows' }}
run: |
python ..\cura_inst\packaging\NSIS\create_windows_installer.py ../cura_inst . "Ultimaker-Cura-$Env:CURA_VERSION_FULL-${{ matrix.os_id }}.exe"
working-directory: dist
- name: Create the Linux AppImage (Bash)
if: ${{ github.event.inputs.installer == 'true' && runner.os == 'Linux' }}
run: python ../cura_inst/packaging/AppImage/create_appimage.py ./Ultimaker-Cura $CURA_VERSION_FULL "Ultimaker-Cura-$CURA_VERSION_FULL-${{ matrix.os_id }}.AppImage"
working-directory: dist
- name: Create the MacOS dmg (Bash)
if: ${{ github.event.inputs.installer == 'true' && runner.os == 'Macos' }}
run: python ../cura_inst/packaging/dmg/dmg_sign_noterize.py ../cura_inst . "Ultimaker-Cura-$CURA_VERSION_FULL-${{ matrix.os_id }}.dmg"
working-directory: dist
- name: Upload the artifacts
uses: actions/upload-artifact@v3
with:
name: Ultimaker-Cura-${{ env.CURA_VERSION_FULL }}-${{ matrix.os_id }}
path: |
dist/*.tar.gz
dist/*.zip
dist/*.exe
dist/*.msi
dist/*.dmg
dist/*.AppImage
dist/*.asc
retention-days: 5
notify-export:
if: ${{ always() }}
needs: [ cura-installer-create ]
uses: ultimaker/cura/.github/workflows/notify.yml@main
with:
success: ${{ contains(join(needs.*.result, ','), 'success') }}
success_title: "Create the Cura distributions"
success_body: "Installers for ${{ inputs.cura_conan_version }}"
failure_title: "Failed to create the Cura distributions"
failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}"
secrets: inherit

31
.github/workflows/no-response.yml vendored Normal file
View file

@ -0,0 +1,31 @@
name: No Response
# Both `issue_comment` and `scheduled` event types are required for this Action
# to work properly.
on:
issue_comment:
types: [created]
schedule:
# Schedule for ten minutes after the hour, every hour
- cron: '* */12 * * *'
# By specifying the access of one of the scopes, all of those that are not
# specified are set to 'none'.
permissions:
issues: write
jobs:
noResponse:
runs-on: ubuntu-latest
steps:
- uses: lee-dohm/no-response@v0.5.0
with:
token: ${{ github.token }}
daysUntilClose: 14
responseRequiredLabel: 'Status: Needs Info'
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. With only the
information that is currently in the issue, we don't have enough information
to take action. Please reach out if you have or find the answers we need so
that we can investigate further.

54
.github/workflows/notify.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: Get Conan Recipe Version
on:
workflow_call:
inputs:
success:
required: true
type: boolean
success_title:
required: true
type: string
success_body:
required: true
type: string
failure_title:
required: true
type: string
failure_body:
required: true
type: string
jobs:
slackNotification:
name: Slack Notification
runs-on: ubuntu-latest
steps:
- name: Slack notify on-success
if: ${{ inputs.success }}
uses: rtCamp/action-slack-notify@v2
env:
SLACK_USERNAME: ${{ github.repository }}
SLACK_COLOR: green
SLACK_ICON: https://github.com/Ultimaker/Cura/blob/main/icons/cura-128.png?raw=true
SLACK_TITLE: ${{ inputs.success_title }}
SLACK_MESSAGE: ${{ inputs.success_body }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
- name: Slack notify on-failure
if: ${{ !inputs.success }}
uses: rtCamp/action-slack-notify@v2
env:
SLACK_USERNAME: ${{ github.repository }}
SLACK_COLOR: red
SLACK_ICON: https://github.com/Ultimaker/Cura/blob/main/icons/cura-128.png?raw=true
SLACK_TITLE: ${{ inputs.failure_title }}
SLACK_MESSAGE: ${{ inputs.failure_body }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

View file

@ -0,0 +1,36 @@
name: notify_on_print_profile_change
on:
push:
branches: [ "main" ]
paths:
- 'resources/definitions/fdmprinter.def.json'
- 'resources/definitions/ultimaker**'
- 'resources/extruders/ultimaker**'
- 'resources/intent/ultimaker**'
- 'resources/quality/ultimaker**'
- 'resources/variants/ultimaker**'
pull_request:
branches: [ "main" ]
paths:
- 'resources/definitions/fdmprinter.def.json'
- 'resources/definitions/ultimaker**'
- 'resources/extruders/ultimaker**'
- 'resources/intent/ultimaker**'
- 'resources/quality/ultimaker**'
- 'resources/variants/ultimaker**'
permissions: {}
jobs:
slackNotification:
name: Slack Notification
runs-on: ubuntu-latest
steps:
- name: Ultimaker Print Profile Changed
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: profile-changes
SLACK_USERNAME: ${{ github.repository }}
SLACK_COLOR: '#00FF00'
SLACK_TITLE: Print profiles changed
MSG_MINIMAL: commit
SLACK_WEBHOOK: ${{ secrets.SLACK_CURA_PPM_HOOK }}

View file

@ -0,0 +1,2 @@
conan!=1.51.0,!=1.51.1,!=1.51.2,!=1.51.3,!=1.52.0
sip

180
.github/workflows/unit-test.yml vendored Normal file
View file

@ -0,0 +1,180 @@
---
name: unit-test
# FIXME: This should be a reusable workflow
on:
push:
paths:
- 'plugins/**'
- 'resources/**'
- 'cura/**'
- 'icons/**'
- 'tests/**'
- 'packaging/**'
- '.github/workflows/conan-*.yml'
- '.github/workflows/unit-test.yml'
- '.github/workflows/notify.yml'
- '.github/workflows/requirements-conan-package.txt'
- 'requirements*.txt'
- 'conanfile.py'
- 'conandata.yml'
- 'GitVersion.yml'
- '*.jinja'
branches:
- main
- 'CURA-*'
- '[1-9]+.[0-9]+'
tags:
- '[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+-beta'
pull_request:
paths:
- 'plugins/**'
- 'resources/**'
- 'cura/**'
- 'icons/**'
- 'tests/**'
- 'packaging/**'
- '.github/workflows/conan-*.yml'
- '.github/workflows/unit-test.yml'
- '.github/workflows/notify.yml'
- '.github/workflows/requirements-conan-package.txt'
- 'requirements*.txt'
- 'conanfile.py'
- 'conandata.yml'
- 'GitVersion.yml'
- '*.jinja'
branches:
- main
- '[1-9]+.[0-9]+'
tags:
- '[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+-beta'
env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: info
CONAN_NON_INTERACTIVE: 1
permissions:
contents: read
jobs:
conan-recipe-version:
uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main
with:
project_name: cura
testing:
runs-on: ubuntu-20.04
needs: [ conan-recipe-version ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: '3.10.x'
architecture: 'x64'
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements and Create default Conan profile
run: |
pip install -r requirements-conan-package.txt
conan profile new default --detect
working-directory: .github/workflows/
- name: Use Conan download cache (Bash)
if: ${{ runner.os != 'Windows' }}
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
- name: Cache Conan local repository packages (Bash)
uses: actions/cache@v3
if: ${{ runner.os != 'Windows' }}
with:
path: |
$HOME/.conan/data
$HOME/.conan/conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-unit-cache
- name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }}
run: |
sudo apt update
sudo apt upgrade
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
- name: Use GCC-10 on ubuntu-20.04
run: |
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10
- name: Get Conan configuration
run: conan config install https://github.com/Ultimaker/conan-config.git
- name: Install dependencies
run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o cura:devtools=True -g VirtualPythonEnv -if venv
- name: Upload the Dependency package(s)
run: conan upload "*" -r cura --all -c
- name: Set Environment variables for Cura (bash)
if: ${{ runner.os != 'Windows' }}
run: |
. ./venv/bin/activate_github_actions_env.sh
- name: Run Unit Test
id: run-test
run: |
pytest --junitxml=junit_cura.xml
working-directory: tests
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v3
with:
name: Test Results
path: "tests/**/*.xml"
publish-test-results:
permissions:
contents: read # to fetch code (actions/checkout)
checks: write
pull-requests: write # to comment on pull request
runs-on: ubuntu-20.04
needs: [ testing ]
if: success() || failure()
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: '3.10.x'
architecture: 'x64'
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Download Artifacts
uses: actions/download-artifact@v3
with:
path: artifacts
- name: Publish Unit Test Results
id: test-results
uses: EnricoMi/publish-unit-test-result-action@v1
with:
files: "artifacts/**/*.xml"
- name: Conclusion
run: echo "Conclusion is ${{ fromJSON( steps.test-results.outputs.json ).conclusion }}"

13
.gitignore vendored
View file

@ -38,6 +38,7 @@ cura.desktop
.settings
#Externally located plug-ins commonly installed by our devs.
plugins/BarbarianPlugin
plugins/cura-big-flame-graph
plugins/cura-camera-position
plugins/cura-god-mode-plugin
@ -64,6 +65,7 @@ plugins/CuraRemoteSupport
plugins/ModelCutter
plugins/PrintProfileCreator
plugins/MultiPrintPlugin
plugins/CuraOrientationPlugin
#Build stuff
CMakeCache.txt
@ -87,4 +89,13 @@ CuraEngine
#Prevents import failures when plugin running tests
plugins/__init__.py
/venv
venv/
build/
dist/
conaninfo.txt
conan.lock
conan_imports_manifest.txt
conanbuildinfo.txt
graph_info.json
Ultimaker-Cura.spec
.run/

View file

@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="{{ name }}" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
<module name="{{ module_name }}" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />{% for key, value in env_vars.items() %}
<env name="{{ key }}" value="{{ value }}" />{% endfor %}
</envs>
<option name="SDK_HOME" value="{{ sdk_path }}" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/{{ script_name }}" />
<option name="PARAMETERS" value="{{ parameters }}" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="{{ name }}" type="tests" factoryName="py.test" nameIsGenerated="true">
<module name="{{ module_name }}" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />{% for key, value in env_vars.items() %}
<env name="{{ key }}" value="{{ value }}" />{% endfor %}
</envs>
<option name="SDK_HOME" value="{{ sdk_path }}" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="_new_keywords" value="&quot;&quot;" />
<option name="_new_parameters" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/{{ script_name }}&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" />
</configuration>
</component>

View file

@ -1,11 +1,30 @@
# YAML 1.2
---
authors:
cff-version: "1.1.0"
date-released: 2021-06-28
license: "LGPL-3.0"
message: "If you use this software, please cite it using these metadata."
repository-code: "https://github.com/ultimaker/cura/"
title: "Ultimaker Cura"
version: "4.12.0"
cff-version: 1.2.0
type: software
message: >-
If you use this software, please cite it using the
metadata from this file.
title: Ultimaker Cura
abstract: >-
A state-of-the-art slicer application built on top
of the Uranium framework.
authors:
- name: Ultimaker B.V.
contact:
- email: info@ultimaker.com
name: "Ultimaker B.V."
url: 'https://ultimaker.com/software/ultimaker-cura'
repository-code: 'https://github.com/Ultimaker/Cura'
license: LGPL-3.0
license-url: "https://github.com/Ultimaker/Cura/blob/main/LICENSE"
version: 5.0.0
date-released: '2022-05-17'
keywords:
- Ultimaker
- Cura
- Uranium
- Arachne
- 3DPrinting
- Slicer
...

View file

@ -1,6 +1,10 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# NOTE: This is only being used for translation scripts.
# For MSVC flags, will be ignored on non-Windows OS's and this project in general. Only needed for cura-build-environment.
cmake_policy(SET CMP0091 NEW)
project(cura)
cmake_minimum_required(VERSION 3.18)
@ -11,47 +15,8 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE PATH "The location of the Uranium repository")
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE PATH "The location of the scripts directory of the Uranium repository")
option(CURA_DEBUGMODE "Enable debug dialog and other debug features" OFF)
if(CURA_DEBUGMODE)
set(_cura_debugmode "ON")
endif()
option(GENERATE_TRANSLATIONS "Should the translations be generated?" ON)
set(CURA_APP_NAME "cura" CACHE STRING "Short name of Cura, used for configuration folder")
set(CURA_APP_DISPLAY_NAME "Ultimaker Cura" CACHE STRING "Display name of Cura")
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version")
set(CURA_MARKETPLACE_ROOT "" CACHE STRING "Alternative Marketplace location")
set(CURA_DIGITAL_FACTORY_URL "" CACHE STRING "Alternative Digital Factory location")
configure_file(${CMAKE_SOURCE_DIR}/com.ultimaker.cura.desktop.in ${CMAKE_BINARY_DIR}/com.ultimaker.cura.desktop @ONLY)
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
if(NOT DEFINED Python_VERSION)
set(Python_VERSION
3.10
CACHE STRING "Python Version" FORCE)
message(STATUS "Setting Python version to ${Python_VERSION}. Set Python_VERSION if you want to compile against an other version.")
endif()
if(APPLE)
set(Python_FIND_FRAMEWORK NEVER)
endif()
find_package(Python ${Python_VERSION} EXACT REQUIRED COMPONENTS Interpreter)
message(STATUS "Linking and building ${project_name} against Python ${Python_VERSION}")
if(NOT DEFINED Python_SITELIB_LOCAL)
set(Python_SITELIB_LOCAL
"${Python_SITELIB}"
CACHE PATH "Local alternative site-package location to install Cura" FORCE)
endif()
# Tests
include(CuraTests)
if(NOT ${URANIUM_DIR} STREQUAL "")
set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${URANIUM_DIR}/cmake")
endif()
@ -64,24 +29,4 @@ if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
if(${GENERATE_TRANSLATIONS})
CREATE_TRANSLATION_TARGETS()
endif()
endif()
install(DIRECTORY resources DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
include(CuraPluginInstall)
install(FILES cura_app.py DESTINATION ${CMAKE_INSTALL_BINDIR}
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
install(DIRECTORY cura DESTINATION "${Python_SITELIB_LOCAL}")
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py DESTINATION "${Python_SITELIB_LOCAL}/cura/")
if(NOT APPLE AND NOT WIN32)
install(FILES ${CMAKE_BINARY_DIR}/com.ultimaker.cura.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
install(FILES ${CMAKE_SOURCE_DIR}/resources/images/cura-icon.png
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps/)
install(FILES com.ultimaker.cura.appdata.xml
DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo)
install(FILES cura.sharedmimeinfo
DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages/
RENAME cura.xml )
endif()
endif()

14
CuraVersion.py.jinja Normal file
View file

@ -0,0 +1,14 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
CuraAppName = "{{ cura_app_name }}"
CuraAppDisplayName = "{{ cura_app_display_name }}"
CuraVersion = "{{ cura_version }}"
CuraBuildType = "{{ cura_build_type }}"
CuraDebugMode = {{ cura_debug_mode }}
CuraCloudAPIRoot = "{{ cura_cloud_api_root }}"
CuraCloudAPIVersion = "{{ cura_cloud_api_version }}"
CuraCloudAccountAPIRoot = "{{ cura_cloud_account_api_root }}"
CuraMarketplaceRoot = "{{ cura_marketplace_root }}"
CuraDigitalFactoryURL = "{{ cura_digital_factory_url }}"
CuraLatestURL = "{{ cura_latest_url }}"

View file

@ -1,45 +0,0 @@
FROM ultimaker/cura-build-environment:1
# Environment vars for easy configuration
ENV CURA_APP_DIR=/srv/cura
# Ensure our sources dir exists
RUN mkdir $CURA_APP_DIR
# Setup CuraEngine
ENV CURA_ENGINE_BRANCH=master
WORKDIR $CURA_APP_DIR
RUN git clone -b $CURA_ENGINE_BRANCH --depth 1 https://github.com/Ultimaker/CuraEngine
WORKDIR $CURA_APP_DIR/CuraEngine
RUN mkdir build
WORKDIR $CURA_APP_DIR/CuraEngine/build
RUN cmake3 ..
RUN make
RUN make install
# TODO: setup libCharon
# Setup Uranium
ENV URANIUM_BRANCH=master
WORKDIR $CURA_APP_DIR
RUN git clone -b $URANIUM_BRANCH --depth 1 https://github.com/Ultimaker/Uranium
# Setup materials
ENV MATERIALS_BRANCH=master
WORKDIR $CURA_APP_DIR
RUN git clone -b $MATERIALS_BRANCH --depth 1 https://github.com/Ultimaker/fdm_materials materials
# Setup Cura
WORKDIR $CURA_APP_DIR/Cura
ADD . .
RUN mv $CURA_APP_DIR/materials resources/materials
# Make sure Cura can find CuraEngine
RUN ln -s /usr/local/bin/CuraEngine $CURA_APP_DIR/Cura
# Run Cura
WORKDIR $CURA_APP_DIR/Cura
ENV PYTHONPATH=${PYTHONPATH}:$CURA_APP_DIR/Uranium
RUN chmod +x ./CuraEngine
RUN chmod +x ./run_in_docker.sh
CMD "./run_in_docker.sh"

74
Jenkinsfile vendored
View file

@ -1,74 +0,0 @@
parallel_nodes(['linux && cura', 'windows && cura'])
{
timeout(time: 2, unit: "HOURS")
{
// Prepare building
stage('Prepare')
{
// Ensure we start with a clean build directory.
step([$class: 'WsCleanup'])
// Checkout whatever sources are linked to this pipeline.
checkout scm
}
// If any error occurs during building, we want to catch it and continue with the "finale" stage.
catchError
{
// Building and testing should happen in a subdirectory.
dir('build')
{
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.
stage('Build')
{
def branch = env.BRANCH_NAME
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
{
branch = "master"
}
// Ensure CMake is setup. Note that since this is Python code we do not really "build" it.
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
cmake("..", "-DCMAKE_PREFIX_PATH=\"${env.CURA_ENVIRONMENT_PATH}/${branch}\" -DCMAKE_BUILD_TYPE=Release -DURANIUM_DIR=\"${uranium_dir}\"")
}
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
stage('Unit Test')
{
if (isUnix())
{
// For Linux
try {
sh 'make CTEST_OUTPUT_ON_FAILURE=TRUE test'
} catch(e)
{
currentBuild.result = "UNSTABLE"
}
}
else
{
// For Windows
try
{
// This also does code style checks.
bat 'ctest -V'
} catch(e)
{
currentBuild.result = "UNSTABLE"
}
}
}
}
}
// Perform any post-build actions like notification and publishing of unit tests.
stage('Finalize')
{
// Publish the test results to Jenkins.
junit allowEmptyResults: true, testResults: 'build/junit*.xml'
notify_build_result(env.CURA_EMAIL_RECIPIENTS, '#cura-dev', ['master', '2.'])
}
}
}

125
README.md
View file

@ -1,61 +1,96 @@
Cura
====
Ultimaker Cura is a state-of-the-art slicer application to prepare your 3D models for printing with a 3D printer. With hundreds of settings and hundreds of community-managed print profiles, Ultimaker Cura is sure to lead your next project to a success.
![Screenshot](cura-logo.PNG)
<br>
Logging Issues
------------
For crashes and similar issues, please attach the following information:
<div align = center>
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output)
* The Cura GUI log file, located at
* `%APPDATA%\cura\<Cura version>\cura.log` (Windows), or usually `C:\Users\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
* `$HOME/Library/Application Support/cura/<Cura version>/cura.log` (OSX)
* `$HOME/.local/share/cura/<Cura version>/cura.log` (Ubuntu/Linux)
[![Badge Issues]][Issues]
[![Badge PullRequests]][PullRequests]
[![Badge Closed]][Closed]
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
[![Badge Size]][#]
[![Badge License]][License]
[![Badge Contributors]][Contributors]
For additional support, you could also ask in the [#cura channel](https://web.libera.chat/#cura) on [libera.chat](https://libera.chat/). For help with development, there is also the [#cura-dev channel](https://web.libera.chat/#cura-dev).
[![Badge Test]][Test]
[![Badge Conan]][Conan]
Dependencies
------------
* [Uranium](https://github.com/Ultimaker/Uranium) Cura is built on top of the Uranium framework.
* [CuraEngine](https://github.com/Ultimaker/CuraEngine) This will be needed at runtime to perform the actual slicing.
* [fdm_materials](https://github.com/Ultimaker/fdm_materials) Required to load a printer that has swappable material profiles.
* [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support.
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers.
<br>
<br>
For a list of required Python packages, with their recommended version, see `requirements.txt`.
![Logo]
This list is not exhaustive at the moment, please check the links in the next section for more details.
# Ultimaker Cura
Build scripts
-------------
Please check out [cura-build](https://github.com/Ultimaker/cura-build) for detailed building instructions.
*State-of-the-art slicer app to prepare* <br>
*your 3D models for your 3D printer.*
If you want to build the entire environment from scratch before building Cura as well, [cura-build-environment](https://github.com/Ultimaker/cura-build-environment) might be a starting point before cura-build. (Again, see cura-build for more details.)
*With hundreds of settings & community-managed print profiles,* <br>
*Ultimaker Cura is sure to lead your next project to a success.*
Running from Source
-------------
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Running-Cura-from-Source) for details about running Cura from source.
<br>
<br>
Plugins
-------------
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Plugin-Directory) for details about creating and using plugins.
[![Button Building]][Building]
[![Button Plugins]][Plugins]
[![Button Machines]][Machines]
Supported printers
-------------
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Adding-new-machine-profiles-to-Cura) for guidelines about adding support for new machines.
[![Button Report]][Report]
[![Button Settings]][Settings]
[![Button Localize]][Localize]
Configuring Cura
----------------
Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Cura-Settings) about configuration options for developers.
<br>
<br>
<picture>
<source media="(prefers-color-scheme: light)" srcset="./cura-logo.PNG">
<source media="(prefers-color-scheme: dark)" srcset="./cura-logo-dark.PNG">
<img alt="Shows cura open on the preview screen with a large benchy model in the center." src="./cura-logo.PNG">
</picture>
</div>
<br>
<!----------------------------------------------------------------------------->
[Contributors]: https://github.com/Ultimaker/Cura/graphs/contributors
[PullRequests]: https://github.com/Ultimaker/Cura/pulls
[Machines]: https://github.com/Ultimaker/Cura/wiki/Adding-new-machine-profiles-to-Cura
[Building]: https://github.com/Ultimaker/Cura/wiki/Running-Cura-from-Source
[Localize]: https://github.com/Ultimaker/Cura/wiki/Translating-Cura
[Settings]: https://github.com/Ultimaker/Cura/wiki/Cura-Settings
[Plugins]: https://github.com/Ultimaker/Cura/wiki/Plugin-Directory
[Closed]: https://github.com/Ultimaker/Cura/issues?q=is%3Aissue+is%3Aclosed
[Issues]: https://github.com/Ultimaker/Cura/issues
[Conan]: https://github.com/Ultimaker/Cura/actions/workflows/conan-package.yml
[Test]: https://github.com/Ultimaker/Cura/actions/workflows/unit-test.yml
[License]: LICENSE
[Report]: docs/Report.md
[Logo]: resources/images/cura-icon.png
[#]: #
<!---------------------------------[ Badges ]---------------------------------->
[Badge Contributors]: https://img.shields.io/github/contributors/ultimaker/cura?style=for-the-badge&logoColor=white&labelColor=db5e8a&color=ab4a6c&logo=GitHub
[Badge PullRequests]: https://img.shields.io/github/issues-pr/ultimaker/cura?style=for-the-badge&logoColor=white&labelColor=bb9f3e&color=937d31&logo=GitExtensions
[Badge License]: https://img.shields.io/badge/License-LGPL3-336887.svg?style=for-the-badge&labelColor=458cb5&logoColor=white&logo=GNU
[Badge Closed]: https://img.shields.io/github/issues-closed/ultimaker/cura?style=for-the-badge&logoColor=white&labelColor=629944&color=446a30&logo=AddThis
[Badge Issues]: https://img.shields.io/github/issues/ultimaker/cura?style=for-the-badge&logoColor=white&labelColor=c34360&color=933349&logo=AdBlock
[Badge Conan]: https://img.shields.io/github/workflow/status/Ultimaker/Cura/conan-package?style=for-the-badge&logoColor=white&labelColor=6185aa&color=4c6987&logo=Conan&label=Conan%20Package
[Badge Test]: https://img.shields.io/github/workflow/status/Ultimaker/Cura/unit-test?style=for-the-badge&logoColor=white&labelColor=4a999d&color=346c6e&logo=Codacy&label=Unit%20Test
[Badge Size]: https://img.shields.io/github/repo-size/ultimaker/cura?style=for-the-badge&logoColor=white&labelColor=715a97&color=584674&logo=GoogleAnalytics
<!---------------------------------[ Buttons ]--------------------------------->
[Button Localize]: https://img.shields.io/badge/Help_Localize-e2467d?style=for-the-badge&logoColor=white&logo=GoogleTranslate
[Button Machines]: https://img.shields.io/badge/Adding_Machines-yellow?style=for-the-badge&logoColor=white&logo=CloudFoundry
[Button Settings]: https://img.shields.io/badge/Configuration-00979D?style=for-the-badge&logoColor=white&logo=CodeReview
[Button Building]: https://img.shields.io/badge/Building_Cura-blue?style=for-the-badge&logoColor=white&logo=GitBook
[Button Plugins]: https://img.shields.io/badge/Plugin_Usage-569A31?style=for-the-badge&logoColor=white&logo=ROS
[Button Report]: https://img.shields.io/badge/Report_Issues-C9284D?style=for-the-badge&logoColor=white&logo=Cliqz
Translating Cura
----------------
Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Translating-Cura) about how to translate Cura into other languages.
License
----------------
Cura is released under the terms of the LGPLv3 or higher. A copy of this license should be included with the software.

273
Ultimaker-Cura.spec.jinja Normal file
View file

@ -0,0 +1,273 @@
# -*- mode: python ; coding: utf-8 -*-
import os
from pathlib import Path
from PyInstaller.utils.hooks import collect_all
datas = {{ datas }}
binaries = {{ binaries }}
hiddenimports = {{ hiddenimports }}
{% for value in collect_all %}tmp_ret = collect_all('{{ value }}')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
{% endfor %}
# Add dynamic libs in the venv bin/Script Path. This is needed because we might copy some additional libs
# e.q.: OpenSSL 1.1.1l in that directory with a separate:
# `conan install openssl@1.1.1l -g deploy && cp openssl/bin/*.so cura_inst/bin`
binaries.extend([(str(bin), ".") for bin in Path("{{ venv_script_path }}").glob("*.so*")])
binaries.extend([(str(bin), ".") for bin in Path("{{ venv_script_path }}").glob("*.dll")])
binaries.extend([(str(bin), ".") for bin in Path("{{ venv_script_path }}").glob("*.dylib")])
block_cipher = None
a = Analysis(
[{{ entrypoint }}],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name=r'{{ name }}',
debug=False,
bootloader_ignore_signals=False,
strip={{ strip }},
upx={{ upx }},
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch={{ target_arch }},
codesign_identity=os.getenv('CODESIGN_IDENTITY', None),
entitlements_file={{ entitlements_file }},
icon={{ icon }}
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name=r'{{ name }}'
)
{% if macos == true %}
# PyInstaller seems to copy everything in the resource folder for the MacOS, this causes issues with codesigning and notarizing
# 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",
"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,
name='{{ name }}.app',
icon={{ icon }},
bundle_identifier={{ osx_bundle_identifier }},
version={{ version }},
info_plist={
'CFBundleDisplayName': '{{ display_name }}',
'NSPrincipalClass': 'NSApplication',
'CFBundleDevelopmentRegion': 'English',
'CFBundleExecutable': '{{ name }}',
'CFBundleInfoDictionaryVersion': '6.0',
'CFBundlePackageType': 'APPL',
'CFBundleShortVersionString': {{ short_version }},
'CFBundleDocumentTypes': [{
'CFBundleTypeRole': 'Viewer',
'CFBundleTypeExtensions': ['*'],
'CFBundleTypeName': 'Model Files',
}]
},
){% endif %}

View file

@ -1,34 +0,0 @@
set -e
set -u
export OLD_PWD=`pwd`
export CMAKE=/c/software/PCL/cmake-3.0.1-win32-x86/bin/cmake.exe
export MAKE=mingw32-make.exe
export PATH=/c/mingw-w64/i686-4.9.2-posix-dwarf-rt_v3-rev1/mingw32/bin:$PATH
mkdir -p /c/software/protobuf/_build
cd /c/software/protobuf/_build
$CMAKE ../
$MAKE install
mkdir -p /c/software/libArcus/_build
cd /c/software/libArcus/_build
$CMAKE ../
$MAKE install
mkdir -p /c/software/PinkUnicornEngine/_build
cd /c/software/PinkUnicornEngine/_build
$CMAKE ../
$MAKE
cd $OLD_PWD
export PYTHONPATH=`pwd`/../libArcus/python:/c/Software/Uranium/
/c/python34/python setup.py py2exe
cp /c/software/PinkUnicornEngine/_build/CuraEngine.exe dist/
cp /c/software/libArcus/_install/bin/libArcus.dll dist/
cp /c/mingw-w64/i686-4.9.2-posix-dwarf-rt_v3-rev1/mingw32/bin/libgcc_s_dw2-1.dll dist/
cp /c/mingw-w64/i686-4.9.2-posix-dwarf-rt_v3-rev1/mingw32/bin/libwinpthread-1.dll dist/
cp /c/mingw-w64/i686-4.9.2-posix-dwarf-rt_v3-rev1/mingw32/bin/libstdc++-6.dll dist/
/c/program\ files\ \(x86\)/NSIS/makensis.exe installer.nsi

View file

@ -1,92 +0,0 @@
# Copyright (c) 2022 Ultimaker B.V.
# CuraPluginInstall.cmake is released under the terms of the LGPLv3 or higher.
#
# This module detects all plugins that need to be installed and adds them using the CMake install() command.
# It detects all plugin folder in the path "plugins/*" where there's a "plugin.json" in it.
#
# Plugins can be configured to NOT BE INSTALLED via the variable "CURA_NO_INSTALL_PLUGINS" as a list of string in the
# form of "a;b;c" or "a,b,c". By default all plugins will be installed.
#
option(PRINT_PLUGIN_LIST "Should the list of plugins that are installed be printed?" ON)
# Options or configuration variables
set(CURA_NO_INSTALL_PLUGINS "" CACHE STRING "A list of plugins that should not be installed, separated with ';' or ','.")
file(GLOB_RECURSE _plugin_json_list ${CMAKE_SOURCE_DIR}/plugins/*/plugin.json)
list(LENGTH _plugin_json_list _plugin_json_list_len)
# Sort the lists alphabetically so we can handle cases like this:
# - plugins/my_plugin/plugin.json
# - plugins/my_plugin/my_module/plugin.json
# In this case, only "plugins/my_plugin" should be added via install().
set(_no_install_plugin_list ${CURA_NO_INSTALL_PLUGINS})
# Sanitize the string so the comparison will be case-insensitive.
string(STRIP "${_no_install_plugin_list}" _no_install_plugin_list)
string(TOLOWER "${_no_install_plugin_list}" _no_install_plugin_list)
# WORKAROUND counterpart of what's in cura-build.
string(REPLACE "," ";" _no_install_plugin_list "${_no_install_plugin_list}")
list(LENGTH _no_install_plugin_list _no_install_plugin_list_len)
if(_no_install_plugin_list_len GREATER 0)
list(SORT _no_install_plugin_list)
endif()
if(_plugin_json_list_len GREATER 0)
list(SORT _plugin_json_list)
endif()
# Check all plugin directories and add them via install() if needed.
set(_install_plugin_list "")
foreach(_plugin_json_path ${_plugin_json_list})
get_filename_component(_plugin_dir ${_plugin_json_path} DIRECTORY)
file(RELATIVE_PATH _rel_plugin_dir ${CMAKE_CURRENT_SOURCE_DIR} ${_plugin_dir})
get_filename_component(_plugin_dir_name ${_plugin_dir} NAME)
# Make plugin name comparison case-insensitive
string(TOLOWER "${_plugin_dir_name}" _plugin_dir_name_lowercase)
# Check if this plugin needs to be skipped for installation
set(_add_plugin ON) # Indicates if this plugin should be added to the build or not.
set(_is_no_install_plugin OFF) # If this plugin will not be added, this indicates if it's because the plugin is
# specified in the NO_INSTALL_PLUGINS list.
if(_no_install_plugin_list)
if("${_plugin_dir_name_lowercase}" IN_LIST _no_install_plugin_list)
set(_add_plugin OFF)
set(_is_no_install_plugin ON)
endif()
endif()
# Make sure this is not a subdirectory in a plugin that's already in the install list
if(_add_plugin)
foreach(_known_install_plugin_dir ${_install_plugin_list})
if(_plugin_dir MATCHES "${_known_install_plugin_dir}.+")
set(_add_plugin OFF)
break()
endif()
endforeach()
endif()
if(_add_plugin)
if(${PRINT_PLUGIN_LIST})
message(STATUS "[+] PLUGIN TO INSTALL: ${_rel_plugin_dir}")
endif()
get_filename_component(_rel_plugin_parent_dir ${_rel_plugin_dir} DIRECTORY)
install(DIRECTORY ${_rel_plugin_dir}
DESTINATION lib${LIB_SUFFIX}/cura/${_rel_plugin_parent_dir}
PATTERN "__pycache__" EXCLUDE
PATTERN "*.qmlc" EXCLUDE
)
list(APPEND _install_plugin_list ${_plugin_dir})
elseif(_is_no_install_plugin)
if(${PRINT_PLUGIN_LIST})
message(STATUS "[-] PLUGIN TO REMOVE : ${_rel_plugin_dir}")
endif()
execute_process(COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/mod_bundled_packages_json.py
-d ${CMAKE_CURRENT_SOURCE_DIR}/resources/bundled_packages
${_plugin_dir_name}
RESULT_VARIABLE _mod_json_result)
endif()
endforeach()

View file

@ -1,77 +0,0 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
include(CTest)
include(CMakeParseArguments)
add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose)
function(cura_add_test)
set(_single_args NAME DIRECTORY PYTHONPATH)
cmake_parse_arguments("" "" "${_single_args}" "" ${ARGN})
if(NOT _NAME)
message(FATAL_ERROR "cura_add_test requires a test name argument")
endif()
if(NOT _DIRECTORY)
message(FATAL_ERROR "cura_add_test requires a directory to test")
endif()
if(NOT _PYTHONPATH)
set(_PYTHONPATH ${_DIRECTORY})
endif()
if(WIN32)
string(REPLACE "|" "\\;" _PYTHONPATH ${_PYTHONPATH})
set(_PYTHONPATH "${_PYTHONPATH}\\;$ENV{PYTHONPATH}")
else()
string(REPLACE "|" ":" _PYTHONPATH ${_PYTHONPATH})
set(_PYTHONPATH "${_PYTHONPATH}:$ENV{PYTHONPATH}")
endif()
get_test_property(${_NAME} ENVIRONMENT test_exists) #Find out if the test exists by getting a property from it that always exists (such as ENVIRONMENT because we set that ourselves).
if (NOT ${test_exists})
add_test(
NAME ${_NAME}
COMMAND ${Python_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
)
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
else()
message(WARNING "Duplicate test ${_NAME}!")
endif()
endfunction()
#Add code style test.
add_test(
NAME "code-style"
COMMAND ${Python_EXECUTABLE} run_mypy.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
#Add test for import statements which are not compatible with all builds
add_test(
NAME "invalid-imports"
COMMAND ${Python_EXECUTABLE} scripts/check_invalid_imports.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
file(GLOB_RECURSE _plugins plugins/*/__init__.py)
foreach(_plugin ${_plugins})
get_filename_component(_plugin_directory ${_plugin} DIRECTORY)
if(EXISTS ${_plugin_directory}/tests)
get_filename_component(_plugin_name ${_plugin_directory} NAME)
cura_add_test(NAME pytest-${_plugin_name} DIRECTORY ${_plugin_directory} PYTHONPATH "${_plugin_directory}|${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
endif()
endforeach()
#Add test for whether the shortcut alt-keys are unique in every translation.
add_test(
NAME "shortcut-keys"
COMMAND ${Python_EXECUTABLE} scripts/check_shortcut_keys.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)

View file

@ -1,73 +0,0 @@
#!/usr/bin/env python3
#
# This script removes the given package entries in the bundled_packages JSON files. This is used by the PluginInstall
# CMake module.
#
import argparse
import collections
import json
import os
import sys
def find_json_files(work_dir: str) -> list:
"""Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths.
:param work_dir: The directory to look for JSON files recursively.
:return: A list of JSON files in absolute paths that are found in the given directory.
"""
json_file_list = []
for root, dir_names, file_names in os.walk(work_dir):
for file_name in file_names:
abs_path = os.path.abspath(os.path.join(root, file_name))
json_file_list.append(abs_path)
return json_file_list
def remove_entries_from_json_file(file_path: str, entries: list) -> None:
"""Removes the given entries from the given JSON file. The file will modified in-place.
:param file_path: The JSON file to modify.
:param entries: A list of strings as entries to remove.
:return: None
"""
try:
with open(file_path, "r", encoding = "utf-8") as f:
package_dict = json.load(f, object_hook = collections.OrderedDict)
except Exception as e:
msg = "Failed to load '{file_path}' as a JSON file. This file will be ignored Exception: {e}"\
.format(file_path = file_path, e = e)
sys.stderr.write(msg + os.linesep)
return
for entry in entries:
if entry in package_dict:
del package_dict[entry]
print("[INFO] Remove entry [{entry}] from [{file_path}]".format(file_path = file_path, entry = entry))
try:
with open(file_path, "w", encoding = "utf-8", newline = "\n") as f:
json.dump(package_dict, f, indent = 4)
except Exception as e:
msg = "Failed to write '{file_path}' as a JSON file. Exception: {e}".format(file_path = file_path, e = e)
raise IOError(msg)
def main() -> None:
parser = argparse.ArgumentParser("mod_bundled_packages_json")
parser.add_argument("-d", "--dir", dest = "work_dir",
help = "The directory to look for bundled packages JSON files, recursively.")
parser.add_argument("entries", metavar = "ENTRIES", type = str, nargs = "+")
args = parser.parse_args()
json_file_list = find_json_files(args.work_dir)
for json_file_path in json_file_list:
remove_entries_from_json_file(json_file_path, args.entries)
if __name__ == "__main__":
main()

View file

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> -->
<component type="desktop">
<id>com.ultimaker.cura.desktop</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>LGPL-3.0 and CC-BY-SA-4.0</project_license>
<name>Cura</name>
<summary>The world's most advanced 3d printer software</summary>
<description>
<p>
Cura creates a seamless integration between hardware, software and
materials for the best 3D printing experience around.
Cura supports the 3MF, OBJ and STL file formats and is available on
Windows, Mac and Linux.
</p>
<ul>
<li>Novices can start printing right away</li>
<li>Experts are able to customize 300 settings to achieve the best results</li>
<li>Optimized profiles for Ultimaker materials</li>
<li>Supported by a global network of Ultimaker certified service partners</li>
<li>Print multiple objects at once with different settings for each object</li>
<li>Cura supports STL, 3MF and OBJ file formats</li>
<li>Open source and completely free</li>
</ul>
</description>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/Ultimaker/Cura/master/screenshot.png</image>
</screenshot>
</screenshots>
<url type="homepage">https://ultimaker.com/software/ultimaker-cura?utm_source=cura&amp;utm_medium=software&amp;utm_campaign=cura-update-linux</url>
<translation type="gettext">Cura</translation>
</component>

844
conandata.yml Normal file
View file

@ -0,0 +1,844 @@
---
# Usage: defaults to the first entry in this conandata.yml file
# If you're on a release branch create an entry for that **version** e.q.: `5.1.0` update the requirements (use pinned versions, not latest)
# also create a beta entry for that **version** e.q.: `5.1.0-beta`, update the requirements (use the <dep_name>/(latest)@ultimaker/stable)
#
# If you're working on a feature/bugfix branch from a release branch, create an entry for that **channel**, update the requirements (use
# the <dep_name>/(latest)@ultimaker/stable)
#
# If you're working on a feature/bugfix branch from a main branch, it is optional to create an entry for that **channel**, update the
# requirements (use the <dep_name>/(latest)@ultimaker/testing)
#
# Subject to change in the future!
"5.3.0-alpha":
requirements:
- "pyarcus/(latest)@ultimaker/testing"
- "curaengine/(latest)@ultimaker/testing"
- "pysavitar/(latest)@ultimaker/testing"
- "pynest2d/(latest)@ultimaker/testing"
- "uranium/(latest)@ultimaker/testing"
- "fdm_materials/(latest)@ultimaker/testing"
- "cura_binary_data/(latest)@ultimaker/testing"
- "cpython/3.10.4"
internal_requirements:
- "fdm_materials_private/(latest)@ultimaker/testing"
- "cura_private_data/(latest)@ultimaker/testing"
runinfo:
entrypoint: "cura_app.py"
pyinstaller:
datas:
cura_plugins:
package: "cura"
src: "plugins"
dst: "share/cura/plugins"
cura_resources:
package: "cura"
src: "resources"
dst: "share/cura/resources"
cura_private_data:
package: "cura_private_data"
src: "resources"
dst: "share/cura/resources"
internal: true
uranium_plugins:
package: "uranium"
src: "plugins"
dst: "share/uranium/plugins"
uranium_resources:
package: "uranium"
src: "resources"
dst: "share/uranium/resources"
uranium_um_qt_qml_um:
package: "uranium"
src: "site-packages/UM/Qt/qml/UM"
dst: "PyQt6/Qt6/qml/UM"
cura_binary_data:
package: "cura_binary_data"
src: "resources/cura/resources"
dst: "share/cura/resources"
uranium_binary_data:
package: "cura_binary_data"
src: "resources/uranium/resources"
dst: "share/uranium/resources"
windows_binary_data:
package: "cura_binary_data"
src: "windows"
dst: "share/windows"
fdm_materials:
package: "fdm_materials"
src: "materials"
dst: "share/cura/resources/materials"
fdm_materials_private:
package: "fdm_materials_private"
src: "resources/materials"
dst: "share/cura/resources/materials"
internal: true
tcl:
package: "tcl"
src: "lib/tcl8.6"
dst: "tcl"
tk:
package: "tk"
src: "lib/tk8.6"
dst: "tk"
binaries:
curaengine:
package: "curaengine"
src: "bin"
dst: "."
binary: "CuraEngine"
hiddenimports:
- "pySavitar"
- "pyArcus"
- "pynest2d"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "logging.handlers"
- "zeroconf"
- "fcntl"
- "stl"
- "serial"
collect_all:
- "cura"
- "UM"
- "serial"
- "Charon"
- "sqlite3"
- "trimesh"
- "win32ctypes"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "stl"
icon:
Windows: "./icons/Cura.ico"
Macos: "./icons/cura.icns"
Linux: "./icons/cura-128.png"
"5.2.1":
requirements:
- "pyarcus/5.2.0"
- "curaengine/5.2.1"
- "pysavitar/5.2.0"
- "pynest2d/5.2.0"
- "uranium/5.2.0"
- "fdm_materials/5.2.0"
- "cura_binary_data/5.2.1"
- "cpython/3.10.4"
internal_requirements:
- "fdm_materials_private/(latest)@ultimaker/testing"
- "cura_private_data/(latest)@ultimaker/testing"
runinfo:
entrypoint: "cura_app.py"
pyinstaller:
datas:
cura_plugins:
package: "cura"
src: "plugins"
dst: "share/cura/plugins"
cura_resources:
package: "cura"
src: "resources"
dst: "share/cura/resources"
cura_private_data:
package: "cura_private_data"
src: "resources"
dst: "share/cura/resources"
internal: true
uranium_plugins:
package: "uranium"
src: "plugins"
dst: "share/uranium/plugins"
uranium_resources:
package: "uranium"
src: "resources"
dst: "share/uranium/resources"
uranium_um_qt_qml_um:
package: "uranium"
src: "site-packages/UM/Qt/qml/UM"
dst: "PyQt6/Qt6/qml/UM"
cura_binary_data:
package: "cura_binary_data"
src: "resources/cura/resources"
dst: "share/cura/resources"
uranium_binary_data:
package: "cura_binary_data"
src: "resources/uranium/resources"
dst: "share/uranium/resources"
windows_binary_data:
package: "cura_binary_data"
src: "windows"
dst: "share/windows"
fdm_materials:
package: "fdm_materials"
src: "materials"
dst: "share/cura/resources/materials"
fdm_materials_private:
package: "fdm_materials_private"
src: "resources/materials"
dst: "share/cura/resources/materials"
internal: true
tcl:
package: "tcl"
src: "lib/tcl8.6"
dst: "tcl"
tk:
package: "tk"
src: "lib/tk8.6"
dst: "tk"
binaries:
curaengine:
package: "curaengine"
src: "bin"
dst: "."
binary: "CuraEngine"
hiddenimports:
- "pySavitar"
- "pyArcus"
- "pynest2d"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "logging.handlers"
- "zeroconf"
- "fcntl"
- "stl"
- "serial"
collect_all:
- "cura"
- "UM"
- "serial"
- "Charon"
- "sqlite3"
- "trimesh"
- "win32ctypes"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "stl"
icon:
Windows: "./icons/Cura.ico"
Macos: "./icons/cura.icns"
Linux: "./icons/cura-128.png"
"5.2.0-beta.2":
requirements:
- "pyarcus/(latest)@ultimaker/stable"
- "curaengine/(latest)@ultimaker/stable"
- "pysavitar/(latest)@ultimaker/stable"
- "pynest2d/(latest)@ultimaker/stable"
- "uranium/(latest)@ultimaker/stable"
- "fdm_materials/(latest)@ultimaker/stable"
- "cura_binary_data/(latest)@ultimaker/stable"
- "cpython/3.10.4"
internal_requirements:
- "fdm_materials_private/(latest)@ultimaker/testing"
- "cura_private_data/(latest)@ultimaker/testing"
runinfo:
entrypoint: "cura_app.py"
pyinstaller:
datas:
cura_plugins:
package: "cura"
src: "plugins"
dst: "share/cura/plugins"
cura_resources:
package: "cura"
src: "resources"
dst: "share/cura/resources"
cura_private_data:
package: "cura_private_data"
src: "resources"
dst: "share/cura/resources"
internal: true
uranium_plugins:
package: "uranium"
src: "plugins"
dst: "share/uranium/plugins"
uranium_resources:
package: "uranium"
src: "resources"
dst: "share/uranium/resources"
uranium_um_qt_qml_um:
package: "uranium"
src: "site-packages/UM/Qt/qml/UM"
dst: "PyQt6/Qt6/qml/UM"
cura_binary_data:
package: "cura_binary_data"
src: "resources/cura/resources"
dst: "share/cura/resources"
uranium_binary_data:
package: "cura_binary_data"
src: "resources/uranium/resources"
dst: "share/uranium/resources"
windows_binary_data:
package: "cura_binary_data"
src: "windows"
dst: "share/windows"
fdm_materials:
package: "fdm_materials"
src: "materials"
dst: "share/cura/resources/materials"
fdm_materials_private:
package: "fdm_materials_private"
src: "resources/materials"
dst: "share/cura/resources/materials"
internal: true
tcl:
package: "tcl"
src: "lib/tcl8.6"
dst: "tcl"
tk:
package: "tk"
src: "lib/tk8.6"
dst: "tk"
binaries:
curaengine:
package: "curaengine"
src: "bin"
dst: "."
binary: "CuraEngine"
hiddenimports:
- "pySavitar"
- "pyArcus"
- "pynest2d"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "logging.handlers"
- "zeroconf"
- "fcntl"
- "stl"
- "serial"
collect_all:
- "cura"
- "UM"
- "serial"
- "Charon"
- "sqlite3"
- "trimesh"
- "win32ctypes"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "stl"
icon:
Windows: "./icons/Cura.ico"
Macos: "./icons/cura.icns"
Linux: "./icons/cura-128.png"
"5.2.0-beta.1":
requirements:
- "pyarcus/5.2.0-beta.1"
- "curaengine/5.2.0-beta.1"
- "pysavitar/5.2.0-beta.1"
- "pynest2d/5.2.0-beta.1"
- "uranium/5.2.0-beta.1"
- "fdm_materials/5.2.0-beta.1"
- "cura_binary_data/5.2.0-beta.1"
- "cpython/3.10.4"
internal_requirements:
- "fdm_materials_private/(latest)@ultimaker/testing"
- "cura_private_data/(latest)@ultimaker/testing"
runinfo:
entrypoint: "cura_app.py"
pyinstaller:
datas:
cura_plugins:
package: "cura"
src: "plugins"
dst: "share/cura/plugins"
cura_resources:
package: "cura"
src: "resources"
dst: "share/cura/resources"
cura_private_data:
package: "cura_private_data"
src: "resources"
dst: "share/cura/resources"
internal: true
uranium_plugins:
package: "uranium"
src: "plugins"
dst: "share/uranium/plugins"
uranium_resources:
package: "uranium"
src: "resources"
dst: "share/uranium/resources"
uranium_um_qt_qml_um:
package: "uranium"
src: "site-packages/UM/Qt/qml/UM"
dst: "PyQt6/Qt6/qml/UM"
cura_binary_data:
package: "cura_binary_data"
src: "resources/cura/resources"
dst: "share/cura/resources"
uranium_binary_data:
package: "cura_binary_data"
src: "resources/uranium/resources"
dst: "share/uranium/resources"
windows_binary_data:
package: "cura_binary_data"
src: "windows"
dst: "share/windows"
fdm_materials:
package: "fdm_materials"
src: "materials"
dst: "share/cura/resources/materials"
fdm_materials_private:
package: "fdm_materials_private"
src: "resources/materials"
dst: "share/cura/resources/materials"
internal: true
tcl:
package: "tcl"
src: "lib/tcl8.6"
dst: "tcl"
tk:
package: "tk"
src: "lib/tk8.6"
dst: "tk"
binaries:
curaengine:
package: "curaengine"
src: "bin"
dst: "."
binary: "CuraEngine"
hiddenimports:
- "pySavitar"
- "pyArcus"
- "pynest2d"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "logging.handlers"
- "zeroconf"
- "fcntl"
- "stl"
- "serial"
collect_all:
- "cura"
- "UM"
- "serial"
- "Charon"
- "sqlite3"
- "trimesh"
- "win32ctypes"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "stl"
icon:
Windows: "./icons/Cura.ico"
Macos: "./icons/cura.icns"
Linux: "./icons/cura-128.png"
"5.2.0":
requirements:
- "pyarcus/5.2.0"
- "curaengine/5.2.0"
- "pysavitar/5.2.0"
- "pynest2d/5.2.0"
- "uranium/5.2.0"
- "fdm_materials/5.2.0"
- "cura_binary_data/5.2.0"
- "cpython/3.10.4"
internal_requirements:
- "fdm_materials_private/(latest)@ultimaker/testing"
- "cura_private_data/(latest)@ultimaker/testing"
runinfo:
entrypoint: "cura_app.py"
pyinstaller:
datas:
cura_plugins:
package: "cura"
src: "plugins"
dst: "share/cura/plugins"
cura_resources:
package: "cura"
src: "resources"
dst: "share/cura/resources"
cura_private_data:
package: "cura_private_data"
src: "resources"
dst: "share/cura/resources"
internal: true
uranium_plugins:
package: "uranium"
src: "plugins"
dst: "share/uranium/plugins"
uranium_resources:
package: "uranium"
src: "resources"
dst: "share/uranium/resources"
uranium_um_qt_qml_um:
package: "uranium"
src: "site-packages/UM/Qt/qml/UM"
dst: "PyQt6/Qt6/qml/UM"
cura_binary_data:
package: "cura_binary_data"
src: "resources/cura/resources"
dst: "share/cura/resources"
uranium_binary_data:
package: "cura_binary_data"
src: "resources/uranium/resources"
dst: "share/uranium/resources"
windows_binary_data:
package: "cura_binary_data"
src: "windows"
dst: "share/windows"
fdm_materials:
package: "fdm_materials"
src: "materials"
dst: "share/cura/resources/materials"
fdm_materials_private:
package: "fdm_materials_private"
src: "resources/materials"
dst: "share/cura/resources/materials"
internal: true
tcl:
package: "tcl"
src: "lib/tcl8.6"
dst: "tcl"
tk:
package: "tk"
src: "lib/tk8.6"
dst: "tk"
binaries:
curaengine:
package: "curaengine"
src: "bin"
dst: "."
binary: "CuraEngine"
hiddenimports:
- "pySavitar"
- "pyArcus"
- "pynest2d"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "logging.handlers"
- "zeroconf"
- "fcntl"
- "stl"
- "serial"
collect_all:
- "cura"
- "UM"
- "serial"
- "Charon"
- "sqlite3"
- "trimesh"
- "win32ctypes"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "stl"
icon:
Windows: "./icons/Cura.ico"
Macos: "./icons/cura.icns"
Linux: "./icons/cura-128.png"
"5.2.0-alpha":
requirements:
- "pyarcus/5.2@ultimaker/testing"
- "curaengine/(latest)@ultimaker/testing"
- "pysavitar/(latest)@ultimaker/testing"
- "pynest2d/(latest)@ultimaker/testing"
- "uranium/(latest)@ultimaker/testing"
- "fdm_materials/(latest)@ultimaker/testing"
- "cura_binary_data/(latest)@ultimaker/testing"
- "cpython/3.10.4"
internal_requirements:
- "fdm_materials_private/(latest)@ultimaker/testing"
- "cura_private_data/(latest)@ultimaker/testing"
runinfo:
entrypoint: "cura_app.py"
pyinstaller:
datas:
cura_plugins:
package: "cura"
src: "plugins"
dst: "share/cura/plugins"
cura_resources:
package: "cura"
src: "resources"
dst: "share/cura/resources"
cura_private_data:
package: "cura_private_data"
src: "resources"
dst: "share/cura/resources"
internal: true
uranium_plugins:
package: "uranium"
src: "plugins"
dst: "share/uranium/plugins"
uranium_resources:
package: "uranium"
src: "resources"
dst: "share/uranium/resources"
uranium_um_qt_qml_um:
package: "uranium"
src: "site-packages/UM/Qt/qml/UM"
dst: "PyQt6/Qt6/qml/UM"
cura_binary_data:
package: "cura_binary_data"
src: "resources/cura/resources"
dst: "share/cura/resources"
uranium_binary_data:
package: "cura_binary_data"
src: "resources/uranium/resources"
dst: "share/uranium/resources"
windows_binary_data:
package: "cura_binary_data"
src: "windows"
dst: "share/windows"
fdm_materials:
package: "fdm_materials"
src: "materials"
dst: "share/cura/resources/materials"
fdm_materials_private:
package: "fdm_materials_private"
src: "resources/materials"
dst: "share/cura/resources/materials"
internal: true
tcl:
package: "tcl"
src: "lib/tcl8.6"
dst: "tcl"
tk:
package: "tk"
src: "lib/tk8.6"
dst: "tk"
binaries:
curaengine:
package: "curaengine"
src: "bin"
dst: "."
binary: "CuraEngine"
hiddenimports:
- "pySavitar"
- "pyArcus"
- "pynest2d"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "logging.handlers"
- "zeroconf"
- "fcntl"
- "stl"
- "serial"
collect_all:
- "cura"
- "UM"
- "serial"
- "Charon"
- "sqlite3"
- "trimesh"
- "win32ctypes"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "stl"
icon:
Windows: "./icons/Cura.ico"
Macos: "./icons/cura.icns"
Linux: "./icons/cura-128.png"
"5.1.0":
requirements:
- "arcus/5.1.0"
- "curaengine/5.1.0"
- "savitar/5.1.0"
- "pynest2d/5.1.0"
- "uranium/5.1.0"
- "fdm_materials/5.1.0"
- "cura_binary_data/5.1.0"
- "cpython/3.10.4"
runinfo:
entrypoint: "cura_app.py"
pyinstaller:
datas:
cura_plugins:
package: "cura"
src: "plugins"
dst: "share/cura/plugins"
cura_resources:
package: "cura"
src: "resources"
dst: "share/cura/resources"
uranium_plugins:
package: "uranium"
src: "plugins"
dst: "share/uranium/plugins"
uranium_resources:
package: "uranium"
src: "resources"
dst: "share/uranium/resources"
uranium_um_qt_qml_um:
package: "uranium"
src: "site-packages/UM/Qt/qml/UM"
dst: "PyQt6/Qt6/qml/UM"
cura_binary_data:
package: "cura_binary_data"
src: "resources/cura/resources"
dst: "share/cura/resources"
uranium_binary_data:
package: "cura_binary_data"
src: "resources/uranium/resources"
dst: "share/uranium/resources"
windows_binary_data:
package: "cura_binary_data"
src: "windows"
dst: "share/windows"
fdm_materials:
package: "fdm_materials"
src: "materials"
dst: "share/cura/resources/materials"
tcl:
package: "tcl"
src: "lib/tcl8.6"
dst: "tcl"
tk:
package: "tk"
src: "lib/tk8.6"
dst: "tk"
binaries:
curaengine:
package: "curaengine"
src: "bin"
dst: "."
binary: "CuraEngine"
hiddenimports:
- "pySavitar"
- "pyArcus"
- "pynest2d"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "logging.handlers"
- "zeroconf"
- "fcntl"
- "stl"
- "serial"
collect_all:
- "cura"
- "UM"
- "serial"
- "Charon"
- "sqlite3"
- "trimesh"
- "win32ctypes"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "stl"
icon:
Windows: "./icons/Cura.ico"
Macos: "./icons/cura.icns"
Linux: "./icons/cura-128.png"
pycharm_targets:
- jinja_path: .run_templates/pycharm_cura_run.run.xml.jinja
module_name: Cura
name: cura
script_name: cura_app.py
- jinja_path: .run_templates/pycharm_cura_run.run.xml.jinja
module_name: Cura
name: cura_external_engine
parameters: --external-backend
script_name: cura_app.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in tests
script_name: tests/
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestBuildVolume.py
script_name: tests/TestBuildVolume.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestConvexHullDecorator.py
script_name: tests/TestConvexHullDecorator.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestCuraSceneNode.py
script_name: tests/TestCuraSceneNode.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestCuraSceneNode.py
script_name: tests/TestExtruderManager.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestGCodeListDecorator.py
script_name: tests/TestGCodeListDecorator.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestIntentManager.py
script_name: tests/TestIntentManager.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestLayer.py
script_name: tests/TestLayer.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestMachineAction.py
script_name: tests/TestMachineAction.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestMachineManager.py
script_name: tests/TestMachineManager.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestOAuth2.py
script_name: tests/TestOAuth2.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestObjectsModel.py
script_name: tests/TestObjectsModel.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestPrintInformation.py
script_name: tests/TestPrintInformation.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestProfileRequirements.py
script_name: tests/TestProfileRequirements.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestThemes.py
script_name: tests/TestThemes.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestContainerManager.py
script_name: tests/Settings/TestContainerManager.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestCuraContainerRegistry.py
script_name: tests/Settings/TestCuraContainerRegistry.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestCuraStackBuilder.py
script_name: tests/Settings/TestCuraStackBuilder.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestDefinitionContainer.py
script_name: tests/Settings/TestDefinitionContainer.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestExtruderStack.py
script_name: tests/Settings/TestExtruderStack.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestGlobalStack.py
script_name: tests/Settings/TestGlobalStack.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestProfiles.py
script_name: tests/Settings/TestProfiles.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestSettingInheritanceManager.py
script_name: tests/Settings/TestSettingInheritanceManager.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestSettingOverrideDecorator.py
script_name: tests/Settings/TestSettingOverrideDecorator.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestSettingVisibilityPresets.py
script_name: tests/Settings/TestSettingVisibilityPresets.py

457
conanfile.py Normal file
View file

@ -0,0 +1,457 @@
import os
from pathlib import Path
from jinja2 import Template
from conan import ConanFile
from conan.tools.files import copy, rmdir, save
from conan.tools.env import VirtualRunEnv, Environment
from conan.tools.scm import Version
from conan.errors import ConanInvalidConfiguration, ConanException
required_conan_version = ">=1.50.0"
class CuraConan(ConanFile):
name = "cura"
license = "LGPL-3.0"
author = "Ultimaker B.V."
url = "https://github.com/Ultimaker/cura"
description = "3D printer / slicing GUI built on top of the Uranium framework"
topics = ("conan", "python", "pyqt5", "qt", "qml", "3d-printing", "slicer")
build_policy = "missing"
exports = "LICENSE*", "Ultimaker-Cura.spec.jinja", "CuraVersion.py.jinja"
settings = "os", "compiler", "build_type", "arch"
no_copy_source = True # We won't build so no need to copy sources to the build folder
# FIXME: Remove specific branch once merged to main
# Extending the conanfile with the UMBaseConanfile https://github.com/Ultimaker/conan-ultimaker-index/tree/CURA-9177_Fix_CI_CD/recipes/umbase
python_requires = "umbase/[>=0.1.7]@ultimaker/stable"
python_requires_extend = "umbase.UMBaseConanfile"
options = {
"enterprise": ["True", "False", "true", "false"], # Workaround for GH Action passing boolean as lowercase string
"staging": ["True", "False", "true", "false"], # Workaround for GH Action passing boolean as lowercase string
"devtools": [True, False], # FIXME: Split this up in testing and (development / build (pyinstaller) / system installer) tools
"cloud_api_version": "ANY",
"display_name": "ANY", # TODO: should this be an option??
"cura_debug_mode": [True, False], # FIXME: Use profiles
"internal": [True, False]
}
default_options = {
"enterprise": "False",
"staging": "False",
"devtools": False,
"cloud_api_version": "1",
"display_name": "Ultimaker Cura",
"cura_debug_mode": False, # Not yet implemented
"internal": False,
}
scm = {
"type": "git",
"subfolder": ".",
"url": "auto",
"revision": "auto"
}
@property
def _pycharm_targets(self):
return self.conan_data["pycharm_targets"]
# FIXME: These env vars should be defined in the runenv.
_cura_env = None
@property
def _cura_run_env(self):
if self._cura_env:
return self._cura_env
self._cura_env = Environment()
self._cura_env.define("QML2_IMPORT_PATH", str(self._site_packages.joinpath("PyQt6", "Qt6", "qml")))
self._cura_env.define("QT_PLUGIN_PATH", str(self._site_packages.joinpath("PyQt6", "Qt6", "plugins")))
if self.settings.os == "Linux":
self._cura_env.define("QT_QPA_FONTDIR", "/usr/share/fonts")
self._cura_env.define("QT_QPA_PLATFORMTHEME", "xdgdesktopportal")
self._cura_env.define("QT_XKB_CONFIG_ROOT", "/usr/share/X11/xkb")
return self._cura_env
@property
def _staging(self):
return self.options.staging in ["True", 'true']
@property
def _enterprise(self):
return self.options.enterprise in ["True", 'true']
@property
def _cloud_api_root(self):
return "https://api-staging.ultimaker.com" if self._staging else "https://api.ultimaker.com"
@property
def _cloud_account_api_root(self):
return "https://account-staging.ultimaker.com" if self._staging else "https://account.ultimaker.com"
@property
def _marketplace_root(self):
return "https://marketplace-staging.ultimaker.com" if self._staging else "https://marketplace.ultimaker.com"
@property
def _digital_factory_url(self):
return "https://digitalfactory-staging.ultimaker.com" if self._staging else "https://digitalfactory.ultimaker.com"
@property
def _cura_latest_url(self):
return "https://software.ultimaker.com/latest.json"
@property
def requirements_txts(self):
if self.options.devtools:
return ["requirements.txt", "requirements-ultimaker.txt", "requirements-dev.txt"]
return ["requirements.txt", "requirements-ultimaker.txt"]
@property
def _base_dir(self):
if self.install_folder is None:
if self.build_folder is not None:
return Path(self.build_folder)
return Path(os.getcwd(), "venv")
if self.in_local_cache:
return Path(self.install_folder)
else:
return Path(self.source_folder, "venv")
@property
def _share_dir(self):
return self._base_dir.joinpath("share")
@property
def _script_dir(self):
if self.settings.os == "Windows":
return self._base_dir.joinpath("Scripts")
return self._base_dir.joinpath("bin")
@property
def _site_packages(self):
if self.settings.os == "Windows":
return self._base_dir.joinpath("Lib", "site-packages")
py_version = Version(self.deps_cpp_info["cpython"].version)
return self._base_dir.joinpath("lib", f"python{py_version.major}.{py_version.minor}", "site-packages")
@property
def _py_interp(self):
py_interp = self._script_dir.joinpath(Path(self.deps_user_info["cpython"].python).name)
if self.settings.os == "Windows":
py_interp = Path(*[f'"{p}"' if " " in p else p for p in py_interp.parts])
return py_interp
def _generate_cura_version(self, location):
with open(Path(__file__).parent.joinpath("CuraVersion.py.jinja"), "r") as f:
cura_version_py = Template(f.read())
# If you want a specific Cura version to show up on the splash screen add the user configuration `user.cura:version=VERSION`
# the global.conf, profile, package_info (of dependency) or via the cmd line `-c user.cura:version=VERSION`
cura_version = Version(self.conf.get("user.cura:version", default = self.version, check_type = str))
pre_tag = f"-{cura_version.pre}" if cura_version.pre else ""
build_tag = f"+{cura_version.build}" if cura_version.build else ""
internal_tag = f"+internal" if self.options.internal else ""
cura_version = f"{cura_version.major}.{cura_version.minor}.{cura_version.patch}{pre_tag}{build_tag}{internal_tag}"
with open(Path(location, "CuraVersion.py"), "w") as f:
f.write(cura_version_py.render(
cura_app_name = self.name,
cura_app_display_name = self.options.display_name,
cura_version = cura_version,
cura_build_type = "Enterprise" if self._enterprise else "",
cura_debug_mode = self.options.cura_debug_mode,
cura_cloud_api_root = self._cloud_api_root,
cura_cloud_api_version = self.options.cloud_api_version,
cura_cloud_account_api_root = self._cloud_account_api_root,
cura_marketplace_root = self._marketplace_root,
cura_digital_factory_url = self._digital_factory_url,
cura_latest_url = self._cura_latest_url))
def _generate_pyinstaller_spec(self, location, entrypoint_location, icon_path, entitlements_file):
pyinstaller_metadata = self._um_data()["pyinstaller"]
datas = [(str(self._base_dir.joinpath("conan_install_info.json")), ".")]
for data in pyinstaller_metadata["datas"].values():
if not self.options.internal and data.get("internal", False):
continue
if "package" in data: # get the paths from conan package
if data["package"] == self.name:
if self.in_local_cache:
src_path = Path(self.package_folder, data["src"])
else:
src_path = Path(self.source_folder, data["src"])
else:
src_path = Path(self.deps_cpp_info[data["package"]].rootpath, data["src"])
elif "root" in data: # get the paths relative from the sourcefolder
src_path = Path(self.source_folder, data["root"], data["src"])
else:
continue
if src_path.exists():
datas.append((str(src_path), data["dst"]))
binaries = []
for binary in pyinstaller_metadata["binaries"].values():
if "package" in binary: # get the paths from conan package
src_path = Path(self.deps_cpp_info[binary["package"]].rootpath, binary["src"])
elif "root" in binary: # get the paths relative from the sourcefolder
src_path = Path(self.source_folder, binary["root"], binary["src"])
else:
continue
if not src_path.exists():
self.output.warning(f"Source path for binary {binary['binary']} does not exist")
continue
for bin in src_path.glob(binary["binary"] + "*[.exe|.dll|.so|.dylib|.so.]*"):
binaries.append((str(bin), binary["dst"]))
for bin in src_path.glob(binary["binary"]):
binaries.append((str(bin), binary["dst"]))
# Make sure all Conan dependencies which are shared are added to the binary list for pyinstaller
for _, dependency in self.dependencies.host.items():
for bin_paths in dependency.cpp_info.bindirs:
binaries.extend([(f"{p}", ".") for p in Path(bin_paths).glob("**/*.dll")])
for lib_paths in dependency.cpp_info.libdirs:
binaries.extend([(f"{p}", ".") for p in Path(lib_paths).glob("**/*.so*")])
binaries.extend([(f"{p}", ".") for p in Path(lib_paths).glob("**/*.dylib*")])
# Copy dynamic libs from lib path
binaries.extend([(f"{p}", ".") for p in Path(self._base_dir.joinpath("lib")).glob("**/*.dylib*")])
binaries.extend([(f"{p}", ".") for p in Path(self._base_dir.joinpath("lib")).glob("**/*.so*")])
# Collect all dll's from PyQt6 and place them in the root
binaries.extend([(f"{p}", ".") for p in Path(self._site_packages, "PyQt6", "Qt6").glob("**/*.dll")])
with open(Path(__file__).parent.joinpath("Ultimaker-Cura.spec.jinja"), "r") as f:
pyinstaller = Template(f.read())
version = self.conf_info.get("user.cura:version", default = self.version, check_type = str)
cura_version = Version(version)
with open(Path(location, "Ultimaker-Cura.spec"), "w") as f:
f.write(pyinstaller.render(
name = str(self.options.display_name).replace(" ", "-"),
display_name = self.options.display_name,
entrypoint = entrypoint_location,
datas = datas,
binaries = binaries,
venv_script_path = str(self._script_dir),
hiddenimports = pyinstaller_metadata["hiddenimports"],
collect_all = pyinstaller_metadata["collect_all"],
icon = icon_path,
entitlements_file = entitlements_file,
osx_bundle_identifier = "'nl.ultimaker.cura'" if self.settings.os == "Macos" else "None",
upx = str(self.settings.os == "Windows"),
strip = False, # This should be possible on Linux and MacOS but, it can also cause issues on some distributions. Safest is to disable it for now
target_arch = "'x86_64'" if self.settings.os == "Macos" else "None", # FIXME: Make this dependent on the settings.arch_target
macos = self.settings.os == "Macos",
version = f"'{version}'",
short_version = f"'{cura_version.major}.{cura_version.minor}.{cura_version.patch}'",
))
def set_version(self):
if self.version is None:
self.version = self._umdefault_version()
def configure(self):
self.options["pyarcus"].shared = True
self.options["pysavitar"].shared = True
self.options["pynest2d"].shared = True
self.options["cpython"].shared = True
def validate(self):
version = self.conf_info.get("user.cura:version", default = self.version, check_type = str)
if version and Version(version) <= Version("4"):
raise ConanInvalidConfiguration("Only versions 5+ are support")
def requirements(self):
for req in self._um_data()["requirements"]:
self.requires(req)
if self.options.internal:
for req in self._um_data()["internal_requirements"]:
self.requires(req)
def layout(self):
self.folders.source = "."
self.folders.build = "venv"
self.folders.generators = Path(self.folders.build, "conan")
self.cpp.package.libdirs = [os.path.join("site-packages", "cura")]
self.cpp.package.bindirs = ["bin"]
self.cpp.package.resdirs = ["resources", "plugins", "packaging", "pip_requirements"] # pip_requirements should be the last item in the list
def build(self):
pass
def generate(self):
cura_run_envvars = self._cura_run_env.vars(self, scope = "run")
ext = ".ps1" if self.settings.os == "Windows" else ".sh"
cura_run_envvars.save_script(self.folders.generators.joinpath(f"cura_run_environment{ext}"))
vr = VirtualRunEnv(self)
vr.generate()
self._generate_cura_version(Path(self.source_folder, "cura"))
if self.options.devtools:
entitlements_file = "'{}'".format(Path(self.source_folder, "packaging", "dmg", "cura.entitlements"))
self._generate_pyinstaller_spec(location = self.generators_folder,
entrypoint_location = "'{}'".format(Path(self.source_folder, self._um_data()["runinfo"]["entrypoint"])).replace("\\", "\\\\"),
icon_path = "'{}'".format(Path(self.source_folder, "packaging", self._um_data()["pyinstaller"]["icon"][str(self.settings.os)])).replace("\\", "\\\\"),
entitlements_file = entitlements_file if self.settings.os == "Macos" else "None")
def imports(self):
self.copy("CuraEngine.exe", root_package = "curaengine", src = "@bindirs", dst = "", keep_path = False)
self.copy("CuraEngine", root_package = "curaengine", src = "@bindirs", dst = "", keep_path = False)
rmdir(self, os.path.join(self.source_folder, "resources", "materials"))
self.copy("*.fdm_material", root_package = "fdm_materials", src = "@resdirs", dst = "resources/materials", keep_path = False)
self.copy("*.sig", root_package = "fdm_materials", src = "@resdirs", dst = "resources/materials", keep_path = False)
if self.options.internal:
self.copy("*.fdm_material", root_package = "fdm_materials_private", src = "@resdirs", dst = "resources/materials", keep_path = False)
self.copy("*.sig", root_package = "fdm_materials_private", src = "@resdirs", dst = "resources/materials", keep_path = False)
self.copy("*", root_package = "cura_private_data", src = self.deps_cpp_info["cura_private_data"].resdirs[0],
dst = self._share_dir.joinpath("cura", "resources"), keep_path = True)
# Copy resources of cura_binary_data
self.copy("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[0],
dst = self._share_dir.joinpath("cura", "resources"), keep_path = True)
self.copy("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[1],
dst =self._share_dir.joinpath("uranium", "resources"), keep_path = True)
self.copy("*.dll", src = "@bindirs", dst = self._site_packages)
self.copy("*.pyd", src = "@libdirs", dst = self._site_packages)
self.copy("*.pyi", src = "@libdirs", dst = self._site_packages)
self.copy("*.dylib", src = "@libdirs", dst = self._script_dir)
def deploy(self):
# Copy CuraEngine.exe to bindirs of Virtual Python Environment
# TODO: Fix source such that it will get the curaengine relative from the executable (Python bindir in this case)
self.copy_deps("CuraEngine.exe", root_package = "curaengine", src = self.deps_cpp_info["curaengine"].bindirs[0],
dst = self._base_dir,
keep_path = False)
self.copy_deps("CuraEngine", root_package = "curaengine", src = self.deps_cpp_info["curaengine"].bindirs[0], dst = self._base_dir,
keep_path = False)
# Copy resources of Cura (keep folder structure)
self.copy("*", src = self.cpp_info.bindirs[0], dst = self._base_dir, keep_path = False)
self.copy("*", src = self.cpp_info.libdirs[0], dst = self._site_packages.joinpath("cura"), keep_path = True)
self.copy("*", src = self.cpp_info.resdirs[0], dst = self._share_dir.joinpath("cura", "resources"), keep_path = True)
self.copy("*", src = self.cpp_info.resdirs[1], dst = self._share_dir.joinpath("cura", "plugins"), keep_path = True)
# Copy materials (flat)
self.copy_deps("*.fdm_material", root_package = "fdm_materials", src = self.deps_cpp_info["fdm_materials"].resdirs[0],
dst = self._share_dir.joinpath("cura", "resources", "materials"), keep_path = False)
self.copy_deps("*.sig", root_package = "fdm_materials", src = self.deps_cpp_info["fdm_materials"].resdirs[0],
dst = self._share_dir.joinpath("cura", "resources", "materials"), keep_path = False)
# Copy internal resources
if self.options.internal:
self.copy_deps("*.fdm_material", root_package = "fdm_materials_private", src = self.deps_cpp_info["fdm_materials_private"].resdirs[0],
dst = self._share_dir.joinpath("cura", "resources", "materials"), keep_path = False)
self.copy_deps("*.sig", root_package = "fdm_materials_private", src = self.deps_cpp_info["fdm_materials_private"].resdirs[0],
dst = self._share_dir.joinpath("cura", "resources", "materials"), keep_path = False)
self.copy_deps("*", root_package = "cura_private_data", src = self.deps_cpp_info["cura_private_data"].resdirs[0],
dst = self._share_dir.joinpath("cura", "resources"), keep_path = True)
# Copy resources of Uranium (keep folder structure)
self.copy_deps("*", root_package = "uranium", src = self.deps_cpp_info["uranium"].resdirs[0],
dst = self._share_dir.joinpath("uranium", "resources"), keep_path = True)
self.copy_deps("*", root_package = "uranium", src = self.deps_cpp_info["uranium"].resdirs[1],
dst = self._share_dir.joinpath("uranium", "plugins"), keep_path = True)
self.copy_deps("*", root_package = "uranium", src = self.deps_cpp_info["uranium"].libdirs[0],
dst = self._site_packages.joinpath("UM"),
keep_path = True)
self.copy_deps("*", root_package = "uranium", src = str(Path(self.deps_cpp_info["uranium"].libdirs[0], "Qt", "qml", "UM")),
dst = self._site_packages.joinpath("PyQt6", "Qt6", "qml", "UM"),
keep_path = True)
# Copy resources of cura_binary_data
self.copy_deps("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[0],
dst = self._share_dir.joinpath("cura"), keep_path = True)
self.copy_deps("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[1],
dst = self._share_dir.joinpath("uranium"), keep_path = True)
if self.settings.os == "Windows":
self.copy_deps("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[2],
dst = self._share_dir.joinpath("windows"), keep_path = True)
self.copy_deps("*.dll", src = "@bindirs", dst = self._site_packages)
self.copy_deps("*.pyd", src = "@libdirs", dst = self._site_packages)
self.copy_deps("*.pyi", src = "@libdirs", dst = self._site_packages)
self.copy_deps("*.dylib", src = "@libdirs", dst = self._base_dir.joinpath("lib"))
# Copy packaging scripts
self.copy("*", src = self.cpp_info.resdirs[2], dst = self._base_dir.joinpath("packaging"))
# Copy requirements.txt's
self.copy("*.txt", src = self.cpp_info.resdirs[-1], dst = self._base_dir.joinpath("pip_requirements"))
# Generate the GitHub Action version info Environment
version = self.conf_info.get("user.cura:version", default = self.version, check_type = str)
cura_version = Version(version)
env_prefix = "Env:" if self.settings.os == "Windows" else ""
activate_github_actions_version_env = Template(r"""echo "CURA_VERSION_MAJOR={{ cura_version_major }}" >> ${{ env_prefix }}GITHUB_ENV
echo "CURA_VERSION_MINOR={{ cura_version_minor }}" >> ${{ env_prefix }}GITHUB_ENV
echo "CURA_VERSION_PATCH={{ cura_version_patch }}" >> ${{ env_prefix }}GITHUB_ENV
echo "CURA_VERSION_BUILD={{ cura_version_build }}" >> ${{ env_prefix }}GITHUB_ENV
echo "CURA_VERSION_FULL={{ cura_version_full }}" >> ${{ env_prefix }}GITHUB_ENV
""").render(cura_version_major = cura_version.major,
cura_version_minor = cura_version.minor,
cura_version_patch = cura_version.patch,
cura_version_build = cura_version.build if cura_version.build != "" else "0",
cura_version_full = self.version,
env_prefix = env_prefix)
ext = ".sh" if self.settings.os != "Windows" else ".ps1"
save(self, self._script_dir.joinpath(f"activate_github_actions_version_env{ext}"), activate_github_actions_version_env)
self._generate_cura_version(Path(self._site_packages, "cura"))
entitlements_file = "'{}'".format(Path(self.cpp_info.res_paths[2], "dmg", "cura.entitlements"))
self._generate_pyinstaller_spec(location = self._base_dir,
entrypoint_location = "'{}'".format(Path(self.cpp_info.bin_paths[0], self._um_data()["runinfo"]["entrypoint"])).replace("\\", "\\\\"),
icon_path = "'{}'".format(Path(self.cpp_info.res_paths[2], self._um_data()["pyinstaller"]["icon"][str(self.settings.os)])).replace("\\", "\\\\"),
entitlements_file = entitlements_file if self.settings.os == "Macos" else "None")
def package(self):
self.copy("cura_app.py", src = ".", dst = self.cpp.package.bindirs[0])
self.copy("*", src = "cura", dst = self.cpp.package.libdirs[0])
self.copy("*", src = "resources", dst = self.cpp.package.resdirs[0])
self.copy("*", src = "plugins", dst = self.cpp.package.resdirs[1])
self.copy("requirement*.txt", src = ".", dst = self.cpp.package.resdirs[-1])
self.copy("*", src = "packaging", dst = self.cpp.package.resdirs[2])
def package_info(self):
self.user_info.pip_requirements = "requirements.txt"
self.user_info.pip_requirements_git = "requirements-ultimaker.txt"
self.user_info.pip_requirements_build = "requirements-dev.txt"
if self.in_local_cache:
self.runenv_info.append_path("PYTHONPATH", str(Path(self.cpp_info.lib_paths[0]).parent))
self.runenv_info.append_path("PYTHONPATH", self.cpp_info.res_paths[1]) # Add plugins to PYTHONPATH
else:
self.runenv_info.append_path("PYTHONPATH", self.source_folder)
self.runenv_info.append_path("PYTHONPATH", os.path.join(self.source_folder, "plugins"))
def package_id(self):
del self.info.settings.os
del self.info.settings.compiler
del self.info.settings.build_type
del self.info.settings.arch
# The following options shouldn't be used to determine the hash, since these are only used to set the CuraVersion.py
# which will als be generated by the deploy method during the `conan install cura/5.1.0@_/_`
del self.info.options.enterprise
del self.info.options.staging
del self.info.options.devtools
del self.info.options.cloud_api_version
del self.info.options.display_name
del self.info.options.cura_debug_mode
# TODO: Use the hash of requirements.txt and requirements-ultimaker.txt, Because changing these will actually result in a different
# Cura. This is needed because the requirements.txt aren't managed by Conan and therefor not resolved in the package_id. This isn't
# ideal but an acceptable solution for now.

View file

@ -16,4 +16,6 @@ Making pull requests
--------------------
If you want to propose a change to Cura's source code, please create a pull request in the appropriate repository (being [Cura](https://github.com/Ultimaker/Cura), [Uranium](https://github.com/Ultimaker/Uranium), [CuraEngine](https://github.com/Ultimaker/CuraEngine), [fdm_materials](https://github.com/Ultimaker/fdm_materials), [libArcus](https://github.com/Ultimaker/libArcus), [cura-build](https://github.com/Ultimaker/cura-build), [cura-build-environment](https://github.com/Ultimaker/cura-build-environment), [libSavitar](https://github.com/Ultimaker/libSavitar), [libCharon](https://github.com/Ultimaker/libCharon) or [cura-binary-data](https://github.com/Ultimaker/cura-binary-data)) and if your change requires changes on multiple of these repositories, please link them together so that we know to merge them together.
Some of these repositories will have automated tests running when you create a pull request, indicated by green check marks or red crosses in the Github web page. If you see a red cross, that means that a test has failed. If the test doesn't fail on the Master branch but does fail on your branch, that indicates that you've probably made a mistake and you need to do that. Click on the cross for more details, or run the test locally by running `cmake . && ctest --verbose`.
The style guide for code contributions to Cura and other Ultimaker projects can be found [here](https://github.com/Ultimaker/Meta/blob/master/general/generic_code_conventions.md).
Some of these repositories will have automated tests running when you create a pull request, indicated by green check marks or red crosses in the Github web page. If you see a red cross, that means that a test has failed. If the test doesn't fail on the Master branch but does fail on your branch, that indicates that you've probably made a mistake and you need to do that. Click on the cross for more details, or run the test locally by running `cmake . && ctest --verbose`.

BIN
cura-logo-dark.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 KiB

After

Width:  |  Height:  |  Size: 1 MiB

Before After
Before After

View file

@ -1,19 +1,26 @@
# Copyright (c) 2021 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import enum
from datetime import datetime
import json
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, pyqtEnum
from typing import Any, Optional, Dict, TYPE_CHECKING, Callable
from PyQt6.QtNetwork import QNetworkRequest
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
from UM.Decorators import deprecated
from UM.Logger import Logger
from UM.Message import Message
from UM.i18n import i18nCatalog
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings, UserProfile
from cura.UltimakerCloud import UltimakerCloudConstants
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
from PyQt6.QtNetwork import QNetworkReply
i18n_catalog = i18nCatalog("cura")
@ -78,6 +85,7 @@ class Account(QObject):
self._logged_in = False
self._user_profile: Optional[UserProfile] = None
self._additional_rights: Dict[str, Any] = {}
self._permissions: List[str] = [] # List of account permission keys, e.g. ["digital-factory.print-job.write"]
self._sync_state = SyncState.IDLE
self._manual_sync_enabled = False
self._update_packages_enabled = False
@ -109,6 +117,7 @@ class Account(QObject):
self._sync_services: Dict[str, int] = {}
"""contains entries "service_name" : SyncState"""
self.syncRequested.connect(self._updatePermissions)
def initialize(self) -> None:
self._authorization_service.initialize(self._application.getPreferences())
@ -311,13 +320,63 @@ class Account(QObject):
self._authorization_service.deleteAuthData()
@deprecated("Get permissions from the 'permissions' property", since = "5.2.0")
def updateAdditionalRight(self, **kwargs) -> None:
"""Update the additional rights of the account.
The argument(s) are the rights that need to be set"""
self._additional_rights.update(kwargs)
self.additionalRightsChanged.emit(self._additional_rights)
@deprecated("Get permissions from the 'permissions' property", since = "5.2.0")
@pyqtProperty("QVariantMap", notify = additionalRightsChanged)
def additionalRights(self) -> Dict[str, Any]:
"""A dictionary which can be queried for additional account rights."""
return self._additional_rights
permissionsChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = permissionsChanged)
def permissions(self) -> List[str]:
"""
The permission keys that the user has in his account.
"""
return self._permissions
def _updatePermissions(self) -> None:
"""
Update the list of permissions that the user has.
"""
def callback(reply: "QNetworkReply"):
status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
if status_code is None:
Logger.error("Server did not respond to request to get list of permissions.")
return
if status_code >= 300:
Logger.error(f"Request to get list of permission resulted in HTTP error {status_code}")
return
try:
reply_data = json.loads(bytes(reply.readAll()).decode("UTF-8"))
except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as e:
Logger.logException("e", f"Could not parse response to permission list request: {e}")
return
if "errors" in reply_data:
Logger.error(f"Request to get list of permission resulted in error response: {reply_data['errors']}")
return
if "data" in reply_data and "permissions" in reply_data["data"]:
permissions = sorted(reply_data["data"]["permissions"])
if permissions != self._permissions:
self._permissions = permissions
self.permissionsChanged.emit()
def error_callback(reply: "QNetworkReply", error: "QNetworkReply.NetworkError"):
Logger.error(f"Request for user permissions list failed. Network error: {error}")
HttpRequestManager.getInstance().get(
url = f"{self._oauth_root}/users/permissions",
scope = JsonDecoratorScope(UltimakerCloudScope(self._application)),
callback = callback,
error_callback = error_callback,
timeout = 10
)

View file

@ -6,14 +6,22 @@
# ---------
DEFAULT_CURA_APP_NAME = "cura"
DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
DEFAULT_CURA_VERSION = "master"
DEFAULT_CURA_VERSION = "dev"
DEFAULT_CURA_BUILD_TYPE = ""
DEFAULT_CURA_DEBUG_MODE = False
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
# 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.
CuraSDKVersion = "8.0.0"
CuraSDKVersion = "8.2.0"
try:
from cura.CuraVersion import CuraLatestURL
if CuraLatestURL == "":
CuraLatestURL = DEFAULT_CURA_LATEST_URL
except ImportError:
CuraLatestURL = DEFAULT_CURA_LATEST_URL
try:
from cura.CuraVersion import CuraAppName # type: ignore
@ -60,3 +68,14 @@ try:
except ImportError:
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
DEPENDENCY_INFO = {}
try:
from pathlib import Path
conan_install_info = Path(__file__).parent.parent.joinpath("conan_install_info.json")
if conan_install_info.exists():
import json
with open(conan_install_info, "r") as f:
DEPENDENCY_INFO = json.loads(f.read())
except:
pass

View file

@ -21,6 +21,7 @@ class ArrangeObjectsJob(Job):
self._min_offset = min_offset
def run(self):
found_solution_for_all = False
status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"),
lifetime = 0,
dismissable = False,
@ -28,18 +29,19 @@ class ArrangeObjectsJob(Job):
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
status_message.show()
found_solution_for_all = None
try:
found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes)
except: # If the thread crashes, the message should still close
Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.")
status_message.hide()
if found_solution_for_all is not None and not found_solution_for_all:
if not found_solution_for_all:
no_full_solution_message = Message(
i18n_catalog.i18nc("@info:status",
"Unable to find a location within the build volume for all objects"),
title = i18n_catalog.i18nc("@info:title", "Can't Find Location"),
message_type = Message.MessageType.ERROR)
no_full_solution_message.show()
self.finished.emit(self)

View file

@ -74,14 +74,14 @@ class ShapeArray:
# If the child-nodes are included, adjust convex hulls as well:
if include_children:
children = node.getAllChildren()
if not children is None:
if children is not None:
for child in children:
# 'Inefficient' combination of convex hulls through known code rather than mess it up:
child_hull = child.callDecoration("getConvexHull")
if not child_hull is None:
if child_hull is not None:
hull_verts = hull_verts.unionConvexHulls(child_hull)
child_hull_head = child.callDecoration("getConvexHullHead") or child_hull
if not child_hull_head is None:
if child_hull_head is not None:
hull_head_verts = hull_head_verts.unionConvexHulls(child_hull_head)
offset_verts = hull_head_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
@ -159,4 +159,4 @@ class ShapeArray:
max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1]
sign = numpy.sign(p2[0] - p1[0])
return idxs[1] * sign <= max_col_idx * sign
return idxs[1] * sign <= max_col_idx * sign

View file

@ -136,7 +136,7 @@ class Backup:
return False
current_version = Version(self._application.getVersion())
version_to_restore = Version(self.meta_data.get("cura_release", "master"))
version_to_restore = Version(self.meta_data.get("cura_release", "dev"))
if current_version < version_to_restore:
# Cannot restore version newer than current because settings might have changed.

View file

@ -565,8 +565,8 @@ class BuildVolume(SceneNode):
self._updateScaleFactor()
self._volume_aabb = AxisAlignedBox(
minimum = Vector(min_w, min_h - 1.0, min_d).scale(self._scale_vector),
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d).scale(self._scale_vector)
minimum = Vector(min_w, min_h - 1.0, min_d),
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d)
)
bed_adhesion_size = self.getEdgeDisallowedSize()
@ -575,8 +575,8 @@ class BuildVolume(SceneNode):
# This is probably wrong in all other cases. TODO!
# The +1 and -1 is added as there is always a bit of extra room required to work properly.
scale_to_max_bounds = AxisAlignedBox(
minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1).scale(self._scale_vector),
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1).scale(self._scale_vector)
minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1),
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1)
)
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds # type: ignore
@ -645,7 +645,7 @@ class BuildVolume(SceneNode):
for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingPropertyChanged)
self._width = self._global_container_stack.getProperty("machine_width", "value") * self._scale_vector.x
self._width = self._global_container_stack.getProperty("machine_width", "value")
machine_height = self._global_container_stack.getProperty("machine_height", "value")
if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
self._height = min(self._global_container_stack.getProperty("gantry_height", "value") * self._scale_vector.z, machine_height)
@ -656,7 +656,7 @@ class BuildVolume(SceneNode):
else:
self._height = self._global_container_stack.getProperty("machine_height", "value")
self._build_volume_message.hide()
self._depth = self._global_container_stack.getProperty("machine_depth", "value") * self._scale_vector.y
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
self._updateDisallowedAreas()
@ -752,8 +752,8 @@ class BuildVolume(SceneNode):
return
self._updateScaleFactor()
self._height = self._global_container_stack.getProperty("machine_height", "value") * self._scale_vector.z
self._width = self._global_container_stack.getProperty("machine_width", "value") * self._scale_vector.x
self._depth = self._global_container_stack.getProperty("machine_depth", "value") * self._scale_vector.y
self._width = self._global_container_stack.getProperty("machine_width", "value")
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
def _updateDisallowedAreasAndRebuild(self):
@ -770,14 +770,6 @@ class BuildVolume(SceneNode):
self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
self.rebuild()
def _scaleAreas(self, result_areas: List[Polygon]) -> None:
if self._global_container_stack is None:
return
for i, polygon in enumerate(result_areas):
result_areas[i] = polygon.scale(
100.0 / max(100.0, self._global_container_stack.getProperty("material_shrinkage_percentage_xy", "value"))
)
def _updateDisallowedAreas(self) -> None:
if not self._global_container_stack:
return
@ -818,11 +810,6 @@ class BuildVolume(SceneNode):
break
if prime_tower_collision: # Already found a collision.
break
if self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
brim_size = self._calculateBedAdhesionSize(used_extruders, "brim")
# Use 2x the brim size, since we need 1x brim size distance due to the object brim and another
# times the brim due to the brim of the prime tower
prime_tower_areas[extruder_id][area_index] = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(2 * brim_size, num_segments = 24))
if not prime_tower_collision:
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
@ -833,11 +820,9 @@ class BuildVolume(SceneNode):
self._disallowed_areas = []
for extruder_id in result_areas:
self._scaleAreas(result_areas[extruder_id])
self._disallowed_areas.extend(result_areas[extruder_id])
self._disallowed_areas_no_brim = []
for extruder_id in result_areas_no_brim:
self._scaleAreas(result_areas_no_brim[extruder_id])
self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
def _computeDisallowedAreasPrinted(self, used_extruders):
@ -850,9 +835,13 @@ class BuildVolume(SceneNode):
result = {}
skirt_brim_extruder: ExtruderStack = None
skirt_brim_extruder_nr = self._global_container_stack.getProperty("skirt_brim_extruder_nr", "value")
for extruder in used_extruders:
if int(extruder.getProperty("extruder_nr", "value")) == int(self._global_container_stack.getProperty("skirt_brim_extruder_nr", "value")):
skirt_brim_extruder = extruder
if skirt_brim_extruder_nr == -1:
skirt_brim_extruder = used_extruders[0] # The prime tower brim is always printed with the first extruder
elif int(extruder.getProperty("extruder_nr", "value")) == int(skirt_brim_extruder_nr):
skirt_brim_extruder = extruder
result[extruder.getId()] = []
# Currently, the only normally printed object is the prime tower.
@ -866,15 +855,6 @@ class BuildVolume(SceneNode):
prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
prime_tower_y = prime_tower_y + machine_depth / 2
if skirt_brim_extruder is not None and self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
brim_size = (
skirt_brim_extruder.getProperty("brim_line_count", "value") *
skirt_brim_extruder.getProperty("skirt_brim_line_width", "value") / 100.0 *
skirt_brim_extruder.getProperty("initial_layer_line_width_factor", "value")
)
prime_tower_x -= brim_size
prime_tower_y += brim_size
radius = prime_tower_size / 2
prime_tower_area = Polygon.approximatedCircle(radius, num_segments = 24)
prime_tower_area = prime_tower_area.translate(prime_tower_x - radius, prime_tower_y - radius)
@ -993,6 +973,9 @@ class BuildVolume(SceneNode):
half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
# We need at a minimum a very small border around the edge so that models can't go off the build plate
border_size = max(border_size, 0.1)
if self._shape != "elliptic":
if border_size - left_unreachable_border > 0:
result[extruder_id].append(Polygon(numpy.array([
@ -1083,7 +1066,7 @@ class BuildVolume(SceneNode):
all_values[i] = 0
return all_values
def _calculateBedAdhesionSize(self, used_extruders, adhesion_override = None):
def _calculateBedAdhesionSize(self, used_extruders):
"""Get the bed adhesion size for the global container stack and used extruders
:param adhesion_override: override adhesion type.
@ -1093,52 +1076,12 @@ class BuildVolume(SceneNode):
return None
container_stack = self._global_container_stack
adhesion_type = adhesion_override
if adhesion_type is None:
adhesion_type = container_stack.getProperty("adhesion_type", "value")
adhesion_type = container_stack.getProperty("adhesion_type", "value")
# Skirt_brim_line_width is a bit of an odd one out. The primary bit of the skirt/brim is printed
# with the adhesion extruder, but it also prints one extra line by all other extruders. As such, the
# setting does *not* have a limit_to_extruder setting (which means that we can't ask the global extruder what
# the value is.
skirt_brim_extruder_nr = self._global_container_stack.getProperty("skirt_brim_extruder_nr", "value")
try:
skirt_brim_stack = self._global_container_stack.extruderList[int(skirt_brim_extruder_nr)]
except IndexError:
Logger.warning(f"Couldn't find extruder with index '{skirt_brim_extruder_nr}', defaulting to 0 instead.")
skirt_brim_stack = self._global_container_stack.extruderList[0]
skirt_brim_line_width = skirt_brim_stack.getProperty("skirt_brim_line_width", "value")
initial_layer_line_width_factor = skirt_brim_stack.getProperty("initial_layer_line_width_factor", "value")
# Use brim width if brim is enabled OR the prime tower has a brim.
if adhesion_type == "brim":
brim_line_count = skirt_brim_stack.getProperty("brim_line_count", "value")
brim_gap = skirt_brim_stack.getProperty("brim_gap", "value")
bed_adhesion_size = brim_gap + skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
for extruder_stack in used_extruders:
bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0
# We don't create an additional line for the extruder we're printing the brim with.
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
elif adhesion_type == "skirt":
skirt_distance = skirt_brim_stack.getProperty("skirt_gap", "value")
skirt_line_count = skirt_brim_stack.getProperty("skirt_line_count", "value")
bed_adhesion_size = skirt_distance + (
skirt_brim_line_width * skirt_line_count) * initial_layer_line_width_factor / 100.0
for extruder_stack in used_extruders:
bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0
# We don't create an additional line for the extruder we're printing the skirt with.
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
elif adhesion_type == "raft":
if adhesion_type == "raft":
bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value") # Should refer to the raft extruder if set.
elif adhesion_type == "none":
else: # raft, brim or skirt. Those last two are handled by CuraEngine.
bed_adhesion_size = 0
else:
raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")
max_length_available = 0.5 * min(
self._global_container_stack.getProperty("machine_width", "value"),

View file

@ -261,7 +261,7 @@ class CrashHandler:
opengl_instance = OpenGL.getInstance()
if not opengl_instance:
self.data["opengl"] = {"version": "n/a", "vendor": "n/a", "type": "n/a"}
return catalog.i18nc("@label", "Not yet initialized<br/>")
return catalog.i18nc("@label", "Not yet initialized") + "<br />"
info = "<ul>"
info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = opengl_instance.getOpenGLVersion())
@ -291,6 +291,7 @@ class CrashHandler:
if with_sentry_sdk:
with configure_scope() as scope:
scope.set_tag("opengl_version", opengl_instance.getOpenGLVersion())
scope.set_tag("opengl_version_short", opengl_instance.getOpenGLVersionShort())
scope.set_tag("gpu_vendor", opengl_instance.getGPUVendorName())
scope.set_tag("gpu_type", opengl_instance.getGPUType())
scope.set_tag("active_machine", active_machine_definition_id)

View file

@ -115,6 +115,10 @@ from . import CuraActions
from . import PlatformPhysics
from . import PrintJobPreviewImageProvider
from .AutoSave import AutoSave
from .Machines.Models.CompatibleMachineModel import CompatibleMachineModel
from .Machines.Models.MachineListModel import MachineListModel
from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel
from .Machines.Models.IntentSelectionModel import IntentSelectionModel
from .SingleInstance import SingleInstance
if TYPE_CHECKING:
@ -150,6 +154,7 @@ class CuraApplication(QtApplication):
super().__init__(name = ApplicationMetadata.CuraAppName,
app_display_name = ApplicationMetadata.CuraAppDisplayName,
version = ApplicationMetadata.CuraVersion if not ApplicationMetadata.IsAlternateVersion else ApplicationMetadata.CuraBuildType,
latest_url = ApplicationMetadata.CuraLatestURL,
api_version = ApplicationMetadata.CuraSDKVersion,
build_type = ApplicationMetadata.CuraBuildType,
is_debug_mode = ApplicationMetadata.CuraDebugMode,
@ -257,6 +262,7 @@ class CuraApplication(QtApplication):
from UM.CentralFileStorage import CentralFileStorage
CentralFileStorage.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
Resources.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
@pyqtProperty(str, constant=True)
def ultimakerCloudApiRootUrl(self) -> str:
@ -315,7 +321,7 @@ class CuraApplication(QtApplication):
def initialize(self) -> None:
self.__addExpectedResourceDirsAndSearchPaths() # Must be added before init of super
super().initialize()
super().initialize(ApplicationMetadata.IsEnterpriseVersion)
self._preferences.addPreference("cura/single_instance", False)
self._use_single_instance = self._preferences.getValue("cura/single_instance") or self._cli_args.single_instance
@ -348,13 +354,18 @@ class CuraApplication(QtApplication):
Resources.addExpectedDirNameInData(dir_name)
app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable)))
Resources.addSearchPath(os.path.join(app_root, "share", "cura", "resources"))
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "share", "cura", "resources"))
Resources.addSecureSearchPath(os.path.join(app_root, "share", "cura", "resources"))
Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
Resources.addSecureSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
if not hasattr(sys, "frozen"):
resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
Resources.addSearchPath(resource_path)
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources"))
# local Conan cache
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "..", "resources"))
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "..", "plugins"))
# venv site-packages
Resources.addSearchPath(os.path.join(app_root, "..", "share", "cura", "resources"))
@classmethod
def _initializeSettingDefinitions(cls):
@ -812,6 +823,12 @@ class CuraApplication(QtApplication):
def run(self):
super().run()
if len(ApplicationMetadata.DEPENDENCY_INFO) > 0:
Logger.debug("Using Conan managed dependencies: " + ", ".join(
[dep["recipe"]["id"] for dep in ApplicationMetadata.DEPENDENCY_INFO["installed"] if dep["recipe"]["version"] != "latest"]))
else:
Logger.warning("Could not find conan_install_info.json")
Logger.log("i", "Initializing machine error checker")
self._machine_error_checker = MachineErrorChecker(self)
self._machine_error_checker.initialize()
@ -943,6 +960,7 @@ class CuraApplication(QtApplication):
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing engine..."))
self.initializeEngine()
self.getTheme().setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion)
# Initialize UI state
controller.setActiveStage("PrepareStage")
@ -1173,6 +1191,8 @@ class CuraApplication(QtApplication):
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
qmlRegisterType(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel")
qmlRegisterType(MachineListModel, "Cura", 1, 0, "MachineListModel")
qmlRegisterType(CompatibleMachineModel, "Cura", 1, 0, "CompatibleMachineModel")
self.processEvents()
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
@ -1191,6 +1211,8 @@ class CuraApplication(QtApplication):
qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
qmlRegisterType(IntentModel, "Cura", 1, 6, "IntentModel")
qmlRegisterType(IntentCategoryModel, "Cura", 1, 6, "IntentCategoryModel")
qmlRegisterType(IntentSelectionModel, "Cura", 1, 7, "IntentSelectionModel")
qmlRegisterType(ActiveIntentQualitiesModel, "Cura", 1, 7, "ActiveIntentQualitiesModel")
self.processEvents()
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
@ -1425,7 +1447,7 @@ class CuraApplication(QtApplication):
bounding_box = node.getBoundingBox()
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
# Arrange only the unlocked nodes and keep the locked ones in place
if UM.Util.parseBool(node.getSetting(SceneNodeSettings.LockPosition)):
if node.getSetting(SceneNodeSettings.LockPosition):
locked_nodes.append(node)
else:
nodes_to_arrange.append(node)

View file

@ -1,14 +1,21 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import glob
import os
from pathlib import Path
from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from cura.CuraApplication import CuraApplication # To find some resource types.
from cura.Settings.GlobalStack import GlobalStack
from UM.PackageManager import PackageManager # The class we're extending.
from UM.Resources import Resources # To find storage paths for some resource types.
from UM.i18n import i18nCatalog
from urllib.parse import unquote_plus
catalog = i18nCatalog("cura")
if TYPE_CHECKING:
@ -48,9 +55,62 @@ class CuraPackageManager(PackageManager):
def initialize(self) -> None:
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer)
self._installation_dirs_dict["variants"] = Resources.getStoragePath(CuraApplication.ResourceTypes.VariantInstanceContainer)
# Due to a bug in Cura 5.1.0 we needed to change the directory structure of the curapackage on the server side (See SD-3871).
# Although the material intent profiles will be installed in the `intent` folder, the curapackage from the server side will
# have an `intents` folder. For completeness, we will look in both locations of in the curapackage and map them both to the
# `intent` folder.
self._installation_dirs_dict["intents"] = Resources.getStoragePath(CuraApplication.ResourceTypes.IntentInstanceContainer)
self._installation_dirs_dict["intent"] = Resources.getStoragePath(CuraApplication.ResourceTypes.IntentInstanceContainer)
super().initialize()
def isMaterialBundled(self, file_name: str, guid: str):
""" Check if there is a bundled material name with file_name and guid """
for path in Resources.getSecureSearchPaths():
# Secure search paths are install directory paths, if a material is in here it must be bundled.
paths = [Path(p) for p in glob.glob(path + '/**/*.xml.fdm_material', recursive=True)]
for material in paths:
if material.name == file_name:
Logger.info(f"Found bundled material: {material.name}. Located in path: {str(material)}")
with open(material, encoding="utf-8") as f:
# Make sure the file we found has the same guid as our material
# Parsing this xml would be better but the namespace is needed to search it.
parsed_guid = PluginRegistry.getInstance().getPluginObject(
"XmlMaterialProfile").getMetadataFromSerialized(
f.read(), "GUID")
if guid == parsed_guid:
# The material we found matches both filename and GUID
return True
return False
def getMaterialFilePackageId(self, file_name: str, guid: str) -> str:
"""Get the id of the installed material package that contains file_name"""
file_name = unquote_plus(file_name)
for material_package in [f for f in os.scandir(self._installation_dirs_dict["materials"]) if f.is_dir()]:
package_id = material_package.name
for root, _, file_names in os.walk(material_package.path):
if file_name not in file_names:
# File with the name we are looking for is not in this directory
continue
with open(os.path.join(root, file_name), encoding="utf-8") as f:
# Make sure the file we found has the same guid as our material
# Parsing this xml would be better but the namespace is needed to search it.
parsed_guid = PluginRegistry.getInstance().getPluginObject("XmlMaterialProfile").getMetadataFromSerialized(
f.read(), "GUID")
if guid == parsed_guid:
return package_id
Logger.error("Could not find package_id for file: {} with GUID: {} ".format(file_name, guid))
Logger.error(f"Bundled paths searched: {list(Resources.getSecureSearchPaths())}")
return ""
def getMachinesUsingPackage(self, package_id: str) -> Tuple[List[Tuple[GlobalStack, str, str]], List[Tuple[GlobalStack, str, str]]]:
"""Returns a list of where the package is used

View file

@ -1,13 +0,0 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
CuraAppName = "@CURA_APP_NAME@"
CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@"
CuraVersion = "@CURA_VERSION@"
CuraBuildType = "@CURA_BUILDTYPE@"
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
CuraMarketplaceRoot = "@CURA_MARKETPLACE_ROOT@"
CuraDigitalFactoryURL = "@CURA_DIGITAL_FACTORY_URL@"

View file

@ -24,9 +24,12 @@ class LayerPolygon:
PrimeTowerType = 11
__number_of_types = 12
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType,
numpy.arange(__number_of_types) == MoveCombingType),
numpy.arange(__number_of_types) == MoveRetractionType)
def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray, line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None:
def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray,
line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None:
"""LayerPolygon, used in ProcessSlicedLayersJob
:param extruder: The position of the extruder
@ -39,10 +42,12 @@ class LayerPolygon:
self._extruder = extruder
self._types = line_types
for i in range(len(self._types)):
if self._types[i] >= self.__number_of_types: # Got faulty line data from the engine.
Logger.log("w", "Found an unknown line type: %s", i)
self._types[i] = self.NoneType
unknown_types = numpy.where(self._types >= self.__number_of_types, self._types, None)
if unknown_types.any():
# Got faulty line data from the engine.
for idx in unknown_types:
Logger.warning(f"Found an unknown line type at: {idx}")
self._types[idx] = self.NoneType
self._data = data
self._line_widths = line_widths
self._line_thicknesses = line_thicknesses
@ -58,14 +63,16 @@ class LayerPolygon:
self._mesh_line_count = len(self._types) - self._jump_count
self._vertex_count = self._mesh_line_count + numpy.sum(self._types[1:] == self._types[:-1])
# Buffering the colors shouldn't be necessary as it is not
# Buffering the colors shouldn't be necessary as it is not
# re-used and can save a lot of memory usage.
self._color_map = LayerPolygon.getColorMap()
self._colors = self._color_map[self._types] # type: numpy.ndarray
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
# When type is used as index returns true if type == LayerPolygon.InfillType
# or type == LayerPolygon.SkinType
# or type == LayerPolygon.SupportInfillType
# Should be generated in better way, not hardcoded.
self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = bool)
self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype=bool)
self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
@ -80,12 +87,14 @@ class LayerPolygon:
# Only if the type of line segment changes do we need to add an extra vertex to change colors
self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
# Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask
numpy.logical_and(self._build_cache_needed_points, self._build_cache_line_mesh_mask, self._build_cache_needed_points )
numpy.logical_and(self._build_cache_needed_points, self._build_cache_line_mesh_mask, self._build_cache_needed_points)
self._vertex_begin = 0
self._vertex_end = cast(int, numpy.sum(self._build_cache_needed_points))
def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray, colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray, extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None:
def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray,
colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray,
extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None:
"""Set all the arrays provided by the function caller, representing the LayerPolygon
The arrays are either by vertex or by indices.
@ -111,19 +120,20 @@ class LayerPolygon:
line_mesh_mask = self._build_cache_line_mesh_mask
needed_points_list = self._build_cache_needed_points
# Index to the points we need to represent the line mesh. This is constructed by generating simple
# start and end points for each line. For line segment n these are points n and n+1. Row n reads [n n+1]
# Then then the indices for the points we don't need are thrown away based on the pre-calculated list.
index_list = ( numpy.arange(len(self._types)).reshape((-1, 1)) + numpy.array([[0, 1]]) ).reshape((-1, 1))[needed_points_list.reshape((-1, 1))]
# Index to the points we need to represent the line mesh.
# This is constructed by generating simple start and end points for each line.
# For line segment n, these are points n and n+1. Row n reads [n n+1]
# Then the indices for the points we don't need are thrown away based on the pre-calculated list.
index_list = (numpy.arange(len(self._types)).reshape((-1, 1)) + numpy.array([[0, 1]])).reshape((-1, 1))[needed_points_list.reshape((-1, 1))]
# The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset.
self._vertex_begin += vertex_offset
self._vertex_end += vertex_offset
# Points are picked based on the index list to get the vertices needed.
# Points are picked based on the index list to get the vertices needed.
vertices[self._vertex_begin:self._vertex_end, :] = self._data[index_list, :]
# Create an array with colors for each vertex and remove the color data for the points that has been thrown away.
# Create an array with colors for each vertex and remove the color data for the points that has been thrown away.
colors[self._vertex_begin:self._vertex_end, :] = numpy.tile(self._colors, (1, 2)).reshape((-1, 4))[needed_points_list.ravel()]
# Create an array with line widths and thicknesses for each vertex.
@ -138,14 +148,15 @@ class LayerPolygon:
# Convert type per vertex to type per line
line_types[self._vertex_begin:self._vertex_end] = numpy.tile(self._types, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
# The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset.
# The relative values of begin and end indices have already been set in buildCache,
# so we only need to offset them to the parents offset.
self._index_begin += index_offset
self._index_end += index_offset
indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype = numpy.int32).reshape((-1, 1))
indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype=numpy.int32).reshape((-1, 1))
# When the line type changes the index needs to be increased by 2.
indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1))
# Each line segment goes from it's starting point p to p+1, offset by the vertex index.
# Each line segment goes from it's starting point p to p+1, offset by the vertex index.
# The -1 is to compensate for the necessarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin])
@ -214,13 +225,12 @@ class LayerPolygon:
"""
normals = numpy.copy(self._data)
normals[:, 1] = 0.0 # We are only interested in 2D normals
normals[:, 1] = 0.0 # We are only interested in 2D normals
# Calculate the edges between points.
# The call to numpy.roll shifts the entire array by one so that
# we end up subtracting each next point from the current, wrapping
# around. This gives us the edges from the next point to the current
# point.
# The call to numpy.roll shifts the entire array by one
# so that we end up subtracting each next point from the current, wrapping around.
# This gives us the edges from the next point to the current point.
normals = numpy.diff(normals, 1, 0)
# Calculate the length of each edge using standard Pythagoras
@ -245,17 +255,17 @@ class LayerPolygon:
if cls.__color_map is None:
theme = cast(Theme, QtApplication.getInstance().getTheme())
cls.__color_map = numpy.array([
theme.getColor("layerview_none").getRgbF(), # NoneType
theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type
theme.getColor("layerview_inset_x").getRgbF(), # InsetXType
theme.getColor("layerview_skin").getRgbF(), # SkinType
theme.getColor("layerview_support").getRgbF(), # SupportType
theme.getColor("layerview_skirt").getRgbF(), # SkirtType
theme.getColor("layerview_infill").getRgbF(), # InfillType
theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
theme.getColor("layerview_none").getRgbF(), # NoneType
theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type
theme.getColor("layerview_inset_x").getRgbF(), # InsetXType
theme.getColor("layerview_skin").getRgbF(), # SkinType
theme.getColor("layerview_support").getRgbF(), # SupportType
theme.getColor("layerview_skirt").getRgbF(), # SkirtType
theme.getColor("layerview_infill").getRgbF(), # InfillType
theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
theme.getColor("layerview_prime_tower").getRgbF() # PrimeTowerType
])

View file

@ -33,8 +33,11 @@ class MachineAction(QObject, PluginObject):
self._qml_url = ""
self._view = None
self._finished = False
self._open_as_dialog = True
self._visible = True
labelChanged = pyqtSignal()
visibilityChanged = pyqtSignal()
onFinished = pyqtSignal()
def getKey(self) -> str:
@ -79,6 +82,15 @@ class MachineAction(QObject, PluginObject):
pass
@pyqtSlot()
def execute(self) -> None:
self._execute()
def _execute(self) -> None:
"""Protected implementation of execute."""
pass
@pyqtSlot()
def setFinished(self) -> None:
self._finished = True
@ -94,7 +106,7 @@ class MachineAction(QObject, PluginObject):
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
if plugin_path is None:
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
Logger.error(f"Cannot create QML view: cannot find plugin path for plugin {self.getPluginId()}")
return None
path = os.path.join(plugin_path, self._qml_url)
@ -106,7 +118,7 @@ class MachineAction(QObject, PluginObject):
def qmlPath(self) -> "QUrl":
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
if plugin_path is None:
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
Logger.error(f"Cannot create QML view: cannot find plugin path for plugin {self.getPluginId()}")
return QUrl("")
path = os.path.join(plugin_path, self._qml_url)
return QUrl.fromLocalFile(path)
@ -114,3 +126,30 @@ class MachineAction(QObject, PluginObject):
@pyqtSlot(result = QObject)
def getDisplayItem(self) -> Optional["QObject"]:
return self._createViewFromQML()
@pyqtProperty(bool, constant=True)
def shouldOpenAsDialog(self) -> bool:
"""Whether this action will show a dialog.
If not, the action will directly run the function inside execute().
:return: Defaults to true to be in line with the old behaviour.
"""
return self._open_as_dialog
@pyqtSlot()
def setVisible(self, visible: bool) -> None:
if self._visible != visible:
self._visible = visible
self.visibilityChanged.emit()
@pyqtProperty(bool, notify = visibilityChanged)
def visible(self) -> bool:
"""Whether this action button will be visible.
Example: Show only when isLoggedIn
:return: Defaults to true to be in line with the old behaviour.
"""
return self._visible

View file

@ -43,13 +43,9 @@ class MachineErrorChecker(QObject):
self._application = cura.CuraApplication.CuraApplication.getInstance()
self._machine_manager = self._application.getMachineManager()
self._start_time = 0. # measure checking time
self._check_start_time = time.time()
# This timer delays the starting of error check so we can react less frequently if the user is frequently
# changing settings.
self._error_check_timer = QTimer(self)
self._error_check_timer.setInterval(100)
self._error_check_timer.setSingleShot(True)
self._setCheckTimer()
self._keys_to_check = set() # type: Set[str]
@ -66,6 +62,18 @@ class MachineErrorChecker(QObject):
self._onMachineChanged()
def _setCheckTimer(self) -> None:
"""A QTimer to regulate error check frequency
This timer delays the starting of error check
so we can react less frequently if the user is frequently
changing settings.
"""
self._error_check_timer = QTimer(self)
self._error_check_timer.setInterval(100)
self._error_check_timer.setSingleShot(True)
def _onMachineChanged(self) -> None:
if self._global_stack:
self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
@ -152,7 +160,7 @@ class MachineErrorChecker(QObject):
self._stacks_and_keys_to_check.append((stack, key))
self._application.callLater(self._checkStack)
self._start_time = time.time()
self._check_start_time = time.time()
Logger.log("d", "New error check scheduled.")
def _checkStack(self) -> None:
@ -194,7 +202,7 @@ class MachineErrorChecker(QObject):
keys_to_recheck = {setting_key for stack, setting_key in self._stacks_and_keys_to_check}
keys_to_recheck.add(key)
self._setResult(True, keys_to_recheck = keys_to_recheck)
continue
return
# Schedule the check for the next key
self._application.callLater(self._checkStack)
@ -204,12 +212,10 @@ class MachineErrorChecker(QObject):
self._has_errors = result
self.hasErrorUpdated.emit()
self._machine_manager.stacksValidationChanged.emit()
if keys_to_recheck is None:
self._keys_to_check = set()
else:
self._keys_to_check = keys_to_recheck
self._keys_to_check = keys_to_recheck if keys_to_recheck else set()
self._need_to_check = False
self._check_in_progress = False
self.needToWaitForResultChanged.emit()
self.errorCheckFinished.emit()
Logger.log("i", "Error check finished, result = %s, time = %0.1fs", result, time.time() - self._start_time)
execution_time = time.time() - self._check_start_time
Logger.info(f"Error check finished, result = {result}, time = {execution_time:.2f}s")

View file

@ -0,0 +1,131 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Set, Dict, List, Any
from PyQt6.QtCore import Qt, QObject, QTimer
import cura.CuraApplication
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerStack import ContainerStack
from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.Models.MachineModelUtils import fetchLayerHeight
from cura.Machines.MaterialNode import MaterialNode
from cura.Machines.QualityGroup import QualityGroup
from cura.Settings.IntentManager import IntentManager
class ActiveIntentQualitiesModel(ListModel):
NameRole = Qt.ItemDataRole.UserRole + 1
DisplayTextRole = Qt.ItemDataRole.UserRole + 2
QualityTypeRole = Qt.ItemDataRole.UserRole + 3
LayerHeightRole = Qt.ItemDataRole.UserRole + 4
IntentCategeoryRole = Qt.ItemDataRole.UserRole + 5
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.QualityTypeRole, "quality_type")
self.addRoleName(self.LayerHeightRole, "layer_height")
self.addRoleName(self.DisplayTextRole, "display_text")
self.addRoleName(self.IntentCategeoryRole, "intent_category")
self._intent_category = ""
IntentManager.intentCategoryChangedSignal.connect(self._update)
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
machine_manager.activeQualityGroupChanged.connect(self._update)
machine_manager.globalContainerChanged.connect(self._updateDelayed)
machine_manager.extruderChanged.connect(self._updateDelayed) # We also need to update if an extruder gets disabled
self._update_timer = QTimer()
self._update_timer.setInterval(100)
self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._update)
self._update()
def _updateDelayed(self):
self._update_timer.start()
def _onChanged(self, container: ContainerStack) -> None:
if container.getMetaDataEntry("type") == "intent":
self._updateDelayed()
def _update(self):
active_extruder_stack = cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeStack
if active_extruder_stack:
self._intent_category = active_extruder_stack.intent.getMetaDataEntry("intent_category", "")
new_items: List[Dict[str, Any]] = []
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack:
self.setItems(new_items)
return
quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
material_nodes = self._getActiveMaterials()
added_quality_type_set: Set[str] = set()
for material_node in material_nodes:
intents = self._getIntentsForMaterial(material_node, quality_groups)
for intent in intents:
if intent["quality_type"] not in added_quality_type_set:
new_items.append(intent)
added_quality_type_set.add(intent["quality_type"])
new_items = sorted(new_items, key=lambda x: x["layer_height"])
self.setItems(new_items)
def _getActiveMaterials(self) -> Set["MaterialNode"]:
"""Get the active materials for all extruders. No duplicates will be returned"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return set()
container_tree = ContainerTree.getInstance()
machine_node = container_tree.machines[global_stack.definition.getId()]
nodes: Set[MaterialNode] = set()
for extruder in global_stack.extruderList:
active_variant_name = extruder.variant.getMetaDataEntry("name")
if active_variant_name not in machine_node.variants:
Logger.log("w", "Could not find the variant %s", active_variant_name)
continue
active_variant_node = machine_node.variants[active_variant_name]
active_material_node = active_variant_node.materials.get(extruder.material.getMetaDataEntry("base_file"))
if active_material_node is None:
Logger.log("w", "Could not find the material %s", extruder.material.getMetaDataEntry("base_file"))
continue
nodes.add(active_material_node)
return nodes
def _getIntentsForMaterial(self, active_material_node: "MaterialNode", quality_groups: Dict[str, "QualityGroup"]) -> List[Dict[str, Any]]:
extruder_intents: List[Dict[str, Any]] = []
for quality_id, quality_node in active_material_node.qualities.items():
if quality_node.quality_type not in quality_groups: # Don't add the empty quality type (or anything else that would crash, defensively).
continue
quality_group = quality_groups[quality_node.quality_type]
if not quality_group.is_available:
continue
layer_height = fetchLayerHeight(quality_group)
for intent_id, intent_node in quality_node.intents.items():
if intent_node.intent_category != self._intent_category:
continue
extruder_intents.append({"name": quality_group.name,
"display_text": f"<b>{quality_group.name}</b> - {layer_height}mm",
"quality_type": quality_group.quality_type,
"layer_height": layer_height,
"intent_category": self._intent_category
})
return extruder_intents

View file

@ -0,0 +1,81 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from PyQt6.QtCore import Qt, QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.i18n import i18nCatalog
class CompatibleMachineModel(ListModel):
NameRole = Qt.ItemDataRole.UserRole + 1
UniqueIdRole = Qt.ItemDataRole.UserRole + 2
ExtrudersRole = Qt.ItemDataRole.UserRole + 3
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self._catalog = i18nCatalog("cura")
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.UniqueIdRole, "unique_id")
self.addRoleName(self.ExtrudersRole, "extruders")
self._update()
from cura.CuraApplication import CuraApplication
machine_manager = CuraApplication.getInstance().getMachineManager()
machine_manager.globalContainerChanged.connect(self._update)
machine_manager.outputDevicesChanged.connect(self._update)
@pyqtSlot()
def forceUpdate(self):
self._update()
def _update(self) -> None:
self.clear()
def _makeMaterial(brand, name, color):
if name.lower() in ["", "empty"]:
return {"brand": "", "name": "(empty)", "hexcolor": "#ffffff"}
else:
return {"brand": brand, "name": name, "hexcolor": color}
from cura.CuraApplication import CuraApplication
machine_manager = CuraApplication.getInstance().getMachineManager()
# Loop over the output-devices, not the stacks; need all applicable configurations, not just the current loaded one.
for output_device in machine_manager.printerOutputDevices:
for printer in output_device.printers:
extruder_configs = dict()
# initialize & add current active material:
for extruder in printer.extruders:
materials = [_makeMaterial(
extruder.activeMaterial.brand, extruder.activeMaterial.name, extruder.activeMaterial.color)]
extruder_configs[extruder.getPosition()] = {
"position": extruder.getPosition(),
"core": extruder.hotendID,
"materials": materials
}
# add currently inactive, but possible materials:
for configuration in printer.availableConfigurations:
for extruder in configuration.extruderConfigurations:
if not extruder.position in extruder_configs:
Logger.log("w", f"No active extruder for position {extruder.position}.")
continue
entry = _makeMaterial(extruder.material.brand, extruder.material.name, extruder.material.color)
if entry not in extruder_configs[extruder.position]["materials"]:
extruder_configs[extruder.position]["materials"].append(entry)
if any([len(extruder["materials"]) > 0 for extruder in extruder_configs.values()]):
self.appendItem({
"name": printer.name,
"unique_id": printer.name, # <- Can assume the cloud doesn't have duplicate names?
"extruders": list(extruder_configs.values())
})

View file

@ -44,6 +44,7 @@ class GlobalStacksModel(ListModel):
self._filter_connection_type = None # type: Optional[ConnectionType]
self._filter_online_only = False
self._filter_capabilities: List[str] = [] # Required capabilities that all listed printers must have.
self._filter_abstract_machines: Optional[bool] = None
# Listen to changes
CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
@ -54,6 +55,7 @@ class GlobalStacksModel(ListModel):
filterConnectionTypeChanged = pyqtSignal()
filterCapabilitiesChanged = pyqtSignal()
filterOnlineOnlyChanged = pyqtSignal()
filterAbstractMachinesChanged = pyqtSignal()
def setFilterConnectionType(self, new_filter: Optional[ConnectionType]) -> None:
if self._filter_connection_type != new_filter:
@ -98,6 +100,22 @@ class GlobalStacksModel(ListModel):
"""
return self._filter_capabilities
def setFilterAbstractMachines(self, new_filter: Optional[bool]) -> None:
if self._filter_abstract_machines != new_filter:
self._filter_abstract_machines = new_filter
self.filterAbstractMachinesChanged.emit()
@pyqtProperty(bool, fset = setFilterAbstractMachines, notify = filterAbstractMachinesChanged)
def filterAbstractMachines(self) -> Optional[bool]:
"""
Weather we include abstract printers, non-abstract printers or both
if this is set to None both abstract and non-abstract printers will be included in the list
set to True will only include abstract printers
set to False will only inclde non-abstract printers
"""
return self._filter_abstract_machines
def _onContainerChanged(self, container) -> None:
"""Handler for container added/removed events from registry"""
@ -130,12 +148,16 @@ class GlobalStacksModel(ListModel):
if self._filter_online_only and not is_online:
continue
is_abstract_machine = parseBool(container_stack.getMetaDataEntry("is_abstract_machine", False))
if self._filter_abstract_machines is not None and self._filter_abstract_machines is not is_abstract_machine:
continue
capabilities = set(container_stack.getMetaDataEntry(META_CAPABILITIES, "").split(","))
if set(self._filter_capabilities) - capabilities: # Not all required capabilities are met.
continue
device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName())
section_name = "Connected printers" if has_remote_connection else "Preset printers"
section_name = self._catalog.i18nc("@label", "Connected printers") if has_remote_connection else self._catalog.i18nc("@label", "Preset printers")
section_name = self._catalog.i18nc("@info:title", section_name)
default_removal_warning = self._catalog.i18nc(

View file

@ -111,7 +111,7 @@ class IntentCategoryModel(ListModel):
except ValueError:
weight = 99
result.append({
"name": IntentCategoryModel.translation(category, "name", category),
"name": IntentCategoryModel.translation(category, "name", category.title()),
"description": IntentCategoryModel.translation(category, "description", None),
"intent_category": category,
"weight": weight,

View file

@ -0,0 +1,129 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import collections
from typing import OrderedDict, Optional
from PyQt6.QtCore import Qt, QTimer, QObject
import cura
from UM import i18nCatalog
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import ContainerInterface
from cura.Settings.IntentManager import IntentManager
catalog = i18nCatalog("cura")
class IntentSelectionModel(ListModel):
NameRole = Qt.ItemDataRole.UserRole + 1
IntentCategoryRole = Qt.ItemDataRole.UserRole + 2
WeightRole = Qt.ItemDataRole.UserRole + 3
DescriptionRole = Qt.ItemDataRole.UserRole + 4
IconRole = Qt.ItemDataRole.UserRole + 5
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.IntentCategoryRole, "intent_category")
self.addRoleName(self.WeightRole, "weight")
self.addRoleName(self.DescriptionRole, "description")
self.addRoleName(self.IconRole, "icon")
application = cura.CuraApplication.CuraApplication.getInstance()
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChange)
ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChange)
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
machine_manager.activeMaterialChanged.connect(self._update)
machine_manager.activeVariantChanged.connect(self._update)
machine_manager.extruderChanged.connect(self._update)
extruder_manager = application.getExtruderManager()
extruder_manager.extrudersChanged.connect(self._update)
self._update_timer: QTimer = QTimer()
self._update_timer.setInterval(100)
self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._update)
self._onChange()
@staticmethod
def _getDefaultProfileInformation() -> OrderedDict[str, dict]:
""" Default information user-visible string. Ordered by weight. """
default_profile_information = collections.OrderedDict()
default_profile_information["default"] = {
"name": catalog.i18nc("@label", "Default"),
"icon": "GearCheck"
}
default_profile_information["visual"] = {
"name": catalog.i18nc("@label", "Visual"),
"description": catalog.i18nc("@text", "The visual profile is designed to print visual prototypes and models with the intent of high visual and surface quality."),
"icon" : "Visual"
}
default_profile_information["engineering"] = {
"name": catalog.i18nc("@label", "Engineering"),
"description": catalog.i18nc("@text", "The engineering profile is designed to print functional prototypes and end-use parts with the intent of better accuracy and for closer tolerances."),
"icon": "Nut"
}
default_profile_information["quick"] = {
"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."),
"icon": "SpeedOMeter"
}
return default_profile_information
def _onContainerChange(self, container: ContainerInterface) -> None:
"""Updates the list of intents if an intent profile was added or removed."""
if container.getMetaDataEntry("type") == "intent":
self._update()
def _onChange(self) -> None:
self._update_timer.start()
def _update(self) -> None:
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
self.setItems([])
Logger.log("d", "No active GlobalStack, set quality profile model as empty.")
return
# Check for material compatibility
if not cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeMaterialsCompatible():
Logger.log("d", "No active material compatibility, set quality profile model as empty.")
self.setItems([])
return
default_profile_info = self._getDefaultProfileInformation()
available_categories = IntentManager.getInstance().currentAvailableIntentCategories()
result = []
for i, category in enumerate(available_categories):
profile_info = default_profile_info.get(category, {})
try:
weight = list(default_profile_info.keys()).index(category)
except ValueError:
weight = len(available_categories) + i
result.append({
"name": profile_info.get("name", category.title()),
"description": profile_info.get("description", None),
"icon" : profile_info.get("icon", ""),
"intent_category": category,
"weight": weight,
})
result.sort(key=lambda k: k["weight"])
self.setItems(result)

View file

@ -0,0 +1,144 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# The MachineListModel is used to display the connected printers in the interface. Both the abstract machines and all
# online cloud connected printers are represented within this ListModel. Additional information such as the number of
# connected printers for each printer type is gathered.
from typing import Optional
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.Interfaces import ContainerInterface
from UM.i18n import i18nCatalog
from UM.Util import parseBool
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.GlobalStack import GlobalStack
class MachineListModel(ListModel):
NameRole = Qt.ItemDataRole.UserRole + 1
IdRole = Qt.ItemDataRole.UserRole + 2
HasRemoteConnectionRole = Qt.ItemDataRole.UserRole + 3
MetaDataRole = Qt.ItemDataRole.UserRole + 4
IsOnlineRole = Qt.ItemDataRole.UserRole + 5
MachineCountRole = Qt.ItemDataRole.UserRole + 6
IsAbstractMachineRole = Qt.ItemDataRole.UserRole + 7
ComponentTypeRole = Qt.ItemDataRole.UserRole + 8
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self._show_cloud_printers = False
self._catalog = i18nCatalog("cura")
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
self.addRoleName(self.MetaDataRole, "metadata")
self.addRoleName(self.IsOnlineRole, "isOnline")
self.addRoleName(self.MachineCountRole, "machineCount")
self.addRoleName(self.IsAbstractMachineRole, "isAbstractMachine")
self.addRoleName(self.ComponentTypeRole, "componentType")
self._change_timer = QTimer()
self._change_timer.setInterval(200)
self._change_timer.setSingleShot(True)
self._change_timer.timeout.connect(self._update)
# Listen to changes
CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
self._updateDelayed()
showCloudPrintersChanged = pyqtSignal(bool)
@pyqtProperty(bool, notify=showCloudPrintersChanged)
def showCloudPrinters(self) -> bool:
return self._show_cloud_printers
@pyqtSlot(bool)
def setShowCloudPrinters(self, show_cloud_printers: bool) -> None:
self._show_cloud_printers = show_cloud_printers
self._updateDelayed()
self.showCloudPrintersChanged.emit(show_cloud_printers)
def _onContainerChanged(self, container: ContainerInterface) -> None:
"""Handler for container added/removed events from registry"""
# We only need to update when the added / removed container GlobalStack
if isinstance(container, GlobalStack):
self._updateDelayed()
def _updateDelayed(self) -> None:
self._change_timer.start()
def _update(self) -> None:
self.clear()
from cura.CuraApplication import CuraApplication
machines_manager = CuraApplication.getInstance().getMachineManager()
other_machine_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type="machine")
other_machine_stacks.sort(key = lambda machine: machine.getName().upper())
abstract_machine_stacks = CuraContainerRegistry.getInstance().findContainerStacks(is_abstract_machine = "True")
abstract_machine_stacks.sort(key = lambda machine: machine.getName().upper(), reverse = True)
for abstract_machine in abstract_machine_stacks:
definition_id = abstract_machine.definition.getId()
online_machine_stacks = machines_manager.getMachinesWithDefinition(definition_id, online_only = True)
online_machine_stacks = list(filter(lambda machine: machine.hasNetworkedConnection(), online_machine_stacks))
online_machine_stacks.sort(key=lambda machine: machine.getName().upper())
other_machine_stacks.remove(abstract_machine)
if abstract_machine in online_machine_stacks:
online_machine_stacks.remove(abstract_machine)
# Create a list item for abstract machine
self.addItem(abstract_machine, True, len(online_machine_stacks))
# Create list of machines that are children of the abstract machine
for stack in online_machine_stacks:
if self._show_cloud_printers:
self.addItem(stack, True)
# Remove this machine from the other stack list
if stack in other_machine_stacks:
other_machine_stacks.remove(stack)
if len(abstract_machine_stacks) > 0:
if self._show_cloud_printers:
self.appendItem({"componentType": "HIDE_BUTTON",
"isOnline": True,
"isAbstractMachine": False,
"machineCount": 0
})
else:
self.appendItem({"componentType": "SHOW_BUTTON",
"isOnline": True,
"isAbstractMachine": False,
"machineCount": 0
})
for stack in other_machine_stacks:
self.addItem(stack, False)
def addItem(self, container_stack: ContainerStack, is_online: bool, machine_count: int = 0) -> None:
if parseBool(container_stack.getMetaDataEntry("hidden", False)):
return
self.appendItem({
"componentType": "MACHINE",
"name": container_stack.getName(),
"id": container_stack.getId(),
"metadata": container_stack.getMetaData().copy(),
"isOnline": is_online,
"isAbstractMachine": parseBool(container_stack.getMetaDataEntry("is_abstract_machine", False)),
"machineCount": machine_count,
})

View file

@ -34,62 +34,6 @@ class MaterialManagementModel(QObject):
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent = parent)
self._material_sync = CloudMaterialSync(parent=self)
self._checkIfNewMaterialsWereInstalled()
def _checkIfNewMaterialsWereInstalled(self) -> None:
"""
Checks whether new material packages were installed in the latest startup. If there were, then it shows
a message prompting the user to sync the materials with their printers.
"""
application = cura.CuraApplication.CuraApplication.getInstance()
for package_id, package_data in application.getPackageManager().getPackagesInstalledOnStartup().items():
if package_data["package_info"]["package_type"] == "material":
# At least one new material was installed
# TODO: This should be enabled again once CURA-8609 is merged
#self._showSyncNewMaterialsMessage()
break
def _showSyncNewMaterialsMessage(self) -> None:
sync_materials_message = Message(
text = catalog.i18nc("@action:button",
"Please sync the material profiles with your printers before starting to print."),
title = catalog.i18nc("@action:button", "New materials installed"),
message_type = Message.MessageType.WARNING,
lifetime = 0
)
sync_materials_message.addAction(
"sync",
name = catalog.i18nc("@action:button", "Sync materials"),
icon = "",
description = "Sync your newly installed materials with your printers.",
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT
)
sync_materials_message.addAction(
"learn_more",
name = catalog.i18nc("@action:button", "Learn more"),
icon = "",
description = "Learn more about syncing your newly installed materials with your printers.",
button_align = Message.ActionButtonAlignment.ALIGN_LEFT,
button_style = Message.ActionButtonStyle.LINK
)
sync_materials_message.actionTriggered.connect(self._onSyncMaterialsMessageActionTriggered)
# Show the message only if there are printers that support material export
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
global_stacks = container_registry.findContainerStacks(type = "machine")
if any([stack.supportsMaterialExport for stack in global_stacks]):
sync_materials_message.show()
def _onSyncMaterialsMessageActionTriggered(self, sync_message: Message, sync_message_action: str):
if sync_message_action == "sync":
QDesktopServices.openUrl(QUrl("https://example.com/openSyncAllWindow"))
# self.openSyncAllWindow()
sync_message.hide()
elif sync_message_action == "learn_more":
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-printer-message"))
@pyqtSlot("QVariant", result = bool)
def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool:

View file

@ -358,8 +358,9 @@ class QualityManagementModel(ListModel):
"quality_type": quality_type,
"quality_changes_group": None,
"intent_category": intent_category,
"section_name": catalog.i18nc("@label", intent_translations.get(intent_category, {}).get("name", catalog.i18nc("@label", "Unknown"))),
"section_name": catalog.i18nc("@label", intent_translations.get(intent_category, {}).get("name", catalog.i18nc("@label", intent_category.title()))),
})
# Sort by quality_type for each intent category
intent_translations_list = list(intent_translations)

View file

@ -50,8 +50,13 @@ class PlatformPhysics:
if not self._enabled:
return
app_instance = Application.getInstance()
app_preferences = app_instance.getPreferences()
app_automatic_drop_down = app_preferences.getValue("physics/automatic_drop_down")
app_automatic_push_free = app_preferences.getValue("physics/automatic_push_free")
root = self._controller.getScene().getRoot()
build_volume = Application.getInstance().getBuildVolume()
build_volume = app_instance.getBuildVolume()
build_volume.updateNodeBoundaryCheck()
# Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
@ -75,7 +80,7 @@ class PlatformPhysics:
# Move it downwards if bottom is above platform
move_vector = Vector()
if Application.getInstance().getPreferences().getValue("physics/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) 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
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
move_vector = move_vector.set(y = -bbox.bottom + z_offset)
@ -84,7 +89,7 @@ class PlatformPhysics:
node.addDecorator(ConvexHullDecorator())
# only push away objects if this node is a printing mesh
if not node.callDecoration("isNonPrintingMesh") and Application.getInstance().getPreferences().getValue("physics/automatic_push_free"):
if not node.callDecoration("isNonPrintingMesh") and app_automatic_push_free:
# Do not move locked nodes
if node.getSetting(SceneNodeSettings.LockPosition):
continue

View file

@ -13,9 +13,9 @@ class ExtruderConfigurationModel(QObject):
def __init__(self, position: int = -1) -> None:
super().__init__()
self._position = position # type: int
self._material = None # type: Optional[MaterialOutputModel]
self._hotend_id = None # type: Optional[str]
self._position: int = position
self._material: Optional[MaterialOutputModel] = None
self._hotend_id: Optional[str] = None
def setPosition(self, position: int) -> None:
self._position = position

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING, List
@ -6,6 +6,8 @@ from typing import Optional, TYPE_CHECKING, List
from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot, QUrl
from PyQt6.QtGui import QImage
from cura.CuraApplication import CuraApplication
if TYPE_CHECKING:
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
@ -86,6 +88,18 @@ class PrintJobOutputModel(QObject):
self._owner = owner
self.ownerChanged.emit()
@pyqtProperty(bool, notify = ownerChanged)
def isMine(self) -> bool:
"""
Returns whether this print job was sent by the currently logged in user.
This checks the owner of the print job with the owner of the currently
logged in account. Both of these are human-readable account names which
may be duplicate. In practice the harm here is limited, but it's the
best we can do with the information available to the API.
"""
return self._owner == CuraApplication.getInstance().getCuraAPI().account.userName
@pyqtProperty(QObject, notify=assignedPrinterChanged)
def assignedPrinter(self):
return self._assigned_printer

View file

@ -350,5 +350,6 @@ class PrinterOutputModel(QObject):
self.availableConfigurationsChanged.emit()
def setAvailableConfigurations(self, new_configurations: List[PrinterConfigurationModel]) -> None:
self._available_printer_configurations = new_configurations
self.availableConfigurationsChanged.emit()
if self._available_printer_configurations != new_configurations:
self._available_printer_configurations = new_configurations
self.availableConfigurationsChanged.emit()

View file

@ -1,20 +1,19 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from dataclasses import dataclass
@dataclass
class Peripheral:
"""Data class that represents a peripheral for a printer.
Output device plug-ins may specify that the printer has a certain set of
peripherals. This set is then possibly shown in the interface of the monitor
stage.
Args:
type (string): A unique ID for the type of peripheral.
name (string): A human-readable name for the peripheral.
"""
def __init__(self, peripheral_type: str, name: str) -> None:
"""Constructs the peripheral.
:param peripheral_type: A unique ID for the type of peripheral.
:param name: A human-readable name for the peripheral.
"""
self.type = peripheral_type
self.name = name
type: str
name: str

View file

@ -50,13 +50,12 @@ class PrinterOutputDevice(QObject, OutputDevice):
The assumption is made the printer is a FDM printer.
Note that a number of settings are marked as "final". This is because decorators
are not inherited by children. To fix this we use the private counter part of those
are not inherited by children. To fix this we use the private counterpart of those
functions to actually have the implementation.
For all other uses it should be used in the same way as a "regular" OutputDevice.
"""
printersChanged = pyqtSignal()
connectionStateChanged = pyqtSignal(str)
acceptsCommandsChanged = pyqtSignal()
@ -183,8 +182,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
@pyqtProperty(QObject, constant = True)
def monitorItem(self) -> QObject:
# Note that we specifically only check if the monitor component is created.
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
# create the item (and fail) every time.
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try
# to create the item (and fail) every time.
if not self._monitor_component:
self._createMonitorViewFromQML()
return self._monitor_item
@ -237,9 +236,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
self.acceptsCommandsChanged.emit()
# Returns the unique configurations of the printers within this output device
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]:
""" Returns the unique configurations of the printers within this output device """
return self._unique_configurations
def _updateUniqueConfigurations(self) -> None:
@ -248,17 +247,19 @@ class PrinterOutputDevice(QObject, OutputDevice):
if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded():
all_configurations.add(printer.printerConfiguration)
all_configurations.update(printer.availableConfigurations)
if None in all_configurations: # Shouldn't happen, but it does. I don't see how it could ever happen. Skip adding that configuration. List could end up empty!
if None in all_configurations:
# Shouldn't happen, but it does. I don't see how it could ever happen. Skip adding that configuration.
# List could end up empty!
Logger.log("e", "Found a broken configuration in the synced list!")
all_configurations.remove(None)
new_configurations = sorted(all_configurations, key = lambda config: config.printerType or "")
new_configurations = sorted(all_configurations, key = lambda config: config.printerType or "", reverse = True)
if new_configurations != self._unique_configurations:
self._unique_configurations = new_configurations
self.uniqueConfigurationsChanged.emit()
# Returns the unique configurations of the printers within this output device
@pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
def uniquePrinterTypes(self) -> List[str]:
""" Returns the unique configurations of the printers within this output device """
return list(sorted(set([configuration.printerType or "" for configuration in self._unique_configurations])))
def _onPrintersChanged(self) -> None:

View file

@ -390,7 +390,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time":
# Find the root node that's placed in the scene; the root of the mesh group.
ancestor = self.getNode()
while ancestor.getParent() != self._root:
while ancestor.getParent() != self._root and ancestor.getParent() is not None:
ancestor = ancestor.getParent()
center = ancestor.getBoundingBox().center
else:

View file

@ -139,7 +139,7 @@ class CuraSceneController(QObject):
def setActiveBuildPlate(self, nr):
if nr == self._active_build_plate:
return
Logger.log("d", "Select build plate: %s" % nr)
Logger.debug(f"Selected build plate: {nr}")
self._active_build_plate = nr
Selection.clear()

View file

@ -114,7 +114,7 @@ class ContainerManager(QObject):
for _ in range(len(entries)):
item = item.get(entries.pop(0), {})
if item[entry_name] != entry_value:
if entry_name not in item or item[entry_name] != entry_value:
sub_item_changed = True
item[entry_name] = entry_value
@ -206,7 +206,7 @@ class ContainerManager(QObject):
if os.path.exists(file_url):
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_url))
if result == QMessageBox.ButtonRole.NoRole:
if result == QMessageBox.StandardButton.No:
return {"status": "cancelled", "message": "User cancelled"}
try:

View file

@ -108,7 +108,7 @@ class CuraContainerRegistry(ContainerRegistry):
:param container_type: :type{string} Type of the container (machine, quality, ...)
:param container_name: :type{string} Name to check
"""
container_class = ContainerStack if container_type == "machine" else InstanceContainer
container_class = ContainerStack if "machine" in container_type else InstanceContainer
return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \
self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type)
@ -139,7 +139,7 @@ class CuraContainerRegistry(ContainerRegistry):
if os.path.exists(file_name):
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
if result == QMessageBox.ButtonRole.NoRole:
if result == QMessageBox.StandardButton.No:
return False
profile_writer = self._findProfileWriter(extension, description)

View file

@ -49,7 +49,7 @@ class CuraContainerStack(ContainerStack):
self._empty_material = cura_empty_instance_containers.empty_material_container #type: InstanceContainer
self._empty_variant = cura_empty_instance_containers.empty_variant_container #type: InstanceContainer
self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] #type: List[ContainerInterface]
self._containers: List[ContainerInterface] = [self._empty_instance_container for i in _ContainerIndexes.IndexTypeMap]
self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes
self._containers[_ContainerIndexes.Quality] = self._empty_quality
self._containers[_ContainerIndexes.Material] = self._empty_material
@ -427,4 +427,4 @@ class _ContainerIndexes:
}
# Reverse lookup: type -> index
TypeIndexMap = dict([(v, k) for k, v in IndexTypeMap.items()])
TypeIndexMap = {v: k for k, v in IndexTypeMap.items()}

View file

@ -1,7 +1,9 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
import copy
from typing import Optional, cast
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Logger import Logger
@ -17,7 +19,7 @@ class CuraStackBuilder:
"""Contains helper functions to create new machines."""
@classmethod
def createMachine(cls, name: str, definition_id: str, machine_extruder_count: Optional[int] = None) -> Optional[GlobalStack]:
def createMachine(cls, name: str, definition_id: str, machine_extruder_count: Optional[int] = None, show_warning_message: bool = True) -> Optional[GlobalStack]:
"""Create a new instance of a machine.
:param name: The name of the new machine.
@ -27,14 +29,15 @@ class CuraStackBuilder:
:return: The new global stack or None if an error occurred.
"""
from cura.CuraApplication import CuraApplication
from cura.CuraApplication import CuraApplication # inline import needed due to circular import
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
container_tree = ContainerTree.getInstance()
definitions = registry.findDefinitionContainers(id = definition_id)
if not definitions:
ConfigurationErrorMessage.getInstance().addFaultyContainers(definition_id)
if show_warning_message:
ConfigurationErrorMessage.getInstance().addFaultyContainers(definition_id)
Logger.log("w", "Definition {definition} was not found!", definition = definition_id)
return None
@ -90,7 +93,7 @@ class CuraStackBuilder:
:param extruder_position: The position of the current extruder.
"""
from cura.CuraApplication import CuraApplication
from cura.CuraApplication import CuraApplication # inline import needed due to circular import
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
@ -198,13 +201,21 @@ class CuraStackBuilder:
:return: A new Global stack instance with the specified parameters.
"""
from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
stack = GlobalStack(new_stack_id)
stack.setDefinition(definition)
cls.createUserContainer(new_stack_id, definition, stack, variant_container, material_container, quality_container)
return stack
@classmethod
def createUserContainer(cls, new_stack_id: str, definition: DefinitionContainerInterface,
stack: GlobalStack,
variant_container: "InstanceContainer",
material_container: "InstanceContainer",
quality_container: "InstanceContainer") -> None:
from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
# Create user container
user_container = cls.createUserChangesContainer(new_stack_id + "_user", definition.getId(), new_stack_id,
@ -220,8 +231,6 @@ class CuraStackBuilder:
registry.addContainer(user_container)
return stack
@classmethod
def createUserChangesContainer(cls, container_name: str, definition_id: str, stack_id: str,
is_global_stack: bool) -> "InstanceContainer":
@ -258,3 +267,36 @@ class CuraStackBuilder:
container_stack.definitionChanges = definition_changes_container
return definition_changes_container
@classmethod
def createAbstractMachine(cls, definition_id: str) -> Optional[GlobalStack]:
"""Create a new instance of an abstract machine.
:param definition_id: The ID of the machine definition to use.
:return: The new Abstract Machine or None if an error occurred.
"""
abstract_machine_id = f"{definition_id}_abstract_machine"
from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
abstract_machines = registry.findContainerStacks(id = abstract_machine_id)
if abstract_machines:
return cast(GlobalStack, abstract_machines[0])
definitions = registry.findDefinitionContainers(id=definition_id)
name = ""
if definitions:
name = definitions[0].getName()
stack = cls.createMachine(abstract_machine_id, definition_id, show_warning_message=False)
if not stack:
return None
stack.setName(name)
stack.setMetaDataEntry("is_abstract_machine", True)
stack.setMetaDataEntry("is_online", True)
return stack

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
@ -275,7 +275,7 @@ class ExtruderManager(QObject):
for extruder_setting in used_adhesion_extruders:
extruder_str_nr = str(global_stack.getProperty(extruder_setting, "value"))
if extruder_str_nr == "-1":
extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition
continue # An optional extruder doesn't force any extruder to be used if it isn't used already
if extruder_str_nr in self.extruderIds:
used_extruder_stack_ids.add(self.extruderIds[extruder_str_nr])
@ -298,7 +298,7 @@ class ExtruderManager(QObject):
# Starts with the adhesion extruder.
adhesion_type = global_stack.getProperty("adhesion_type", "value")
if adhesion_type in {"skirt", "brim"}:
return global_stack.getProperty("skirt_brim_extruder_nr", "value")
return max(0, int(global_stack.getProperty("skirt_brim_extruder_nr", "value"))) # optional skirt/brim extruder defaults to zero
if adhesion_type == "raft":
return global_stack.getProperty("raft_base_extruder_nr", "value")
@ -382,7 +382,10 @@ class ExtruderManager(QObject):
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
container_registry = ContainerRegistry.getInstance()
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
expected_extruder_stack = global_stack.getMetaDataEntry("machine_extruder_trains")
if expected_extruder_stack is None:
return
expected_extruder_definition_0_id = expected_extruder_stack["0"]
try:
extruder_stack_0 = global_stack.extruderList[0]
except IndexError:

View file

@ -142,8 +142,6 @@ class ExtruderStack(CuraContainerStack):
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
if limit_to_extruder is not None:
if limit_to_extruder == -1:
limit_to_extruder = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
limit_to_extruder = str(limit_to_extruder)
if (limit_to_extruder is not None and limit_to_extruder != "-1") and self.getMetaDataEntry("position") != str(limit_to_extruder):

View file

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from collections import defaultdict
@ -8,10 +8,9 @@ import uuid
from PyQt6.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
from UM.Decorators import deprecated, override
from UM.Decorators import override
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.SettingInstance import InstanceState
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import PropertyEvaluationContext
from UM.Logger import Logger
@ -91,7 +90,6 @@ class GlobalStack(CuraContainerStack):
@pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged)
def configuredConnectionTypes(self) -> List[int]:
"""The configured connection types can be used to find out if the global
stack is configured to be connected with a printer, without having to
know all the details as to how this is exactly done (and without
actually setting the stack to be active).
@ -228,8 +226,6 @@ class GlobalStack(CuraContainerStack):
# Handle the "limit_to_extruder" property.
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
if limit_to_extruder is not None:
if limit_to_extruder == -1:
limit_to_extruder = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
limit_to_extruder = str(limit_to_extruder)
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in self._extruders:
if super().getProperty(key, "settable_per_extruder", context):
@ -293,7 +289,6 @@ class GlobalStack(CuraContainerStack):
for extruder_train in extruder_trains:
extruder_position = extruder_train.getMetaDataEntry("position")
extruder_check_position.add(extruder_position)
for check_position in range(machine_extruder_count):
if str(check_position) not in extruder_check_position:
return False
@ -344,13 +339,17 @@ class GlobalStack(CuraContainerStack):
def getName(self) -> str:
return self._metadata.get("group_name", self._metadata.get("name", ""))
def setName(self, name: "str") -> None:
def setName(self, name: str) -> None:
super().setName(name)
nameChanged = pyqtSignal()
name = pyqtProperty(str, fget=getName, fset=setName, notify=nameChanged)
def hasNetworkedConnection(self) -> bool:
has_connection = False
for connection_type in [ConnectionType.NetworkConnection.value, ConnectionType.CloudConnection.value]:
has_connection |= connection_type in self.configuredConnectionTypes
return has_connection
## private:
global_stack_mime = MimeType(

View file

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
@ -8,6 +8,7 @@ from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer
import cura.CuraApplication
from UM.Signal import Signal
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.cura_empty_instance_containers import empty_intent_container
@ -29,6 +30,7 @@ class IntentManager(QObject):
return cls.__instance
intentCategoryChanged = pyqtSignal() #Triggered when we switch categories.
intentCategoryChangedSignal = Signal()
def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]:
"""Gets the metadata dictionaries of all intent profiles for a given
@ -189,3 +191,4 @@ class IntentManager(QObject):
application.getMachineManager().setQualityGroupByQualityType(quality_type)
if old_intent_category != intent_category:
self.intentCategoryChanged.emit()
self.intentCategoryChangedSignal.emit()

View file

@ -1,4 +1,4 @@
# Copyright (c) 2021 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import time
@ -19,6 +19,7 @@ from UM.Logger import Logger
from UM.Message import Message
from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.ContainerStack import ContainerStack
from UM.Signal import postponeSignals, CompressTechnique
import cura.CuraApplication # Imported like this to prevent circular references.
@ -98,7 +99,7 @@ class MachineManager(QObject):
self._application.getPreferences().addPreference("cura/active_machine", "")
self._printer_output_devices = [] # type: List[PrinterOutputDevice]
self._printer_output_devices: List[PrinterOutputDevice] = []
self._application.getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
# There might already be some output devices by the time the signal is connected
self._onOutputDevicesChanged()
@ -111,7 +112,7 @@ class MachineManager(QObject):
self._application.callLater(self.setInitialActiveMachine)
containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) # type: List[InstanceContainer]
containers: List[InstanceContainer] = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId)
if containers:
containers[0].nameChanged.connect(self._onMaterialNameChanged)
@ -186,6 +187,32 @@ class MachineManager(QObject):
self.outputDevicesChanged.emit()
def getMachinesWithDefinition(self, definition_id: str, online_only=False) -> List[ContainerStack]:
""" Fetches all container stacks that match definition_id.
:param definition_id: The id of the machine definition.
:return: A list of Containers that match definition_id
"""
from cura.CuraApplication import CuraApplication # In function to avoid circular import
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
machines = registry.findContainerStacks(type="machine")
# Filter machines that match definition
machines = filter(lambda machine: machine.definition.id == definition_id, machines)
# Filter only LAN and Cloud printers
machines = filter(lambda machine: ConnectionType.CloudConnection in machine.configuredConnectionTypes or
ConnectionType.NetworkConnection in machine.configuredConnectionTypes,
machines)
if online_only:
# LAN printers can have is_online = False but should still be included,
# their online status is only checked when they are the active printer.
machines = filter(lambda machine: parseBool(machine.getMetaDataEntry("is_online", False) or
ConnectionType.NetworkConnection in machine.configuredConnectionTypes),
machines)
return list(machines)
@pyqtProperty(QObject, notify = currentConfigurationChanged)
def currentConfiguration(self) -> PrinterConfigurationModel:
return self._current_printer_configuration
@ -332,6 +359,7 @@ class MachineManager(QObject):
extruder_manager = ExtruderManager.getInstance()
extruder_manager.fixSingleExtrusionMachineExtruderDefinition(global_stack)
if not global_stack.isValid():
Logger.warning("Global stack isn't valid, adding it to faulty container list")
# Mark global stack as invalid
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
return # We're done here
@ -503,6 +531,10 @@ class MachineManager(QObject):
def printerConnected(self) -> bool:
return bool(self._printer_output_devices)
@pyqtProperty(bool, notify = globalContainerChanged)
def activeMachineIsAbstractCloudPrinter(self) -> bool:
return len(self._printer_output_devices) == 1 and self._printer_output_devices[0].__class__.__name__ == "AbstractCloudOutputDevice"
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
def activeMachineIsGroup(self) -> bool:
if self.activeMachine is None:
@ -872,7 +904,8 @@ class MachineManager(QObject):
if self._global_container_stack is None \
or self._global_container_stack.getProperty(setting_key, "value") == new_value \
or self.numberExtrudersEnabled < 2:
or self._global_container_stack.definitionChanges.getProperty("extruders_enabled_count", "value") is None \
or self._global_container_stack.definitionChanges.getProperty("extruders_enabled_count", "value") < 2:
return
user_changes_container = self._global_container_stack.userChanges
@ -977,7 +1010,7 @@ class MachineManager(QObject):
for position, extruder in enumerate(self._global_container_stack.extruderList):
if extruder.isEnabled and int(position) < machine_extruder_count:
extruder_count += 1
if self.numberExtrudersEnabled != extruder_count:
if self._global_container_stack.definitionChanges.getProperty("extruders_enabled_count", "value") != extruder_count:
definition_changes_container.setProperty("extruders_enabled_count", "value", extruder_count)
self.numberExtrudersEnabledChanged.emit()
@ -1611,7 +1644,7 @@ class MachineManager(QObject):
if intent_category != "default":
intent_display_name = IntentCategoryModel.translation(intent_category,
"name",
catalog.i18nc("@label", "Unknown"))
intent_category.title())
display_name = "{intent_name} - {the_rest}".format(intent_name = intent_display_name,
the_rest = display_name)
@ -1778,3 +1811,31 @@ class MachineManager(QObject):
abbr_machine += stripped_word
return abbr_machine
@pyqtSlot(str, str, result = bool)
def intentCategoryHasQuality(self, intent_category: str, quality_type: str) -> bool:
""" Checks if there are any quality groups for active extruders that have an intent category """
quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
if quality_type in quality_groups:
quality_group = quality_groups[quality_type]
for node in quality_group.nodes_for_extruders.values():
if any(intent.intent_category == intent_category for intent in node.intents.values()):
return True
return False
@pyqtSlot(str, result = str)
def getDefaultQualityTypeForIntent(self, intent_category) -> str:
""" If there is an intent category for the default machine quality return it, otherwise return the first quality for this intent category """
machine = ContainerTree.getInstance().machines.get(self._global_container_stack.definition.getId())
if self.intentCategoryHasQuality(intent_category, machine.preferred_quality_type):
return machine.preferred_quality_type
for quality_type, quality_group in ContainerTree.getInstance().getCurrentQualityGroups().items():
for node in quality_group.nodes_for_extruders.values():
if any(intent.intent_category == intent_category for intent in node.intents.values()):
return quality_type
return ""

View file

@ -10,10 +10,13 @@ if TYPE_CHECKING:
#
# This class manages a all registered upon-exit checks that need to be perform when the application tries to exit.
# For example, to show a confirmation dialog when there is USB printing in progress, etc. All callbacks will be called
# in the order of when they got registered. If all callbacks "passes", that is, for example, if the user clicks "yes"
# on the exit confirmation dialog or nothing that's blocking the exit, then the application will quit after that.
# This class manages all registered upon-exit checks
# that need to be performed when the application tries to exit.
# For example, show a confirmation dialog when there is USB printing in progress.
# All callbacks will be called in the order of when they were registered.
# If all callbacks "pass", for example:
# if the user clicks "yes" on the exit confirmation dialog
# and nothing else is blocking the exit, then the application will quit.
#
class OnExitCallbackManager:
@ -35,10 +38,12 @@ class OnExitCallbackManager:
def getIsAllChecksPassed(self) -> bool:
return self._is_all_checks_passed
# Trigger the next callback if available. If not, it means that all callbacks have "passed", which means we should
# not block the application to quit, and it will call the application to actually quit.
# Trigger the next callback if there is one.
# If not, all callbacks have "passed",
# which means we should not prevent the application from quitting,
# and we call the application to actually quit.
def triggerNextCallback(self) -> None:
# Get the next callback and schedule that if
# Get the next callback and schedule it
this_callback = None
if self._current_callback_idx < len(self._on_exit_callback_list):
this_callback = self._on_exit_callback_list[self._current_callback_idx]
@ -55,10 +60,11 @@ class OnExitCallbackManager:
# Tell the application to exit
self._application.callLater(self._application.closeApplication)
# This is the callback function which an on-exit callback should call when it finishes, it should provide the
# "should_proceed" flag indicating whether this check has "passed", or in other words, whether quitting the
# application should be blocked. If the last on-exit callback doesn't block the quitting, it will call the next
# registered on-exit callback if available.
# Callback function which an on-exit callback calls when it finishes.
# It provides a "should_proceed" flag indicating whether the check has "passed",
# or whether quitting the application should be blocked.
# If the last on-exit callback doesn't block quitting, it will call the next
# registered on-exit callback if one is available.
def onCurrentCallbackFinished(self, should_proceed: bool = True) -> None:
if not should_proceed:
Logger.log("d", "on-app-exit callback finished and we should not proceed.")

View file

@ -12,7 +12,7 @@ class AddPrinterPagesModel(WelcomePagesModel):
def initialize(self, cancellable: bool = True) -> None:
self._pages.append({"id": "add_network_or_local_printer",
"page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"),
"page_url": self._getBuiltinWelcomePagePath("AddUltimakerOrThirdPartyPrinterStack.qml"),
"next_page_id": "machine_actions",
"next_page_button_text": self._catalog.i18nc("@action:button", "Add"),
})

View file

@ -23,9 +23,9 @@ catalog = i18nCatalog("cura")
class PrintInformation(QObject):
"""A class for processing and the print times per build plate as well as managing the job name
"""A class for processing the print times per build plate and managing the job name
This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
This class also combines the current machine name and the filename of the first loaded mesh into a job name.
This job name is requested by the JobSpecs qml file.
"""
@ -38,6 +38,8 @@ class PrintInformation(QObject):
self.initializeCuraMessagePrintTimeProperties()
self.slice_uuid: Optional[str] = None
# Indexed by build plate number
self._material_lengths = {} # type: Dict[int, List[float]]
self._material_weights = {} # type: Dict[int, List[float]]
@ -184,7 +186,7 @@ class PrintInformation(QObject):
if time != time: # Check for NaN. Engine can sometimes give us weird values.
duration.setDuration(0)
Logger.log("w", "Received NaN for print duration message")
Logger.warning("Received NaN for print duration message")
continue
total_estimated_time += time
@ -366,7 +368,7 @@ class PrintInformation(QObject):
mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
data = mime_type.stripExtension(name)
except MimeTypeNotFoundError:
Logger.log("w", "Unsupported Mime Type Database file extension %s", name)
Logger.warning(f"Unsupported Mime Type Database file extension {name}")
if data is not None and check_name is not None:
self._base_name = data
@ -390,7 +392,7 @@ class PrintInformation(QObject):
return self._base_name
def _defineAbbreviatedMachineName(self) -> None:
"""Created an acronym-like abbreviated machine name from the currently active machine name.
"""Creates an abbreviated machine name from the currently active machine name.
Called each time the global stack is switched.
"""
@ -444,7 +446,7 @@ class PrintInformation(QObject):
self.setToZeroPrintInformation(self._active_build_plate)
def _onOutputStart(self, output_device: OutputDevice) -> None:
"""If this is the sort of output 'device' (like local or online file storage, rather than a printer),
"""If this is a sort of output 'device' (like local or online file storage, rather than a printer),
the user could have altered the file-name, and thus the project name should be altered as well."""
if isinstance(output_device, ProjectOutputDevice):
new_name = output_device.getLastOutputName()

View file

@ -265,7 +265,7 @@ class WelcomePagesModel(ListModel):
"should_show_function": self.shouldShowCloudPage,
},
{"id": "add_network_or_local_printer",
"page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"),
"page_url": self._getBuiltinWelcomePagePath("AddUltimakerOrThirdPartyPrinterStack.qml"),
"next_page_id": "machine_actions",
},
{"id": "add_printer_by_ip",

View file

@ -62,15 +62,21 @@ class WhatsNewPagesModel(WelcomePagesModel):
def initialize(self) -> None:
self._pages = []
self._pages.append({"id": "whats_new",
"page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"),
"next_page_button_text": self._catalog.i18nc("@action:button", "Skip"),
"next_page_id": "changelog"
})
self._pages.append({"id": "changelog",
"page_url": self._getBuiltinWelcomePagePath("ChangelogContent.qml"),
"next_page_button_text": self._catalog.i18nc("@action:button", "Close"),
})
try:
self._pages.append({"id": "whats_new",
"page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"),
"next_page_button_text": self._catalog.i18nc("@action:button", "Skip"),
"next_page_id": "changelog"
})
except FileNotFoundError:
Logger.warning("Unable to find what's new page")
try:
self._pages.append({"id": "changelog",
"page_url": self._getBuiltinWelcomePagePath("ChangelogContent.qml"),
"next_page_button_text": self._catalog.i18nc("@action:button", "Close"),
})
except FileNotFoundError:
Logger.warning("Unable to find changelog page")
self.setItems(self._pages)
images, max_image = WhatsNewPagesModel._collectOrdinalFiles(Resources.Images, WhatsNewPagesModel.image_formats)

View file

@ -1,4 +1,4 @@
# Copyright (c) 2021 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
@ -18,6 +18,7 @@ if TYPE_CHECKING:
from UM.Signal import Signal
catalog = i18nCatalog("cura")
class CloudMaterialSync(QObject):
"""
Handles the synchronisation of material profiles with cloud accounts.
@ -44,7 +45,6 @@ class CloudMaterialSync(QObject):
break
def openSyncAllWindow(self):
self.reset()
if self.sync_all_dialog is None:

View file

@ -18,6 +18,7 @@ import os
if sys.platform != "linux": # Turns out the Linux build _does_ use this, but we're not making an Enterprise release for that system anyway.
os.environ["QT_PLUGIN_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
os.environ["QML2_IMPORT_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
os.environ["QT_OPENGL_DLL"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
from PyQt6.QtNetwork import QSslConfiguration, QSslSocket

View file

@ -1,71 +0,0 @@
#!/usr/bin/env bash
# Abort at the first error.
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
PROJECT_DIR="$( cd "${SCRIPT_DIR}/.." && pwd )"
# Make sure that environment variables are set properly
export PATH="${CURA_BUILD_ENV_PATH}/bin:${PATH}"
export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}"
export LD_LIBRARY_PATH="${CURA_BUILD_ENV_PATH}/lib:${LD_LIBRARY_PATH}"
cd "${PROJECT_DIR}"
#
# Clone Uranium and set PYTHONPATH first
#
# Check the branch to use for Uranium.
# It tries the following branch names and uses the first one that's available.
# - GITHUB_HEAD_REF: the branch name of a PR. If it's not a PR, it will be empty.
# - GITHUB_BASE_REF: the branch a PR is based on. If it's not a PR, it will be empty.
# - GITHUB_REF: the branch name if it's a branch on the repository;
# refs/pull/123/merge if it's a pull_request.
# - master: the master branch. It should always exist.
# For debugging.
echo "GITHUB_REF: ${GITHUB_REF}"
echo "GITHUB_HEAD_REF: ${GITHUB_HEAD_REF}"
echo "GITHUB_BASE_REF: ${GITHUB_BASE_REF}"
GIT_REF_NAME_LIST=( "${GITHUB_HEAD_REF}" "${GITHUB_BASE_REF}" "${GITHUB_REF}" "master" )
for git_ref_name in "${GIT_REF_NAME_LIST[@]}"
do
if [ -z "${git_ref_name}" ]; then
continue
fi
git_ref_name="$(basename "${git_ref_name}")"
# Skip refs/pull/1234/merge as pull requests use it as GITHUB_REF
if [[ "${git_ref_name}" == "merge" ]]; then
echo "Skip [${git_ref_name}]"
continue
fi
URANIUM_BRANCH="${git_ref_name}"
output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")"
if [ -n "${output}" ]; then
echo "Found Uranium branch [${URANIUM_BRANCH}]."
break
else
echo "Could not find Uranium branch [${URANIUM_BRANCH}], try next."
fi
done
echo "Using Uranium branch ${URANIUM_BRANCH} ..."
git clone --depth=1 -b "${URANIUM_BRANCH}" https://github.com/Ultimaker/Uranium.git "${PROJECT_DIR}"/Uranium
export PYTHONPATH="${PROJECT_DIR}/Uranium:.:${PYTHONPATH}"
mkdir build
cd build
cmake \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_PREFIX_PATH="${CURA_BUILD_ENV_PATH}" \
-DURANIUM_DIR="${PROJECT_DIR}/Uranium" \
-DBUILD_TESTS=ON \
-DPRINT_PLUGIN_LIST=OFF \
-DGENERATE_TRANSLATIONS=OFF \
..
make

View file

@ -1,3 +0,0 @@
#!/usr/bin/env bash
cd build
ctest -j4 --output-on-failure -T Test

81
docs/Report.md Normal file
View file

@ -0,0 +1,81 @@
# Reporting Issues
Please attach the following information in case <br>
you want to report crashing or similar issues.
<br>
## DxDiag
### ![Badge Windows]
The log as produced by **dxdiag**.
<kbd>start</kbd>  »  <kbd>run</kbd>  »  <kbd>dxdiag</kbd>  »  <kbd>save output</kbd>
<br>
<br>
## Cura GUI Log
If the Cura user interface still starts, you can also <br>
reach these directories from the application menu:
<kbd>Help</kbd>  »  <kbd>Show settings folder</kbd>
<br>
### ![Badge Windows]
```
%APPDATA%\cura\< >\cura.log
```
or
```
C:\Users\<your username>\AppData\Roaming\cura\< >\cura.log
```
<br>
### ![Badge Linux]
```
~/.local/share/cura/< >/cura.log
```
<br>
### ![Badge MacOS]
```
~/Library/Application Support/cura/< >/cura.log
```
<br>
<br>
## Alternative
An alternative is to install the **[ExtensiveSupportLogging]** <br>
plugin this creates a zip folder of the relevant log files.
If you're experiencing performance issues, we might ask <br>
you to connect the CPU profiler in this plugin and attach <br>
the collected data to your support ticket.
<br>
<!----------------------------------------------------------------------------->
[ExtensiveSupportLogging]: https://marketplace.ultimaker.com/app/cura/plugins/UltimakerPackages/ExtensiveSupportLogging
<!---------------------------------[ Badges ]---------------------------------->
[Badge Windows]: https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logoColor=white&logo=Windows
[Badge Linux]: https://img.shields.io/badge/Linux-00A95C?style=for-the-badge&logoColor=white&logo=Linux
[Badge MacOS]: https://img.shields.io/badge/MacOS-403C3D?style=for-the-badge&logoColor=white&logo=MacOS

View file

@ -2,32 +2,77 @@ Setting Properties
====
Each setting in Cura has a number of properties. It's not just a key and a value. This page lists the properties that a setting can define.
* `key` (string): The identifier by which the setting is referenced. This is not a human-readable name, but just a reference string, such as `layer_height_0`. Typically these are named with the most significant category first, in order to sort them better, such as `material_print_temperature`. This is not actually a real property but just an identifier; it can't be changed.
* `value` (optional): The current value of the setting. This can be a function, an arbitrary Python expression that depends on the values of other settings. If it's not present, the `default_value` is used.
* `default_value`: A default value for the setting if `value` is undefined. This property is required however. It can't be a Python expression, but it can be any JSON type. This is made separate so that CuraEngine can read it out as well for its debugging mode via the command line, without needing a complete Python interpreter.
* `label` (string): The human-readable name for the setting. This label is translated.
* `description` (string): A longer description of what the setting does when you change it. This description is translated as well.
* `type` (string): The type of value that this setting contains. Allowed types are: `bool`, `str`, `float`, `int`, `enum`, `category`, `[int]`, `vec3`, `polygon` and `polygons`.
* `unit` (optional string): A unit that is displayed at the right-hand side of the text field where the user enters the setting value.
* `resolve` (optional string): A Python expression that resolves disagreements for global settings if multiple per-extruder profiles define different values for a setting. Typically this takes the values for the setting from all stacks and computes one final value for it that will be used for the global setting. For instance, the `resolve` function for the build plate temperature is `max(extruderValues('material_bed_temperature')`, meaning that it will use the hottest bed temperature of all materials of the extruders in use.
* `limit_to_extruder` (optional): A Python expression that indicates which extruder a setting will be obtained from. This is used for settings that may be extruder-specific but the extruder is not necessarily the current extruder. For instance, support settings need to be evaluated for the support extruder. Infill settings need to be evaluated for the infill extruder if the infill extruder is changed.
* `enabled` (optional string or boolean): Whether the setting can currently be made visible for the user. This can be a simple true/false, or a Python expression that depends on other settings. Typically used for settings that don't apply when another setting is disabled, such as to hide the support settings if support is disabled.
* `minimum_value` (optional): The lowest acceptable value for this setting. If it's any lower, Cura will not allow the user to slice. By convention this is used to prevent setting values that are technically or physically impossible, such as a layer height of 0mm. This property only applies to numerical settings.
* `maximum_value` (optional): The highest acceptable value for this setting. If it's any higher, Cura will not allow the user to slice. By convention this is used to prevent setting values that are technically or physically impossible, such as a support overhang angle of more than 90 degrees. This property only applies to numerical settings.
* `minimum_value_warning` (optional): The threshold under which a warning is displayed to the user. By convention this is used to indicate that it will probably not print very nicely with such a low setting value. This property only applies to numerical settings.
* `maximum_value_warning` (optional): The threshold above which a warning is displayed to the user. By convention this is used to indicate that it will probably not print very nicely with such a high setting value. This property only applies to numerical settings.
* `settable_globally` (optional boolean): Whether the setting can be changed globally. For some mesh-type settings such as `support_mesh` this doesn't make sense, so those can't be changed globally. They are not displayed in the main settings list then.
* `settable_per_meshgroup` (optional boolean): Whether a setting can be changed per group of meshes. Currently unused in Cura.
* `settable_per_extruder` (optional boolean): Whether a setting can be changed per extruder. Some settings, like the build plate temperature, can't be adjusted separately for each extruder. An icon is shown in the interface to indicate this. If the user changes these settings they are stored in the global stack.
* `settable_per_mesh` (optional boolean): Whether a setting can be changed per mesh. The settings that can be changed per mesh are shown in the list of available settings in the per-object settings tool.
* `children` (optional list): A list of child settings. These are displayed with an indentation. If all child settings are overridden by the user, the parent setting gets greyed out to indicate that the parent setting has no effect any more. This is not strictly always the case though, because that would depend on the inheritance functions in the `value`.
* `icon` (optional string): A path to an icon to be displayed. Only applies to setting categories.
* `allow_empty` (optional bool): Whether the setting is allowed to be empty. If it's not, this will be treated as a setting error and Cura will not allow the user to slice. Only applies to string-type settings.
* `warning_description` (optional string): A warning message to display when the setting has a warning value. This is currently unused by Cura.
* `error_description` (optional string): An error message to display when the setting has an error value. This is currently unused by Cura.
* `options` (dictionary): A list of values that the user can choose from. The keys of this dictionary are keys that CuraEngine identifies the option with. The values are human-readable strings and will be translated. Only applies to (and only required for) enum-type settings.
* `comments` (optional string): Comments to other programmers about the setting. This is not used by Cura.
* `is_uuid` (optional boolean): Whether or not this setting indicates a UUID-4. If it is, the setting will indicate an error if it's not in the correct format. Only applies to string-type settings.
* `regex_blacklist_pattern` (optional string): A regular expression, where if the setting value matches with this regular expression, it gets an error state. Only applies to string-type settings.
* `error_value` (optional): If the setting value is equal to this value, it will show a setting error. This is used to display errors for non-numerical settings such as checkboxes.
* `warning_value` (optional): If the setting value is equal to this value, it will show a setting warning. This is used to display warnings for non-numerical settings such as checkboxes.
* `key` (string): __The identifier by which the setting is referenced.__
* This is not a human-readable name, but just a reference string, such as `layer_height_0`.
* This is not actually a real property but just an identifier; it can't be changed.
* Typically these are named with the most significant category first, in order to sort them better, such as `material_print_temperature`.
* `value` (optional): __The current value of the setting.__
* This can be a function (an arbitrary Python expression) that depends on the values of other settings.
* If it's not present, the `default_value` is used.
* `default_value`: __A default value for the setting if `value` is undefined.__
* This property is required.
* It can't be a Python expression, but it can be any JSON type.
* This is made separate so that CuraEngine can read it out for its debugging mode via the command line, without needing a complete Python interpreter.
* `label` (string): __The human-readable name for the setting.__
* This label is translated.
* `description` (string): __A longer description of what the setting does when you change it.__
* This description is translated.
* `type` (string): __The type of value that this setting contains.__
* Allowed types are: `bool`, `str`, `float`, `int`, `enum`, `category`, `[int]`, `vec3`, `polygon` and `polygons`.
* `unit` (optional string): __A unit that is displayed at the right-hand side of the text field where the user enters the setting value.__
* `resolve` (optional string): __A Python expression that resolves disagreements for global settings if multiple per-extruder profiles define different values for a setting.__
* Typically this takes the values for the setting from all stacks and computes one final value for it that will be used for the global setting. For instance, the `resolve` function for the build plate temperature is `max(extruderValues('material_bed_temperature')`, meaning that it will use the hottest bed temperature of all materials of the extruders in use.
* `limit_to_extruder` (optional): __A Python expression that indicates which extruder a setting will be obtained from.__
* This is used for settings that may be extruder-specific but the extruder is not necessarily the current extruder. For instance, support settings need to be evaluated for the support extruder. Infill settings need to be evaluated for the infill extruder if the infill extruder is changed.
* `enabled` (optional string or boolean): __Whether the setting can currently be made visible for the user.__
* This can be a simple true/false, or a Python expression that depends on other settings.
* Typically used for settings that don't apply when another setting is disabled, such as to hide the support settings if support is disabled.
* `minimum_value` (optional): __The lowest acceptable value for this setting.__
* If it's any lower, Cura will not allow the user to slice.
* This property only applies to numerical settings.
* By convention this is used to prevent setting values that are technically or physically impossible, such as a layer height of 0mm.
* `maximum_value` (optional): __The highest acceptable value for this setting.__
* If it's any higher, Cura will not allow the user to slice.
* This property only applies to numerical settings.
* By convention this is used to prevent setting values that are technically or physically impossible, such as a support overhang angle of more than 90 degrees.
* `minimum_value_warning` (optional): __The threshold under which a warning is displayed to the user.__
* This property only applies to numerical settings.
* By convention this is used to indicate that it will probably not print very nicely with such a low setting value.
* `maximum_value_warning` (optional): __The threshold above which a warning is displayed to the user.__
* This property only applies to numerical settings.
* By convention this is used to indicate that it will probably not print very nicely with such a high setting value.
* `settable_globally` (optional boolean): __Whether the setting can be changed globally.__
* For some mesh-type settings such as `support_mesh` this doesn't make sense, so those can't be changed globally. They are not displayed in the main settings list then.
* `settable_per_meshgroup` (optional boolean): __Whether a setting can be changed per group of meshes.__
* *This is currently unused by Cura.*
* `settable_per_extruder` (optional boolean): __Whether a setting can be changed per extruder.__
* Some settings, like the build plate temperature, can't be adjusted separately for each extruder. An icon is shown in the interface to indicate this.
* If the user changes these settings they are stored in the global stack.
* `settable_per_mesh` (optional boolean): __Whether a setting can be changed per mesh.__
* The settings that can be changed per mesh are shown in the list of available settings in the per-object settings tool.
* `children` (optional list): __A list of child settings.__
* These are displayed with an indentation. If all child settings are overridden by the user, the parent setting gets greyed out to indicate that the parent setting has no effect any more. This is not strictly always the case though, because that would depend on the inheritance functions in the `value`.
* `icon` (optional string): __A path to an icon to be displayed.__
* Only applies to setting categories.
* `allow_empty` (optional bool): __Whether the setting is allowed to be empty.__
* If it's not, this will be treated as a setting error and Cura will not allow the user to slice.
* Only applies to string-type settings.
* `warning_description` (optional string): __A warning message to display when the setting has a warning value.__
* *This is currently unused by Cura.*
* `error_description` (optional string): __An error message to display when the setting has an error value.__
* *This is currently unused by Cura.*
* `options` (dictionary): __A list of values that the user can choose from.__
* The keys of this dictionary are keys that CuraEngine identifies the option with.
* The values are human-readable strings and will be translated.
* Only applies to (and only required for) enum-type settings.
* `comments` (optional string): __Comments to other programmers about the setting.__
* *This is currently unused by Cura.*
* `is_uuid` (optional boolean): __Whether or not this setting indicates a UUID-4.__
* If it is, the setting will indicate an error if it's not in the correct format.
* Only applies to string-type settings.
* `regex_blacklist_pattern` (optional string): __A regular expression, where if the setting value matches with this regular expression, it gets an error state.__
* Only applies to string-type settings.
* `error_value` (optional): __If the setting value is equal to this value, it will show a setting error.__
* This is used to display errors for non-numerical settings such as checkboxes.
* `warning_value` (optional): __If the setting value is equal to this value, it will show a setting warning.__
* This is used to display warnings for non-numerical settings such as checkboxes.

View file

@ -1,21 +1,33 @@
Repositories
====
Cura uses a number of repositories where parts of our source code are separated, in order to get a cleaner architecture. Those repositories are:
* [Cura](https://github.com/Ultimaker/Cura), the main repository for the front-end of Cura. This contains all of the business logic for the front-end, including the specific types of profiles that are available, the concept of 3D printers and materials, specific tools for handling 3D printed models, pretty much all of the GUI, as well as Ultimaker services such as the Marketplace and accounts.
* The Cura repository is built on [Uranium](https://github.com/Ultimaker/Uranium), a framework for desktop applications that handle 3D models and have a separate back-end. This provides Cura with a basic GUI framework ([Qt](https://www.qt.io/)), a 3D scene, a rendering system, a plug-in system and a system for stacked profiles that change settings.
* In order to slice, Cura starts [CuraEngine](https://github.com/Ultimaker/CuraEngine) in the background. This does the actual process that converts 3D models into a toolpath for the printer.
* Communication to CuraEngine goes via [libArcus](https://github.com/Ultimaker/libArcus), a small library that wraps around [Protobuf](https://developers.google.com/protocol-buffers/) in order to make it run over a local socket.
* Cura's build scripts are in [cura-build](https://github.com/Ultimaker/cura-build) and build scripts for building dependencies are in [cura-build-environment](https://github.com/Ultimaker/cura-build-environment).
* [Cura](https://github.com/Ultimaker/Cura) is the main repository for the front-end of Cura. This contains:
- all of the business logic for the front-end, including the specific types of profiles that are available
- the concept of 3D printers and materials
- specific tools for handling 3D printed models
- pretty much all of the GUI
- Ultimaker services such as the Marketplace and accounts.
* [Uranium](https://github.com/Ultimaker/Uranium) is the underlying framework the Cura repository is built on. [Uranium](https://github.com/Ultimaker/Uranium) is a framework for desktop applications that handle 3D models. It has a separate back-end. This provides Cura with:
- a basic GUI framework ([Qt](https://www.qt.io/))
- a 3D scene, a rendering system
- a plug-in system
- a system for stacked profiles that change settings.
* [CuraEngine](https://github.com/Ultimaker/CuraEngine) is the slicer used by Cura in the background. This does the actual process that converts 3D models into a toolpath for the printer.
* [libArcus](https://github.com/Ultimaker/libArcus) handles the communication to CuraEngine. [libArcus](https://github.com/Ultimaker/libArcus) is a small library that wraps around [Protobuf](https://developers.google.com/protocol-buffers/) in order to make it run over a local socket.
* [cura-build](https://github.com/Ultimaker/cura-build): Cura's build scripts.
* [cura-build-environment](https://github.com/Ultimaker/cura-build-environment) build scripts for building dependencies.
There are also a number of repositories under our control that are not integral parts of Cura's architecture, but more like separated side-gigs:
* Loading and writing 3MF files is done through [libSavitar](https://github.com/Ultimaker/libSavitar).
* Loading and writing UFP files is done through [libCharon](https://github.com/Ultimaker/libCharon).
* To make the build system a bit simpler, some parts are pre-compiled in [cura-binary-data](https://github.com/Ultimaker/cura-binary-data). This holds things like the machine-readable translation files and the Marlin builds for firmware updates, which would require considerable tooling to build automatically.
* There are automated GUI tests in [Cura-squish-tests](https://github.com/Ultimaker/Cura-squish-tests).
* Material profiles are stored in [fdm_materials](https://github.com/Ultimaker/fdm_materials). This is separated out and combined in our build process, so that the firmware for Ultimaker's printers can use the same set of profiles too.
* [libSavitar](https://github.com/Ultimaker/libSavitar) is used for loading and writing 3MF files.
* [libCharon](https://github.com/Ultimaker/libCharon) is used for loading and writing UFP files.
* [cura-binary-data](https://github.com/Ultimaker/cura-binary-data) pre-compiled parts to make the build system a bit simpler. This holds things which would require considerable tooling to build automatically like:
- the machine-readable translation files
- the Marlin builds for firmware updates
* [Cura-squish-tests](https://github.com/Ultimaker/Cura-squish-tests): automated GUI tests.
* [fdm_materials](https://github.com/Ultimaker/fdm_materials) stores Material profiles. This is separated out and combined in our build process, so that the firmware for Ultimaker's printers can use the same set of profiles too.
Interplay
----
At a very high level, Cura's repositories interconnect as follows:
![Overview of interplay between repositories](resources/repositories.svg)
![Overview of interplay between repositories](resources/repositories.svg)

54
docs/resources/deps.dot Normal file
View file

@ -0,0 +1,54 @@
digraph {
"cpython/3.10.4@ultimaker/testing" -> "zlib/1.2.12"
"cpython/3.10.4@ultimaker/testing" -> "openssl/1.1.1l"
"cpython/3.10.4@ultimaker/testing" -> "expat/2.4.1"
"cpython/3.10.4@ultimaker/testing" -> "libffi/3.2.1"
"cpython/3.10.4@ultimaker/testing" -> "mpdecimal/2.5.0@ultimaker/testing"
"cpython/3.10.4@ultimaker/testing" -> "libuuid/1.0.3"
"cpython/3.10.4@ultimaker/testing" -> "libxcrypt/4.4.25"
"cpython/3.10.4@ultimaker/testing" -> "bzip2/1.0.8"
"cpython/3.10.4@ultimaker/testing" -> "gdbm/1.19"
"cpython/3.10.4@ultimaker/testing" -> "sqlite3/3.36.0"
"cpython/3.10.4@ultimaker/testing" -> "tk/8.6.10"
"cpython/3.10.4@ultimaker/testing" -> "ncurses/6.2"
"cpython/3.10.4@ultimaker/testing" -> "xz_utils/5.2.5"
"pynest2d/5.1.0-beta+3@ultimaker/stable" -> "libnest2d/5.1.0-beta+3@ultimaker/stable"
"pynest2d/5.1.0-beta+3@ultimaker/stable" -> "cpython/3.10.4@ultimaker/testing"
"freetype/2.12.1" -> "libpng/1.6.37"
"freetype/2.12.1" -> "zlib/1.2.12"
"freetype/2.12.1" -> "bzip2/1.0.8"
"freetype/2.12.1" -> "brotli/1.0.9"
"savitar/5.1.0-beta+3@ultimaker/stable" -> "pugixml/1.12.1"
"savitar/5.1.0-beta+3@ultimaker/stable" -> "cpython/3.10.4@ultimaker/testing"
"arcus/5.1.0-beta+3@ultimaker/stable" -> "protobuf/3.17.1"
"arcus/5.1.0-beta+3@ultimaker/stable" -> "cpython/3.10.4@ultimaker/testing"
"arcus/5.1.0-beta+3@ultimaker/stable" -> "zlib/1.2.12"
"libpng/1.6.37" -> "zlib/1.2.12"
"curaengine/5.1.0-beta+3@ultimaker/stable" -> "clipper/6.4.2"
"curaengine/5.1.0-beta+3@ultimaker/stable" -> "boost/1.78.0"
"curaengine/5.1.0-beta+3@ultimaker/stable" -> "rapidjson/1.1.0"
"curaengine/5.1.0-beta+3@ultimaker/stable" -> "stb/20200203"
"curaengine/5.1.0-beta+3@ultimaker/stable" -> "protobuf/3.17.1"
"curaengine/5.1.0-beta+3@ultimaker/stable" -> "arcus/5.1.0-beta+3@ultimaker/stable"
"tcl/8.6.10" -> "zlib/1.2.12"
"uranium/5.1.0-beta+3@ultimaker/stable" -> "arcus/5.1.0-beta+3@ultimaker/stable"
"uranium/5.1.0-beta+3@ultimaker/stable" -> "cpython/3.10.4@ultimaker/testing"
"libnest2d/5.1.0-beta+3@ultimaker/stable" -> "boost/1.78.0"
"libnest2d/5.1.0-beta+3@ultimaker/stable" -> "clipper/6.4.2"
"libnest2d/5.1.0-beta+3@ultimaker/stable" -> "nlopt/2.7.0"
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "arcus/5.1.0-beta+3@ultimaker/stable"
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "curaengine/5.1.0-beta+3@ultimaker/stable"
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "savitar/5.1.0-beta+3@ultimaker/stable"
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "pynest2d/5.1.0-beta+3@ultimaker/stable"
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "uranium/5.1.0-beta+3@ultimaker/stable"
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "fdm_materials/5.1.0-beta+3@ultimaker/stable"
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "cura_binary_data/5.1.0-beta+3@ultimaker/stable"
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "cpython/3.10.4@ultimaker/testing"
"fontconfig/2.13.93" -> "freetype/2.12.1"
"fontconfig/2.13.93" -> "expat/2.4.1"
"fontconfig/2.13.93" -> "libuuid/1.0.3"
"tk/8.6.10" -> "tcl/8.6.10"
"tk/8.6.10" -> "fontconfig/2.13.93"
"tk/8.6.10" -> "xorg/system"
"protobuf/3.17.1" -> "zlib/1.2.12"
}

Binary file not shown.

20
packaging/AppImage/AppRun Normal file
View file

@ -0,0 +1,20 @@
#!/bin/sh
scriptdir=$(dirname $0)
export PYTHONPATH="$scriptdir/lib/python3.10"
export LD_LIBRARY_PATH=$scriptdir
export QT_PLUGIN_PATH="$scriptdir/qt/plugins"
export QML2_IMPORT_PATH="$scriptdir/qt/qml"
export QT_QPA_FONTDIR=/usr/share/fonts
export QT_QPA_PLATFORMTHEME=xdgdesktopportal
export QT_XKB_CONFIG_ROOT=/usr/share/X11/xkb
# Use the openssl.cnf packaged in the AppImage
export OPENSSL_CONF="$scriptdir/openssl.cnf"
# If this variable is set on Zorin OS 16 Cura would crash
# unset `QT_STYLE_OVERRIDE` as a precaution
unset QT_STYLE_OVERRIDE
$scriptdir/Ultimaker-Cura "$@"

View file

@ -0,0 +1,76 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import argparse # Command line arguments parsing and help.
from jinja2 import Template
import os # Finding installation directory.
import os.path # Finding files.
import shutil # Copying files.
import stat # For setting file permissions.
import subprocess # For calling system commands.
def build_appimage(dist_path, version, appimage_filename):
"""
Creates an AppImage file from the build artefacts created so far.
"""
copy_metadata_files(dist_path, version)
try:
os.remove(os.path.join(dist_path, appimage_filename)) # Ensure any old file is removed, if it exists.
except FileNotFoundError:
pass # If it didn't exist, that's even better.
generate_appimage(dist_path, appimage_filename)
sign_appimage(dist_path, appimage_filename)
def copy_metadata_files(dist_path, version):
"""
Copy metadata files for the metadata of the AppImage.
"""
copied_files = {
os.path.join("..", "icons", "cura-icon_256x256.png"): "cura-icon.png",
"cura.appdata.xml": "cura.appdata.xml",
"AppRun": "AppRun"
}
packaging_dir = os.path.dirname(__file__)
for source, dest in copied_files.items():
print("Copying", os.path.join(packaging_dir, source), "to", os.path.join(dist_path, dest))
shutil.copyfile(os.path.join(packaging_dir, source), os.path.join(dist_path, dest))
# Ensure that AppRun has the proper permissions: 755 (user reads, writes and executes, group reads and executes, world reads and executes).
print("Changing permissions for AppRun")
os.chmod(os.path.join(dist_path, "AppRun"), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
# Provision the Desktop file with the correct version number.
template_path = os.path.join(packaging_dir, "cura.desktop.jinja")
desktop_path = os.path.join(dist_path, "cura.desktop")
print("Provisioning desktop file from", template_path, "to", desktop_path)
with open(template_path, "r") as f:
desktop_file = Template(f.read())
with open(desktop_path, "w") as f:
f.write(desktop_file.render(cura_version = version))
def generate_appimage(dist_path, appimage_filename):
appimage_path = os.path.join(dist_path, "..", appimage_filename)
appimagetool = os.getenv("APPIMAGETOOL_LOCATION", "appimagetool")
command = [appimagetool, "--appimage-extract-and-run", f"{dist_path}/", appimage_path]
result = subprocess.call(command)
if result != 0:
raise RuntimeError(f"The AppImageTool command returned non-zero: {result}")
def sign_appimage(dist_path, appimage_filename):
appimage_path = os.path.join(dist_path, "..", appimage_filename)
command = ["gpg", "--yes", "--armor", "--detach-sig", appimage_path]
result = subprocess.call(command)
if result != 0:
raise RuntimeError(f"The GPG command returned non-zero: {result}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description = "Create AppImages of Cura.")
parser.add_argument("dist_path", type=str, help="Path to where PyInstaller installed the distribution of Cura.")
parser.add_argument("version", type=str, help="Full version number of Cura (e.g. '5.1.0-beta')")
parser.add_argument("filename", type = str, help = "Filename of the AppImage (e.g. 'Ultimaker-Cura-5.1.0-beta-Linux-X64.AppImage')")
args = parser.parse_args()
build_appimage(args.dist_path, args.version, args.filename)

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.ultimaker.cura</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>LGPL-3.0</project_license>
<name>Ultimaker Cura</name>
<summary>Slicer to prepare your 3D printing projects</summary>
<description>
<p>Ultimaker Cura is a slicer, an application that prepares your model for 3D printing. Optimized, expert-tested profiles for 3D printers and materials mean you can start printing reliably in no time. And with industry-standard software integration, you can streamline your workflow for maximum efficiency.</p>
</description>
<url type="homepage">https://ultimaker.com/en/software/ultimaker-cura</url>
<screenshots>
<screenshot type="default">
<caption>Print preparation screen</caption>
<image>https://raw.githubusercontent.com/Ultimaker/Cura/master/screenshot.png</image>
</screenshot>
</screenshots>
</component>

View file

@ -1,19 +1,15 @@
[Desktop Entry]
Name=Ultimaker Cura
Name[de]=Ultimaker Cura
Name[nl]=Ultimaker Cura
GenericName=3D Printing Software
GenericName[de]=3D-Druck-Software
GenericName[nl]=3D-printsoftware
GenericName[nl]=3D-Print Software
Comment=Cura converts 3D models into paths for a 3D printer. It prepares your print for maximum accuracy, minimum printing time and good reliability with many extra features that make your print come out great.
Comment[de]=Cura wandelt 3D-Modelle in Pfade für einen 3D-Drucker um. Es bereitet Ihren Druck für maximale Genauigkeit, minimale Druckzeit und guter Zuverlässigkeit mit vielen zusätzlichen Funktionen vor, damit Ihr Druck großartig wird.
Comment[nl]=Cura converteert 3D-modellen naar paden voor een 3D printer. Het bereidt je print voor om zeer precies, snel en betrouwbaar te kunnen printen, met veel extra functionaliteit om je print er goed uit te laten komen.
Exec=@CMAKE_INSTALL_FULL_BINDIR@/cura %F
TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
Exec=Ultimaker-Cura %F
Icon=cura-icon
Terminal=false
Type=Application
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;text/x-gcode;application/x-amf;application/x-ply;application/x-ctm;model/vnd.collada+xml;model/gltf-binary;model/gltf+json;model/vnd.collada+xml+zip;
Categories=Graphics;
Keywords=3D;Printing;Slicer;
StartupWMClass=cura.real
Keywords=3D;Printing;
X-AppImage-Version={{ cura_version }}

View file

@ -0,0 +1,194 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura's build system is released under the terms of the AGPLv3 or higher.
!define APP_NAME "{{ app_name }}"
!define COMP_NAME "{{ company }}"
!define WEB_SITE "{{ web_site }}"
!define VERSION "{{ version }}"
!define VIVERSION "{{ version_major }}.{{ version_minor }}.{{ version_patch }}.0"
!define COPYRIGHT "Copyright (c) {{ year }} {{ company }}"
!define DESCRIPTION "Application"
!define LICENSE_TXT "{{ cura_license_file }}"
!define INSTALLER_NAME "{{ destination }}"
!define MAIN_APP_EXE "{{ main_app }}"
!define INSTALL_TYPE "SetShellVarContext all"
!define REG_ROOT "HKLM"
!define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${APP_NAME}-${VERSION}"
!define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}-${VERSION}"
!define REG_START_MENU "Start Menu Folder"
;Require administrator access
RequestExecutionLevel admin
var SM_Folder
######################################################################
VIProductVersion "${VIVERSION}"
VIAddVersionKey "ProductName" "{{ app_name }}"
VIAddVersionKey "CompanyName" "${COMP_NAME}"
VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
VIAddVersionKey "FileDescription" "${DESCRIPTION}"
VIAddVersionKey "FileVersion" "${VIVERSION}"
######################################################################
SetCompressor {{ compression_method }}
Name "${APP_NAME}"
Caption "${APP_NAME}"
OutFile "${INSTALLER_NAME}"
BrandingText "${APP_NAME}"
InstallDir "$PROGRAMFILES64\${APP_NAME}"
######################################################################
!include "MUI2.nsh"
!include fileassoc.nsh
!define MUI_ABORTWARNING
!define MUI_UNABORTWARNING
!define MUI_ICON "{{ cura_icon }}"
!define MUI_WELCOMEFINISHPAGE_BITMAP "{{ cura_banner_img }}"
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "{{ cura_banner_img }}"
!insertmacro MUI_PAGE_WELCOME
!ifdef LICENSE_TXT
!insertmacro MUI_PAGE_LICENSE "${LICENSE_TXT}"
!endif
!insertmacro MUI_PAGE_DIRECTORY
!ifdef REG_START_MENU
!define MUI_STARTMENUPAGE_NODISABLE
!define MUI_STARTMENUPAGE_DEFAULTFOLDER "Ultimaker Cura"
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}"
!define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}"
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}"
!insertmacro MUI_PAGE_STARTMENU Application $SM_Folder
!endif
!insertmacro MUI_PAGE_INSTFILES
# Set up explorer to run Cura instead of directly, so it's not executed elevated (with all negative consequences that brings for an unelevated user).
!define MUI_FINISHPAGE_RUN "$WINDIR\explorer.exe"
!define MUI_FINISHPAGE_RUN_PARAMETERS "$INSTDIR\${MAIN_APP_EXE}"
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH
!insertmacro MUI_LANGUAGE "English"
######################################################################
Section -MainProgram
${INSTALL_TYPE}
SetOverwrite ifnewer
{% for out_path, files in mapped_out_paths.items() %}SetOutPath "{{ out_path }}"{% for file in files %}
File "{{ file[0] }}"{% endfor %}
{% endfor %}SectionEnd
######################################################################
Section -Extension_Reg
!insertmacro APP_ASSOCIATE "stl" "Cura.model" "Standard Tessellation Language (STL) files" "$INSTDIR\${MAIN_APP_EXE},0" "Open with {{ app_name }}" "$INSTDIR\${MAIN_APP_EXE} $\"%1$\""
!insertmacro APP_ASSOCIATE "3mf" "Cura.project" "3D Manufacturing Format (3MF) files" "$INSTDIR\${MAIN_APP_EXE},0" "Open with {{ app_name }}" "$INSTDIR\${MAIN_APP_EXE} $\"%1$\""
SectionEnd
Section -Icons_Reg
SetOutPath "$INSTDIR"
WriteUninstaller "$INSTDIR\uninstall.exe"
!ifdef REG_START_MENU
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
CreateDirectory "$SMPROGRAMS\$SM_Folder"
CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}"
CreateShortCut "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe"
!ifdef WEB_SITE
WriteIniStr "$INSTDIR\Ultimaker Cura website.url" "InternetShortcut" "URL" "${WEB_SITE}"
CreateShortCut "$SMPROGRAMS\$SM_Folder\Ultimaker Cura website.lnk" "$INSTDIR\Ultimaker Cura website.url"
!endif
!insertmacro MUI_STARTMENU_WRITE_END
!endif
!ifndef REG_START_MENU
CreateDirectory "$SMPROGRAMS\{{ app_name }}"
CreateShortCut "$SMPROGRAMS\{{ app_name }}\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}"
CreateShortCut "$SMPROGRAMS\{{ app_name }}\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe"
!ifdef WEB_SITE
WriteIniStr "$INSTDIR\Ultimaker Cura website.url" "InternetShortcut" "URL" "${WEB_SITE}"
CreateShortCut "$SMPROGRAMS\{{ app_name }}\Ultimaker Cura website.lnk" "$INSTDIR\Ultimaker Cura website.url"
!endif
!endif
WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayName" "${APP_NAME}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "UninstallString" "$INSTDIR\uninstall.exe"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\${MAIN_APP_EXE}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayVersion" "${VERSION}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}"
!ifdef WEB_SITE
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "URLInfoAbout" "${WEB_SITE}"
!endif
SectionEnd
######################################################################
Section Uninstall
${INSTALL_TYPE}{% for files in mapped_out_paths.values() %}{% for file in files %}
Delete "{{ file[1] }}"{% endfor %}{% endfor %}{% for rem_dir in rmdir_paths %}
RmDir "{{ rem_dir }}"{% endfor %}
# FIXME: dirty solution, but for some reason these directories aren't removed
RmDir "$INSTDIR\share\cura\resources\scripts"
RmDir "$INSTDIR\share\cura\resources"
RmDir "$INSTDIR\share\cura"
RmDir "$INSTDIR\share\uranium\resources\scripts"
RmDir "$INSTDIR\share\uranium\resources"
RmDir "$INSTDIR\share\uranium"
RmDir "$INSTDIR\share"
Delete "$INSTDIR\uninstall.exe"
!ifdef WEB_SITE
Delete "$INSTDIR\${APP_NAME} website.url"
!endif
RmDir /r /REBOOTOK "$INSTDIR"
!ifdef REG_START_MENU
!insertmacro MUI_STARTMENU_GETFOLDER "Application" $SM_Folder
Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk"
Delete "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk"
!ifdef WEB_SITE
Delete "$SMPROGRAMS\$SM_Folder\Ultimaker Cura website.lnk"
!endif
RmDir "$SMPROGRAMS\$SM_Folder"
!endif
!ifndef REG_START_MENU
Delete "$SMPROGRAMS\{{ app_name }}\${APP_NAME}.lnk"
Delete "$SMPROGRAMS\{{ app_name }}\Uninstall ${APP_NAME}.lnk"
!ifdef WEB_SITE
Delete "$SMPROGRAMS\{{ app_name }}\Ultimaker Cura website.lnk"
!endif
RmDir "$SMPROGRAMS\{{ app_name }}"
!endif
!insertmacro APP_UNASSOCIATE "stl" "Cura.model"
!insertmacro APP_UNASSOCIATE "3mf" "Cura.project"
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
SectionEnd
######################################################################

View file

@ -0,0 +1,80 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
import argparse # Command line arguments parsing and help.
import subprocess
import shutil
from datetime import datetime
from pathlib import Path
from jinja2 import Template
def generate_nsi(source_path: str, dist_path: str, filename: str):
dist_loc = Path(os.getcwd(), dist_path)
source_loc = Path(os.getcwd(), source_path)
instdir = Path("$INSTDIR")
dist_paths = [p.relative_to(dist_loc.joinpath("Ultimaker-Cura")) for p in sorted(dist_loc.joinpath("Ultimaker-Cura").rglob("*")) if p.is_file()]
mapped_out_paths = {}
for dist_path in dist_paths:
if "__pycache__" not in dist_path.parts:
out_path = instdir.joinpath(dist_path).parent
if out_path not in mapped_out_paths:
mapped_out_paths[out_path] = [(dist_loc.joinpath("Ultimaker-Cura", dist_path), instdir.joinpath(dist_path))]
else:
mapped_out_paths[out_path].append((dist_loc.joinpath("Ultimaker-Cura", dist_path), instdir.joinpath(dist_path)))
rmdir_paths = set()
for rmdir_f in mapped_out_paths.values():
for _, rmdir_p in rmdir_f:
for rmdir in rmdir_p.parents:
rmdir_paths.add(rmdir)
rmdir_paths = sorted(list(rmdir_paths), reverse = True)[:-2] # Removes the `.` and `..` from the list
jinja_template_path = Path(source_loc.joinpath("packaging", "NSIS", "Ultimaker-Cura.nsi.jinja"))
with open(jinja_template_path, "r") as f:
template = Template(f.read())
nsis_content = template.render(
app_name = f"Ultimaker Cura {os.getenv('CURA_VERSION_FULL')}",
main_app = "Ultimaker-Cura.exe",
version = os.getenv('CURA_VERSION_FULL'),
version_major = os.environ.get("CURA_VERSION_MAJOR"),
version_minor = os.environ.get("CURA_VERSION_MINOR"),
version_patch = os.environ.get("CURA_VERSION_PATCH"),
company = "Ultimaker B.V.",
web_site = "https://ultimaker.com",
year = datetime.now().year,
cura_license_file = str(source_loc.joinpath("packaging", "cura_license.txt")),
compression_method = "LZMA", # ZLIB, BZIP2 or LZMA
cura_banner_img = str(source_loc.joinpath("packaging", "NSIS", "cura_banner_nsis.bmp")),
cura_icon = str(source_loc.joinpath("packaging", "icons", "Cura.ico")),
mapped_out_paths = mapped_out_paths,
rmdir_paths = rmdir_paths,
destination = filename
)
with open(dist_loc.joinpath("Ultimaker-Cura.nsi"), "w") as f:
f.write(nsis_content)
shutil.copy(source_loc.joinpath("packaging", "NSIS", "fileassoc.nsh"), dist_loc.joinpath("fileassoc.nsh"))
def build(dist_path: str):
dist_loc = Path(os.getcwd(), dist_path)
command = ["makensis", "/V2", "/P4", str(dist_loc.joinpath("Ultimaker-Cura.nsi"))]
subprocess.run(command)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description = "Create Windows exe installer of Cura.")
parser.add_argument("source_path", type=str, help="Path to Conan install Cura folder.")
parser.add_argument("dist_path", type=str, help="Path to Pyinstaller dist folder")
parser.add_argument("filename", type = str, help = "Filename of the exe (e.g. 'Ultimaker-Cura-5.1.0-beta-Windows-X64.exe')")
args = parser.parse_args()
generate_nsi(args.source_path, args.dist_path, args.filename)
build(args.dist_path)

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View file

@ -0,0 +1,134 @@
; fileassoc.nsh
; File association helper macros
; Written by Saivert
;
; Improved by Nikku<https://github.com/nikku>.
;
; Features automatic backup system and UPDATEFILEASSOC macro for
; shell change notification.
;
; |> How to use <|
; To associate a file with an application so you can double-click it in explorer, use
; the APP_ASSOCIATE macro like this:
;
; Example:
; !insertmacro APP_ASSOCIATE "txt" "myapp.textfile" "Description of txt files" \
; "$INSTDIR\myapp.exe,0" "Open with myapp" "$INSTDIR\myapp.exe $\"%1$\""
;
; Never insert the APP_ASSOCIATE macro multiple times, it is only ment
; to associate an application with a single file and using the
; the "open" verb as default. To add more verbs (actions) to a file
; use the APP_ASSOCIATE_ADDVERB macro.
;
; Example:
; !insertmacro APP_ASSOCIATE_ADDVERB "myapp.textfile" "edit" "Edit with myapp" \
; "$INSTDIR\myapp.exe /edit $\"%1$\""
;
; To have access to more options when registering the file association use the
; APP_ASSOCIATE_EX macro. Here you can specify the verb and what verb is to be the
; standard action (default verb).
;
; Note, that this script takes into account user versus global installs.
; To properly work you must initialize the SHELL_CONTEXT variable via SetShellVarContext.
;
; And finally: To remove the association from the registry use the APP_UNASSOCIATE
; macro. Here is another example just to wrap it up:
; !insertmacro APP_UNASSOCIATE "txt" "myapp.textfile"
;
; |> Note <|
; When defining your file class string always use the short form of your application title
; then a period (dot) and the type of file. This keeps the file class sort of unique.
; Examples:
; Winamp.Playlist
; NSIS.Script
; Photoshop.JPEGFile
;
; |> Tech info <|
; The registry key layout for a global file association is:
;
; HKEY_LOCAL_MACHINE\Software\Classes
; <".ext"> = <applicationID>
; <applicationID> = <"description">
; shell
; <verb> = <"menu-item text">
; command = <"command string">
;
;
; The registry key layout for a per-user file association is:
;
; HKEY_CURRENT_USER\Software\Classes
; <".ext"> = <applicationID>
; <applicationID> = <"description">
; shell
; <verb> = <"menu-item text">
; command = <"command string">
;
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
!macroend
!macro APP_ASSOCIATE_EX EXT FILECLASS DESCRIPTION ICON VERB DEFAULTVERB SHELLNEW COMMANDTEXT COMMAND
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
StrCmp "${SHELLNEW}" "0" +2
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}\ShellNew" "NullFile" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" `${DEFAULTVERB}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}`
!macroend
!macro APP_ASSOCIATE_ADDVERB FILECLASS VERB COMMANDTEXT COMMAND
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}`
!macroend
!macro APP_ASSOCIATE_REMOVEVERB FILECLASS VERB
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}\shell\${VERB}`
!macroend
!macro APP_UNASSOCIATE EXT FILECLASS
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
!macroend
!macro APP_ASSOCIATE_GETFILECLASS OUTPUT EXT
ReadRegStr ${OUTPUT} SHELL_CONTEXT "Software\Classes\.${EXT}" ""
!macroend
; !defines for use with SHChangeNotify
!ifdef SHCNE_ASSOCCHANGED
!undef SHCNE_ASSOCCHANGED
!endif
!define SHCNE_ASSOCCHANGED 0x08000000
!ifdef SHCNF_FLUSH
!undef SHCNF_FLUSH
!endif
!define SHCNF_FLUSH 0x1000
!macro UPDATEFILEASSOC
; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we
; can update the shell.
System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)"
!macroend

165
packaging/cura_license.txt Normal file
View file

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View file

@ -0,0 +1,67 @@
import os
import argparse # Command line arguments parsing and help.
import subprocess
ULTIMAKER_CURA_DOMAIN = os.environ.get("ULTIMAKER_CURA_DOMAIN", "nl.ultimaker.cura")
def build_dmg(source_path: str, dist_path: str, filename: str) -> None:
create_dmg_executable = os.environ.get("CREATE_DMG_EXECUTABLE", "create-dmg")
arguments = [create_dmg_executable,
"--window-pos", "640", "360",
"--window-size", "690", "503",
"--app-drop-link", "520", "272",
"--volicon", f"{source_path}/packaging/icons/VolumeIcons_Cura.icns",
"--icon-size", "90",
"--icon", "Ultimaker-Cura.app", "169", "272",
"--eula", f"{source_path}/packaging/cura_license.txt",
"--background", f"{source_path}/packaging/dmg/cura_background_dmg.png",
f"{dist_path}/{filename}",
f"{dist_path}/Ultimaker-Cura.app"]
subprocess.run(arguments)
def sign(dist_path: str, filename: str) -> None:
codesign_executable = os.environ.get("CODESIGN", "codesign")
codesign_identity = os.environ.get("CODESIGN_IDENTITY")
arguments = [codesign_executable,
"-s", codesign_identity,
"--timestamp",
"-i", f"{ULTIMAKER_CURA_DOMAIN}.dmg", # TODO: check if this really should have the extra dmg. We seem to be doing this also in the old Rundeck scripts
f"{dist_path}/{filename}"]
subprocess.run(arguments)
def notarize(dist_path: str, filename: str) -> None:
notarize_user = os.environ.get("MAC_NOTARIZE_USER")
notarize_password = os.environ.get("MAC_NOTARIZE_PASS")
altool_executable = os.environ.get("ALTOOL_EXECUTABLE", "altool")
arguments = [
"xcrun", altool_executable,
"--notarize-app",
"--primary-bundle-id", ULTIMAKER_CURA_DOMAIN,
"--username", notarize_user,
"--password", notarize_password,
"--file", f"{dist_path}/{filename}"
]
subprocess.run(arguments)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description = "Create dmg of Cura.")
parser.add_argument("source_path", type=str, help="Path to Conan install Cura folder.")
parser.add_argument("dist_path", type=str, help="Path to Pyinstaller dist folder")
parser.add_argument("filename", type = str, help = "Filename of the dmg (e.g. 'Ultimaker-Cura-5.1.0-beta-Linux-X64.dmg')")
args = parser.parse_args()
build_dmg(args.source_path, args.dist_path, args.filename)
sign(args.dist_path, args.filename)
notarize_dmg = bool(os.environ.get("NOTARIZE_DMG", "TRUE"))
if notarize_dmg:
notarize(args.dist_path, args.filename)

View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more