Skip to content

npxpy.nodes.project.Project

Bases: Node

Class: project nodes.

Attributes:

Name Type Description
presets list

List of presets for the project.

resources list

List of resources for the project.

project_info dict

Information about the project including author, objective, resin, substrate, and creation date.

Source code in npxpy/nodes/project.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
class Project(Node):
    """
    Class: project nodes.

    Attributes:
        presets (list): List of presets for the project.
        resources (list): List of resources for the project.
        project_info (dict): Information about the project including author, objective, resin, substrate, and creation date.
    """

    def __init__(
        self,
        objective: str,
        resin: str,
        substrate: str,
        auto_load_presets: bool = False,
        auto_load_meshes: bool = False,
        auto_load_images: bool = False,
    ):
        """
        Initialize the project with the specified parameters.

        Parameters:
            objective (str): Objective of the project.
            resin (str): Resin used in the project.
            substrate (str): Substrate used in the project.
            auto_load_presets (bool): Whether or not to automatically load any attached presets.
            auto_load_meshes (bool): Whether or not to automatically load any attached meshes.
            auto_load_images (bool): Whether or not to automatically load any attached images.

        Raises:
            ValueError: If any of the parameters have invalid values.
        """
        super().__init__(node_type="project", name="Project")

        self.objective = objective
        self.resin = resin
        self.substrate = substrate

        self._presets = []
        self._resources = []
        self.project_info = {
            "author": os.getlogin(),
            "objective": self.objective,
            "resist": self.resin,
            "substrate": self.substrate,
            "creation_date": datetime.now().replace(microsecond=0).isoformat(),
        }
        self._visibility_in_plotter_disabled = []
        self._loaded_resource_ids = set()
        self._loaded_preset_ids = set()
        self._auto_load_presets = auto_load_presets
        self._auto_load_meshes = auto_load_meshes
        self._auto_load_images = auto_load_images

    # Setters for the attributes with validation
    @property
    def objective(self):
        return self._objective

    @objective.setter
    def objective(self, value: str):
        if value == "10x":
            self._objective = "10xW"
        else:
            valid_objectives = {
                "10xW",
                "25x",
                "63x",
                "*",
            }
            if value not in valid_objectives:
                raise ValueError(
                    f"Invalid objective: {value}. Must be one of {valid_objectives}."
                )
            self._objective = value

    @property
    def resin(self):
        return self._resin

    @resin.setter
    def resin(self, value: str):
        valid_resins = {
            "IP-Dip",
            "IP-Dip2",
            "IP-L",
            "IP-n162",
            "IP-PDMS",
            "IP-S",
            "IP-Visio",
            "IPX-Clear",
            "IPX-Q",
            "IPX-S",
            "*",
        }
        if value not in valid_resins:
            raise ValueError(
                f"Invalid resin: {value}. Must be one of {valid_resins}."
            )
        self._resin = value

    @property
    def substrate(self):
        return self._substrate

    @substrate.setter
    def substrate(self, value: str):
        valid_substrates = {"*", "Si", "FuSi"}
        if value not in valid_substrates:
            raise ValueError(
                f"Invalid substrate: {value}. Must be one of {valid_substrates}."
            )
        self._substrate = value

    # Read-only public properties
    @property
    def presets(self):
        """Get the list of presets."""
        return self._presets

    @property
    def resources(self):
        """Get the list of resources."""
        return self._resources

    def load_resources(self, *resourcess: Union[Resource, List[Resource]]):
        """
        Adds resources to the resources list, skipping duplicates based on UUID.
        """
        for resources in resourcess:
            if not isinstance(resources, list):
                resources = [resources]

            if not all(
                isinstance(resource, Resource) for resource in resources
            ):
                raise TypeError(
                    "All resources must be instances of the Resource class or its subclasses."
                )

            for resource in resources:
                if resource.id in self._loaded_resource_ids:
                    warnings.warn(
                        f"Resource with ID {resource.id} already loaded. Skipping."
                    )
                    continue
                self._resources.append(resource)
                self._loaded_resource_ids.add(resource.id)

    def load_presets(self, *presetss: Union[Preset, List[Preset]]):
        """
        Adds presets to the presets list, skipping duplicates based on UUID.
        """
        for presets in presetss:
            if not isinstance(presets, list):
                presets = [presets]

            if not all(isinstance(preset, Preset) for preset in presets):
                raise TypeError(
                    "All presets must be instances of the Preset class."
                )

            for preset in presets:
                if preset.id in self._loaded_preset_ids:
                    warnings.warn(
                        f"Preset with ID {preset.id} already loaded. Skipping."
                    )
                    continue
                self._presets.append(preset)
                self._loaded_preset_ids.add(preset.id)

    def _auto_load_resources_presets(self):
        """
        Loads all Resource/Preset nodes into the Project node they
        are attached to.
        """
        all_structures = self.grab_all_nodes_bfs("structure")
        all_marker_aligners = self.grab_all_nodes_bfs("marker_alignment")

        if self._auto_load_meshes:
            all_meshes = [
                structure.mesh
                for structure in all_structures
                if structure._mesh
            ]
            self.load_resources(all_meshes)

        if self._auto_load_presets:
            all_presets = [structure.preset for structure in all_structures]
            self.load_presets(all_presets)

        if self._auto_load_images:
            all_images = [
                marker_aligner.image for marker_aligner in all_marker_aligners
            ]
            self.load_resources(all_images)

    def _create_toml_data(
        self, presets: List[Any], resources: List[Any], nodes: List[Node]
    ) -> str:
        """
        Creates TOML data for the project.
        """
        data = {
            "presets": [preset.to_dict() for preset in presets],
            "resources": [resource.to_dict() for resource in resources],
            "nodes": [node.to_dict() for node in nodes],
        }
        return toml.dumps(data)

    def _create_project_info(self, project_info_json: Dict[str, Any]) -> str:
        """
        Creates JSON data for project info.
        """
        return json.dumps(project_info_json, indent=4)

    def _add_file_to_zip(
        self, zip_file: zipfile.ZipFile, file_path: str, arcname: str
    ):
        """
        Adds a file to a zip archive.
        """
        with open(file_path, "rb") as f:
            zip_file.writestr(arcname, f.read())

    def nano(self, project_name: str = "Project", path: str = "./"):
        """
        Creates a .nano file for the project.
        """
        print("npxpy: Attempting to create .nano-file...")

        # Trigger user warning if project contains structures outside scenes
        for i_node in self.all_descendants:
            if i_node._type == "structure":
                if not "scene" in [i._type for i in self.all_ancestors]:
                    UserWarning("Structures have to be inside Scene nodes!")

        # Autoload presets/resources if desired
        if any(
            [
                self._auto_load_images,
                self._auto_load_meshes,
                self._auto_load_presets,
            ]
        ):
            self._auto_load_resources_presets()

        # Ensure the path ends with a slash
        if not path.endswith("/"):
            path += "/"

        # Prepare paths and data
        nano_file_path = os.path.join(path, f"{project_name}.nano")
        toml_data = self._create_toml_data(
            self._presets, self._resources, [self] + self.all_descendants
        )
        project_info_data = self._create_project_info(self.project_info)

        with zipfile.ZipFile(
            nano_file_path, "w", zipfile.ZIP_STORED
        ) as nano_zip:
            # Add the __main__.toml to the zip file
            nano_zip.writestr("__main__.toml", toml_data)

            # Add the project_info.json to the zip file
            nano_zip.writestr("project_info.json", project_info_data)

            # Add the resources to the zip file
            already_zipped_resources = set()
            for resource in self._resources:
                src_path = resource.file_path
                arcname = resource.safe_path
                if (
                    os.path.isfile(src_path)
                    and arcname not in already_zipped_resources
                ):
                    self._add_file_to_zip(nano_zip, src_path, arcname)
                    already_zipped_resources.add(arcname)
                elif not os.path.isfile(src_path):
                    print(f"File not found: {src_path}")
                else:
                    print(f"File already loaded: {src_path}")
        print("npxpy: .nano-file created successfully.")

    def to_dict(self) -> Dict:
        """
        Convert the Project object into a dictionary.
        """
        node_dict = super().to_dict()
        node_dict.update(
            {
                "objective": self.objective,
                "resin": self.resin,
                "substrate": self.substrate,
            }
        )
        return node_dict

