[docs]@attrs.define(frozen=True,slots=False)classBallSet:"""A ballset. **What is a ballset?** A ballset specifies the set that a ball belongs to. **Why is it important?** Ballsets are important for properly rendering balls in a scene. By specifying a ballset for a ball, you declare the visual texture / skin that the ball should be wrapped in. If a ball's ballset is not declared, it will be rendered with the default skin. **What ballsets are available?** See :func:`pooltool.objects.get_ballset_names`. **Where are ballsets stored?** Each ballset is represented as a subdirectory within the following directory: .. code:: $ echo $(python -c "import pooltool; print(pooltool.__file__[:-12])")/models/balls Each ball model is a ``.glb`` file. Its base name represents the model's ID, and matching ball IDs will be textured by this model. To associate multiple ball IDs to the same model, a ``conversion.json`` file is used. For example, see how the ``generic_snooker`` ballset matches the red ball IDs to the same model ID: .. code:: $ cat $(python -c "import pooltool; print(pooltool.__file__[:-12])")/models/balls/generic_snooker/conversion.json { "red_01": "red", "red_02": "red", "red_03": "red", "red_04": "red", "red_05": "red", "red_06": "red", "red_07": "red", "red_08": "red", "red_09": "red", "red_10": "red", "red_11": "red", "red_12": "red", "red_13": "red", "red_14": "red", "red_15": "red" } Attributes: name: The name of the ballset. During instantiation, the validity of this name will be checked, and a ValueError will be raised if the ballset doesn't exist. """name:str=attrs.field()@name.validator# type: ignoredef_check_name(self,_,value):path=(model_dir/"balls")/valueifnotpath.exists()andnotpath.is_dir():raiseValueError(f"Invalid BallSet: '{value}'. {path} must exist as directory")@propertydefpath(self)->Path:"""The path of the ballset directory This directory holds the ball models. """return(model_dir/"balls")/self.name@cached_propertydef_conversion_dict(self)->dict[str,str]:conversion_path=self.path/_expected_conversion_nameifconversion_path.exists():returnserialize.conversion.structure_from(conversion_path,dict[str,str])return{}@propertydefids(self)->list[str]:return[path.stemforpathinself.path.glob("*glb")]def_ensure_valid(self,id:str)->str:"""Checks that Ball ID matches to a model in in ballset. Args: id: The ball ID. Raises: ValueError: If Ball ID doesn't match to BallSet. Returns: model_id: The model ID associated with the passed ball ID. """ifidinself.ids:returnidifidinself._conversion_dict:returnself._conversion_dict[id]raiseValueError(f"Ball ID '{id}' doesn't match to BallSet: {self.ids}")
[docs]defball_path(self,id:str)->Path:"""The model path used for a given ball ID Args: id: The ball ID. Raises: ValueError: If Ball ID doesn't match to the ballset. Returns: Path: The model path. """model_id=self._ensure_valid(id)returnself.path/f"{model_id}.glb"
[docs]defget_ballset(name:str)->BallSet:"""Return the ballset with the given name. Args: name: The name of the ballset. To list available ballset names, call :func:`pooltool.objects.get_ballset_names`. Raises: ValueError: If Ball ID doesn't match to the ballset. Returns: BallSet: A ballset. """assertnameinballsets,(f"Unknown ballset name: {name}, available: {get_ballset_names()}")returnballsets[name]
[docs]defget_ballset_names()->list[str]:"""Returns a list of available ballset names"""returnlist(ballsets.keys())