diff --git a/pyslm/geometry/__init__.py b/pyslm/geometry/__init__.py index 97381db..65ddce1 100644 --- a/pyslm/geometry/__init__.py +++ b/pyslm/geometry/__init__.py @@ -1,11 +1,13 @@ try: - from libSLM import Header, BuildStyle, Model, Layer, LayerGeometry, ContourGeometry, HatchGeometry, PointsGeometry + from libSLM import Header, BuildStyle, Model, Layer, LayerGeometry, ContourGeometry, HatchGeometry, PointsGeometry, LaserMode except BaseException as E: """ The libSLM library is not available so instead use the fallback python equivalent in order to store the layer and geometry information for use later. This removes the capability to export to machine build file format """ - from .geometry import Header, BuildStyle, Model, Layer, LayerGeometry, ContourGeometry, HatchGeometry, PointsGeometry + from .geometry import Header, BuildStyle, Model, Layer, LayerGeometry, ContourGeometry, HatchGeometry, PointsGeometry, LaserMode + +from .utils import * diff --git a/pyslm/geometry/geometry.py b/pyslm/geometry/geometry.py index da1a48f..ea73193 100644 --- a/pyslm/geometry/geometry.py +++ b/pyslm/geometry/geometry.py @@ -6,6 +6,13 @@ from typing import Any, List, Optional, Tuple +class LaserMode(Enum): + CW = 0 + """ Continious Wave """ + + PULSE = 1 + """ Pulsed mode (Default option) """ + class Header: """ The Header provides basic information about the machine build file, such as the name of the file @@ -37,6 +44,8 @@ def __init__(self): self._laserMode = 1 self._pointDistance = 0 self._pointExposureTime = 0 + self._jumpDelay = 0 + self._jumpSpeed = 0 @property def bid(self) -> int: @@ -76,6 +85,10 @@ def laserId(self, value: int): @property def laserMode(self) -> int: + """ + Determines the laser mode to use via :class:`LaserMode` which is either continious wave (CW) or + pulsed (Pulsed) laser operation + """ return self._laserMode @laserMode.setter @@ -129,6 +142,31 @@ def pointDistance(self) -> int: def pointDistance(self, pointDistance: int): self._pointDistance = pointDistance + @property + def jumpDelay(self) ->int: + """ The jump delay time (usually expressed as an integer :math:`\\mu m`) """ + return self._jumpDelay + + @jumpDelay.setter + def jumpDelay(self, delay: int): + """ + The jump speed between scan vectors (usually expressed as an integer :math:`mm/s`). This must be set to + zero (default) if it is not explicitly used. + """ + self._jumpDelay = delay + + @property + def jumpSpeed(self) -> int: + """ + The jump speed between scan vectors (usually expressed as an integer :math:`mm/s`). This must be set to + zero (default) if it is not explicitly used. + """ + return self._jumpSpeed + + @jumpSpeed.setter + def jumpSpeed(self, speed: int): + self._jumpSpeed = speed + def setStyle(self, bid: int, focus: int, power: float, pointExposureTime: int, pointExposureDistance: int, laserSpeed: Optional[float] = 0.0, laserId: Optional[int] = 1, laserMode: Optional[int] = 1, @@ -209,7 +247,7 @@ def buildStyleDescription(self, description: str): @property def buildStyleName(self) -> str: - """ The BuildStyle applied to the Model""" + """ The BuildStyle name applied to the Model""" return self._buildStyleName @buildStyleName.setter @@ -398,6 +436,15 @@ def __init__(self, z: Optional[int] = 0, id: Optional[int] = 0): self._id = id self._geometry = [] self._name = "" + self._layerFilePosition = 0 + + @property + def layerFilePosition(self): + """ The position of the layer in the build file, when available. """ + return self._layerFilePosition + + def isLoaded(self) -> bool: + return True @property def name(self) -> str: diff --git a/pyslm/geometry/utils.py b/pyslm/geometry/utils.py new file mode 100644 index 0000000..fc4fa59 --- /dev/null +++ b/pyslm/geometry/utils.py @@ -0,0 +1,154 @@ +from typing import List, Optional, Tuple, Union +import numpy as np +from warnings import warn + +from . import Layer, LayerGeometry, HatchGeometry, ContourGeometry, PointsGeometry, BuildStyle, Model + +def getBuildStyleById(models: List[Model], mid: int, bid: int) -> Union[BuildStyle, None]: + """ + Returns the Buildstyle found from a list of :class:`Model` given a model id and build id. + + :param models: A list of models + :param mid: The selected model id + :param bid: The selected buildstyle id + + :return: The BuildStyle if found or `None` + """ + model = next(x for x in models if x.mid == mid) + + if model: + bstyle = next(x for x in model.buildStyles if x.bid == bid) + + return bstyle + + return None + + +def getModel(models: List[Model], mid: int) -> Union[BuildStyle, None]: + """ + Returns the Model found from a list of :class:`Model` given a model id and build id. + + :param models: A list of models + :param mid: The selected model id + + :return: The BuildStyle if found or `None` + """ + model = next(x for x in models if x.mid == mid) + + return model if model else None + + +class ModelValidator: + + @staticmethod + def _buildStyleIndex(models): + + index = {} + for model in models: + for bstyle in model.buildStyles: + index[model.mid, bstyle.bid] = bstyle + + return index + + @staticmethod + def _modelIndex(models): + + index = {} + for model in models: + index[model.mid] = model + + return index + + @staticmethod + def validateBuildStyle(bstyle: BuildStyle): + if bstyle.bid < 1 or not isinstance(bstyle.bid, int): + raise Exception("BuildStyle ({:d}) should have a positive integer id".format(bstyle.bid)) + + if bstyle.laserPower < 0 or not isinstance(bstyle.laserPower, float): + raise Exception("BuildStyle({:d}).laserPower must be a positive integer".format(bstyle.bid)) + + if bstyle.laserSpeed < 0 or not isinstance(bstyle.laserSpeed, float): + raise Exception("BuildStyle({:d}).laserPower must be a positive integer".format(bstyle.bid)) + + if bstyle.pointDistance < 1 or not isinstance(bstyle.pointDistance, int): + raise Exception("BuildStyle({:d}).pointDistance must be a positive integer (>0)".format(bstyle.bid)) + + if bstyle.pointExposureTime < 1 or not isinstance(bstyle.pointExposureTime, int): + raise Exception("BuildStyle({:d}).pointExposureTime must be a positive integer (>0)".format(bstyle.bid)) + + if bstyle.jumpDelay < 0 or not isinstance(bstyle.jumpDelay, int): + raise Exception("BuildStyle({:d}).jumpDelay must be a positive integer ".format(bstyle.bid)) + + if bstyle.jumpSpeed < 0 or not isinstance(bstyle.jumpSpeed, int): + raise Exception("BuildStyle({:d}).jumpSpeed must be a positive integer (>0)".format(bstyle.bid)) + + if bstyle.laserId < 1 or not isinstance(bstyle.laserId, int): + raise Exception("BuildStyle({:d}).laserId must be a positive integer (>0)".format(bstyle.bid)) + + if bstyle.laserMode < 0 or not isinstance(bstyle.laserMode, int): + raise Exception("BuildStyle({:d}).laserMode must be a positive integer (0)".format(bstyle.bid)) + + if bstyle.laserId < 1 or not isinstance(bstyle.laserId, int): + raise Exception("BuildStyle({:d}).laserId must be a positive integer (>0)".format(bstyle.bid)) + + @staticmethod + def validateModel(model: Model): + + bstyleList = [] + + if model.topLayerId == 0: + raise Exception('The top layer id of Model ({:s}) has not been set'.format(model.name)) + + if len(model.buildStyles) == 0: + raise Exception('Model ({:s} does not contain any build styles'.format(model.name)) + + for bstyle in model.buildStyles: + + if bstyle in bstyleList: + raise Exception('Model ({:s} does not contain build styles with unique id'.format(model.name)) + else: + bstyleList.append(bstyle.bid) + + ModelValidator.validateBuildStyle(bstyle) + + @staticmethod + def validateBuild(models: List[Model], layers: List[Layer]): + + # Build the indices for the models and the build styles + modelIdx = ModelValidator._modelIndex(models) + bstyleIdx = ModelValidator._buildStyleIndex(models) + modelTopLayerIdx = dict() + layerDelta = np.array([layer.z for layer in layers]) + + for model in models: + ModelValidator.validateModel(model) + + """ Iterate across each layer and validate the input """ + for layer in layers: + + if len(layer.geometry) == 0: + warn("Warning: Layer ({:d}) does not contain any layer geometry. It is advised to check this is valid".format(layer.layerId)) + + for layerGeom in layer.geometry: + model = modelIdx.get(model.mid, None) + + if not model: + raise Exception("Layer Geometry in layer ({:d} @ {:.3f}) has not been assigned a model".format(layer.layerId, layer.z)) + + bstyle = bstyleIdx.get((layerGeom.mid, layerGeom.bid), None) + + if not bstyle: + raise Exception("Layer Geometry in layer ({:d} @ {:.3f}) has not been assigned a buildstyle".format(layer.layerId, layer.z)) + + modelTopLayerIdx[model.mid] = layer.layerId + + """ Check to see if all models were assigned to a layer geometry""" + for model in models: + if modelTopLayerIdx.get(model.mid, False): + warn("Warning: Model({:s} was not used in any layer)".format(model.name)) + + if model.topLayerId != modelTopLayerIdx[model.mid]: + raise Exception("Top Layer Id of Model ({:d}) differs in the layers used ({:d})".format(model.topLayerId, + modelTopLayerIdx[model.mid])) + + return True