presets property

Get the list of presets.

resources property

Get the list of resources.

__init__(objective, resin, substrate, auto_load_presets=False, auto_load_meshes=False, auto_load_images=False)

Initialize the project with the specified parameters.

Parameters:

Name Type Description Default
objective str

Objective of the project.

required
resin str

Resin used in the project.

required
substrate str

Substrate used in the project.

required
auto_load_presets bool

Whether or not to automatically load any attached presets.

False
auto_load_meshes bool

Whether or not to automatically load any attached meshes.

False
auto_load_images bool

Whether or not to automatically load any attached images.

False

Raises:

Type Description
ValueError

If any of the parameters have invalid values.

Source code in npxpy/nodes/project.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def __init__(
    self,
    objective: str,
    resin: str,
    substrate: str,
    auto_load_presets: bool = False,
    auto_load_meshes: bool = False,
    auto_load_images: bool = False,
):
    """
    Initialize the project with the specified parameters.

    Parameters:
        objective (str): Objective of the project.
        resin (str): Resin used in the project.
        substrate (str): Substrate used in the project.
        auto_load_presets (bool): Whether or not to automatically load any attached presets.
        auto_load_meshes (bool): Whether or not to automatically load any attached meshes.
        auto_load_images (bool): Whether or not to automatically load any attached images.

    Raises:
        ValueError: If any of the parameters have invalid values.
    """
    super().__init__(node_type="project", name="Project")

    self.objective = objective
    self.resin = resin
    self.substrate = substrate

    self._presets = []
    self._resources = []
    self.project_info = {
        "author": os.getlogin(),
        "objective": self.objective,
        "resist": self.resin,
        "substrate": self.substrate,
        "creation_date": datetime.now().replace(microsecond=0).isoformat(),
    }
    self._visibility_in_plotter_disabled = []
    self._loaded_resource_ids = set()
    self._loaded_preset_ids = set()
    self._auto_load_presets = auto_load_presets
    self._auto_load_meshes = auto_load_meshes
    self._auto_load_images = auto_load_images

