Skip to content

Commit

Permalink
Merge pull request Ultimaker#5 from Ultimaker/master
Browse files Browse the repository at this point in the history
resync
  • Loading branch information
TobbyTravel authored Jan 4, 2018
2 parents 5104a48 + 9784faf commit 860a050
Show file tree
Hide file tree
Showing 79 changed files with 2,484 additions and 1,065 deletions.
70 changes: 36 additions & 34 deletions Jenkinsfile
Original file line number Diff line number Diff line change
@@ -1,45 +1,47 @@
parallel_nodes(['linux && cura', 'windows && cura']) {
// Prepare building
stage('Prepare') {
// Ensure we start with a clean build directory.
step([$class: 'WsCleanup'])
timeout(time: 2, unit: "HOURS") {
parallel_nodes(['linux && cura', 'windows && cura']) {
// 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
}
// 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"
}
// 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}\"")
}
// 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') {
try {
make('test')
} catch(e) {
currentBuild.result = "UNSTABLE"
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
stage('Unit Test') {
try {
make('test')
} 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'
// 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.'])
notify_build_result(env.CURA_EMAIL_RECIPIENTS, '#cura-dev', ['master', '2.'])
}
}
}
4 changes: 3 additions & 1 deletion cmake/CuraTests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ function(cura_add_test)

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 (${test_exists} EQUAL "NOTFOUND")
if (NOT ${test_exists})
add_test(
NAME ${_NAME}
COMMAND ${PYTHON_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
Expand Down
25 changes: 17 additions & 8 deletions cura/Arrange.py → cura/Arranging/Arrange.py
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger
from UM.Math.Vector import Vector
from cura.ShapeArray import ShapeArray
from cura import ZOffsetDecorator
from cura.Arranging.ShapeArray import ShapeArray
from cura.Scene import ZOffsetDecorator

from collections import namedtuple

Expand Down Expand Up @@ -30,6 +30,7 @@ def __init__(self, x, y, offset_x, offset_y, scale= 1.0):
self._offset_x = offset_x
self._offset_y = offset_y
self._last_priority = 0
self._is_empty = True

## Helper to create an Arranger instance
#
Expand All @@ -38,8 +39,8 @@ def __init__(self, x, y, offset_x, offset_y, scale= 1.0):
# \param scene_root Root for finding all scene nodes
# \param fixed_nodes Scene nodes to be placed
@classmethod
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5):
arranger = Arrange(220, 220, 110, 110, scale = scale)
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 220, y = 220):
arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
arranger.centerFirst()

if fixed_nodes is None:
Expand All @@ -64,7 +65,7 @@ def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5):
for area in disallowed_areas:
points = copy.deepcopy(area._points)
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
arranger.place(0, 0, shape_arr)
arranger.place(0, 0, shape_arr, update_empty = False)
return arranger

