1# Copyright (c) 2019 Ultimaker B.V.
2# Cura is released under the terms of the LGPLv3 or higher.
3
4from typing import Optional
5
6from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
7from UM.Logger import Logger
8from UM.Settings.Interfaces import DefinitionContainerInterface
9from UM.Settings.InstanceContainer import InstanceContainer
10
11from cura.Machines.ContainerTree import ContainerTree
12from .GlobalStack import GlobalStack
13from .ExtruderStack import ExtruderStack
14
15
16class CuraStackBuilder:
17    """Contains helper functions to create new machines."""
18
19    @classmethod
20    def createMachine(cls, name: str, definition_id: str, machine_extruder_count: Optional[int] = None) -> Optional[GlobalStack]:
21        """Create a new instance of a machine.
22
23        :param name: The name of the new machine.
24        :param definition_id: The ID of the machine definition to use.
25        :param machine_extruder_count: The number of extruders in the machine.
26
27        :return: The new global stack or None if an error occurred.
28        """
29
30        from cura.CuraApplication import CuraApplication
31        application = CuraApplication.getInstance()
32        registry = application.getContainerRegistry()
33        container_tree = ContainerTree.getInstance()
34
35        definitions = registry.findDefinitionContainers(id = definition_id)
36        if not definitions:
37            ConfigurationErrorMessage.getInstance().addFaultyContainers(definition_id)
38            Logger.log("w", "Definition {definition} was not found!", definition = definition_id)
39            return None
40
41        machine_definition = definitions[0]
42        machine_node = container_tree.machines[machine_definition.getId()]
43
44        generated_name = registry.createUniqueName("machine", "", name, machine_definition.getName())
45        # Make sure the new name does not collide with any definition or (quality) profile
46        # createUniqueName() only looks at other stacks, but not at definitions or quality profiles
47        # Note that we don't go for uniqueName() immediately because that function matches with ignore_case set to true
48        if registry.findContainersMetadata(id = generated_name):
49            generated_name = registry.uniqueName(generated_name)
50
51        new_global_stack = cls.createGlobalStack(
52            new_stack_id = generated_name,
53            definition = machine_definition,
54            variant_container = application.empty_variant_container,
55            material_container = application.empty_material_container,
56            quality_container = machine_node.preferredGlobalQuality().container,
57        )
58        new_global_stack.setName(generated_name)
59
60        # Create ExtruderStacks
61        extruder_dict = machine_definition.getMetaDataEntry("machine_extruder_trains")
62        for position in extruder_dict:
63            try:
64                cls.createExtruderStackWithDefaultSetup(new_global_stack, position)
65            except IndexError as e:
66                Logger.logException("e", "Failed to create an extruder stack for position {pos}: {err}".format(pos = position, err = str(e)))
67                return None
68
69        # If given, set the machine_extruder_count when creating the machine, or else the extruderList used bellow will
70        # not return the correct extruder list (since by default, the machine_extruder_count is 1) in machines with
71        # settable number of extruders.
72        if machine_extruder_count and 0 <= machine_extruder_count <= len(extruder_dict):
73            new_global_stack.setProperty("machine_extruder_count", "value", machine_extruder_count)
74
75        # Only register the extruders if we're sure that all of them are correct.
76        for new_extruder in new_global_stack.extruderList:
77            registry.addContainer(new_extruder)
78
79        # Register the global stack after the extruder stacks are created. This prevents the registry from adding another
80        # extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1).
81        registry.addContainer(new_global_stack)
82
83        return new_global_stack
84
85    @classmethod
86    def createExtruderStackWithDefaultSetup(cls, global_stack: "GlobalStack", extruder_position: int) -> None:
87        """Create a default Extruder Stack
88
89        :param global_stack: The global stack this extruder refers to.
90        :param extruder_position: The position of the current extruder.
91        """
92
93        from cura.CuraApplication import CuraApplication
94        application = CuraApplication.getInstance()
95        registry = application.getContainerRegistry()
96
97        # Get the extruder definition.
98        extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains")
99        extruder_definition_id = extruder_definition_dict[str(extruder_position)]
100        try:
101            extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
102        except IndexError:
103            # It still needs to break, but we want to know what extruder ID made it break.
104            msg = "Unable to find extruder definition with the id [%s]" % extruder_definition_id
105            Logger.logException("e", msg)
106            raise IndexError(msg)
107
108        # Find out what filament diameter we need.
109        approximate_diameter = round(extruder_definition.getProperty("material_diameter", "value"))  # Can't be modified by definition changes since we are just initialising the stack here.
110
111        # Find the preferred containers.
112        machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
113        extruder_variant_node = machine_node.variants.get(machine_node.preferred_variant_name)
114        if not extruder_variant_node:
115            Logger.log("w", "Could not find preferred nozzle {nozzle_name}. Falling back to {fallback}.".format(nozzle_name = machine_node.preferred_variant_name, fallback = next(iter(machine_node.variants))))
116            extruder_variant_node = next(iter(machine_node.variants.values()))
117        extruder_variant_container = extruder_variant_node.container
118        material_node = extruder_variant_node.preferredMaterial(approximate_diameter)
119        material_container = material_node.container
120        quality_node = material_node.preferredQuality()
121
122        new_extruder_id = registry.uniqueName(extruder_definition_id)
123        new_extruder = cls.createExtruderStack(
124            new_extruder_id,
125            extruder_definition = extruder_definition,
126            machine_definition_id = global_stack.definition.getId(),
127            position = extruder_position,
128            variant_container = extruder_variant_container,
129            material_container = material_container,
130            quality_container = quality_node.container
131        )
132        new_extruder.setNextStack(global_stack)
133
134        registry.addContainer(new_extruder)
135
136    @classmethod
137    def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface,
138                            machine_definition_id: str,
139                            position: int,
140                            variant_container: "InstanceContainer",
141                            material_container: "InstanceContainer",
142                            quality_container: "InstanceContainer") -> ExtruderStack:
143
144        """Create a new Extruder stack
145
146        :param new_stack_id: The ID of the new stack.
147        :param extruder_definition: The definition to base the new stack on.
148        :param machine_definition_id: The ID of the machine definition to use for the user container.
149        :param position: The position the extruder occupies in the machine.
150        :param variant_container: The variant selected for the current extruder.
151        :param material_container: The material selected for the current extruder.
152        :param quality_container: The quality selected for the current extruder.
153
154        :return: A new Extruder stack instance with the specified parameters.
155        """
156
157        from cura.CuraApplication import CuraApplication
158        application = CuraApplication.getInstance()
159        registry = application.getContainerRegistry()
160
161        stack = ExtruderStack(new_stack_id)
162        stack.setName(extruder_definition.getName())
163        stack.setDefinition(extruder_definition)
164
165        stack.setMetaDataEntry("position", str(position))
166
167        user_container = cls.createUserChangesContainer(new_stack_id + "_user", machine_definition_id, new_stack_id,
168                                                        is_global_stack = False)
169
170        stack.definitionChanges = cls.createDefinitionChangesContainer(stack, new_stack_id + "_settings")
171        stack.variant = variant_container
172        stack.material = material_container
173        stack.quality = quality_container
174        stack.intent = application.empty_intent_container
175        stack.qualityChanges = application.empty_quality_changes_container
176        stack.userChanges = user_container
177
178        # Only add the created containers to the registry after we have set all the other
179        # properties. This makes the create operation more transactional, since any problems
180        # setting properties will not result in incomplete containers being added.
181        registry.addContainer(user_container)
182
183        return stack
184
185    @classmethod
186    def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface,
187                          variant_container: "InstanceContainer",
188                          material_container: "InstanceContainer",
189                          quality_container: "InstanceContainer") -> GlobalStack:
190
191        """Create a new Global stack
192
193        :param new_stack_id: The ID of the new stack.
194        :param definition: The definition to base the new stack on.
195        :param variant_container: The variant selected for the current stack.
196        :param material_container: The material selected for the current stack.
197        :param quality_container: The quality selected for the current stack.
198
199        :return: A new Global stack instance with the specified parameters.
200        """
201
202        from cura.CuraApplication import CuraApplication
203        application = CuraApplication.getInstance()
204        registry = application.getContainerRegistry()
205
206        stack = GlobalStack(new_stack_id)
207        stack.setDefinition(definition)
208
209        # Create user container
210        user_container = cls.createUserChangesContainer(new_stack_id + "_user", definition.getId(), new_stack_id,
211                                                        is_global_stack = True)
212
213        stack.definitionChanges = cls.createDefinitionChangesContainer(stack, new_stack_id + "_settings")
214        stack.variant = variant_container
215        stack.material = material_container
216        stack.quality = quality_container
217        stack.intent = application.empty_intent_container
218        stack.qualityChanges = application.empty_quality_changes_container
219        stack.userChanges = user_container
220
221        registry.addContainer(user_container)
222
223        return stack
224
225    @classmethod
226    def createUserChangesContainer(cls, container_name: str, definition_id: str, stack_id: str,
227                                   is_global_stack: bool) -> "InstanceContainer":
228        from cura.CuraApplication import CuraApplication
229        application = CuraApplication.getInstance()
230        registry = application.getContainerRegistry()
231
232        unique_container_name = registry.uniqueName(container_name)
233
234        container = InstanceContainer(unique_container_name)
235        container.setDefinition(definition_id)
236        container.setMetaDataEntry("type", "user")
237        container.setMetaDataEntry("setting_version", CuraApplication.SettingVersion)
238
239        metadata_key_to_add = "machine" if is_global_stack else "extruder"
240        container.setMetaDataEntry(metadata_key_to_add, stack_id)
241
242        return container
243
244    @classmethod
245    def createDefinitionChangesContainer(cls, container_stack, container_name):
246        from cura.CuraApplication import CuraApplication
247        application = CuraApplication.getInstance()
248        registry = application.getContainerRegistry()
249
250        unique_container_name = registry.uniqueName(container_name)
251
252        definition_changes_container = InstanceContainer(unique_container_name)
253        definition_changes_container.setDefinition(container_stack.getBottom().getId())
254        definition_changes_container.setMetaDataEntry("type", "definition_changes")
255        definition_changes_container.setMetaDataEntry("setting_version", CuraApplication.SettingVersion)
256
257        registry.addContainer(definition_changes_container)
258        container_stack.definitionChanges = definition_changes_container
259
260        return definition_changes_container
261