load_presets(*presetss)

Adds presets to the presets list, skipping duplicates based on UUID.

Source code in npxpy/nodes/project.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def load_presets(self, *presetss: Union[Preset, List[Preset]]):
    """
    Adds presets to the presets list, skipping duplicates based on UUID.
    """
    for presets in presetss:
        if not isinstance(presets, list):
            presets = [presets]

        if not all(isinstance(preset, Preset) for preset in presets):
            raise TypeError(
                "All presets must be instances of the Preset class."
            )

        for preset in presets:
            if preset.id in self._loaded_preset_ids:
                warnings.warn(
                    f"Preset with ID {preset.id} already loaded. Skipping."
                )
                continue
            self._presets.append(preset)
            self._loaded_preset_ids.add(preset.id)

load_resources(*resourcess)

Adds resources to the resources list, skipping duplicates based on UUID.

Source code in npxpy/nodes/project.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def load_resources(self, *resourcess: Union[Resource, List[Resource]]):
    """
    Adds resources to the resources list, skipping duplicates based on UUID.
    """
    for resources in resourcess:
        if not isinstance(resources, list):
            resources = [resources]

        if not all(
            isinstance(resource, Resource) for resource in resources
        ):
            raise TypeError(
                "All resources must be instances of the Resource class or its subclasses."
            )

        for resource in resources:
            if resource.id in self._loaded_resource_ids:
                warnings.warn(
                    f"Resource with ID {resource.id} already loaded. Skipping."
                )
                continue
            self._resources.append(resource)
            self._loaded_resource_ids.add(resource.id)

nano(project_name='Project', path='./')

Creates a .nano file for the project.

Source code in npxpy/nodes/project.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
def nano(self, project_name: str = "Project", path: str = "./"):
    """
    Creates a .nano file for the project.
    """
    print("npxpy: Attempting to create .nano-file...")

    # Trigger user warning if project contains structures outside scenes
    for i_node in self.all_descendants:
        if i_node._type == "structure":
            if not "scene" in [i._type for i in self.all_ancestors]:
                UserWarning("Structures have to be inside Scene nodes!")

    # Autoload presets/resources if desired
    if any(
        [
            self._auto_load_images,
            self._auto_load_meshes,
            self._auto_load_presets,
        ]
    ):
        self._auto_load_resources_presets()

    # Ensure the path ends with a slash
    if not path.endswith("/"):
        path += "/"

    # Prepare paths and data
    nano_file_path = os.path.join(path, f"{project_name}.nano")
    toml_data = self._create_toml_data(
        self._presets, self._resources, [self] + self.all_descendants
    )
    project_info_data = self._create_project_info(self.project_info)

    with zipfile.ZipFile(
        nano_file_path, "w", zipfile.ZIP_STORED
    ) as nano_zip:
        # Add the __main__.toml to the zip file
        nano_zip.writestr("__main__.toml", toml_data)

        # Add the project_info.json to the zip file
        nano_zip.writestr("project_info.json", project_info_data)

        # Add the resources to the zip file
        already_zipped_resources = set()
        for resource in self._resources:
            src_path = resource.file_path
            arcname = resource.safe_path
            if (
                os.path.isfile(src_path)
                and arcname not in already_zipped_resources
            ):
                self._add_file_to_zip(nano_zip, src_path, arcname)
                already_zipped_resources.add(arcname)
            elif not os.path.isfile(src_path):
                print(f"File not found: {src_path}")
            else:
                print(f"File already loaded: {src_path}")
    print("npxpy: .nano-file created successfully.")

to_dict()

Convert the Project object into a dictionary.

Source code in npxpy/nodes/project.py
310
311
312
313
314
315
316
317
318
319
320
321
322
def to_dict(self) -> Dict:
    """
    Convert the Project object into a dictionary.
    """
    node_dict = super().to_dict()
    node_dict.update(
        {
            "objective": self.objective,
            "resin": self.resin,
            "substrate": self.substrate,
        }
    )
    return node_dict