## Find placement for a node (using offset shape) and place it (using hull shape)
Expand Down Expand Up @@ -168,7 +169,8 @@ def bestSpot(self, shape_arr, start_prio = 0, step = 1):
# \param x x-coordinate
# \param y y-coordinate
# \param shape_arr ShapeArray object
def place(self, x, y, shape_arr):
# \param update_empty updates the _is_empty, used when adding disallowed areas
def place(self, x, y, shape_arr, update_empty = True):
x = int(self._scale * x)
y = int(self._scale * y)
offset_x = x + self._offset_x + shape_arr.offset_x
Expand All @@ -181,10 +183,17 @@ def place(self, x, y, shape_arr):
max_y = min(max(offset_y + shape_arr.arr.shape[0], 0), shape_y - 1)
occupied_slice = self._occupied[min_y:max_y, min_x:max_x]
# we use a slice of shape because it can be out of bounds
occupied_slice[numpy.where(shape_arr.arr[
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 1
new_occupied = numpy.where(shape_arr.arr[
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)
if update_empty and new_occupied:
self._is_empty = False
occupied_slice[new_occupied] = 1

# Set priority to low (= high number), so it won't get picked at trying out.
prio_slice = self._priority[min_y:max_y, min_x:max_x]
prio_slice[numpy.where(shape_arr.arr[
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999

@property
def isEmpty(self):
return self._is_empty
154 changes: 154 additions & 0 deletions cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.

from UM.Job import Job
from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector
from UM.Operations.TranslateOperation import TranslateOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Message import Message
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")

from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Arranging.Arrange import Arrange
from cura.Arranging.ShapeArray import ShapeArray

from typing import List


class ArrangeArray:
def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]):
self._x = x
self._y = y
self._fixed_nodes = fixed_nodes
self._count = 0
self._first_empty = None
self._has_empty = False
self._arrange = []

def _update_first_empty(self):
for i, a in enumerate(self._arrange):
if a.isEmpty:
self._first_empty = i
self._has_empty = True
return
self._first_empty = None
self._has_empty = False

def add(self):
new_arrange = Arrange.create(x = self._x, y = self._y, fixed_nodes = self._fixed_nodes)
self._arrange.append(new_arrange)
self._count += 1
self._update_first_empty()

def count(self):
return self._count

def get(self, index):
return self._arrange[index]

def getFirstEmpty(self):
if not self._is_empty:
self.add()
return self._arrange[self._first_empty]


class ArrangeObjectsAllBuildPlatesJob(Job):
def __init__(self, nodes: List[SceneNode], min_offset = 8):
super().__init__()
self._nodes = nodes
self._min_offset = min_offset

def run(self):
status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"),
lifetime = 0,
dismissable=False,
progress = 0,
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
status_message.show()


# Collect nodes to be placed
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
for node in self._nodes:
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset)
nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr))

# Sort the nodes with the biggest area first.
nodes_arr.sort(key=lambda item: item[0])
nodes_arr.reverse()

x, y = 200, 200

arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = [])
arrange_array.add()

# Place nodes one at a time
start_priority = 0
grouped_operation = GroupedOperation()
found_solution_for_all = True
left_over_nodes = [] # nodes that do not fit on an empty build plate

for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
# For performance reasons, we assume that when a location does not fit,
# it will also not fit for the next object (while what can be untrue).
# We also skip possibilities by slicing through the possibilities (step = 10)

try_placement = True

current_build_plate_number = 0 # always start with the first one

# # Only for first build plate
# if last_size == size and last_build_plate_number == current_build_plate_number:
# # This optimization works if many of the objects have the same size
# # Continue with same build plate number
# start_priority = last_priority
# else:
# start_priority = 0

while try_placement:
# make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects
while current_build_plate_number >= arrange_array.count():
arrange_array.add()
arranger = arrange_array.get(current_build_plate_number)

best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
x, y = best_spot.x, best_spot.y
node.removeDecorator(ZOffsetDecorator)
if node.getBoundingBox():
center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
else:
center_y = 0
if x is not None: # We could find a place
arranger.place(x, y, hull_shape_arr) # place the object in the arranger

node.callDecoration("setBuildPlateNumber", current_build_plate_number)
grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
try_placement = False
else:
# very naive, because we skip to the next build plate if one model doesn't fit.
if arranger.isEmpty:
# apparently we can never place this object
left_over_nodes.append(node)
try_placement = False
else:
# try next build plate
current_build_plate_number += 1
try_placement = True

status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
Job.yieldThread()

for node in left_over_nodes:
node.callDecoration("setBuildPlateNumber", -1) # these are not on any build plate
found_solution_for_all = False

grouped_operation.push()

status_message.hide()

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"))
no_full_solution_message.show()
7 changes: 3 additions & 4 deletions cura/ArrangeObjectsJob.py → cura/Arranging/ArrangeObjectsJob.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@
from UM.Job import Job
from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector
from UM.Operations.SetTransformOperation import SetTransformOperation
from UM.Operations.TranslateOperation import TranslateOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Logger import Logger
from UM.Message import Message
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")

from cura.ZOffsetDecorator import ZOffsetDecorator
from cura.Arrange import Arrange
from cura.ShapeArray import ShapeArray
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Arranging.Arrange import Arrange
from cura.Arranging.ShapeArray import ShapeArray

from typing import List

Expand Down
6 changes: 5 additions & 1 deletion cura/ShapeArray.py → cura/Arranging/ShapeArray.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ def fromPolygon(cls, vertices, scale = 1):
offset_x = int(numpy.amin(flip_vertices[:, 1]))
flip_vertices[:, 0] = numpy.add(flip_vertices[:, 0], -offset_y)
flip_vertices[:, 1] = numpy.add(flip_vertices[:, 1], -offset_x)
shape = [int(numpy.amax(flip_vertices[:, 0])), int(numpy.amax(flip_vertices[:, 1]))]
shape = numpy.array([int(numpy.amax(flip_vertices[:, 0])), int(numpy.amax(flip_vertices[:, 1]))])
shape[numpy.where(shape == 0)] = 1
arr = cls.arrayFromPolygon(shape, flip_vertices)
if not numpy.ndarray.any(arr):
# set at least 1 pixel
arr[0][0] = 1
return cls(arr, offset_x, offset_y)

## Instantiate an offset and hull ShapeArray from a scene node.
Expand Down
Empty file added cura/Arranging/__init__.py
Empty file.
Loading

0 comments on commit 860a050

Please sign in to comment.