diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c5da9b562..293a74758d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,39 +11,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `create_id` to `compas_ghpython.utilities`. (moved from `compas_fab`) * Added representation for features in `compas.datastructures.Part`. -* Added `split` and `split_by_length` to `compas.geometry.Polyline`. +* Added `split` and `split_by_length` to `compas.geometry.Polyline`. * Added `compas.rpc.XFunc`. -* Added `compas.data.Data.validate_jsonstring`. -* Added `compas.data.Data.validate_jsondata`. -* Added `compas.data.Data.JSONSCHEMA`. -* Added `compas.data.json_validate`. -* Added `compas.datastructures.Graph.JSONSCHEMA`. +* Added attribute `compas.color.Color.DATASCHEMA`. +* Added attribute `compas.data.Data.DATASCHEMA`. +* Added attribute `compas.datastructures.Graph.DATASCHEMA`. +* Added attribute `compas.datastructures.Halfedge.DATASCHEMA`. +* Added attribute `compas.datastructures.Halfface.DATASCHEMA`. +* Added attribute `compas.geometry.Arc.DATASCHEMA`. +* Added attribute `compas.geometry.Bezier.DATASCHEMA`. +* Added attribute `compas.geometry.Box.DATASCHEMA`. +* Added attribute `compas.geometry.Capsule.DATASCHEMA`. +* Added attribute `compas.geometry.Circle.DATASCHEMA`. +* Added attribute `compas.geometry.Cone.DATASCHEMA`. +* Added attribute `compas.geometry.Cylinder.DATASCHEMA`. +* Added attribute `compas.geometry.Ellipse.DATASCHEMA`. +* Added attribute `compas.geometry.Frame.DATASCHEMA`. +* Added attribute `compas.geometry.Line.DATASCHEMA`. +* Added attribute `compas.geometry.NurbsCurve.DATASCHEMA`. +* Added attribute `compas.geometry.NurbsSurface.DATASCHEMA`. +* Added attribute `compas.geometry.Plane.DATASCHEMA`. +* Added attribute `compas.geometry.Point.DATASCHEMA`. +* Added attribute `compas.geometry.Pointcloud.DATASCHEMA`. +* Added attribute `compas.geometry.Polygon.DATASCHEMA`. +* Added attribute `compas.geometry.Polyhedron.DATASCHEMA`. +* Added attribute `compas.geometry.Polyline.DATASCHEMA`. +* Added attribute `compas.geometry.Sphere.DATASCHEMA`. +* Added attribute `compas.geometry.Torus.DATASCHEMA`. +* Added attribute `compas.geometry.Quaternion.DATASCHEMA`. +* Added attribute `compas.geometry.Vector.DATASCHEMA`. +* Added implementation of property `compas.color.Color.data`. +* Added `compas.data.Data.validate_data`. +* Added `compas.data.Data.__jsondump__`. +* Added `compas.data.Data.__jsonload__`. +* Added `compas.data.schema.dataclass_dataschema`. +* Added `compas.data.schema.dataclass_typeschema`. +* Added `compas.data.schema.dataclass_jsonschema`. +* Added `compas.data.schema.compas_jsonschema`. +* Added `compas.data.schema.compas_dataclasses`. * Added `compas.datastructures.Graph.to_jsondata`. * Added `compas.datastructures.Graph.from_jsondata`. -* Added `compas.datastructures.Halfedge.JSONSCHEMA`. -* Added `compas.datastructures.Halfface.JSONSCHEMA`. -* Added `compas.geometry.Arc.JSONSCHEMA`. -* Added `compas.geometry.Bezier.JSONSCHEMA`. -* Added `compas.geometry.Box.JSONSCHEMA`. -* Added `compas.geometry.Capsule.JSONSCHEMA`. -* Added `compas.geometry.Circle.JSONSCHEMA`. -* Added `compas.geometry.Cone.JSONSCHEMA`. -* Added `compas.geometry.Cylinder.JSONSCHEMA`. -* Added `compas.geometry.Ellipse.JSONSCHEMA`. -* Added `compas.geometry.Frame.JSONSCHEMA`. -* Added `compas.geometry.Line.JSONSCHEMA`. -* Added `compas.geometry.NurbsCurve.JSONSCHEMA`. -* Added `compas.geometry.NurbsSurface.JSONSCHEMA`. -* Added `compas.geometry.Plane.JSONSCHEMA`. -* Added `compas.geometry.Point.JSONSCHEMA`. -* Added `compas.geometry.Pointcloud.JSONSCHEMA`. -* Added `compas.geometry.Polygon.JSONSCHEMA`. -* Added `compas.geometry.Polyhedron.JSONSCHEMA`. -* Added `compas.geometry.Polyline.JSONSCHEMA`. -* Added `compas.geometry.Sphere.JSONSCHEMA`. -* Added `compas.geometry.Torus.JSONSCHEMA`. -* Added `compas.geometry.Quaternion.JSONSCHEMA`. -* Added `compas.geometry.Vector.JSONSCHEMA`. * Added `compas.datastructures.Halfedge.halfedge_loop_vertices`. * Added `compas.datastructures.Halfedge.halfedge_strip_faces`. * Added `compas.datastructures.Mesh.vertex_point`. @@ -59,9 +66,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.datastructures.Graph.node_index` and `compas.datastructures.Graph.index_node`. * Added `compas.datastructures.Graph.edge_index` and `compas.datastructures.Graph.index_edge`. * Added `compas.datastructures.Halfedge.vertex_index` and `compas.datastructures.Halfedge.index_vertex`. -* Added `compas.geometry.trimesh_descent_numpy`. -* Added `compas.geometry.trimesh_gradient_numpy`. -* Added a deprecation warning when using `Artist` for `Plotter`. * Added `compas.geometry.Hyperbola`. * Added `compas.geometry.Parabola`. * Added `compas.geometry.PlanarSurface`. @@ -69,6 +73,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.geometry.SphericalSurface`. * Added `compas.geometry.ConicalSurface`. * Added `compas.geometry.ToroidalSurface`. +* Added `compas.geometry.trimesh_descent_numpy`. +* Added `compas.geometry.trimesh_gradient_numpy`. * Added `compas.geometry.boolean_union_polygon_polygon` pluggable. * Added `compas.geometry.boolean_intersection_polygon_polygon` pluggable. * Added `compas.geometry.boolean_difference_polygon_polygon` pluggable. @@ -95,6 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `vertices_and_faces_to_rhino` to `compas_rhino.conversions`. * Added `polyhedron_to_rhino` to `compas_rhino.conversions`. * Added `from_mesh` plugin to `compas_rhino.geometry.RhinoBrep`. +* Added `compas.geometry.Plane.worldYZ` and `compas.geometry.Plane.worldZX`. ### Changed @@ -159,7 +166,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Changed base class of `compas.geometry.Line` to `compas.geometry.Curve.` * Changed base class of `compas.geometry.Polyline` to `compas.geometry.Curve.` * Changed `compas.geometry.oriented_bounding_box_numpy` to minimize volume. +* Fixed data interface `compas.datastructures.Assembly` and `compas.datastructures.Part`. +* Changed data property of `compas.datastructures.Graph` to contain only JSON compatible data. +* Changed data property of `compas.datastructures.Halfedge` to contain only JSON compatible data. +* Changed data property of `compas.datastructures.Halfface` to contain only JSON compatible data. +* Changed `__repr__` of `compas.geometry.Point` and `compas.geometry.Vector` to not use limited precision (`compas.PRECISION`) to ensure proper object reconstruction through `eval(repr(point))`. * Changed `compas.datastructures.Graph.delete_edge` to delete invalid (u, u) edges and not delete edges in opposite directions (v, u) +* Fixed bug in `compas.datastructures.Mesh.insert_vertex`. * Fixed bug in `compas.geometry.angle_vectors_signed`. ### Removed @@ -172,36 +185,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed `compas.datastructures.Halfedge.get_any_vertices`. * Removed `compas.datastructures.Halfedge.get_any_face`. * Removed "schemas" folder and all contained `.json` files from `compas.data`. -* Removed `compas.data.Data.DATASCHEMA`. -* Removed `compas.data.Data.JSONSCHEMANAME`. * Removed `compas.data.Data.jsondefinititions`. * Removed `compas.data.Data.jsonvalidator`. -* Removed `compas.data.Data.validate_data`. -* Removed `compas.datastructures.Graph.DATASCHEMA` and `compas.datastructures.Graph.JSONSCHEMANAME`. -* Removed `compas.datastructures.Halfedge.DATASCHEMA` and `compas.datastructures.Halfedge.JSONSCHEMANAME`. -* Removed `compas.datastructures.Halfface.DATASCHEMA` and `compas.datastructures.Halfface.JSONSCHEMANAME`. -* Removed `compas.geometry.Arc.DATASCHEMA` and `compas.geometry.Arc.JSONSCHEMANAME`. -* Removed `compas.geometry.Bezier.DATASCHEMA` and `compas.geometry.Bezier.JSONSCHEMANAME`. -* Removed `compas.geometry.Box.DATASCHEMA` and `compas.geometry.Box.JSONSCHEMANAME`. -* Removed `compas.geometry.Capsule.DATASCHEMA` and `compas.geometry.Capsule.JSONSCHEMANAME`. -* Removed `compas.geometry.Circle.DATASCHEMA` and `compas.geometry.Circle.JSONSCHEMANAME`. -* Removed `compas.geometry.Cone.DATASCHEMA` and `compas.geometry.Cone.JSONSCHEMANAME`. -* Removed `compas.geometry.Cylinder.DATASCHEMA` and `compas.geometry.Cylinder.JSONSCHEMANAME`. -* Removed `compas.geometry.Ellipse.DATASCHEMA` and `compas.geometry.Ellipse.JSONSCHEMANAME`. -* Removed `compas.geometry.Frame.DATASCHEMA` and `compas.geometry.Frame.JSONSCHEMANAME`. -* Removed `compas.geometry.Line.DATASCHEMA` and `compas.geometry.Line.JSONSCHEMANAME`. -* Removed `compas.geometry.NurbsCurve.DATASCHEMA` and `compas.geometry.NurbsCurve.JSONSCHEMANAME`. -* Removed `compas.geometry.NurbsSurface.DATASCHEMA` and `compas.geometry.NurbsSurface.JSONSCHEMANAME`. -* Removed `compas.geometry.Plane.DATASCHEMA` and `compas.geometry.Plane.JSONSCHEMANAME`. -* Removed `compas.geometry.Point.DATASCHEMA` and `compas.geometry.Point.JSONSCHEMANAME`. -* Removed `compas.geometry.Pointcloud.DATASCHEMA` and `compas.geometry.Pointcloud.JSONSCHEMANAME`. -* Removed `compas.geometry.Polygon.DATASCHEMA` and `compas.geometry.Polygon.JSONSCHEMANAME`. -* Removed `compas.geometry.Polyhedron.DATASCHEMA` and `compas.geometry.Polyhedron.JSONSCHEMANAME`. -* Removed `compas.geometry.Polyline.DATASCHEMA` and `compas.geometry.Polyline.JSONSCHEMANAME`. -* Removed `compas.geometry.Sphere.DATASCHEMA` and `compas.geometry.Sphere.JSONSCHEMANAME`. -* Removed `compas.geometry.Torus.DATASCHEMA` and `compas.geometry.Torus.JSONSCHEMANAME`. -* Removed `compas.geometry.Quaternion.DATASCHEMA` and `compas.geometry.Quaternion.JSONSCHEMANAME`. -* Removed `compas.geometry.Vector.DATASCHEMA` and `compas.geometry.Vector.JSONSCHEMANAME`. +* Removed `compas.data.Data.validate_json`. +* Removed `compas.data.Data.validate_jsondata`. +* Removed `compas.data.Data.validate_jsonstring`. +* Removed `compas.data.Data.__getstate__`. +* Removed `compas.data.Data.__setstate__`. +* Removed setter of property `compas.data.Data.data` and similar setters in all data classes. +* Removed properties `compas.data.Data.DATASCHEMA` and `compas.data.Data.JSONSCHEMANAME`. +* Removed properties `compas.datastructures.Graph.DATASCHEMA` and `compas.datastructures.Graph.JSONSCHEMANAME`. +* Removed properties `compas.datastructures.Halfedge.DATASCHEMA` and `compas.datastructures.Halfedge.JSONSCHEMANAME`. +* Removed properties `compas.datastructures.Halfface.DATASCHEMA` and `compas.datastructures.Halfface.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Arc.DATASCHEMA` and `compas.geometry.Arc.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Bezier.DATASCHEMA` and `compas.geometry.Bezier.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Box.DATASCHEMA` and `compas.geometry.Box.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Capsule.DATASCHEMA` and `compas.geometry.Capsule.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Circle.DATASCHEMA` and `compas.geometry.Circle.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Cone.DATASCHEMA` and `compas.geometry.Cone.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Cylinder.DATASCHEMA` and `compas.geometry.Cylinder.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Ellipse.DATASCHEMA` and `compas.geometry.Ellipse.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Frame.DATASCHEMA` and `compas.geometry.Frame.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Line.DATASCHEMA` and `compas.geometry.Line.JSONSCHEMANAME`. +* Removed properties `compas.geometry.NurbsCurve.DATASCHEMA` and `compas.geometry.NurbsCurve.JSONSCHEMANAME`. +* Removed properties `compas.geometry.NurbsSurface.DATASCHEMA` and `compas.geometry.NurbsSurface.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Plane.DATASCHEMA` and `compas.geometry.Plane.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Point.DATASCHEMA` and `compas.geometry.Point.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Pointcloud.DATASCHEMA` and `compas.geometry.Pointcloud.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Polygon.DATASCHEMA` and `compas.geometry.Polygon.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Polyhedron.DATASCHEMA` and `compas.geometry.Polyhedron.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Polyline.DATASCHEMA` and `compas.geometry.Polyline.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Sphere.DATASCHEMA` and `compas.geometry.Sphere.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Torus.DATASCHEMA` and `compas.geometry.Torus.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Quaternion.DATASCHEMA` and `compas.geometry.Quaternion.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Vector.DATASCHEMA` and `compas.geometry.Vector.JSONSCHEMANAME`. * Removed `compas.datastructures.Graph.key_index`and `compas.datastructures.Graph.index_key`. * Removed `compas.datastructures.Graph.uv_index`and `compas.datastructures.Graph.index_uv`. * Removed `compas.datastructures.Halfedge.key_index` and `compas.datastructures.Halfedge.index_key`. @@ -214,6 +231,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed class attribute `CONTEXT` from `compas.artists.Artist`. * Removed class attribute `AVAILABLE_CONTEXTS` form `compas.artists.Artist`. * Removed `compas.geometry.Primitive`. +* Removed classmethod `compas.color.Color.from_data`. +* Removed `validate_data` from `compas.data.validators`. +* Removed `json_validate` from `compas.data.json`. ## [1.17.5] 2023-02-16 diff --git a/src/compas/colors/color.py b/src/compas/colors/color.py index 6d7d3c6b251..9a8b59558e7 100644 --- a/src/compas/colors/color.py +++ b/src/compas/colors/color.py @@ -3,7 +3,7 @@ from __future__ import print_function try: - basestring + basestring # type: ignore except NameError: basestring = str @@ -104,6 +104,17 @@ class Color(Data): """ + DATASCHEMA = { + "type": "object", + "properties": { + "red": {"type": "number", "minimum": 0.0, "maximum": 1.0}, + "green": {"type": "number", "minimum": 0.0, "maximum": 1.0}, + "blue": {"type": "number", "minimum": 0.0, "maximum": 1.0}, + "alpha": {"type": "number", "minimum": 0.0, "maximum": 1.0}, + }, + "required": ["red", "green", "blue", "alpha"], + } + def __init__(self, red, green, blue, alpha=1.0, **kwargs): super(Color, self).__init__(**kwargs) self._r = 1.0 @@ -115,27 +126,64 @@ def __init__(self, red, green, blue, alpha=1.0, **kwargs): self.b = blue self.a = alpha + def __repr__(self): + return "{0}({1}, {2}, {3}, alpha={4})".format(type(self).__name__, self.r, self.g, self.b, self.a) + + def __getitem__(self, key): + if key == 0: + return self.r + if key == 1: + return self.g + if key == 2: + return self.b + raise KeyError + + def __len__(self): + return 3 + + def __iter__(self): + return iter(self.rgb) + + def __eq__(self, other): + return all(a == b for a, b in zip(self, other)) + + # -------------------------------------------------------------------------- + # Descriptor + # -------------------------------------------------------------------------- + + def __set_name__(self, owner, name): + self.public_name = name + self.private_name = "_" + name + + def __get__(self, obj, otype=None): + return getattr(obj, self.private_name, None) or self + + def __set__(self, obj, value): + if not obj: + return + + if not value: + return + + if Color.is_rgb255(value): + value = Color.from_rgb255(value[0], value[1], value[2]) + elif Color.is_hex(value): + value = Color.from_hex(value) + else: + value = Color(value[0], value[1], value[2]) + + setattr(obj, self.private_name, value) + # -------------------------------------------------------------------------- - # data + # Data # -------------------------------------------------------------------------- @property def data(self): return {"red": self.r, "green": self.g, "blue": self.b, "alpha": self.a} - @data.setter - def data(self, data): - self.r = data["red"] - self.g = data["green"] - self.b = data["blue"] - self.a = data["alpha"] - - @classmethod - def from_data(cls, data): - return cls(data["red"], data["green"], data["blue"], data["alpha"]) - # -------------------------------------------------------------------------- - # properties + # Properties # -------------------------------------------------------------------------- @property @@ -272,59 +320,7 @@ def saturation(self): return (maxval - minval) / maxval # -------------------------------------------------------------------------- - # descriptor - # -------------------------------------------------------------------------- - - def __set_name__(self, owner, name): - self.public_name = name - self.private_name = "_" + name - - def __get__(self, obj, otype=None): - return getattr(obj, self.private_name, None) or self - - def __set__(self, obj, value): - if not obj: - return - - if not value: - return - - if Color.is_rgb255(value): - value = Color.from_rgb255(value[0], value[1], value[2]) - elif Color.is_hex(value): - value = Color.from_hex(value) - else: - value = Color(value[0], value[1], value[2]) - - setattr(obj, self.private_name, value) - - # -------------------------------------------------------------------------- - # customization - # -------------------------------------------------------------------------- - - def __repr__(self): - return "Color({}, {}, {}, {})".format(self.r, self.g, self.b, self.a) - - def __getitem__(self, key): - if key == 0: - return self.r - if key == 1: - return self.g - if key == 2: - return self.b - raise KeyError - - def __len__(self): - return 3 - - def __iter__(self): - return iter(self.rgb) - - def __eq__(self, other): - return all(a == b for a, b in zip(self, other)) - - # -------------------------------------------------------------------------- - # constructors + # Constructors # -------------------------------------------------------------------------- @classmethod @@ -529,7 +525,7 @@ def from_name(cls, name): return cls.from_rgb255(*rgb255) # -------------------------------------------------------------------------- - # presets + # Presets # -------------------------------------------------------------------------- @classmethod @@ -698,7 +694,7 @@ def pink(cls): return cls(1.0, 0.0, 0.5) # -------------------------------------------------------------------------- - # other presets + # Other presets # -------------------------------------------------------------------------- @classmethod @@ -786,7 +782,7 @@ def silver(cls): # salmon # -------------------------------------------------------------------------- - # methods + # Methods # -------------------------------------------------------------------------- @staticmethod diff --git a/src/compas/data/__init__.py b/src/compas/data/__init__.py index 44e1154e6e2..a3a884ac2c6 100644 --- a/src/compas/data/__init__.py +++ b/src/compas/data/__init__.py @@ -8,12 +8,12 @@ from .validators import is_float3 from .validators import is_float4x4 from .validators import is_item_iterable -from .validators import validate_data from .encoders import DataEncoder from .encoders import DataDecoder from .data import Data - from .json import json_load, json_loads, json_dump, json_dumps +from .schema import dataclass_dataschema, dataclass_typeschema, dataclass_jsonschema +from .schema import compas_dataclasses __all__ = [ "Data", @@ -31,5 +31,8 @@ "json_loads", "json_dump", "json_dumps", - "validate_data", + "dataclass_dataschema", + "dataclass_typeschema", + "dataclass_jsonschema", + "compas_dataclasses", ] diff --git a/src/compas/data/data.py b/src/compas/data/data.py index 5d52603a023..03bc409aa93 100644 --- a/src/compas/data/data.py +++ b/src/compas/data/data.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import division -import json import hashlib from uuid import uuid4 from uuid import UUID @@ -104,7 +103,7 @@ class Data(object): """ - JSONSCHEMA = {} + DATASCHEMA = {} def __init__(self, name=None): self._guid = None @@ -112,19 +111,56 @@ def __init__(self, name=None): if name: self.name = name - def __getstate__(self): - """Return the object data for state serialization with older pickle protocols.""" - return { - "__dict__": self.__dict__, + def __jsondump__(self, minimal=False): + """Return the required information for serialization with the COMPAS JSON serializer. + + Parameters + ---------- + minimal : bool, optional + If True, exclude the GUID from the dump dict. + + Returns + ------- + dict + + """ + state = { "dtype": self.dtype, "data": self.data, - "guid": str(self.guid), } + if minimal: + return state + state["guid"] = str(self.guid) + return state + + @classmethod + def __jsonload__(cls, data, guid=None): + """Construct an object of this type from the provided data to support COMPAS JSON serialization. + + Parameters + ---------- + data : dict + The raw Python data representing the object. + guid : str, optional + The GUID of the object. + + Returns + ------- + object + + """ + obj = cls.from_data(data) + if guid is not None: + obj._guid = UUID(guid) + return obj + + def __getstate__(self): + state = self.__jsondump__() + state["__dict__"] = self.__dict__ + return state def __setstate__(self, state): - """Assign a deserialized state to the object data to support older pickle protocols.""" self.__dict__.update(state["__dict__"]) - self.data = state["data"] if "guid" in state: self._guid = UUID(state["guid"]) @@ -136,10 +172,6 @@ def dtype(self): def data(self): raise NotImplementedError - @data.setter - def data(self, data): - raise NotImplementedError - def ToString(self): """Converts the instance to a string. @@ -149,12 +181,10 @@ def ToString(self): printing self.GetType().FullName or similar. Overriding the `ToString` method of .NET object class fixes that and makes Rhino/Grasshopper display proper string representations when the objects are printed or - connected to a panel or other type of string output.""" - return str(self) + connected to a panel or other type of string output. - @property - def jsonstring(self): - return compas.json_dumps(self.data) + """ + return str(self) @property def guid(self): @@ -187,9 +217,7 @@ def from_data(cls, data): An instance of this object type if the data contained in the dict has the correct schema. """ - obj = cls() - obj.data = data - return obj + return cls(**data) def to_data(self): """Convert an object to its native data representation. @@ -202,79 +230,6 @@ def to_data(self): """ return self.data - @classmethod - def from_json(cls, filepath): - """Construct an object from serialized data contained in a JSON file. - - Parameters - ---------- - filepath : path string | file-like object | URL string - The path, file or URL to the file for serialization. - - Returns - ------- - :class:`~compas.data.Data` - An instance of this object type if the data contained in the JSON file has the correct schema. - - """ - data = compas.json_load(filepath) - return cls.from_data(data) - - def to_json(self, filepath, pretty=False, compact=False): - """Serialize the data representation of an object to a JSON file. - - Parameters - ---------- - filepath : path string or file-like object - The path or file-like object to the file containing the data. - pretty : bool, optional - If True, serialize to a "pretty", human-readable representation. - compact : bool, optional - If True, serialize to a compact representation without any whitespace. - - Returns - ------- - None - - """ - compas.json_dump(self.data, filepath, pretty=pretty, compact=compact) - - @classmethod - def from_jsonstring(cls, string): - """Construct an object from serialized data contained in a JSON string. - - Parameters - ---------- - string : str - The object as a JSON string. - - Returns - ------- - :class:`~compas.data.Data` - An instance of this object type if the data contained in the JSON file has the correct schema. - - """ - data = compas.json_loads(string) - return cls.from_data(data) - - def to_jsonstring(self, pretty=False, compact=False): - """Serialize the data representation of an object to a JSON string. - - Parameters - ---------- - pretty : bool, optional - If True serialize a pretty representation of the data. - compact : bool, optional - If True serialize a compact representation of the data. - - Returns - ------- - str - The object's data dict in JSON string format. - - """ - return compas.json_dumps(self.data, pretty=pretty, compact=compact) - def copy(self, cls=None): """Make an independent copy of the data object. @@ -294,70 +249,6 @@ def copy(self, cls=None): cls = type(self) return cls.from_data(deepcopy(self.data)) - @classmethod - def validate_json(cls, filepath): - """Validate the data contained in the JSON document against the object's JSON data schema. - - Parameters - ---------- - filepath : path string | file-like object | URL string - The path, file or URL to the file for validation. - - Returns - ------- - Any - - """ - from jsonschema import Draft202012Validator - - validator = Draft202012Validator(cls.JSONSCHEMA) # type: ignore - jsondata = json.load(filepath) - validator.validate(jsondata) - return jsondata - - @classmethod - def validate_jsonstring(cls, jsonstring): - """Validate the data contained in the JSON string against the objects's JSON data schema. - - Parameters - ---------- - jsonstring : str - The JSON string for validation. - - Returns - ------- - Any - - """ - from jsonschema import Draft202012Validator - - validator = Draft202012Validator(cls.JSONSCHEMA) # type: ignore - jsondata = json.loads(jsonstring) - validator.validate(jsondata) - return jsondata - - @classmethod - def validate_jsondata(cls, jsondata): - """Validate the JSON data against the objects's JSON data schema. - - The JSON data is the result of parsing a JSON string or a JSON document. - - Parameters - ---------- - jsondata : Any - The JSON data for validation. - - Returns - ------- - Any - - """ - from jsonschema import Draft202012Validator - - validator = Draft202012Validator(cls.JSONSCHEMA) # type: ignore - validator.validate(jsondata) - return jsondata - def sha256(self, as_string=False): """Compute a hash of the data for comparison during version control using the sha256 algorithm. @@ -385,7 +276,29 @@ def sha256(self, as_string=False): """ h = hashlib.sha256() - h.update(self.jsonstring.encode()) + h.update(compas.json_dumps(self).encode()) if as_string: return h.hexdigest() return h.digest() + + @classmethod + def validate_data(cls, data): + """Validate the data against the object's data schema. + + The data is the raw data that can be used to construct an object of this type with the classmethod ``from_data``. + + Parameters + ---------- + data : Any + The data for validation. + + Returns + ------- + Any + + """ + from jsonschema import Draft202012Validator + + validator = Draft202012Validator(cls.DATASCHEMA) # type: ignore + validator.validate(data) + return data diff --git a/src/compas/data/encoders.py b/src/compas/data/encoders.py index f14c449af65..e36f4ffb112 100644 --- a/src/compas/data/encoders.py +++ b/src/compas/data/encoders.py @@ -4,9 +4,8 @@ import json import platform -import uuid -from compas.data.exceptions import DecoderError +from .exceptions import DecoderError IDictionary = None numpy_support = False @@ -96,6 +95,8 @@ class DataEncoder(json.JSONEncoder): """ + minimal = False + def default(self, o): """Return an object in serialized form. @@ -110,29 +111,9 @@ def default(self, o): The serialized object. """ - if hasattr(o, "to_jsondata"): - value = o.to_jsondata() - if hasattr(o, "dtype"): - dtype = o.dtype - else: - dtype = "{}/{}".format( - ".".join(o.__class__.__module__.split(".")[:-1]), - o.__class__.__name__, - ) - return {"dtype": dtype, "value": value, "guid": str(o.guid)} - - if hasattr(o, "to_data"): - value = o.to_data() - if hasattr(o, "dtype"): - dtype = o.dtype - else: - dtype = "{}/{}".format( - ".".join(o.__class__.__module__.split(".")[:-1]), - o.__class__.__name__, - ) - - return {"dtype": dtype, "value": value, "guid": str(o.guid)} + if hasattr(o, "__jsondump__"): + return o.__jsondump__(minimal=DataEncoder.minimal) if hasattr(o, "__next__"): return list(o) @@ -154,10 +135,10 @@ def default(self, o): np.uint16, np.uint32, np.uint64, - ), + ), # type: ignore ): return int(o) - if isinstance(o, (np.float_, np.float16, np.float32, np.float64)): + if isinstance(o, (np.float_, np.float16, np.float32, np.float64)): # type: ignore return float(o) if isinstance(o, np.bool_): return bool(o) @@ -233,7 +214,8 @@ def object_hook(self, o): except ValueError: raise DecoderError( "The data type of the object should be in the following format: '{}/{}'".format( - o.__class__.__module__, o.__class__.__name__ + o.__class__.__module__, + o.__class__.__name__, ) ) @@ -243,18 +225,13 @@ def object_hook(self, o): except AttributeError: raise DecoderError("The data type can't be found in the specified module: {}.".format(o["dtype"])) - obj_value = o["value"] + data = o["data"] + guid = o.get("guid") # Kick-off from_data from a rebuilt Python dictionary instead of the C# data type if IDictionary and isinstance(o, IDictionary[str, object]): - obj_value = {key: obj_value[key] for key in obj_value.Keys} - - if hasattr(cls, "from_jsondata"): - obj = cls.from_jsondata(obj_value) - else: - obj = cls.from_data(obj_value) + data = {key: data[key] for key in data.Keys} - if "guid" in o: - obj._guid = uuid.UUID(o["guid"]) + obj = cls.__jsonload__(data, guid) return obj diff --git a/src/compas/data/json.py b/src/compas/data/json.py index cb226d70815..f2715f124c0 100644 --- a/src/compas/data/json.py +++ b/src/compas/data/json.py @@ -8,7 +8,7 @@ from compas.data import DataDecoder -def json_dump(data, fp, pretty=False, compact=False): +def json_dump(data, fp, pretty=False, compact=False, minimal=False): """Write a collection of COMPAS object data to a JSON file. Parameters @@ -44,18 +44,22 @@ def json_dump(data, fp, pretty=False, compact=False): True """ + DataEncoder.minimal = minimal + with _iotools.open_file(fp, "w") as f: kwargs = {} + if pretty: kwargs["sort_keys"] = True kwargs["indent"] = 4 if compact: kwargs["indent"] = None kwargs["separators"] = (",", ":") + return json.dump(data, f, cls=DataEncoder, **kwargs) -def json_dumps(data, pretty=False, compact=False): +def json_dumps(data, pretty=False, compact=False, minimal=False): """Write a collection of COMPAS objects to a JSON string. Parameters @@ -89,6 +93,8 @@ def json_dumps(data, pretty=False, compact=False): True """ + DataEncoder.minimal = minimal + kwargs = {} if pretty: kwargs["sort_keys"] = True @@ -164,47 +170,3 @@ def json_loads(s): """ return json.loads(s, cls=DataDecoder) - - -def json_validate(filepath, schema): - """Validates a JSON document with respect to a schema and return the JSON object instance if it is valid. - - Parameters - ---------- - filepath : path string | file-like object | URL string - The filepath of the JSON document. - schema : string - The JSON schema. - - Raises - ------ - jsonschema.exceptions.SchemaError - If the schema itself is invalid. - jsonschema.exceptions.ValidationError - If the document is invalid with respect to the schema. - - Returns - ------- - object - The JSON object contained in the document. - - """ - import jsonschema - import jsonschema.exceptions - - data = json_load(filepath) - - try: - jsonschema.validate(data, schema) - except jsonschema.exceptions.SchemaError as e: - print("The provided schema is invalid:\n\n{}\n\n".format(schema)) - raise e - except jsonschema.exceptions.ValidationError as e: - print( - "The provided JSON document is invalid compared to the provided schema:\n\n{}\n\n{}\n\n".format( - schema, data - ) - ) - raise e - - return data diff --git a/src/compas/data/schema.py b/src/compas/data/schema.py new file mode 100644 index 00000000000..808c553a8f0 --- /dev/null +++ b/src/compas/data/schema.py @@ -0,0 +1,294 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import json + + +def dataclass_dataschema(cls): + """Generate a JSON schema for a COMPAS object class. + + Parameters + ---------- + cls : :class:`~compas.data.Data` + The COMPAS object class. + + Returns + ------- + dict + The JSON schema. + + """ + return cls.DATASCHEMA + + +def dataclass_typeschema(cls): + """Generate a JSON schema for the data type of a COMPAS object class. + + Parameters + ---------- + cls : :class:`~compas.data.Data` + The COMPAS object class. + + Returns + ------- + dict + The JSON schema. + + """ + return { + "type": "string", + "const": "{}/{}".format(".".join(cls.__module__.split(".")[:2]), cls.__name__), + } + + +def dataclass_jsonschema(cls, filepath=None, draft=None): + """Generate a JSON schema for a COMPAS object class. + + Parameters + ---------- + cls : :class:`~compas.data.Data` + The COMPAS object class. + filepath : str, optional + The path to the file where the schema should be saved. + draft : str, optional + The JSON schema draft to use. + + Returns + ------- + dict + The JSON schema. + + """ + import compas + + draft = draft or "https://json-schema.org/draft/2020-12/schema" + + schema = { + "$schema": draft, + "$id": "{}.json".format(cls.__name__), + "$compas": "{}".format(compas.__version__), + "type": "object", + "properties": { + "dtype": dataclass_typeschema(cls), + "data": dataclass_dataschema(cls), + "guid": {"type": "string", "format": "uuid"}, + }, + "required": ["dtype", "data"], + } + + if filepath: + with open(filepath, "w") as f: + json.dump(schema, f, indent=4) + + return schema + + +def compas_jsonschema(dirname=None): + """Generate a JSON schema for the COMPAS data model. + + Parameters + ---------- + dirname : str, optional + The path to the directory where the schemas should be saved. + + Returns + ------- + list + A list of JSON schemas. + + """ + schemas = [] + dataclasses = compas_dataclasses() + for cls in dataclasses: + filepath = None + if dirname: + filepath = os.path.join(dirname, "{}.json".format(cls.__name__)) + schema = dataclass_jsonschema(cls, filepath=filepath) + schemas.append(schema) + return schemas + + +def compas_dataclasses(): + """Find all classes in the COMPAS data model. + + Returns + ------- + list + + """ + from collections import deque + from compas.data import Data + import compas.colors # noqa: F401 + import compas.datastructures # noqa: F401 + import compas.geometry # noqa: F401 + + tovisit = deque([Data]) + dataclasses = [] + + while tovisit: + cls = tovisit.popleft() + dataclasses.append(cls) + for subcls in cls.__subclasses__(): + tovisit.append(subcls) + + return dataclasses + + +# def validate_json(filepath, schema): +# """Validates a JSON document with respect to a schema and return the JSON object instance if it is valid. + +# Parameters +# ---------- +# filepath : path string | file-like object | URL string +# The filepath of the JSON document. +# schema : string +# The JSON schema. + +# Raises +# ------ +# jsonschema.exceptions.SchemaError +# If the schema itself is invalid. +# jsonschema.exceptions.ValidationError +# If the document is invalid with respect to the schema. + +# Returns +# ------- +# object +# The JSON object contained in the document. + +# Examples +# -------- +# >>> import compas +# >>> from compas.geometry import Point +# >>> compas.json_validate("data.json", Point) +# {'dtype': 'compas.geometry.Point', 'data': {'x': 0.0, 'y': 0.0, 'z': 0.0}, 'guid': '00000000-0000-0000-0000-000000000000'} + +# """ +# import jsonschema +# import jsonschema.exceptions + +# data = json_load(filepath) + +# try: +# jsonschema.validate(data, schema) +# except jsonschema.exceptions.SchemaError as e: +# print("The provided schema is invalid:\n\n{}\n\n".format(schema)) +# raise e +# except jsonschema.exceptions.ValidationError as e: +# print( +# "The provided JSON document is invalid compared to the provided schema:\n\n{}\n\n{}\n\n".format( +# schema, data +# ) +# ) +# raise e + +# return data + + +# def validate_jsonstring(jsonstring, schema): +# """Validate the data contained in the JSON string against the JSON data schema. + +# Parameters +# ---------- +# jsonstring : str +# The JSON string for validation. +# schema : string +# The JSON schema. + +# Raises +# ------ +# jsonschema.exceptions.SchemaError +# If the schema itself is invalid. +# jsonschema.exceptions.ValidationError +# If the document is invalid with respect to the schema. + +# Returns +# ------- +# object +# The JSON object contained in the string. + +# """ +# from jsonschema import Draft202012Validator + +# validator = Draft202012Validator(schema) # type: ignore +# jsondata = json.loads(jsonstring) +# validator.validate(jsondata) +# return jsondata + + +# def validate_jsondata(jsondata, schema): +# """Validate the JSON data against the JSON data schema. + +# Parameters +# ---------- +# jsondata : Any +# The JSON data for validation. +# schema : string +# The JSON schema. + +# Raises +# ------ +# jsonschema.exceptions.SchemaError +# If the schema itself is invalid. +# jsonschema.exceptions.ValidationError +# If the document is invalid with respect to the schema. + +# Returns +# ------- +# object +# The JSON object contained in the data. + +# """ +# from jsonschema import Draft202012Validator + +# validator = Draft202012Validator(schema) # type: ignore +# validator.validate(jsondata) +# return jsondata + + +# def validate_data(data, cls): +# """Validate data against the data and json schemas of an object class. + +# Parameters +# ---------- +# data : dict +# The data representation of an object. +# cls : Type[:class:`~compas.data.Data`] +# The data object class. + +# Returns +# ------- +# dict +# The validated data dict. + +# Raises +# ------ +# jsonschema.exceptions.ValidationError + +# """ +# from jsonschema import RefResolver, Draft7Validator +# from jsonschema.exceptions import ValidationError + +# here = os.path.dirname(__file__) + +# schema_name = "{}.json".format(cls.__name__.lower()) +# schema_path = os.path.join(here, "schemas", schema_name) +# with open(schema_path, "r") as fp: +# schema = json.load(fp) + +# definitions_path = os.path.join(here, "schemas", "compas.json") +# with open(definitions_path, "r") as fp: +# definitions = json.load(fp) + +# resolver = RefResolver.from_schema(definitions) +# validator = Draft7Validator(schema, resolver=resolver) + +# try: +# validator.validate(data) +# except ValidationError as e: +# print("Validation against the JSON schema of this object failed.") +# raise e + +# return json.loads(json.dumps(data, cls=DataEncoder), cls=DataDecoder) diff --git a/src/compas/data/validators.py b/src/compas/data/validators.py index b0c5f8521b1..4f6f17b8dc5 100644 --- a/src/compas/data/validators.py +++ b/src/compas/data/validators.py @@ -7,12 +7,6 @@ except NameError: basestring = str -import os -import json - -from compas.data.encoders import DataEncoder -from compas.data.encoders import DataDecoder - def is_sequence_of_str(items): """Verify that the sequence contains only items of type str. @@ -247,49 +241,3 @@ def is_sequence_of_iterable(items): """ return all(is_item_iterable(item) for item in items) - - -def validate_data(data, cls): - """Validate data against the data and json schemas of an object class. - - Parameters - ---------- - data : dict - The data representation of an object. - cls : Type[:class:`~compas.data.Data`] - The data object class. - - Returns - ------- - dict - The validated data dict. - - Raises - ------ - jsonschema.exceptions.ValidationError - - """ - from jsonschema import RefResolver, Draft7Validator - from jsonschema.exceptions import ValidationError - - here = os.path.dirname(__file__) - - schema_name = "{}.json".format(cls.__name__.lower()) - schema_path = os.path.join(here, "schemas", schema_name) - with open(schema_path, "r") as fp: - schema = json.load(fp) - - definitions_path = os.path.join(here, "schemas", "compas.json") - with open(definitions_path, "r") as fp: - definitions = json.load(fp) - - resolver = RefResolver.from_schema(definitions) - validator = Draft7Validator(schema, resolver=resolver) - - try: - validator.validate(data) - except ValidationError as e: - print("Validation against the JSON schema of this object failed.") - raise e - - return json.loads(json.dumps(data, cls=DataEncoder), cls=DataDecoder) diff --git a/src/compas/datastructures/assembly/assembly.py b/src/compas/datastructures/assembly/assembly.py index af6d33da85d..4a042a1f63a 100644 --- a/src/compas/datastructures/assembly/assembly.py +++ b/src/compas/datastructures/assembly/assembly.py @@ -30,6 +30,15 @@ class Assembly(Datastructure): """ + DATASCHEMA = { + "type": "object", + "properties": { + "attributes": {"type": "object"}, + "graph": Graph.DATASCHEMA, + }, + "required": ["graph"], + } + def __init__(self, name=None, **kwargs): super(Assembly, self).__init__() self.attributes = {"name": name or "Assembly"} @@ -37,41 +46,31 @@ def __init__(self, name=None, **kwargs): self.graph = Graph() self._parts = {} + def __str__(self): + tpl = "" + return tpl.format(self.graph.number_of_nodes(), self.graph.number_of_edges()) + # ========================================================================== - # data + # Data # ========================================================================== - @property - def DATASCHEMA(self): - import schema - - return schema.Schema( - { - "attributes": dict, - "graph": Graph, - } - ) - - @property - def JSONSCHEMANAME(self): - return "assembly" - @property def data(self): - data = { + return { "attributes": self.attributes, - "graph": self.graph, + "graph": self.graph.data, } - return data - @data.setter - def data(self, data): - self.attributes.update(data["attributes"] or {}) - self.graph = data["graph"] - self._parts = {part.guid: part.key for part in self.parts()} + @classmethod + def from_data(cls, data): + assembly = cls() + assembly.attributes.update(data["attributes"] or {}) + assembly.graph = Graph.from_data(data["graph"]) + assembly._parts = {part.guid: part.key for part in assembly.parts()} # type: ignore + return assembly # ========================================================================== - # properties + # Properties # ========================================================================== @property @@ -83,19 +82,11 @@ def name(self, value): self.attributes["name"] = value # ========================================================================== - # customization - # ========================================================================== - - def __str__(self): - tpl = "" - return tpl.format(self.graph.number_of_nodes(), self.graph.number_of_edges()) - - # ========================================================================== - # constructors + # Constructors # ========================================================================== # ========================================================================== - # methods + # Methods # ========================================================================== def add_part(self, part, key=None, **kwargs): diff --git a/src/compas/datastructures/assembly/part.py b/src/compas/datastructures/assembly/part.py index 66b5db2bb8d..323a794884b 100644 --- a/src/compas/datastructures/assembly/part.py +++ b/src/compas/datastructures/assembly/part.py @@ -68,14 +68,15 @@ def __init__(self, *args, **kwargs): def data(self): return {"geometry": self._geometry} - @data.setter - def data(self, value): - self._geometry = value["geometry"] + @classmethod + def from_data(cls, data): + feature = cls() + feature._geometry = data["geometry"] # this will work but is not consistent with validation + return feature class ParametricFeature(Feature): - """Base class for Features that may be applied to the parametric definition - of a :class:`~compas.datastructures.Part`. + """Base class for Features that may be applied to the parametric definition of a :class:`~compas.datastructures.Part`. Examples -------- @@ -151,6 +152,17 @@ class Part(Datastructure): """ + DATASCHEMA = { + "type": "object", + "properties": { + "attributes": {"type": "object"}, + "key": {"type": ["integer", "string"]}, + "frame": Frame.DATASCHEMA, + "features": {"type": "array"}, + }, + "required": ["key", "frame"], + } + def __init__(self, name=None, frame=None, **kwargs): super(Part, self).__init__() self.attributes = {"name": name or "Part"} @@ -159,36 +171,23 @@ def __init__(self, name=None, frame=None, **kwargs): self.frame = frame or Frame.worldXY() self.features = [] - @property - def DATASCHEMA(self): - import schema - - return schema.Schema( - { - "attributes": dict, - "key": int, - "frame": Frame, - } - ) - - @property - def JSONSCHEMANAME(self): - return "part" - @property def data(self): - data = { + return { "attributes": self.attributes, "key": self.key, - "frame": self.frame, + "frame": self.frame.data, + "features": self.features, } - return data - @data.setter - def data(self, data): - self.attributes.update(data["attributes"] or {}) - self.key = data["key"] - self.frame = data["frame"] + @classmethod + def from_data(cls, data): + part = cls() + part.attributes.update(data["attributes"] or {}) + part.key = data["key"] + part.frame = Frame.from_data(data["frame"]) + part.features = data["features"] or [] + return part def get_geometry(self, with_features=False): """ @@ -224,5 +223,6 @@ def add_feature(self, feature, apply=False): Returns ------- None + """ raise NotImplementedError diff --git a/src/compas/datastructures/datastructure.py b/src/compas/datastructures/datastructure.py index b92520b5e7a..8f074dc8af0 100644 --- a/src/compas/datastructures/datastructure.py +++ b/src/compas/datastructures/datastructure.py @@ -4,11 +4,18 @@ from compas.data import Data -__all__ = ["Datastructure"] - class Datastructure(Data): """Base class for all data structures.""" - def __init__(self): - super(Datastructure, self).__init__() + def __init__(self, name=None, **kwargs): + super(Datastructure, self).__init__(**kwargs) + self.attributes = {"name": name or self.__class__.__name__} + + @property + def name(self): + return self.attributes.get("name") or self.__class__.__name__ + + @name.setter + def name(self, value): + self.attributes["name"] = value diff --git a/src/compas/datastructures/graph/graph.py b/src/compas/datastructures/graph/graph.py index a4fc459d9d7..a9e0d908500 100644 --- a/src/compas/datastructures/graph/graph.py +++ b/src/compas/datastructures/graph/graph.py @@ -41,7 +41,7 @@ class Graph(Datastructure): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "attributes": {"type": "object"}, @@ -58,13 +58,6 @@ class Graph(Datastructure): "additionalProperties": {"type": "object"}, }, }, - "adjacency": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {"type": "null"}, - }, - }, "max_node": {"type": "integer", "minimum": -1}, }, "required": [ @@ -73,15 +66,13 @@ class Graph(Datastructure): "dea", "node", "edge", - "adjacency", "max_node", ], } def __init__(self, name=None, default_node_attributes=None, default_edge_attributes=None): - super(Graph, self).__init__() + super(Graph, self).__init__(name=name) self._max_node = -1 - self.attributes = {"name": name or "Graph"} self.node = {} self.edge = {} self.adjacency = {} @@ -97,7 +88,7 @@ def __str__(self): return tpl.format(self.number_of_nodes(), self.number_of_edges()) # -------------------------------------------------------------------------- - # data + # Data # -------------------------------------------------------------------------- @property @@ -106,48 +97,10 @@ def data(self): "attributes": self.attributes, "dna": self.default_node_attributes, "dea": self.default_edge_attributes, - "node": self.node, - "edge": self.edge, + "node": {}, + "edge": {}, "max_node": self._max_node, } - return data - - @data.setter - def data(self, data): - self.node = {} - self.edge = {} - self.adjacency = {} - self._max_node = -1 - self.attributes.update(data.get("attributes") or {}) - self.default_node_attributes.update(data.get("dna") or {}) - self.default_edge_attributes.update(data.get("dea") or {}) - node = data.get("node") or {} - edge = data.get("edge") or {} - for node, attr in iter(node.items()): - self.add_node(key=node, attr_dict=attr) - for u, nbrs in iter(edge.items()): - for v, attr in iter(nbrs.items()): - self.add_edge(u, v, attr_dict=attr) - self._max_node = data.get("max_node", self._max_node) - - def to_jsondata(self): - """Returns a dictionary of structured data representing the graph that can be serialised to JSON format. - - This is effectively a post-processing step for the :meth:`to_data` method. - - Returns - ------- - dict - The serialisable structured data dictionary. - - See Also - -------- - :meth:`from_jsondata` - - """ - data = self.data - data["node"] = {} - data["edge"] = {} for key in self.node: data["node"][repr(key)] = self.node[key] for u in self.edge: @@ -159,58 +112,35 @@ def to_jsondata(self): return data @classmethod - def from_jsondata(cls, data): - """Construct a graph from structured data representing the graph in JSON format. - - This is effectively a pre-processing step for the :meth:`from_data` method. - - Parameters - ---------- - data : dict - The structured data dictionary. + def from_data(cls, data): + dna = data.get("dna") or {} + dea = data.get("dea") or {} + node = data.get("node") or {} + edge = data.get("edge") or {} - Returns - ------- - :class:`~compas.datastructures.Graph` - The constructed graph. + graph = cls(default_node_attributes=dna, default_edge_attributes=dea) + graph.attributes.update(data.get("attributes") or {}) - See Also - -------- - :meth:`to_jsondata` + for node, attr in iter(node.items()): + node = literal_eval(node) + graph.add_node(key=node, attr_dict=attr) - """ - _node = data["node"] or {} - _edge = data["edge"] or {} - # process the nodes - node = {literal_eval(key): attr for key, attr in iter(_node.items())} - data["node"] = node - # process the edges - edge = {} - for u, nbrs in iter(_edge.items()): - nbrs = nbrs or {} + for u, nbrs in iter(edge.items()): u = literal_eval(u) - edge[u] = {} for v, attr in iter(nbrs.items()): - attr = attr or {} v = literal_eval(v) - edge[u][v] = attr - data["edge"] = edge - return cls.from_data(data) + graph.add_edge(u, v, attr_dict=attr) - # -------------------------------------------------------------------------- - # properties - # -------------------------------------------------------------------------- + graph._max_node = data.get("max_node", graph._max_node) - @property - def name(self): - return self.attributes.get("name") or self.__class__.__name__ + return graph - @name.setter - def name(self, value): - self.attributes["name"] = value + # -------------------------------------------------------------------------- + # Properties + # -------------------------------------------------------------------------- # -------------------------------------------------------------------------- - # constructors + # Constructors # -------------------------------------------------------------------------- @classmethod @@ -296,7 +226,7 @@ def to_networkx(self): return G # -------------------------------------------------------------------------- - # helpers + # Helpers # -------------------------------------------------------------------------- def clear(self): @@ -421,7 +351,7 @@ def index_edge(self): return dict(enumerate(self.edges())) # -------------------------------------------------------------------------- - # builders + # Builders # -------------------------------------------------------------------------- def add_node(self, key=None, attr_dict=None, **kwattr): @@ -523,7 +453,7 @@ def add_edge(self, u, v, attr_dict=None, **kwattr): return u, v # -------------------------------------------------------------------------- - # modifiers + # Modifiers # -------------------------------------------------------------------------- def delete_node(self, key): @@ -598,7 +528,7 @@ def delete_edge(self, edge): # else: an edge in an opposite direction exists, we don't want to delete the adjacency # -------------------------------------------------------------------------- - # info + # Info # -------------------------------------------------------------------------- def summary(self): @@ -651,7 +581,7 @@ def number_of_edges(self): return len(list(self.edges())) # -------------------------------------------------------------------------- - # accessors + # Accessors # -------------------------------------------------------------------------- def nodes(self, data=False): @@ -709,6 +639,7 @@ def nodes_where(self, conditions=None, data=False, **kwargs): for key, attr in self.nodes(True): is_match = True + attr = attr or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -848,7 +779,7 @@ def edges_where(self, conditions=None, data=False, **kwargs): for key in self.edges(): is_match = True - attr = self.edge_attributes(key) + attr = self.edge_attributes(key) or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -973,7 +904,7 @@ def update_default_edge_attributes(self, attr_dict=None, **kwattr): update_dea = update_default_edge_attributes # -------------------------------------------------------------------------- - # node attributes + # Node attributes # -------------------------------------------------------------------------- def node_attribute(self, key, name, value=None): @@ -1079,7 +1010,7 @@ def node_attributes(self, key, names=None, values=None): """ if key not in self.node: raise KeyError(key) - if values is not None: + if names and values is not None: # use it as a setter for name, value in zip(names, values): self.node[key][name] = value @@ -1176,7 +1107,7 @@ def nodes_attributes(self, names=None, values=None, keys=None): return [self.node_attributes(key, names) for key in keys] # -------------------------------------------------------------------------- - # edge attributes + # Edge attributes # -------------------------------------------------------------------------- def edge_attribute(self, key, name, value=None): @@ -1289,7 +1220,7 @@ def edge_attributes(self, key, names=None, values=None): u, v = key if u not in self.edge or v not in self.edge[u]: raise KeyError(key) - if values: + if names and values: # use it as a setter for name, value in zip(names, values): self.edge_attribute(key, name, value) @@ -1383,7 +1314,7 @@ def edges_attributes(self, names=None, values=None, keys=None): return [self.edge_attributes(key, names) for key in keys] # -------------------------------------------------------------------------- - # node topology + # Node topology # -------------------------------------------------------------------------- def has_node(self, key): @@ -1640,7 +1571,7 @@ def connected_edges(self, key): return edges # -------------------------------------------------------------------------- - # edge topology + # Edge topology # -------------------------------------------------------------------------- def has_edge(self, edge, directed=True): diff --git a/src/compas/datastructures/halfedge/halfedge.py b/src/compas/datastructures/halfedge/halfedge.py index 1c7f91adce6..07e550986a8 100644 --- a/src/compas/datastructures/halfedge/halfedge.py +++ b/src/compas/datastructures/halfedge/halfedge.py @@ -50,7 +50,7 @@ class HalfEdge(Datastructure): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "attributes": {"type": "object"}, @@ -80,7 +80,7 @@ class HalfEdge(Datastructure): }, "edgedata": { "type": "object", - "patternProperties": {"^\\([0-9]+,[0-9]+\\)$": {"type": "object"}}, + "patternProperties": {"^\\([0-9]+, [0-9]+\\)$": {"type": "object"}}, "additionalProperties": False, }, "max_vertex": {"type": "integer", "minimum": -1}, @@ -107,7 +107,7 @@ def __init__( default_edge_attributes=None, default_face_attributes=None, ): - super(HalfEdge, self).__init__() + super(HalfEdge, self).__init__(name=name) self._max_vertex = -1 self._max_face = -1 self.vertex = {} @@ -115,7 +115,6 @@ def __init__( self.face = {} self.facedata = {} self.edgedata = {} - self.attributes = {"name": name or "HalfEdge"} self.default_vertex_attributes = {} self.default_edge_attributes = {} self.default_face_attributes = {} @@ -131,78 +130,62 @@ def __str__(self): return tpl.format(self.number_of_vertices(), self.number_of_faces(), self.number_of_edges()) # -------------------------------------------------------------------------- - # descriptors + # Data # -------------------------------------------------------------------------- - @property - def name(self): - return self.attributes.get("name") or self.__class__.__name__ - - @name.setter - def name(self, value): - self.attributes["name"] = value - - @property - def adjacency(self): - return self.halfedge - @property def data(self): - """Returns a dictionary of structured data representing the data structure. - - Note that some of the data stored internally is not included in the dictionary representation of the data structure. - This is the case for data that is considered private and/or redundant. - Specifially, the half-edge dictionary is not included in the dictionary representation of the data structure. - This is because the half-edge dictionary can be reconstructed from the face dictionary. - The face dictionary contains the same information as the half-edge dictionary, but in a more compact form. - Therefore, to keep the dictionary representation of the data structure as compact as possible, the half-edge dictionary is not included. - - Returns - ------- - dict - The data dictionary. - - """ return { "attributes": self.attributes, "dva": self.default_vertex_attributes, "dea": self.default_edge_attributes, "dfa": self.default_face_attributes, - "vertex": self.vertex, - "face": self.face, - "facedata": self.facedata, + "vertex": {str(vertex): attr for vertex, attr in self.vertex.items()}, + "face": {str(face): vertices for face, vertices in self.face.items()}, + "facedata": {str(face): attr for face, attr in self.facedata.items()}, "edgedata": self.edgedata, "max_vertex": self._max_vertex, "max_face": self._max_face, } - @data.setter - def data(self, data): - self.vertex = {} - self.halfedge = {} - self.face = {} - self.facedata = {} - self.edgedata = {} - self._max_vertex = -1 - self._max_face = -1 - self.attributes.update(data.get("attributes") or {}) - self.default_vertex_attributes.update(data.get("dva") or {}) - self.default_face_attributes.update(data.get("dfa") or {}) - self.default_edge_attributes.update(data.get("dea") or {}) + @classmethod + def from_data(cls, data): + dva = data.get("dva") or {} + dfa = data.get("dfa") or {} + dea = data.get("dea") or {} + + halfedge = cls(default_vertex_attributes=dva, default_face_attributes=dfa, default_edge_attributes=dea) + halfedge.attributes.update(data.get("attributes") or {}) + vertex = data.get("vertex") or {} face = data.get("face") or {} facedata = data.get("facedata") or {} + edgedata = data.get("edgedata") or {} + for key, attr in iter(vertex.items()): - self.add_vertex(key=key, attr_dict=attr) + halfedge.add_vertex(key=key, attr_dict=attr) + for fkey, vertices in iter(face.items()): attr = facedata.get(fkey) or {} - self.add_face(vertices, fkey=fkey, attr_dict=attr) - self.edgedata.update(data.get("edgedata") or {}) - self._max_vertex = data.get("max_vertex", self._max_vertex) - self._max_face = data.get("max_face", self._max_face) + halfedge.add_face(vertices, fkey=fkey, attr_dict=attr) + + halfedge.edgedata = edgedata + + halfedge._max_vertex = data.get("max_vertex", halfedge._max_vertex) + halfedge._max_face = data.get("max_face", halfedge._max_face) + + return halfedge # -------------------------------------------------------------------------- - # helpers + # Properties + # -------------------------------------------------------------------------- + + @property + def adjacency(self): + return self.halfedge + + # -------------------------------------------------------------------------- + # Helpers # -------------------------------------------------------------------------- def clear(self): @@ -319,7 +302,7 @@ def index_vertex(self): return dict(enumerate(self.vertices())) # -------------------------------------------------------------------------- - # builders + # Builders # -------------------------------------------------------------------------- def add_vertex(self, key=None, attr_dict=None, **kwattr): @@ -439,7 +422,7 @@ def add_face(self, vertices, fkey=None, attr_dict=None, **kwattr): return fkey # -------------------------------------------------------------------------- - # modifiers + # Modifiers # -------------------------------------------------------------------------- def delete_vertex(self, key): @@ -517,13 +500,17 @@ def delete_face(self, fkey): """ for u, v in self.face_halfedges(fkey): - self.halfedge[u][v] = None - if self.halfedge[v][u] is None: - del self.halfedge[u][v] - del self.halfedge[v][u] - edge = "-".join(map(str, sorted([u, v]))) - if edge in self.edgedata: - del self.edgedata[edge] + if self.halfedge[u][v] == fkey: + # if the halfedge still points to the face + # this might not be the case of the deletion is executed + # during the procedure of adding a new (replacement) face + self.halfedge[u][v] = None + if self.halfedge[v][u] is None: + del self.halfedge[u][v] + del self.halfedge[v][u] + edge = "-".join(map(str, sorted([u, v]))) + if edge in self.edgedata: + del self.edgedata[edge] del self.face[fkey] if fkey in self.facedata: del self.facedata[fkey] @@ -551,7 +538,7 @@ def remove_unused_vertices(self): cull_vertices = remove_unused_vertices # -------------------------------------------------------------------------- - # accessors + # Accessors # -------------------------------------------------------------------------- def vertices(self, data=False): @@ -683,6 +670,7 @@ def vertices_where(self, conditions=None, data=False, **kwargs): for key, attr in self.vertices(True): is_match = True + attr = attr or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -796,7 +784,7 @@ def edges_where(self, conditions=None, data=False, **kwargs): for key in self.edges(): is_match = True - attr = self.edge_attributes(key) + attr = self.edge_attributes(key) or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -893,7 +881,7 @@ def faces_where(self, conditions=None, data=False, **kwargs): for fkey in self.faces(): is_match = True - attr = self.face_attributes(fkey) + attr = self.face_attributes(fkey) or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -958,7 +946,7 @@ def faces_where_predicate(self, predicate, data=False): yield fkey # -------------------------------------------------------------------------- - # attributes + # Attributes # -------------------------------------------------------------------------- def update_default_vertex_attributes(self, attr_dict=None, **kwattr): @@ -1101,7 +1089,7 @@ def vertex_attributes(self, key, names=None, values=None): """ if key not in self.vertex: raise KeyError(key) - if values is not None: + if names and values is not None: # use it as a setter for name, value in zip(names, values): self.vertex[key][name] = value @@ -1343,7 +1331,7 @@ def face_attributes(self, key, names=None, values=None): """ if key not in self.face: raise KeyError(key) - if values is not None: + if names and values is not None: # use it as a setter for name, value in zip(names, values): if key not in self.facedata: @@ -1589,7 +1577,7 @@ def edge_attributes(self, edge, names=None, values=None): u, v = edge if u not in self.halfedge or v not in self.halfedge[u]: raise KeyError(edge) - if values is not None: + if names and values is not None: # use it as a setter for name, value in zip(names, values): self.edge_attribute(edge, name, value) @@ -1687,7 +1675,7 @@ def edges_attributes(self, names=None, values=None, keys=None): return [self.edge_attributes(edge, names) for edge in edges] # -------------------------------------------------------------------------- - # mesh info + # Info # -------------------------------------------------------------------------- def summary(self): @@ -2008,34 +1996,8 @@ def euler(self): F = self.number_of_faces() return V - E + F - def genus(self): - """Calculate the genus. - - Returns - ------- - int - The genus. - - See Also - -------- - :meth:`euler` - - References - ---------- - .. [1] Wolfram MathWorld. *Genus*. - Available at: http://mathworld.wolfram.com/Genus.html. - - """ - X = self.euler() - # each boundary must be taken into account as if it was one face - B = len(self.boundaries()) - if self.is_orientable(): - return (2 - (X + B)) / 2 - else: - return 2 - (X + B) - # -------------------------------------------------------------------------- - # vertex topology + # Vertex topology # -------------------------------------------------------------------------- def has_vertex(self, key): @@ -2251,7 +2213,7 @@ def vertex_faces(self, key, ordered=False, include_none=False): return [fkey for fkey in faces if fkey is not None] # -------------------------------------------------------------------------- - # edge topology + # Edge topology # -------------------------------------------------------------------------- def has_edge(self, key): @@ -2353,7 +2315,7 @@ def is_edge_on_boundary(self, edge): return self.halfedge[v][u] is None or self.halfedge[u][v] is None # -------------------------------------------------------------------------- - # polyedge topology + # Polyedge topology # -------------------------------------------------------------------------- def edge_loop(self, edge): @@ -2511,7 +2473,7 @@ def halfedge_strip(self, edge): return edges # -------------------------------------------------------------------------- - # face topology + # Face topology # -------------------------------------------------------------------------- def has_face(self, fkey): diff --git a/src/compas/datastructures/halfface/halfface.py b/src/compas/datastructures/halfface/halfface.py index dea977b7bce..15a6f52839e 100644 --- a/src/compas/datastructures/halfface/halfface.py +++ b/src/compas/datastructures/halfface/halfface.py @@ -48,7 +48,7 @@ class HalfFace(Datastructure): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "attributes": {"type": "object"}, @@ -66,18 +66,34 @@ class HalfFace(Datastructure): "patternProperties": { "^[0-9]+$": { "type": "array", - "minItems": 3, - "items": {"type": "integer", "minimum": 0}, + "minItems": 4, + "items": { + "type": "array", + "minItems": 3, + "items": {"type": "integer", "minimum": 0}, + }, } }, "additionalProperties": False, }, - "edge_data": {"type": "object"}, - "face_data": {"type": "object"}, - "cell_data": {"type": "object"}, - "max_vertex": {"type": "number"}, - "max_face": {"type": "number"}, - "max_cell": {"type": "number"}, + "edge_data": { + "type": "object", + "patternProperties": {"^\\([0-9]+, [0-9]+\\)$": {"type": "object"}}, + "additionalProperties": False, + }, + "face_data": { + "type": "object", + "patternProperties": {"^\\([0-9]+(, [0-9]+){3, }\\)$": {"type": "object"}}, + "additionalProperties": False, + }, + "cell_data": { + "type": "object", + "patternProperties": {"^[0-9]+$": {"type": "object"}}, + "additionalProperties": False, + }, + "max_vertex": {"type": "number", "minimum": -1}, + "max_face": {"type": "number", "minimum": -1}, + "max_cell": {"type": "number", "minimum": -1}, }, "required": [ "attributes", @@ -104,7 +120,7 @@ def __init__( default_face_attributes=None, default_cell_attributes=None, ): - super(HalfFace, self).__init__() + super(HalfFace, self).__init__(name=name) self._max_vertex = -1 self._max_face = -1 self._max_cell = -1 @@ -115,7 +131,6 @@ def __init__( self._edge_data = {} self._face_data = {} self._cell_data = {} - self.attributes = {"name": name or "HalfFace"} self.default_vertex_attributes = {"x": 0.0, "y": 0.0, "z": 0.0} self.default_edge_attributes = {} self.default_face_attributes = {} @@ -139,95 +154,89 @@ def __str__(self): ) # -------------------------------------------------------------------------- - # descriptors + # Data # -------------------------------------------------------------------------- - @property - def name(self): - return self.attributes.get("name") or self.__class__.__name__ - - @name.setter - def name(self, value): - self.attributes["name"] = value - @property def data(self): - """Returns a dictionary of structured data representing the volmesh data object. - - Note that some of the data stored internally in the data structure object is not included in the dictionary representation of the object. - This is the case for data that is considered private and/or redundant. - Specifically, the halfface dictionary and the plane dictionary are not included. - This is because the information in these dictionaries can be reconstructed from the other data. - Therefore, to keep the dictionary representation as compact as possible, these dictionaries are not included. - - To reconstruct the complete object representation from the compact data, the setter of the data property uses the vertex and cell builder methods (:meth:`add_vertex`, :meth:`add_cell`). - - Returns - ------- - dict - The structured data representing the volmesh. - - """ - cell = {} + _cell = {} + # this sometimes changes the cycle order of faces + # for c in self.cells(): + # faces = [] + # for face in self.cell_faces(c): + # vertices = self.halfface_vertices(face) + # faces.append(vertices) + # _cell[c] = faces for c in self._cell: faces = [] for u in sorted(self._cell[c]): for v in sorted(self._cell[c][u]): faces.append(self._halfface[self._cell[c][u][v]]) - cell[c] = faces + _cell[c] = faces + return { "attributes": self.attributes, "dva": self.default_vertex_attributes, "dea": self.default_edge_attributes, "dfa": self.default_face_attributes, "dca": self.default_cell_attributes, - "vertex": self._vertex, - "cell": cell, + "vertex": {str(vertex): attr for vertex, attr in self._vertex.items()}, + "cell": {str(cell): faces for cell, faces in _cell.items()}, "edge_data": self._edge_data, "face_data": self._face_data, - "cell_data": self._cell_data, + "cell_data": {str(cell): attr for cell, attr in self._cell_data}, "max_vertex": self._max_vertex, "max_face": self._max_face, "max_cell": self._max_cell, } - @data.setter - def data(self, data): - self._vertex = {} - self._halfface = {} - self._cell = {} - self._plane = {} - self._edge_data = {} - self._face_data = {} - self._cell_data = {} - self._max_vertex = -1 - self._max_face = -1 - self._max_cell = -1 + @classmethod + def from_data(cls, data): + dva = data.get("dva") or {} + dea = data.get("dea") or {} + dfa = data.get("dfa") or {} + dca = data.get("dca") or {} + + halfface = cls( + default_vertex_attributes=dva, + default_edge_attributes=dea, + default_face_attributes=dfa, + default_cell_attributes=dca, + ) + + halfface.attributes.update(data.get("attributes") or {}) + vertex = data.get("vertex") or {} cell = data.get("cell") or {} edge_data = data.get("edge_data") or {} face_data = data.get("face_data") or {} cell_data = data.get("cell_data") or {} - self.attributes.update(data.get("attributes") or {}) - self.default_vertex_attributes.update(data.get("dva") or {}) - self.default_edge_attributes.update(data.get("dea") or {}) - self.default_face_attributes.update(data.get("dfa") or {}) - self.default_cell_attributes.update(data.get("dca") or {}) + for key, attr in iter(vertex.items()): - self.add_vertex(key=key, attr_dict=attr) + halfface.add_vertex(key=key, attr_dict=attr) + for ckey, faces in iter(cell.items()): attr = cell_data.get(ckey) or {} - self.add_cell(faces, ckey=ckey, attr_dict=attr) + halfface.add_cell(faces, ckey=ckey, attr_dict=attr) + for edge in edge_data: - self._edge_data[edge] = edge_data[edge] or {} + halfface._edge_data[edge] = edge_data[edge] or {} + for face in face_data: - self._face_data[face] = face_data[face] or {} - self._max_vertex = data.get("max_vertex", self._max_vertex) - self._max_face = data.get("max_face", self._max_face) - self._max_cell = data.get("max_cell", self._max_cell) + halfface._face_data[face] = face_data[face] or {} + + halfface._max_vertex = data.get("max_vertex", halfface._max_vertex) + halfface._max_face = data.get("max_face", halfface._max_face) + halfface._max_cell = data.get("max_cell", halfface._max_cell) + + return halfface + + # -------------------------------------------------------------------------- + # Properties + # -------------------------------------------------------------------------- # -------------------------------------------------------------------------- - # helpers + # Helpers # -------------------------------------------------------------------------- def clear(self): @@ -369,7 +378,7 @@ def index_vertex(self): return dict(enumerate(self.vertices())) # -------------------------------------------------------------------------- - # builders + # Builders # -------------------------------------------------------------------------- def add_vertex(self, key=None, attr_dict=None, **kwattr): @@ -545,7 +554,7 @@ def add_cell(self, faces, ckey=None, attr_dict=None, **kwattr): return ckey # -------------------------------------------------------------------------- - # modifiers + # Modifiers # -------------------------------------------------------------------------- def delete_vertex(self, vertex): @@ -634,7 +643,7 @@ def remove_unused_vertices(self): cull_vertices = remove_unused_vertices # -------------------------------------------------------------------------- - # accessors + # Accessors # -------------------------------------------------------------------------- def vertices(self, data=False): @@ -816,6 +825,8 @@ def vertices_where(self, conditions=None, data=False, **kwargs): for key, attr in self.vertices(True): is_match = True + attr = attr or {} + for name, value in conditions.items(): method = getattr(self, name, None) @@ -927,7 +938,7 @@ def edges_where(self, conditions=None, data=False, **kwargs): for key in self.edges(): is_match = True - attr = self.edge_attributes(key) + attr = self.edge_attributes(key) or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -1022,7 +1033,7 @@ def faces_where(self, conditions=None, data=False, **kwargs): for fkey in self.faces(): is_match = True - attr = self.face_attributes(fkey) + attr = self.face_attributes(fkey) or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -1117,7 +1128,7 @@ def cells_where(self, conditions=None, data=False, **kwargs): for ckey in self.cells(): is_match = True - attr = self.cell_attributes(ckey) + attr = self.cell_attributes(ckey) or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -1181,7 +1192,7 @@ def cells_where_predicate(self, predicate, data=False): yield ckey # -------------------------------------------------------------------------- - # attributes - vertices + # Vertex attributes # -------------------------------------------------------------------------- def update_default_vertex_attributes(self, attr_dict=None, **kwattr): @@ -1319,7 +1330,7 @@ def vertex_attributes(self, vertex, names=None, values=None): """ if vertex not in self._vertex: raise KeyError(vertex) - if values is not None: + if names and values is not None: # use it as a setter for name, value in zip(names, values): self._vertex[vertex][name] = value @@ -1417,7 +1428,7 @@ def vertices_attributes(self, names=None, values=None, keys=None): return [self.vertex_attributes(vertex, names) for vertex in vertices] # -------------------------------------------------------------------------- - # attributes - edges + # Edge attributes # -------------------------------------------------------------------------- def update_default_edge_attributes(self, attr_dict=None, **kwattr): @@ -1561,7 +1572,7 @@ def edge_attributes(self, edge, names=None, values=None): if u not in self._plane or v not in self._plane[u]: raise KeyError(edge) key = str(tuple(sorted(edge))) - if values: + if names and values: for name, value in zip(names, values): if key not in self._edge_data: self._edge_data[key] = {} @@ -1653,7 +1664,7 @@ def edges_attributes(self, names=None, values=None, edges=None): return [self.edge_attributes(edge, names) for edge in edges] # -------------------------------------------------------------------------- - # face attributes + # Face attributes # -------------------------------------------------------------------------- def update_default_face_attributes(self, attr_dict=None, **kwattr): @@ -1794,7 +1805,7 @@ def face_attributes(self, face, names=None, values=None): if face not in self._halfface: raise KeyError(face) key = str(tuple(sorted(self.halfface_vertices(face)))) - if values: + if names and values: for name, value in zip(names, values): if key not in self._face_data: self._face_data[key] = {} @@ -1887,7 +1898,7 @@ def faces_attributes(self, names=None, values=None, faces=None): return [self.face_attributes(face, names) for face in faces] # -------------------------------------------------------------------------- - # attributes - cell + # Cell attributes # -------------------------------------------------------------------------- def update_default_cell_attributes(self, attr_dict=None, **kwattr): @@ -2026,7 +2037,7 @@ def cell_attributes(self, cell, names=None, values=None): """ if cell not in self._cell: raise KeyError(cell) - if values is not None: + if names and values is not None: for name, value in zip(names, values): if cell not in self._cell_data: self._cell_data[cell] = {} @@ -2120,7 +2131,7 @@ def cells_attributes(self, names=None, values=None, cells=None): return [self.cell_attributes(cell, names) for cell in cells] # -------------------------------------------------------------------------- - # volmesh info + # Info # -------------------------------------------------------------------------- def number_of_vertices(self): @@ -2196,7 +2207,7 @@ def is_valid(self): raise NotImplementedError # -------------------------------------------------------------------------- - # vertex topology + # Vertex topology # -------------------------------------------------------------------------- def has_vertex(self, vertex): @@ -2411,7 +2422,7 @@ def is_vertex_on_boundary(self, vertex): return False # -------------------------------------------------------------------------- - # edge topology + # Edge topology # -------------------------------------------------------------------------- def has_edge(self, edge): @@ -2518,7 +2529,7 @@ def is_edge_on_boundary(self, edge): return None in self._plane[u][v].values() # -------------------------------------------------------------------------- - # halfface topology + # Halfface topology # -------------------------------------------------------------------------- def has_halfface(self, halfface): @@ -2836,7 +2847,7 @@ def is_halfface_on_boundary(self, halfface): return self._plane[w][v][u] is None # -------------------------------------------------------------------------- - # cell topology + # Cell topology # -------------------------------------------------------------------------- def cell_vertices(self, cell): @@ -3154,7 +3165,7 @@ def is_cell_on_boundary(self, cell): return False # -------------------------------------------------------------------------- - # boundary + # Boundary # -------------------------------------------------------------------------- def vertices_on_boundaries(self): diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index d13eeadc3e7..b6fa26b3ac4 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -794,7 +794,7 @@ def insert_vertex(self, fkey, key=None, xyz=None, return_fkeys=False): w = self.add_vertex(key=key, x=x, y=y, z=z) for u, v in self.face_halfedges(fkey): fkeys.append(self.add_face([u, v, w])) - del self.face[fkey] + self.delete_face(fkey) if return_fkeys: return w, fkeys return w @@ -1422,7 +1422,7 @@ def face_flatness(self, fkey, maxdev=0.02): """ vertices = self.face_vertices(fkey) f = len(vertices) - points = self.vertices_attributes("xyz", keys=vertices) + points = self.vertices_attributes("xyz", keys=vertices) or [] lengths = [distance_point_point(a, b) for a, b in pairwise(points + points[:1])] length = sum(lengths) / f d = distance_line_line((points[0], points[2]), (points[1], points[3])) @@ -1477,8 +1477,8 @@ def face_skewness(self, fkey): angle = angle_points(o, a, b, deg=True) angles.append(angle) return max( - (max(angles) - ideal_angle) / (180 - ideal_angle), - (ideal_angle - min(angles)) / ideal_angle, + (max(angles) - ideal_angle) / (180 - ideal_angle), # type: ignore + (ideal_angle - min(angles)) / ideal_angle, # type: ignore ) def face_curvature(self, fkey): diff --git a/src/compas/datastructures/volmesh/volmesh.py b/src/compas/datastructures/volmesh/volmesh.py index 556ff6ab3b0..172170d0335 100644 --- a/src/compas/datastructures/volmesh/volmesh.py +++ b/src/compas/datastructures/volmesh/volmesh.py @@ -195,9 +195,9 @@ def from_obj(cls, filepath, precision=None): """ obj = OBJ(filepath, precision) - vertices = obj.parser.vertices - faces = obj.parser.faces - groups = obj.parser.groups + vertices = obj.parser.vertices or [] # type: ignore + faces = obj.parser.faces or [] # type: ignore + groups = obj.parser.groups or [] # type: ignore cells = [] for name in groups: group = groups[name] @@ -288,7 +288,7 @@ def to_vertices_and_cells(self): vertex_index = self.vertex_index() vertices = [self.vertex_coordinates(vertex) for vertex in self.vertices()] cells = [] - for cell in self.cell: + for cell in self.cells(): faces = [ [vertex_index[vertex] for vertex in self.halfface_vertices(face)] for face in self.cell_faces(cell) ] diff --git a/src/compas/geometry/brep/brep.py b/src/compas/geometry/brep/brep.py index 8d64ba4001b..aea8e0c2e6b 100644 --- a/src/compas/geometry/brep/brep.py +++ b/src/compas/geometry/brep/brep.py @@ -203,16 +203,6 @@ def __str__(self): # Data # ============================================================================== - @property - def DATASCHEMA(self): - import schema - - return schema.Schema( - { - "faces": list, - } - ) - @property def data(self): faces = [] @@ -220,9 +210,13 @@ def data(self): faces.append(face.data) return {"faces": faces} - @data.setter - def data(self): - raise NotImplementedError + @classmethod + def from_data(cls, data): + # faces = [] + # for face in data["faces"]: + # faces.append(BrepFace.from_data(face)) + # return cls.from_brepfaces(faces) + pass # ============================================================================== # Properties diff --git a/src/compas/geometry/curves/arc.py b/src/compas/geometry/curves/arc.py index 616ef4889a6..83ecdc678f6 100644 --- a/src/compas/geometry/curves/arc.py +++ b/src/compas/geometry/curves/arc.py @@ -111,14 +111,14 @@ class Arc(Curve): """ - JSONSCHEMA = { + DATASCHEMA = { "value": { "type": "object", "properties": { "radius": {"type": "number", "minimum": 0}, "start_angle": {"type": "number", "minimum": 0, "optional": True}, "end_angle": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["frame", "radius", "start_angle", "end_angle"], } @@ -141,7 +141,8 @@ def __init__(self, radius, start_angle, end_angle, frame=None, **kwargs): self.end_angle = end_angle def __repr__(self): - return "Arc(radius={0!r}, start_angle={1!r}, end_angle={2!r}, frame={3!r})".format( + return "{0}(radius={1}, start_angle={2}, end_angle={3}, frame={4!r})".format( + type(self).__name__, self.radius, self.start_angle, self.end_angle, @@ -166,12 +167,21 @@ def __eq__(self, other): @property def data(self): return { - "frame": self.frame, "radius": self.radius, "start_angle": self.start_angle, "end_angle": self.end_angle, + "frame": self.frame.data, } + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + start_angle=data["start_angle"], + end_angle=data["end_angle"], + frame=Frame.from_data(data["frame"]), + ) + # ============================================================================= # Properties # ============================================================================= diff --git a/src/compas/geometry/curves/bezier.py b/src/compas/geometry/curves/bezier.py index fe259bb5ddd..a2a4bd6a610 100644 --- a/src/compas/geometry/curves/bezier.py +++ b/src/compas/geometry/curves/bezier.py @@ -160,10 +160,10 @@ class Bezier(Curve): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "points": {"type": "array", "minItems": 2, "items": Point.JSONSCHEMA}, + "points": {"type": "array", "minItems": 2, "items": Point.DATASCHEMA}, }, "required": ["points"], } @@ -180,13 +180,16 @@ def __init__(self, points, **kwargs): self._points = [] self.points = points + def __repr__(self): + return "{0}(points={1!r})".format(type(self).__name__, self.points) + # ========================================================================== # Data # ========================================================================== @property def data(self): - return {"points": self.points} + return {"points": [point.data for point in self.points]} # ========================================================================== # Properties diff --git a/src/compas/geometry/curves/circle.py b/src/compas/geometry/curves/circle.py index fdf1139b0df..04109bb5410 100644 --- a/src/compas/geometry/curves/circle.py +++ b/src/compas/geometry/curves/circle.py @@ -85,10 +85,10 @@ class Circle(Conic): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, "radius": {"type": "number", "minimum": 0}, }, "required": ["frame", "radius"], @@ -100,7 +100,11 @@ def __init__(self, radius, frame=None, **kwargs): self.radius = radius def __repr__(self): - return "Circle(radius={0!r}, frame={1!r})".format(self.radius, self.frame) + return "{0}(radius={1!r}, frame={2!r})".format( + type(self).__name__, + self.radius, + self.frame, + ) def __eq__(self, other): try: @@ -116,7 +120,11 @@ def __eq__(self, other): @property def data(self): - return {"frame": self.frame, "radius": self.radius} + return {"radius": self.radius, "frame": self.frame.data} + + @classmethod + def from_data(cls, data): + return cls(radius=data["radius"], frame=Frame.from_data(data["frame"])) # ========================================================================== # Properties diff --git a/src/compas/geometry/curves/curve.py b/src/compas/geometry/curves/curve.py index c216524d570..202bf959a05 100644 --- a/src/compas/geometry/curves/curve.py +++ b/src/compas/geometry/curves/curve.py @@ -77,42 +77,11 @@ def __init__(self, frame=None, name=None): self.frame = frame def __repr__(self): - return "Curve(frame={0!r}, domain={1!r})".format(self.frame, self.domain) - - def __str__(self): - return "".format(self.domain, self.frame) - - def __eq__(self, other): - raise NotImplementedError - - # ============================================================================== - # Data - # ============================================================================== - - @property - def data(self): - raise NotImplementedError - - # @data.setter - # def data(self, data): - # raise NotImplementedError - - @classmethod - def from_data(cls, data): - """Construct a curve from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Curve` - The constructed curve. - - """ - return cls(**data) + return "{0}(frame={1!r}, domain={2})".format( + type(self).__name__, + self.frame, + self.domain, + ) # ============================================================================== # Properties diff --git a/src/compas/geometry/curves/ellipse.py b/src/compas/geometry/curves/ellipse.py index 14d755e3e47..3131fbc4f9e 100644 --- a/src/compas/geometry/curves/ellipse.py +++ b/src/compas/geometry/curves/ellipse.py @@ -106,12 +106,12 @@ class Ellipse(Conic): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "major": {"type": "number", "minimum": 0}, "minor": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["frame", "major", "minor"], } @@ -124,7 +124,12 @@ def __init__(self, major=1.0, minor=1.0, frame=None, **kwargs): self.minor = minor def __repr__(self): - return "Ellipse(major={0!r}, minor={1!r}, frame={2!r})".format(self.major, self.minor, self.frame) + return "{0}(major={1!r}, minor={2}, frame={3!r})".format( + type(self).__name__, + self.major, + self.minor, + self.frame, + ) def __eq__(self, other): try: @@ -141,7 +146,19 @@ def __eq__(self, other): @property def data(self): - return {"frame": self.frame, "major": self.major, "minor": self.minor} + return { + "major": self.major, + "minor": self.minor, + "frame": self.frame.data, + } + + @classmethod + def from_data(cls, data): + return cls( + major=data["major"], + minor=data["minor"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # Properties diff --git a/src/compas/geometry/curves/hyperbola.py b/src/compas/geometry/curves/hyperbola.py index fb84922727f..929afa6ca3b 100644 --- a/src/compas/geometry/curves/hyperbola.py +++ b/src/compas/geometry/curves/hyperbola.py @@ -110,12 +110,12 @@ class Hyperbola(Conic): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "major": {"type": "number", "minimum": 0}, "minor": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["frame", "major", "minor"], } @@ -128,7 +128,12 @@ def __init__(self, major, minor, frame=None, **kwargs): self.minor = minor def __repr__(self): - return "Hyperbola(major={0!r}, minor={1!r}, frame={2!r})".format(self.major, self.minor, self.frame) + return "{0}(major={1}, minor={2}, frame={3!r})".format( + type(self).__name__, + self.major, + self.minor, + self.frame, + ) def __eq__(self, other): try: @@ -142,7 +147,19 @@ def __eq__(self, other): @property def data(self): - return {"frame": self.frame, "major": self.major, "minor": self.minor} + return { + "major": self.major, + "minor": self.minor, + "frame": self.frame.data, + } + + @classmethod + def from_data(cls, data): + return cls( + major=data["major"], + minor=data["minor"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # Properties diff --git a/src/compas/geometry/curves/line.py b/src/compas/geometry/curves/line.py index 42fe6bb68e7..fc1f8f19e8f 100644 --- a/src/compas/geometry/curves/line.py +++ b/src/compas/geometry/curves/line.py @@ -65,11 +65,11 @@ class Line(Curve): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "start": Point.JSONSCHEMA, - "end": Point.JSONSCHEMA, + "start": Point.DATASCHEMA, + "end": Point.DATASCHEMA, }, "required": ["start", "end"], } @@ -91,7 +91,11 @@ def __init__(self, start, end, **kwargs): self.end = end def __repr__(self): - return "Line({0!r}, {1!r})".format(self.start, self.end) + return "{0}({1!r}, {2!r})".format( + type(self).__name__, + self.start, + self.end, + ) def __getitem__(self, key): if key == 0: @@ -126,7 +130,7 @@ def __eq__(self, other): @property def data(self): - return {"start": self.start, "end": self.end} + return {"start": self.start.data, "end": self.end.data} # ========================================================================== # properties diff --git a/src/compas/geometry/curves/nurbs.py b/src/compas/geometry/curves/nurbs.py index 59db7ce5638..1829a8fa1ee 100644 --- a/src/compas/geometry/curves/nurbs.py +++ b/src/compas/geometry/curves/nurbs.py @@ -5,6 +5,7 @@ from math import sqrt from compas.plugins import pluggable +from compas.plugins import PluginNotInstalledError from compas.geometry import Point from compas.geometry import Frame @@ -20,22 +21,22 @@ def new_nurbscurve(cls, *args, **kwargs): @pluggable(category="factories") def new_nurbscurve_from_parameters(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbscurve_from_points(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbscurve_from_interpolation(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbscurve_from_step(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError class NurbsCurve(Curve): @@ -67,10 +68,10 @@ class NurbsCurve(Curve): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "points": {"type": "array", "minItems": 2, "items": Point.JSONSCHEMA}, + "points": {"type": "array", "minItems": 2, "items": Point.DATASCHEMA}, "weights": {"type": "array", "items": {"type": "number"}}, "knots": {"type": "array", "items": {"type": "number"}}, "multiplicities": {"type": "array", "items": {"type": "integer"}}, @@ -87,25 +88,16 @@ def __new__(cls, *args, **kwargs): def __init__(self, name=None): super(NurbsCurve, self).__init__(name=name) - def __eq__(self, other): - raise NotImplementedError - - def __str__(self): - lines = [ - "NurbsCurve", - "----------", - "Points: {}".format(self.points), - "Weights: {}".format(self.weights), - "Knots: {}".format(self.knots), - "Mults: {}".format(self.multiplicities), - "Degree: {}".format(self.degree), - "Order: {}".format(self.order), - "Domain: {}".format(self.domain), - "Closed: {}".format(self.is_closed), - "Periodic: {}".format(self.is_periodic), - "Rational: {}".format(self.is_rational), - ] - return "\n".join(lines) + def __repr__(self): + return "{0}(points={1!r}, weigths={2}, knots={3}, multiplicities={4}, degree={5}, is_periodic={6})".format( + type(self).__name__, + self.points, + self.weights, + self.knots, + self.multiplicities, + self.degree, + self.is_periodic, + ) # ============================================================================== # Data @@ -113,14 +105,12 @@ def __str__(self): @property def dtype(self): - """str : The type of the object in the form of a '2-level' import and a class name.""" return "compas.geometry/NurbsCurve" @property def data(self): - """dict : Representation of the curve as a dict containing only native Python data.""" return { - "points": self.points, + "points": [point.data for point in self.points], "weights": self.weights, "knots": self.knots, "multiplicities": self.multiplicities, @@ -128,10 +118,6 @@ def data(self): "is_periodic": self.is_periodic, } - @data.setter - def data(self, data): - raise NotImplementedError - @classmethod def from_data(cls, data): """Construct a NURBS curve from its data representation. @@ -147,13 +133,14 @@ def from_data(cls, data): The constructed curve. """ - points = data["points"] - weights = data["weights"] - knots = data["knots"] - multiplicities = data["multiplicities"] - degree = data["degree"] - is_periodic = data["is_periodic"] - return cls.from_parameters(points, weights, knots, multiplicities, degree, is_periodic) + return cls.from_parameters( + data["points"], + data["weights"], + data["knots"], + data["multiplicities"], + data["degree"], + data["is_periodic"], + ) # ============================================================================== # Properties diff --git a/src/compas/geometry/curves/parabola.py b/src/compas/geometry/curves/parabola.py index 8a55dcf64d2..60923650c53 100644 --- a/src/compas/geometry/curves/parabola.py +++ b/src/compas/geometry/curves/parabola.py @@ -4,6 +4,7 @@ from compas.geometry import Vector from compas.geometry import Point +from compas.geometry import Frame from .line import Line from .conic import Conic @@ -84,7 +85,11 @@ def __init__(self, focal, frame=None, **kwargs): self.focal = focal def __repr__(self): - return "Parabola(focal={0!r}, frame={1!r})".format(self.focal, self.frame) + return "{0}(focal={1}, frame={2!r})".format( + type(self).__name__, + self.focal, + self.frame, + ) def __eq__(self, other): try: @@ -98,7 +103,14 @@ def __eq__(self, other): @property def data(self): - return {"frame": self.frame, "focal": self.focal} + return {"focal": self.focal, "frame": self.frame.data} + + @classmethod + def from_data(cls, data): + return cls( + focal=data["focal"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # properties diff --git a/src/compas/geometry/curves/polyline.py b/src/compas/geometry/curves/polyline.py index 33de77c4379..e8a158c61fd 100644 --- a/src/compas/geometry/curves/polyline.py +++ b/src/compas/geometry/curves/polyline.py @@ -68,10 +68,10 @@ class Polyline(Curve): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "points": {"type": "array", "minItems": 2, "items": Point.JSONSCHEMA}, + "points": {"type": "array", "minItems": 2, "items": Point.DATASCHEMA}, }, "required": ["points"], } @@ -90,7 +90,10 @@ def __init__(self, points, **kwargs): self.points = points def __repr__(self): - return "Polyline([{0}])".format(", ".join(["{0!r}".format(point) for point in self.points])) + return "{0}({1!r})".format( + type(self).__name__, + self.points, + ) def __getitem__(self, key): return self.points[key] @@ -116,7 +119,7 @@ def __eq__(self, other): @property def data(self): - return {"points": self.points} + return {"points": [point.data for point in self.points]} # ========================================================================== # properties diff --git a/src/compas/geometry/frame.py b/src/compas/geometry/frame.py index 6b569ee66b1..91b0d63253f 100644 --- a/src/compas/geometry/frame.py +++ b/src/compas/geometry/frame.py @@ -67,12 +67,12 @@ class Frame(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "point": Point.JSONSCHEMA, - "xaxis": Vector.JSONSCHEMA, - "yaxis": Vector.JSONSCHEMA, + "point": Point.DATASCHEMA, + "xaxis": Vector.DATASCHEMA, + "yaxis": Vector.DATASCHEMA, }, "required": ["point", "xaxis", "yaxis"], } @@ -88,7 +88,12 @@ def __init__(self, point, xaxis, yaxis, **kwargs): self.yaxis = yaxis def __repr__(self): - return "Frame({0!r}, {1!r}, {2!r})".format(self.point, self.xaxis, self.yaxis) + return "{0}(point={1!r}, xaxis={2!r}, yaxis={3!r})".format( + type(self).__name__, + self.point, + self.xaxis, + self.yaxis, + ) def __len__(self): return 3 @@ -128,47 +133,11 @@ def __eq__(self, other, tol=1e-05): @property def data(self): return { - "point": self.point, - "xaxis": self.xaxis, - "yaxis": self.yaxis, + "point": self.point.data, + "xaxis": self.xaxis.data, + "yaxis": self.yaxis.data, } - @data.setter - def data(self, data): - self.point = data["point"] - self.xaxis = data["xaxis"] - self.yaxis = data["yaxis"] - - @classmethod - def from_data(cls, data): - """Construct a frame from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Frame` - The constructed frame. - - Examples - -------- - >>> data = {'point': [0.0, 0.0, 0.0], 'xaxis': [1.0, 0.0, 0.0], 'yaxis': [0.0, 1.0, 0.0]} - >>> frame = Frame.from_data(data) - >>> frame.point - Point(0.000, 0.000, 0.000) - >>> frame.xaxis - Vector(1.000, 0.000, 0.000) - >>> frame.yaxis - Vector(0.000, 1.000, 0.000) - >>> frame.zaxis - Vector(0.000, 0.000, 1.000) - - """ - return cls(data["point"], data["xaxis"], data["yaxis"]) - # ========================================================================== # Properties # ========================================================================== diff --git a/src/compas/geometry/geometry.py b/src/compas/geometry/geometry.py index 3ea17ea2811..45b427d1cf7 100644 --- a/src/compas/geometry/geometry.py +++ b/src/compas/geometry/geometry.py @@ -11,6 +11,9 @@ class Geometry(Data): def __init__(self, *args, **kwargs): super(Geometry, self).__init__(*args, **kwargs) + def __eq__(self, other): + raise NotImplementedError + def __ne__(self, other): # this is not obvious to ironpython return not self.__eq__(other) diff --git a/src/compas/geometry/plane.py b/src/compas/geometry/plane.py index 0cac8de8185..98ca10d6563 100644 --- a/src/compas/geometry/plane.py +++ b/src/compas/geometry/plane.py @@ -42,11 +42,11 @@ class Plane(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "point": Point.JSONSCHEMA, - "normal": Vector.JSONSCHEMA, + "point": Point.DATASCHEMA, + "normal": Vector.DATASCHEMA, }, "required": ["point", "normal"], } @@ -59,7 +59,11 @@ def __init__(self, point, normal, **kwargs): self.normal = normal def __repr__(self): - return "Plane({0!r}, {1!r})".format(self.point, self.normal) + return "{0}(point={1!r}, normal={2!r})".format( + type(self).__name__, + self.point, + self.normal, + ) def __len__(self): return 2 @@ -92,37 +96,10 @@ def __eq__(self, other): @property def data(self): - return {"point": self.point, "normal": self.normal} - - @data.setter - def data(self, data): - self.point = data["point"] - self.normal = data["normal"] - - @classmethod - def from_data(cls, data): - """Construct a plane from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Plane` - The constructed plane. - - Examples - -------- - >>> plane = Plane.from_data({'point': [0.0, 0.0, 0.0], 'normal': [0.0, 0.0, 1.0]}) - >>> plane.point - Point(0.000, 0.000, 0.000) - >>> plane.normal - Vector(0.000, 0.000, 1.000) - - """ - return cls(data["point"], data["normal"]) + return { + "point": self.point.data, + "normal": self.normal.data, + } # ========================================================================== # Properties @@ -262,6 +239,30 @@ def worldXY(cls): """ return cls([0, 0, 0], [0, 0, 1]) + @classmethod + def worldYZ(cls): + """Construct the world YZ plane. + + Returns + ------- + :class:`~compas.geometry.Plane` + The world YZ plane. + + """ + return cls([0, 0, 0], [1, 0, 0]) + + @classmethod + def worldZX(cls): + """Construct the world ZX plane. + + Returns + ------- + :class:`~compas.geometry.Plane` + The world ZX plane. + + """ + return cls([0, 0, 0], [0, 1, 0]) + @classmethod def from_frame(cls, frame): """Construct a plane from a frame. diff --git a/src/compas/geometry/point.py b/src/compas/geometry/point.py index 96316ff1a86..324c18d0afc 100644 --- a/src/compas/geometry/point.py +++ b/src/compas/geometry/point.py @@ -2,8 +2,6 @@ from __future__ import absolute_import from __future__ import division -from compas import PRECISION - from compas.geometry import centroid_points from compas.geometry import normal_polygon from compas.geometry import distance_point_point @@ -18,7 +16,7 @@ from compas.geometry import is_point_behind_plane from compas.geometry import transform_points -from compas.geometry import Geometry +from .geometry import Geometry from .vector import Vector @@ -101,7 +99,7 @@ class Point(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "array", "minItems": 3, "maxItems": 3, @@ -118,7 +116,12 @@ def __init__(self, x, y, z=0.0, **kwargs): self.z = z def __repr__(self): - return "Point({0:.{3}f}, {1:.{3}f}, {2:.{3}f})".format(self.x, self.y, self.z, PRECISION[:1]) + return "{0}({1}, {2}, z={3})".format( + type(self).__name__, + self.x, + self.y, + self.z, + ) def __len__(self): return 3 @@ -210,33 +213,8 @@ def __ipow__(self, n): def data(self): return list(self) - @data.setter - def data(self, data): - self.x = data[0] - self.y = data[1] - self.z = data[2] - @classmethod def from_data(cls, data): - """Construct a point from a data dict. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Point` - The constructed point. - - Examples - -------- - >>> point = Point.from_data([0.0, 0.0, 0.0]) - >>> point - Point(0.000, 0.000, 0.000) - - """ return cls(*data) # ========================================================================== @@ -648,7 +626,7 @@ def in_polyhedron(self, polyhedron): return all(is_point_behind_plane(self, plane) for plane in planes) # ========================================================================== - # transformations + # Transformations # ========================================================================== def transform(self, T): diff --git a/src/compas/geometry/pointcloud.py b/src/compas/geometry/pointcloud.py index 698a3c299c0..2b7ab46744b 100644 --- a/src/compas/geometry/pointcloud.py +++ b/src/compas/geometry/pointcloud.py @@ -33,10 +33,10 @@ class Pointcloud(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "points": {"type": "array", "items": Point.JSONSCHEMA, "minItems": 1}, + "points": {"type": "array", "items": Point.DATASCHEMA, "minItems": 1}, }, "required": ["points"], } @@ -47,7 +47,7 @@ def __init__(self, points, **kwargs): self.points = points def __repr__(self): - return "Pointcloud({0!r})".format(self.points) + return "{0}(points={1!r})".format(type(self).__name__, self.points) def __len__(self): return len(self.points) @@ -78,15 +78,7 @@ def __eq__(self, other): @property def data(self): - return {"points": self.points} - - @data.setter - def data(self, data): - self._points = data["points"] - - @classmethod - def from_data(cls, data): - return cls(data["points"]) + return {"points": [point.data for point in self.points]} # ========================================================================== # Properties diff --git a/src/compas/geometry/polygon.py b/src/compas/geometry/polygon.py index bc3c9bd3f34..25232088398 100644 --- a/src/compas/geometry/polygon.py +++ b/src/compas/geometry/polygon.py @@ -5,24 +5,17 @@ import math from compas.utilities import pairwise - from compas.geometry import allclose from compas.geometry import area_polygon - -# from compas.geometry import cross_vectors from compas.geometry import centroid_polygon from compas.geometry import is_coplanar from compas.geometry import is_polygon_convex from compas.geometry import transform_points from compas.geometry import earclip_polygon from compas.geometry import bounding_box - -# from compas.geometry import bestfit_plane from compas.geometry import Geometry from compas.geometry import Transformation from compas.geometry import Point - -# from compas.geometry import Vector from compas.geometry import Plane from compas.geometry import Frame from compas.geometry import Line @@ -87,9 +80,9 @@ class Polygon(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", - "properties": {"points": {"type": "array", "minItems": 2, "items": Point.JSONSCHEMA}}, + "properties": {"points": {"type": "array", "minItems": 2, "items": Point.DATASCHEMA}}, "required": ["points"], } @@ -102,7 +95,7 @@ def __init__(self, points, **kwargs): self.points = points def __repr__(self): - return "Polygon([{0}])".format(", ".join(["{0!r}".format(point) for point in self.points])) + return "{0}(points={1!r})".format(type(self).__name__, self.points) def __len__(self): return len(self.points) @@ -128,11 +121,7 @@ def __eq__(self, other): @property def data(self): - return {"points": self.points} - - @data.setter - def data(self, data): - self.points = data["points"] + return {"points": [point.data for point in self.points]} # ========================================================================== # Properties @@ -178,21 +167,6 @@ def centroid(self): @property def normal(self): - # o = self.centroid - # points = self.points - # a2 = 0 - # normals = [] - # for i in range(-1, len(points) - 1): - # p1 = points[i] - # p2 = points[i + 1] - # u = [p1[_] - o[_] for _ in range(3)] # type: ignore - # v = [p2[_] - o[_] for _ in range(3)] # type: ignore - # w = cross_vectors(u, v) - # a2 += sum(w[_] ** 2 for _ in range(3)) ** 0.5 - # normals.append(w) - # n = [sum(axis) / a2 for axis in zip(*normals)] - # n = Vector(*n) - # return n return self.plane.normal @property diff --git a/src/compas/geometry/polyhedron.py b/src/compas/geometry/polyhedron.py index 5c3fd099efe..adbb72c7e33 100644 --- a/src/compas/geometry/polyhedron.py +++ b/src/compas/geometry/polyhedron.py @@ -169,7 +169,7 @@ class Polyhedron(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "vertices": { @@ -203,7 +203,11 @@ def __init__(self, vertices, faces, **kwargs): self.faces = faces def __repr__(self): - return "".format(len(self.vertices), len(self.faces)) + return "{0}(vertices={1!r}, faces={2!r})".format( + type(self).__name__, + self.vertices, + self.faces, + ) def __len__(self): return 2 @@ -255,43 +259,15 @@ def __or__(self, other): return self.__add__(other) # ========================================================================== - # data + # Data # ========================================================================== @property def data(self): return {"vertices": self.vertices, "faces": self.faces} - @data.setter - def data(self, data): - self.vertices = data["vertices"] - self.faces = data["faces"] - - @classmethod - def from_data(cls, data): - """Construct a polyhedron from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Polyhedron` - The constructed polyhedron. - - Examples - -------- - >>> from compas.geometry import Polyhedron - >>> p = Polyhedron.from_platonicsolid(4) - >>> q = Polyhedron.from_data(p.data) - - """ - return cls(**data) - # ========================================================================== - # properties + # Properties # ========================================================================== @property diff --git a/src/compas/geometry/projection.py b/src/compas/geometry/projection.py index 69644a6c5cb..7a0aef1823f 100644 --- a/src/compas/geometry/projection.py +++ b/src/compas/geometry/projection.py @@ -48,9 +48,6 @@ def __init__(self, matrix=None, check=False): raise ValueError("This is not a proper projection matrix.") super(Projection, self).__init__(matrix=matrix) - def __repr__(self): - return "Projection({0!r}, check=False)".format(self.matrix) - @classmethod def from_plane(cls, plane): """Construct an orthogonal projection transformation to project onto a plane. diff --git a/src/compas/geometry/quaternion.py b/src/compas/geometry/quaternion.py index 7bac8bb464d..8c5f4927cb6 100644 --- a/src/compas/geometry/quaternion.py +++ b/src/compas/geometry/quaternion.py @@ -110,7 +110,7 @@ class Quaternion(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "w": {"type": "number"}, @@ -132,45 +132,57 @@ def __init__(self, w, x, y, z, **kwargs): self.y = y self.z = z - # ========================================================================== - # data - # ========================================================================== + def __repr__(self): + return "{0}({1}, {2}, {3}, {4})".format(type(self).__name__, self.w, self.x, self.y, self.z) - @property - def data(self): - """dict : Representation of the quaternion as a dict containing only native Python objects.""" - return {"w": self.w, "x": self.x, "y": self.y, "z": self.z} + def __eq__(self, other, tol=1e-05): + if not hasattr(other, "__iter__") or not hasattr(other, "__len__") or len(self) != len(other): + return False + for v1, v2 in zip(self, other): + if math.fabs(v1 - v2) > tol: + return False + return True - @data.setter - def data(self, data): - self.w = data["w"] - self.x = data["x"] - self.y = data["y"] - self.z = data["z"] + def __getitem__(self, key): + if key == 0: + return self.w + if key == 1: + return self.x + if key == 2: + return self.y + if key == 3: + return self.z + raise KeyError - @classmethod - def from_data(cls, data): - """Construct a quaternion from a data dict. + def __setitem__(self, key, value): + if key == 0: + self.w = value + return + if key == 1: + self.x = value + return + if key == 2: + self.y = value + if key == 3: + self.z = value + raise KeyError - Parameters - ---------- - data : dict - The data dictionary. + def __iter__(self): + return iter(self.wxyz) - Returns - ------- - :class:`~compas.geometry.Quaternion` - The constructed quaternion. + def __len__(self): + return 4 - Examples - -------- - >>> + # ========================================================================== + # Data + # ========================================================================== - """ - return cls(data["w"], data["x"], data["y"], data["z"]) + @property + def data(self): + return {"w": self.w, "x": self.x, "y": self.y, "z": self.z} # ========================================================================== - # properties + # Properties # ========================================================================== @property @@ -222,52 +234,9 @@ def is_unit(self): return quaternion_is_unit(self) # ========================================================================== - # customization + # Operators # ========================================================================== - def __getitem__(self, key): - if key == 0: - return self.w - if key == 1: - return self.x - if key == 2: - return self.y - if key == 3: - return self.z - raise KeyError - - def __setitem__(self, key, value): - if key == 0: - self.w = value - return - if key == 1: - self.x = value - return - if key == 2: - self.y = value - if key == 3: - self.z = value - raise KeyError - - def __eq__(self, other, tol=1e-05): - if not hasattr(other, "__iter__") or not hasattr(other, "__len__") or len(self) != len(other): - return False - for v1, v2 in zip(self, other): - if math.fabs(v1 - v2) > tol: - return False - return True - - def __iter__(self): - return iter(self.wxyz) - - def __len__(self): - return 4 - - def __repr__(self): - return "Quaternion({:.{prec}f}, {:.{prec}f}, {:.{prec}f}, {:.{prec}f})".format( - self.w, self.x, self.y, self.z, prec=3 - ) - def __mul__(self, other): """Multiply operator for two quaternions. @@ -301,7 +270,7 @@ def __mul__(self, other): return Quaternion(*p) # ========================================================================== - # constructors + # Constructors # ========================================================================== @classmethod @@ -379,7 +348,7 @@ def from_rotation(cls, R): return cls.from_matrix(R.matrix) # ========================================================================== - # methods + # Methods # ========================================================================== def unitize(self): diff --git a/src/compas/geometry/reflection.py b/src/compas/geometry/reflection.py index 3b106a8c065..7b32a1cf818 100644 --- a/src/compas/geometry/reflection.py +++ b/src/compas/geometry/reflection.py @@ -11,14 +11,13 @@ Ippoliti for providing code and documentation. """ -# from compas.utilities import flatten -# from compas.geometry import allclose +from compas.utilities import flatten +from compas.geometry import allclose from compas.geometry import dot_vectors from compas.geometry import cross_vectors from compas.geometry import normalize_vector - -# from compas.geometry import decompose_matrix -# from compas.geometry import matrix_from_perspective_entries +from compas.geometry import decompose_matrix +from compas.geometry import matrix_from_perspective_entries from compas.geometry import identity_matrix from compas.geometry import Transformation @@ -46,15 +45,11 @@ class Reflection(Transformation): def __init__(self, matrix=None, check=False): if matrix and check: - # _, _, _, _, perspective = decompose_matrix(matrix) - # if not allclose(flatten(matrix), flatten(matrix_from_perspective_entries(perspective))): - # raise ValueError("This is not a proper reflection matrix.") - pass + _, _, _, _, perspective = decompose_matrix(matrix) + if not allclose(flatten(matrix), flatten(matrix_from_perspective_entries(perspective))): + raise ValueError("This is not a proper reflection matrix.") super(Reflection, self).__init__(matrix=matrix) - def __repr__(self): - return "Reflection({0!r}, check=False)".format(self.matrix) - @classmethod def from_plane(cls, plane): """Construct a reflection transformation that mirrors wrt the given plane. diff --git a/src/compas/geometry/rotation.py b/src/compas/geometry/rotation.py index 61db06ff7e8..41925c1baf8 100644 --- a/src/compas/geometry/rotation.py +++ b/src/compas/geometry/rotation.py @@ -79,9 +79,6 @@ def __init__(self, matrix=None, check=False): raise ValueError("This is not a proper rotation matrix.") super(Rotation, self).__init__(matrix=matrix) - def __repr__(self): - return "Rotation({0!r}, check=False)".format(self.matrix) - @property def quaternion(self): from compas.geometry import Quaternion diff --git a/src/compas/geometry/scale.py b/src/compas/geometry/scale.py index 0bcf4769489..4cac8e7af13 100644 --- a/src/compas/geometry/scale.py +++ b/src/compas/geometry/scale.py @@ -62,9 +62,6 @@ def __init__(self, matrix=None, check=False): raise ValueError("This is not a proper scale matrix.") super(Scale, self).__init__(matrix=matrix) - def __repr__(self): - return "Scale({0!r}, check=False)".format(self.matrix) - @classmethod def from_factors(cls, factors, frame=None): """Construct a scale transformation from scale factors. diff --git a/src/compas/geometry/shapes/box.py b/src/compas/geometry/shapes/box.py index 03dcbe574fa..1a0f675f1a4 100644 --- a/src/compas/geometry/shapes/box.py +++ b/src/compas/geometry/shapes/box.py @@ -96,13 +96,13 @@ class Box(Shape): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "xsize": {"type": "number", "minimum": 0}, "ysize": {"type": "number", "minimum": 0}, "zsize": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "additionalProperties": False, "minProperties": 4, @@ -118,8 +118,12 @@ def __init__(self, xsize=1.0, ysize=None, zsize=None, frame=None, **kwargs): self.zsize = xsize if zsize is None else zsize def __repr__(self): - return "Box(xsize={0!r}, ysize={1!r}, zsize={2!r}, frame={3!r})".format( - self.xsize, self.ysize, self.zsize, self.frame + return "{0}(xsize={1}, ysize={2}, zsize={3}, frame={4!r})".format( + type(self).__name__, + self.xsize, + self.ysize, + self.zsize, + self.frame, ) def __len__(self): @@ -159,18 +163,20 @@ def __iter__(self): @property def data(self): return { - "frame": self.frame, "xsize": self.xsize, "ysize": self.ysize, "zsize": self.zsize, + "frame": self.frame.data, } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.xsize = data["xsize"] - self.ysize = data["ysize"] - self.zsize = data["zsize"] + @classmethod + def from_data(cls, data): + return cls( + xsize=data["xsize"], + ysize=data["ysize"], + zsize=data["zsize"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # Properties diff --git a/src/compas/geometry/shapes/capsule.py b/src/compas/geometry/shapes/capsule.py index 8af46cbe713..539076892c0 100644 --- a/src/compas/geometry/shapes/capsule.py +++ b/src/compas/geometry/shapes/capsule.py @@ -73,17 +73,17 @@ class Capsule(Shape): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "frame": Frame.JSONSCHEMA, "radius": {"type": "number", "minimum": 0}, "height": {"type": "number", "minimum": 0}, + "frame": Frame.DATASCHEMA, }, - "required": ["frame", "radius", "height"], + "required": ["radius", "height", "frame"], } - def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs): + def __init__(self, radius, height, frame=None, **kwargs): super(Capsule, self).__init__(frame=frame, **kwargs) self._radius = None self._height = None @@ -91,33 +91,12 @@ def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs): self.height = height def __repr__(self): - return "Capsule(frame={0!r}, radius={1!r}, height={2!r})".format(self.frame, self.radius, self.height) - - def __len__(self): - return 2 - - def __getitem__(self, key): - if key == 0: - return self.frame - elif key == 1: - return self.radius - elif key == 2: - return self.height - else: - raise KeyError - - def __setitem__(self, key, value): - if key == 0: - self.frame = value - elif key == 1: - self.radius = value - elif key == 2: - self.height = value - else: - raise KeyError - - def __iter__(self): - return iter([self.frame, self.radius, self.height]) + return "{0}(radius={1}, height={2}, frame={3!r})".format( + type(self).__name__, + self.radius, + self.height, + self.frame, + ) # ========================================================================== # Data @@ -125,13 +104,19 @@ def __iter__(self): @property def data(self): - return {"frame": self.frame, "radius": self.radius, "height": self.height} + return { + "radius": self.radius, + "height": self.height, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.radius = data["radius"] - self.height = data["height"] + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + height=data["height"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # Properties diff --git a/src/compas/geometry/shapes/cone.py b/src/compas/geometry/shapes/cone.py index 7e4b12f1a55..40d74a6efc5 100644 --- a/src/compas/geometry/shapes/cone.py +++ b/src/compas/geometry/shapes/cone.py @@ -75,17 +75,17 @@ class Cone(Shape): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "frame": Frame.JSONSCHEMA, "radius": {"type": "number", "minimum": 0}, "height": {"type": "number", "minimum": 0}, + "frame": Frame.DATASCHEMA, }, - "required": ["frame", "radius", "height"], + "required": ["radius", "height", "frame"], } - def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs): + def __init__(self, radius, height, frame=None, **kwargs): super(Cone, self).__init__(frame=frame, **kwargs) self._radius = None self._height = None @@ -93,33 +93,12 @@ def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs): self.height = height def __repr__(self): - return "Cone(frame={0!r}, radius={1!r}, height={2!r})".format(self.frame, self.radius, self.height) - - def __len__(self): - return 2 - - def __getitem__(self, key): - if key == 0: - return self.frame - elif key == 1: - return self.radius - if key == 2: - return self.height - else: - raise KeyError - - def __setitem__(self, key, value): - if key == 0: - self.frame = value - elif key == 1: - self.radius = value - elif key == 2: - self.height = value - else: - raise KeyError - - def __iter__(self): - return iter([self.frame, self.radius, self.height]) + return "{0}(radius={1}, height={2}, frame={3!r})".format( + type(self).__name__, + self.radius, + self.height, + self.frame, + ) # ========================================================================== # data @@ -127,13 +106,19 @@ def __iter__(self): @property def data(self): - return {"frame": self.frame, "radius": self.radius, "height": self.height} + return { + "radius": self.radius, + "height": self.height, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.radius = data["radius"] - self.height = data["height"] + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + height=data["height"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # properties diff --git a/src/compas/geometry/shapes/cylinder.py b/src/compas/geometry/shapes/cylinder.py index 834942e12ed..3c2e36f7fa8 100644 --- a/src/compas/geometry/shapes/cylinder.py +++ b/src/compas/geometry/shapes/cylinder.py @@ -72,17 +72,17 @@ class Cylinder(Shape): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "frame": Frame.JSONSCHEMA, "radius": {"type": "number", "minimum": 0}, "height": {"type": "number", "minimum": 0}, + "frame": Frame.DATASCHEMA, }, - "required": ["frame", "radius", "height"], + "required": ["radius", "height", "frame"], } - def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs): + def __init__(self, radius, height, frame=None, **kwargs): super(Cylinder, self).__init__(frame=frame, **kwargs) self._radius = None self._height = None @@ -90,33 +90,12 @@ def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs): self.height = height def __repr__(self): - return "Cylinder(frame={0!r}, radius={1!r}, height={2!r})".format(self.frame, self.radius, self.height) - - def __len__(self): - return 2 - - def __getitem__(self, key): - if key == 0: - return self.frame - elif key == 1: - return self.radius - elif key == 2: - return self.height - else: - raise KeyError - - def __setitem__(self, key, value): - if key == 0: - self.frame = value - elif key == 1: - self.radius = value - elif key == 2: - self.height = value - else: - raise KeyError - - def __iter__(self): - return iter([self.frame, self.radius, self.height]) + return "{0}(radius={1}, height={2}, frame={3!r})".format( + type(self).__name__, + self.radius, + self.height, + self.frame, + ) # ========================================================================== # Data @@ -124,13 +103,19 @@ def __iter__(self): @property def data(self): - return {"frame": self.frame, "radius": self.radius, "height": self.height} + return { + "radius": self.radius, + "height": self.height, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.radius = data["radius"] - self.height = data["height"] + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + height=data["height"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # Properties diff --git a/src/compas/geometry/shapes/shape.py b/src/compas/geometry/shapes/shape.py index c10d94c5a16..140189291b5 100644 --- a/src/compas/geometry/shapes/shape.py +++ b/src/compas/geometry/shapes/shape.py @@ -90,10 +90,6 @@ def point(self): def point(self, point): self.frame.point = point - @property - def normal(self): - return self.frame.zaxis - @property def plane(self): return Plane(self.frame.point, self.frame.zaxis) diff --git a/src/compas/geometry/shapes/sphere.py b/src/compas/geometry/shapes/sphere.py index ea1b39b6d9c..e273c7105d9 100644 --- a/src/compas/geometry/shapes/sphere.py +++ b/src/compas/geometry/shapes/sphere.py @@ -60,16 +60,16 @@ class Sphere(Shape): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "frame": Frame.JSONSCHEMA, "radius": {"type": "number", "minimum": 0}, + "frame": Frame.DATASCHEMA, }, - "required": ["frame", "radius"], + "required": ["radius", "frame"], } - def __init__(self, frame=None, radius=None, point=None, **kwargs): + def __init__(self, radius, point=None, frame=None, **kwargs): super(Sphere, self).__init__(frame=frame, **kwargs) self._radius = 1.0 self.radius = radius @@ -77,29 +77,11 @@ def __init__(self, frame=None, radius=None, point=None, **kwargs): self.frame.point = point def __repr__(self): - return "Sphere(frame={0!r}, radius={1!r})".format(self.frame, self.radius) - - def __len__(self): - return 2 - - def __getitem__(self, key): - if key == 0: - return self.frame - elif key == 1: - return self.radius - else: - raise KeyError - - def __setitem__(self, key, value): - if key == 0: - self.frame = value - elif key == 1: - self.radius = value - else: - raise KeyError - - def __iter__(self): - return iter([self.frame, self.radius]) + return "{0}(radius={1}, frame={2!r})".format( + type(self).__name__, + self.radius, + self.frame, + ) # ========================================================================== # Data @@ -107,12 +89,17 @@ def __iter__(self): @property def data(self): - return {"frame": self.frame, "radius": self.radius} + return { + "radius": self.radius, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.radius = data["radius"] + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # Properties diff --git a/src/compas/geometry/shapes/torus.py b/src/compas/geometry/shapes/torus.py index 64f52ddcd42..bd869272063 100644 --- a/src/compas/geometry/shapes/torus.py +++ b/src/compas/geometry/shapes/torus.py @@ -63,17 +63,17 @@ class Torus(Shape): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "frame": Frame.JSONSCHEMA, "radius_axis": {"type": "number", "minimum": 0}, "radius_pipe": {"type": "number", "minimum": 0}, + "frame": Frame.DATASCHEMA, }, - "required": ["frame", "radius_axis", "radius_pipe"], + "required": ["radius_axis", "radius_pipe", "frame"], } - def __init__(self, frame=None, radius_axis=1.0, radius_pipe=0.3, **kwargs): + def __init__(self, radius_axis, radius_pipe, frame=None, **kwargs): super(Torus, self).__init__(frame=frame, **kwargs) self._radius_axis = None self._radius_pipe = None @@ -87,38 +87,18 @@ def __init__(self, frame=None, radius_axis=1.0, radius_pipe=0.3, **kwargs): @property def data(self): return { - "frame": self.frame, "radius_axis": self.radius_axis, "radius_pipe": self.radius_pipe, + "frame": self.frame.data, } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.radius_axis = data["radius_axis"] - self.radius_pipe = data["radius_pipe"] - @classmethod def from_data(cls, data): - """Construct a torus from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Torus` - The constructed torus. - - Examples - -------- - >>> data = {"frame": Frame.worldXY(), "radius_axis": 1.0, "radius_pipe": 0.3} - >>> torus = Torus.from_data(data) - - """ - return cls(**data) + return cls( + radius_axis=data["radius_axis"], + radius_pipe=data["radius_pipe"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # properties diff --git a/src/compas/geometry/shear.py b/src/compas/geometry/shear.py index 0a5a7557e61..63c2e6d25fa 100644 --- a/src/compas/geometry/shear.py +++ b/src/compas/geometry/shear.py @@ -51,9 +51,6 @@ def __init__(self, matrix=None, check=False): raise ValueError("This is not a proper shear matrix.") super(Shear, self).__init__(matrix=matrix) - def __repr__(self): - return "Shear({0!r}, check=False)".format(self.matrix) - @classmethod def from_angle_direction_plane(cls, angle, direction, plane): """Construct a shear transformation from an angle, direction and plane. diff --git a/src/compas/geometry/surfaces/conical.py b/src/compas/geometry/surfaces/conical.py index 54937a6d2b7..2def358d447 100644 --- a/src/compas/geometry/surfaces/conical.py +++ b/src/compas/geometry/surfaces/conical.py @@ -29,12 +29,12 @@ class ConicalSurface(Surface): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "radius": {"type": "number", "minimum": 0}, "height": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["radius", "height", "frame"], } @@ -54,7 +54,12 @@ def __init__(self, radius, height, frame=None, **kwargs): self.height = height def __repr__(self): - return "ConicalSurface(radius={0!r}, height={1!r}, frame={2!r})".format(self.radius, self.height, self.frame) + return "{0}(radius={1}, height={2}, frame={3!r})".format( + type(self).__name__, + self.radius, + self.height, + self.frame, + ) def __eq__(self, other): try: @@ -65,15 +70,29 @@ def __eq__(self, other): return False return self.radius == other_radius and self.height == other_height and self.frame == other_frame + # ============================================================================= + # Data + # ============================================================================= + @property def data(self): - return {"radius": self.radius, "height": self.height, "frame": self.frame} + return { + "radius": self.radius, + "height": self.height, + "frame": self.frame.data, + } + + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + height=data["height"], + frame=Frame.from_data(data["frame"]), + ) - @data.setter - def data(self, data): - self.radius = data["radius"] - self.height = data["height"] - self.frame = data["frame"] + # ============================================================================= + # Properties + # ============================================================================= @property def center(self): diff --git a/src/compas/geometry/surfaces/cylindrical.py b/src/compas/geometry/surfaces/cylindrical.py index 5f721aadcc1..c6bdb4f60b7 100644 --- a/src/compas/geometry/surfaces/cylindrical.py +++ b/src/compas/geometry/surfaces/cylindrical.py @@ -31,11 +31,11 @@ class CylindricalSurface(Surface): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "radius": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["radius", "frame"], } @@ -53,7 +53,11 @@ def __init__(self, radius, frame=None, **kwargs): self.radius = radius def __repr__(self): - return "CylindricalSurface(radius={0!r}, frame={1!r})".format(self.radius, self.frame) + return "{0}(radius={1}, frame={2!r})".format( + type(self).__name__, + self.radius, + self.frame, + ) def __eq__(self, other): try: @@ -63,14 +67,27 @@ def __eq__(self, other): return False return self.radius == other_radius and self.frame == other_frame + # ============================================================================= + # Data + # ============================================================================= + @property def data(self): - return {"radius": self.radius, "frame": self.frame} + return { + "radius": self.radius, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.radius = data["radius"] - self.frame = data["frame"] + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + frame=Frame.from_data(data["frame"]), + ) + + # ============================================================================= + # Properties + # ============================================================================= @property def center(self): diff --git a/src/compas/geometry/surfaces/nurbs.py b/src/compas/geometry/surfaces/nurbs.py index 9d23b571712..1c52d262816 100644 --- a/src/compas/geometry/surfaces/nurbs.py +++ b/src/compas/geometry/surfaces/nurbs.py @@ -3,6 +3,7 @@ from __future__ import division from compas.plugins import pluggable +from compas.plugins import PluginNotInstalledError from compas.geometry import Point from compas.utilities import linspace from compas.utilities import meshgrid @@ -12,27 +13,27 @@ @pluggable(category="factories") def new_nurbssurface(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbssurface_from_parameters(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbssurface_from_points(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbssurface_from_fill(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbssurface_from_step(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError class NurbsSurface(Surface): @@ -64,10 +65,10 @@ class NurbsSurface(Surface): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "points": {"type": "array", "items": {"type": "array", "items": Point.JSONSCHEMA}}, + "points": {"type": "array", "items": {"type": "array", "items": Point.DATASCHEMA}}, "weights": {"type": "array", "items": {"type": "array", "items": {"type": "number"}}}, "u_knots": {"type": "array", "items": {"type": "number"}}, "v_knots": {"type": "array", "items": {"type": "number"}}, @@ -88,24 +89,20 @@ def __new__(cls, *args, **kwargs): def __init__(self, name=None): super(NurbsSurface, self).__init__(name=name) - def __str__(self): - lines = [ - "NurbsSurface", - "------------", - "Points: {}".format(self.points), - "Weights: {}".format(self.weights), - "U Knots: {}".format(self.u_knots), - "V Knots: {}".format(self.v_knots), - "U Mults: {}".format(self.u_mults), - "V Mults: {}".format(self.v_mults), - "U Degree: {}".format(self.u_degree), - "V Degree: {}".format(self.v_degree), - "U Domain: {}".format(self.u_domain), - "V Domain: {}".format(self.v_domain), - "U Periodic: {}".format(self.is_u_periodic), - "V Periodic: {}".format(self.is_v_periodic), - ] - return "\n".join(lines) + def __repr__(self): + return "{0}(points={1!r}, weigths={2}, u_knots={3}, v_knots={4}, u_mults={5}, v_mults={6}, u_degree={7}, v_degree={8}, is_u_periodic={9}, is_v_periodic={10})".format( + type(self).__name__, + self.points, + self.weights, + self.u_knots, + self.v_knots, + self.u_mults, + self.v_mults, + self.u_degree, + self.v_degree, + self.is_u_periodic, + self.is_v_periodic, + ) # ============================================================================== # Data @@ -117,9 +114,8 @@ def dtype(self): @property def data(self): - """dict : Representation of the curve as a dict containing only native Python objects.""" return { - "points": self.points, + "points": [point.data for point in self.points], "weights": self.weights, "u_knots": self.u_knots, "v_knots": self.v_knots, @@ -131,10 +127,6 @@ def data(self): "is_v_periodic": self.is_v_periodic, } - @data.setter - def data(self, data): - raise NotImplementedError - @classmethod def from_data(cls, data): """Construct a BSpline surface from its data representation. @@ -150,27 +142,17 @@ def from_data(cls, data): The constructed surface. """ - points = data["points"] - weights = data["weights"] - u_knots = data["u_knots"] - v_knots = data["v_knots"] - u_mults = data["u_mults"] - v_mults = data["v_mults"] - u_degree = data["u_degree"] - v_degree = data["v_degree"] - is_u_periodic = data["is_u_periodic"] - is_v_periodic = data["is_v_periodic"] return cls.from_parameters( - points, - weights, - u_knots, - v_knots, - u_mults, - v_mults, - u_degree, - v_degree, - is_u_periodic, - is_v_periodic, + data["points"], + data["weights"], + data["u_knots"], + data["v_knots"], + data["u_mults"], + data["v_mults"], + data["u_degree"], + data["v_degree"], + data["is_u_periodic"], + data["is_v_periodic"], ) # ============================================================================== diff --git a/src/compas/geometry/surfaces/planar.py b/src/compas/geometry/surfaces/planar.py index da357bdcae5..be14cc22e3f 100644 --- a/src/compas/geometry/surfaces/planar.py +++ b/src/compas/geometry/surfaces/planar.py @@ -27,12 +27,12 @@ class PlanarSurface(Surface): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "xsize": {"type": "number", "minimum": 0}, "ysize": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["xsize", "ysize", "frame"], } @@ -52,7 +52,12 @@ def __init__(self, xsize=1.0, ysize=1.0, frame=None): self.ysize = ysize def __repr__(self): - return "PlanarSurface(xsize={0!r}, ysize={1!r}, frame={2!r})".format(self.xsize, self.ysize, self.frame) + return "{0}(xsize={1}, ysize={2}, frame={3!r})".format( + type(self).__name__, + self.xsize, + self.ysize, + self.frame, + ) def __eq__(self, other): try: @@ -63,38 +68,52 @@ def __eq__(self, other): return False return self.xsize == other_xsize and self.ysize == other_ysize and self.frame == other_frame + # ============================================================================= + # Data + # ============================================================================= + @property def data(self): - return {"xsize": self.xsize, "ysize": self.ysize, "frame": self.frame} + return { + "xsize": self.xsize, + "ysize": self.ysize, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.xsize = data["xsize"] - self.ysize = data["ysize"] + @classmethod + def from_data(cls, data): + return cls( + xsize=data["xsize"], + ysize=data["ysize"], + frame=Frame.from_data(data["frame"]), + ) + + # ============================================================================= + # Properties + # ============================================================================= @property def xsize(self): - if not self._xsize: + if self._xsize is None: raise ValueError("The size of the surface in the local X-direction is not set.") return self._xsize @xsize.setter def xsize(self, xsize): - if xsize <= 0: - raise ValueError("The size of the surface in the local X-direction should be larger than zero.") + if xsize < 0: + raise ValueError("The size of the surface in the local X-direction should be at least zero.") self._xsize = float(xsize) @property def ysize(self): - if not self._ysize: + if self._ysize is None: raise ValueError("The size of the surface in the local Y-direction is not set.") return self._ysize @ysize.setter def ysize(self, ysize): - if ysize <= 0: - raise ValueError("The size of the surface in the local Y-direction should be larger than zero.") + if ysize < 0: + raise ValueError("The size of the surface in the local Y-direction should be at least zero.") self._ysize = float(ysize) # ============================================================================= diff --git a/src/compas/geometry/surfaces/spherical.py b/src/compas/geometry/surfaces/spherical.py index 0187a0c32c4..1d072536593 100644 --- a/src/compas/geometry/surfaces/spherical.py +++ b/src/compas/geometry/surfaces/spherical.py @@ -32,11 +32,11 @@ class SphericalSurface(Surface): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "radius": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["radius", "frame"], } @@ -54,7 +54,11 @@ def __init__(self, radius, frame=None, **kwargs): self.radius = radius def __repr__(self): - return "SphericalSurface(radius={0!r}, frame={1!r})".format(self.radius, self.frame) + return "{0}(radius={1}, frame={2!r})".format( + type(self).__name__, + self.radius, + self.frame, + ) def __eq__(self, other): try: @@ -64,14 +68,27 @@ def __eq__(self, other): return False return self.radius == other_radius and self.frame == other_frame + # ============================================================================= + # Data + # ============================================================================= + @property def data(self): - return {"radius": self.radius, "frame": self.frame} + return { + "radius": self.radius, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.radius = data["radius"] - self.frame = data["frame"] + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + frame=Frame.from_data(data["frame"]), + ) + + # ============================================================================= + # Properties + # ============================================================================= @property def center(self): diff --git a/src/compas/geometry/surfaces/surface.py b/src/compas/geometry/surfaces/surface.py index 6f6929ef697..35027d81c74 100644 --- a/src/compas/geometry/surfaces/surface.py +++ b/src/compas/geometry/surfaces/surface.py @@ -63,40 +63,13 @@ def __init__(self, frame=None, name=None): if frame: self.frame = frame - def __eq__(self, other): - raise NotImplementedError - - def __str__(self): - return "".format(self.u_domain, self.v_domain) - - # ============================================================================== - # Data - # ============================================================================== - - @property - def data(self): - raise NotImplementedError - - @data.setter - def data(self, data): - raise NotImplementedError - - @classmethod - def from_data(cls, data): - """Construct a surface from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Surface` - The constructed surface. - - """ - return cls(**data) + def __repr__(self): + return "{0}(frame={1!r}, u_domain={2}, v_domain={3})".format( + type(self).__name__, + self.frame, + self.u_domain, + self.v_domain, + ) # ============================================================================== # Properties diff --git a/src/compas/geometry/surfaces/toroidal.py b/src/compas/geometry/surfaces/toroidal.py index 13c5d7ee47c..7707851e1fb 100644 --- a/src/compas/geometry/surfaces/toroidal.py +++ b/src/compas/geometry/surfaces/toroidal.py @@ -30,14 +30,14 @@ class ToroidalSurface(Surface): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "radius_axis": {"type": "number", "minimum": 0}, "radius_pipe": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, - "required": ["radius", "frame"], + "required": ["radius_axis", "radius_pipe", "frame"], } # overwriting the __new__ method is necessary @@ -55,8 +55,11 @@ def __init__(self, radius_axis, radius_pipe, frame=None, **kwargs): self.radius_pipe = radius_pipe def __repr__(self): - return "ToroidalSurface(radius_axis={0!r}, radius_pipe={1!r}, frame={2!r})".format( - self.radius_axis, self.radius_pipe, self.frame + return "{0}(radius_axis={1}, radius_pipe={2}, frame={3!r})".format( + type(self).__name__, + self.radius_axis, + self.radius_pipe, + self.frame, ) def __eq__(self, other): @@ -72,15 +75,29 @@ def __eq__(self, other): and self.frame == other_frame ) + # ============================================================================= + # Data + # ============================================================================= + @property def data(self): - return {"radius_axis": self.radius_axis, "radius_pipe": self.radius_pipe, "frame": self.frame} + return { + "radius_axis": self.radius_axis, + "radius_pipe": self.radius_pipe, + "frame": self.frame.data, + } + + @classmethod + def from_data(cls, data): + return cls( + radius_axis=data["radius_axis"], + radius_pipe=data["radius_pipe"], + frame=Frame.from_data(data["frame"]), + ) - @data.setter - def data(self, data): - self.radius_axis = data["radius_axis"] - self.radius_pipe = data["radius_pipe"] - self.frame = data["frame"] + # ============================================================================= + # Properties + # ============================================================================= @property def center(self): diff --git a/src/compas/geometry/transformation.py b/src/compas/geometry/transformation.py index 2d214acd4be..ad8b5924efb 100644 --- a/src/compas/geometry/transformation.py +++ b/src/compas/geometry/transformation.py @@ -77,7 +77,7 @@ class Transformation(Data): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "matrix": { @@ -101,17 +101,63 @@ def __init__(self, matrix=None): matrix = identity_matrix(4) self.matrix = matrix + def __mul__(self, other): + return self.concatenated(other) + + def __imul__(self, other): + return self.concatenated(other) + + def __getitem__(self, key): + i, j = key + return self.matrix[i][j] + + def __setitem__(self, key, value): + i, j = key + self.matrix[i][j] = value + + def __iter__(self): + return iter(self.matrix) + + def __eq__(self, other, tol=1e-05): + try: + A = self.matrix + B = other.matrix + for i in range(4): + for j in range(4): + if math.fabs(A[i][j] - B[i][j]) > tol: + return False + return True + except BaseException: + return False + + def __ne__(self, other): + # this is not obvious to ironpython + return not self.__eq__(other) + + def __repr__(self): + return "{0}({1!r}, check=False)".format(self.__class__.__name__, self.matrix) + + def __str__(self): + s = "[[%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[0]]) + s += " [%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[1]]) + s += " [%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[2]]) + s += " [%s]]\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[3]]) + return s + + def __len__(self): + return len(self.matrix) + # ========================================================================== - # descriptors + # Data # ========================================================================== @property def data(self): return {"matrix": self.matrix} - @data.setter - def data(self, data): - self.matrix = data["matrix"] + # ========================================================================== + # Properties + # ========================================================================== @property def scale(self): @@ -161,57 +207,7 @@ def determinant(self): return matrix_determinant(self.matrix) # ========================================================================== - # customisation - # ========================================================================== - - def __mul__(self, other): - return self.concatenated(other) - - def __imul__(self, other): - return self.concatenated(other) - - def __getitem__(self, key): - i, j = key - return self.matrix[i][j] - - def __setitem__(self, key, value): - i, j = key - self.matrix[i][j] = value - - def __iter__(self): - return iter(self.matrix) - - def __eq__(self, other, tol=1e-05): - try: - A = self.matrix - B = other.matrix - for i in range(4): - for j in range(4): - if math.fabs(A[i][j] - B[i][j]) > tol: - return False - return True - except BaseException: - return False - - def __ne__(self, other): - # this is not obvious to ironpython - return not self.__eq__(other) - - def __repr__(self): - return "Transformation({0!r})".format(self.matrix) - - def __str__(self): - s = "[[%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[0]]) - s += " [%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[1]]) - s += " [%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[2]]) - s += " [%s]]\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[3]]) - return s - - def __len__(self): - return len(self.matrix) - - # ========================================================================== - # constructors + # Constructors # ========================================================================== @classmethod @@ -392,7 +388,7 @@ def from_change_of_basis(cls, frame_from, frame_to): return cls(multiply_matrices(matrix_inverse(T2.matrix), T1.matrix)) # ========================================================================== - # methods + # Methods # ========================================================================== def copy(self): diff --git a/src/compas/geometry/translation.py b/src/compas/geometry/translation.py index 017ba1c5630..cb52359950a 100644 --- a/src/compas/geometry/translation.py +++ b/src/compas/geometry/translation.py @@ -82,9 +82,6 @@ def translation_vector(self): z = self.matrix[2][3] return Vector(x, y, z) - def __repr__(self): - return "Translation({0!r}, check=False)".format(self.matrix) - @classmethod def from_vector(cls, vector): """Create a translation transformation from a translation vector. diff --git a/src/compas/geometry/vector.py b/src/compas/geometry/vector.py index 215ffb09e1f..0e43d8beb79 100644 --- a/src/compas/geometry/vector.py +++ b/src/compas/geometry/vector.py @@ -2,8 +2,6 @@ from __future__ import absolute_import from __future__ import division -from compas import PRECISION - from compas.geometry import length_vector from compas.geometry import cross_vectors from compas.geometry import subtract_vectors @@ -65,7 +63,7 @@ class Vector(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "array", "minItems": 3, "maxItems": 3, @@ -82,7 +80,12 @@ def __init__(self, x, y, z=0.0, **kwargs): self.z = z def __repr__(self): - return "Vector({0:.{3}f}, {1:.{3}f}, {2:.{3}f})".format(self.x, self.y, self.z, PRECISION[:1]) + return "{0}(x={1}, y={2}, z={3})".format( + type(self).__name__, + self.x, + self.y, + self.z, + ) def __len__(self): return 3 @@ -172,34 +175,10 @@ def __ipow__(self, n): @property def data(self): - """dict : The data dictionary that represents the vector.""" return list(self) - @data.setter - def data(self, data): - self.x = data[0] - self.y = data[1] - self.z = data[2] - @classmethod def from_data(cls, data): - """Construct a vector from a data dict. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Vector` - The vector constructed from the provided data. - - Examples - -------- - >>> Vector.from_data([0.0, 0.0, 1.0]) - Vector(0.000, 0.000, 1.000) - """ return cls(*data) # ========================================================================== diff --git a/tests/compas/colors/test_color.py b/tests/compas/colors/test_color.py new file mode 100644 index 00000000000..d771c61d613 --- /dev/null +++ b/tests/compas/colors/test_color.py @@ -0,0 +1,70 @@ +import pytest +import json +import compas +from random import random + +from compas.colors import Color +from compas.geometry import allclose + + +@pytest.mark.parametrize( + "color", + [ + (0, 0, 0), + (1, 1, 1), + (0.5, 0.5, 0.5), + (0.5, 0.5, 0.5, 0.5), + (0.5, 0.5, 0.5, 1.0), + (0.5, 0.5, 0.5, 0.0), + (random(), random(), random()), + ], +) +def test_color(color): + c = Color(*color) + assert c.r == color[0] + assert c.g == color[1] + assert c.b == color[2] + assert c.a == color[3] if len(color) == 4 else 1.0 + + assert allclose(eval(repr(c)), c, tol=1e-12) + + +def test_color_data(): + color = Color(random(), random(), random(), random()) + other = Color.from_data(json.loads(json.dumps(color.data))) + + assert color.r == other.r + assert color.g == other.g + assert color.b == other.b + assert color.a == other.a + + assert color == other + + if not compas.IPY: + assert Color.validate_data(color.data) + assert Color.validate_data(other.data) + + +def test_color_predefined(): + assert Color.red() == Color(1.0, 0.0, 0.0) + assert Color.green() == Color(0.0, 1.0, 0.0) + assert Color.blue() == Color(0.0, 0.0, 1.0) + assert Color.cyan() == Color(0.0, 1.0, 1.0) + assert Color.magenta() == Color(1.0, 0.0, 1.0) + assert Color.yellow() == Color(1.0, 1.0, 0.0) + assert Color.white() == Color(1.0, 1.0, 1.0) + assert Color.black() == Color(0.0, 0.0, 0.0) + assert Color.grey() == Color(0.5, 0.5, 0.5) + assert Color.orange() == Color(1.0, 0.5, 0.0) + assert Color.lime() == Color(0.5, 1.0, 0.0) + assert Color.mint() == Color(0.0, 1.0, 0.5) + assert Color.azure() == Color(0.0, 0.5, 1.0) + assert Color.violet() == Color(0.5, 0.0, 1.0) + assert Color.pink() == Color(1.0, 0.0, 0.5) + assert Color.brown() == Color(0.5, 0.25, 0.0) + assert Color.purple() == Color(0.5, 0.0, 0.5) + assert Color.teal() == Color(0.0, 0.5, 0.5) + assert Color.olive() == Color(0.5, 0.5, 0.0) + assert Color.navy() == Color(0.0, 0.0, 0.5) + assert Color.maroon() == Color(0.5, 0.0, 0.0) + assert Color.silver() == Color(0.75, 0.75, 0.75) diff --git a/tests/compas/compas_api.json b/tests/compas/compas_api.json index f7d73179f0e..b45f2f2bdbd 100644 --- a/tests/compas/compas_api.json +++ b/tests/compas/compas_api.json @@ -44,8 +44,7 @@ "json_dump", "json_dumps", "json_load", - "json_loads", - "validate_data" + "json_loads" ], "compas.datastructures": [ "Assembly", diff --git a/tests/compas/compas_api_ipy.json b/tests/compas/compas_api_ipy.json index a1c0833e554..6a9e331710e 100644 --- a/tests/compas/compas_api_ipy.json +++ b/tests/compas/compas_api_ipy.json @@ -44,8 +44,7 @@ "json_dump", "json_dumps", "json_load", - "json_loads", - "validate_data" + "json_loads" ], "compas.datastructures": [ "Assembly", diff --git a/tests/compas/data/test_jsonschema.py b/tests/compas/data/test_dataschema.py similarity index 91% rename from tests/compas/data/test_jsonschema.py rename to tests/compas/data/test_dataschema.py index 4c123933d0e..0a5d1f56a95 100644 --- a/tests/compas/data/test_jsonschema.py +++ b/tests/compas/data/test_dataschema.py @@ -35,7 +35,7 @@ ], ) def test_schema_point_valid(point): - Point.validate_jsondata(point) + Point.validate_data(point) @pytest.mark.parametrize( "point", @@ -47,7 +47,7 @@ def test_schema_point_valid(point): ) def test_schema_point_invalid(point): with pytest.raises(jsonschema.exceptions.ValidationError): - Point.validate_jsondata(point) + Point.validate_data(point) @pytest.mark.parametrize( "vector", @@ -58,7 +58,7 @@ def test_schema_point_invalid(point): ], ) def test_schema_vector_valid(vector): - Vector.validate_jsondata(vector) + Vector.validate_data(vector) @pytest.mark.parametrize( "vector", @@ -70,7 +70,7 @@ def test_schema_vector_valid(vector): ) def test_schema_vector_invalid(vector): with pytest.raises(jsonschema.exceptions.ValidationError): - Vector.validate_jsondata(vector) + Vector.validate_data(vector) @pytest.mark.parametrize( "line", @@ -80,7 +80,7 @@ def test_schema_vector_invalid(vector): ], ) def test_schema_line_valid(line): - Line.validate_jsondata(line) + Line.validate_data(line) @pytest.mark.parametrize( "line", @@ -91,7 +91,7 @@ def test_schema_line_valid(line): ) def test_schema_line_invalid(line): with pytest.raises(jsonschema.exceptions.ValidationError): - Line.validate_jsondata(line) + Line.validate_data(line) @pytest.mark.parametrize( "plane", @@ -100,7 +100,7 @@ def test_schema_line_invalid(line): ], ) def test_schema_plane_valid(plane): - Plane.validate_jsondata(plane) + Plane.validate_data(plane) @pytest.mark.parametrize( "plane", @@ -111,7 +111,7 @@ def test_schema_plane_valid(plane): ) def test_schema_plane_invalid(plane): with pytest.raises(jsonschema.exceptions.ValidationError): - Plane.validate_jsondata(plane) + Plane.validate_data(plane) @pytest.mark.parametrize( "circle", @@ -121,7 +121,7 @@ def test_schema_plane_invalid(plane): ], ) def test_schema_circle_valid(circle): - Circle.validate_jsondata(circle) + Circle.validate_data(circle) @pytest.mark.parametrize( "circle", @@ -139,7 +139,7 @@ def test_schema_circle_valid(circle): ) def test_schema_circle_invalid(circle): with pytest.raises(jsonschema.exceptions.ValidationError): - Circle.validate_jsondata(circle) + Circle.validate_data(circle) @pytest.mark.parametrize( "ellipse", @@ -167,7 +167,7 @@ def test_schema_circle_invalid(circle): ], ) def test_schema_ellipse_valid(ellipse): - Ellipse.validate_jsondata(ellipse) + Ellipse.validate_data(ellipse) @pytest.mark.parametrize( "ellipse", @@ -211,7 +211,7 @@ def test_schema_ellipse_valid(ellipse): ) def test_schema_ellipse_invalid(ellipse): with pytest.raises(jsonschema.exceptions.ValidationError): - Ellipse.validate_jsondata(ellipse) + Ellipse.validate_data(ellipse) @pytest.mark.parametrize( "frame", @@ -233,7 +233,7 @@ def test_schema_ellipse_invalid(ellipse): ], ) def test_schema_frame_valid(frame): - Frame.validate_jsondata(frame) + Frame.validate_data(frame) @pytest.mark.parametrize( "frame", @@ -244,7 +244,7 @@ def test_schema_frame_valid(frame): ) def test_schema_frame_invalid(frame): with pytest.raises(jsonschema.exceptions.ValidationError): - Frame.validate_jsondata(frame) + Frame.validate_data(frame) @pytest.mark.parametrize( "quaternion", @@ -255,7 +255,7 @@ def test_schema_frame_invalid(frame): ], ) def test_schema_quaternion_valid(quaternion): - Quaternion.validate_jsondata(quaternion) + Quaternion.validate_data(quaternion) @pytest.mark.parametrize( "quaternion", @@ -268,7 +268,7 @@ def test_schema_quaternion_valid(quaternion): ) def test_schema_quaternion_invalid(quaternion): with pytest.raises(jsonschema.exceptions.ValidationError): - Quaternion.validate_jsondata(quaternion) + Quaternion.validate_data(quaternion) @pytest.mark.parametrize( "polygon", @@ -278,7 +278,7 @@ def test_schema_quaternion_invalid(quaternion): ], ) def test_schema_polygon_valid(polygon): - Polygon.validate_jsondata(polygon) + Polygon.validate_data(polygon) @pytest.mark.parametrize( "polygon", @@ -290,7 +290,7 @@ def test_schema_polygon_valid(polygon): ) def test_schema_polygon_invalid(polygon): with pytest.raises(jsonschema.exceptions.ValidationError): - Polygon.validate_jsondata(polygon) + Polygon.validate_data(polygon) @pytest.mark.parametrize( "polyline", @@ -300,7 +300,7 @@ def test_schema_polygon_invalid(polygon): ], ) def test_schema_polyline_valid(polyline): - Polyline.validate_jsondata(polyline) + Polyline.validate_data(polyline) @pytest.mark.parametrize( "polyline", @@ -312,7 +312,7 @@ def test_schema_polyline_valid(polyline): ) def test_schema_polyline_invalid(polyline): with pytest.raises(jsonschema.exceptions.ValidationError): - Polyline.validate_jsondata(polyline) + Polyline.validate_data(polyline) @pytest.mark.parametrize( "box", @@ -326,7 +326,7 @@ def test_schema_polyline_invalid(polyline): ], ) def test_schema_box_valid(box): - Box.validate_jsondata(box) + Box.validate_data(box) @pytest.mark.parametrize( "box", @@ -359,7 +359,7 @@ def test_schema_box_valid(box): ) def test_schema_box_invalid(box): with pytest.raises(jsonschema.exceptions.ValidationError): - Box.validate_jsondata(box) + Box.validate_data(box) @pytest.mark.parametrize( "capsule", @@ -371,7 +371,7 @@ def test_schema_box_invalid(box): ], ) def test_schema_capsule_valid(capsule): - Capsule.validate_jsondata(capsule) + Capsule.validate_data(capsule) @pytest.mark.parametrize( "capsule", @@ -386,7 +386,7 @@ def test_schema_capsule_valid(capsule): ) def test_schema_capsule_invalid(capsule): with pytest.raises(jsonschema.exceptions.ValidationError): - Capsule.validate_jsondata(capsule) + Capsule.validate_data(capsule) @pytest.mark.parametrize( "cone", @@ -398,7 +398,7 @@ def test_schema_capsule_invalid(capsule): ], ) def test_schema_cone_valid(cone): - Cone.validate_jsondata(cone) + Cone.validate_data(cone) @pytest.mark.parametrize( "cone", @@ -413,7 +413,7 @@ def test_schema_cone_valid(cone): ) def test_schema_cone_invalid(cone): with pytest.raises(jsonschema.exceptions.ValidationError): - Cone.validate_jsondata(cone) + Cone.validate_data(cone) @pytest.mark.parametrize( "cylinder", @@ -425,7 +425,7 @@ def test_schema_cone_invalid(cone): ], ) def test_schema_cylinder_valid(cylinder): - Cylinder.validate_jsondata(cylinder) + Cylinder.validate_data(cylinder) @pytest.mark.parametrize( "cylinder", @@ -440,7 +440,7 @@ def test_schema_cylinder_valid(cylinder): ) def test_schema_cylinder_invalid(cylinder): with pytest.raises(jsonschema.exceptions.ValidationError): - Cylinder.validate_jsondata(cylinder) + Cylinder.validate_data(cylinder) @pytest.mark.parametrize( "polyhedron", @@ -452,7 +452,7 @@ def test_schema_cylinder_invalid(cylinder): ], ) def test_schema_polyhedron_valid(polyhedron): - Polyhedron.validate_jsondata(polyhedron) + Polyhedron.validate_data(polyhedron) @pytest.mark.parametrize( "polyhedron", @@ -472,7 +472,7 @@ def test_schema_polyhedron_valid(polyhedron): ) def test_schema_polyhedron_invalid(polyhedron): with pytest.raises(jsonschema.exceptions.ValidationError): - Polyhedron.validate_jsondata(polyhedron) + Polyhedron.validate_data(polyhedron) @pytest.mark.parametrize( "sphere", @@ -482,7 +482,7 @@ def test_schema_polyhedron_invalid(polyhedron): ], ) def test_schema_sphere_valid(sphere): - Sphere.validate_jsondata(sphere) + Sphere.validate_data(sphere) @pytest.mark.parametrize( "sphere", @@ -494,7 +494,7 @@ def test_schema_sphere_valid(sphere): ) def test_schema_sphere_invalid(sphere): with pytest.raises(jsonschema.exceptions.ValidationError): - Sphere.validate_jsondata(sphere) + Sphere.validate_data(sphere) @pytest.mark.parametrize( "torus", @@ -522,7 +522,7 @@ def test_schema_sphere_invalid(sphere): ], ) def test_schema_torus_valid(torus): - Torus.validate_jsondata(torus) + Torus.validate_data(torus) @pytest.mark.parametrize( "torus", @@ -566,7 +566,7 @@ def test_schema_torus_valid(torus): ) def test_schema_torus_invalid(torus): with pytest.raises(jsonschema.exceptions.ValidationError): - Torus.validate_jsondata(torus) + Torus.validate_data(torus) @pytest.mark.parametrize( "pointcloud", @@ -577,7 +577,7 @@ def test_schema_torus_invalid(torus): ], ) def test_schema_pointcloud_valid(pointcloud): - Pointcloud.validate_jsondata(pointcloud) + Pointcloud.validate_data(pointcloud) @pytest.mark.parametrize( "pointcloud", @@ -590,7 +590,7 @@ def test_schema_pointcloud_valid(pointcloud): ) def test_schema_pointcloud_invalid(pointcloud): with pytest.raises(jsonschema.exceptions.ValidationError): - Pointcloud.validate_jsondata(pointcloud) + Pointcloud.validate_data(pointcloud) @pytest.mark.parametrize( "graph", @@ -601,7 +601,6 @@ def test_schema_pointcloud_invalid(pointcloud): "dea": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": -1, }, { @@ -610,7 +609,6 @@ def test_schema_pointcloud_invalid(pointcloud): "dea": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": 0, }, { @@ -619,13 +617,12 @@ def test_schema_pointcloud_invalid(pointcloud): "dea": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": 1000, }, ], ) def test_schema_graph_valid(graph): - Graph.validate_jsondata(graph) + Graph.validate_data(graph) @pytest.mark.parametrize( "graph", @@ -636,7 +633,6 @@ def test_schema_graph_valid(graph): "dea": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": -2, }, { @@ -644,7 +640,6 @@ def test_schema_graph_valid(graph): "dea": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": -1, }, { @@ -652,7 +647,6 @@ def test_schema_graph_valid(graph): "dea": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": -1, }, { @@ -660,7 +654,6 @@ def test_schema_graph_valid(graph): "dna": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": -1, }, { @@ -668,7 +661,6 @@ def test_schema_graph_valid(graph): "dna": {}, "dea": {}, "edge": {}, - "adjacency": {}, "max_node": -1, }, { @@ -676,7 +668,6 @@ def test_schema_graph_valid(graph): "dna": {}, "dea": {}, "node": {}, - "adjacency": {}, "max_node": -1, }, { @@ -685,21 +676,12 @@ def test_schema_graph_valid(graph): "dea": {}, "node": {}, "edge": {}, - "max_node": -1, - }, - { - "attributes": {}, - "dna": {}, - "dea": {}, - "node": {}, - "edge": {}, - "adjacency": {}, }, ], ) def test_schema_graph_invalid(graph): with pytest.raises(jsonschema.exceptions.ValidationError): - Graph.validate_jsondata(graph) + Graph.validate_data(graph) @pytest.mark.parametrize( "halfedge", @@ -784,14 +766,14 @@ def test_schema_graph_invalid(graph): "vertex": {"0": {}, "1": {}, "2": {}}, "face": {"0": [0, 1, 2]}, "facedata": {"0": {}}, - "edgedata": {"(0,1)": {}}, + "edgedata": {"(0, 1)": {}}, "max_vertex": -1, "max_face": -1, }, ], ) def test_schema_halfedge_valid(halfedge): - HalfEdge.validate_jsondata(halfedge) + HalfEdge.validate_data(halfedge) @pytest.mark.parametrize( "halfedge", @@ -848,7 +830,7 @@ def test_schema_halfedge_valid(halfedge): ) def test_schema_halfedge_invalid(halfedge): with pytest.raises(jsonschema.exceptions.ValidationError): - HalfEdge.validate_jsondata(halfedge) + HalfEdge.validate_data(halfedge) @pytest.mark.parametrize( "halfedge", @@ -881,4 +863,4 @@ def test_schema_halfedge_invalid(halfedge): ) def test_schema_halfedge_failing(halfedge): with pytest.raises(TypeError): - HalfEdge.validate_jsondata(halfedge) + HalfEdge.validate_data(halfedge) diff --git a/tests/compas/data/test_json_dotnet.py b/tests/compas/data/test_json_dotnet.py index 3bd7bc2b10a..3f2132bddf1 100644 --- a/tests/compas/data/test_json_dotnet.py +++ b/tests/compas/data/test_json_dotnet.py @@ -1,7 +1,7 @@ import compas try: - import System + import System # type: ignore def test_decimal(): before = System.Decimal(100.0) diff --git a/tests/compas/data/test_schema.py b/tests/compas/data/test_schema.py new file mode 100644 index 00000000000..11462132c69 --- /dev/null +++ b/tests/compas/data/test_schema.py @@ -0,0 +1,37 @@ +import compas +from compas.data import Data +from compas.data import compas_dataclasses +from compas.data import dataclass_dataschema +from compas.data import dataclass_typeschema +from compas.data import dataclass_jsonschema + + +def test_schema_dataclasses(): + for cls in compas_dataclasses(): + assert issubclass(cls, Data) + + +def test_schema_dataclasses_typeschema(): + for cls in compas_dataclasses(): + dtype = dataclass_typeschema(cls) + modname, clsname = dtype["const"].split("/") # type: ignore + assert cls.__name__ == clsname + # module = __import__(modname, fromlist=[clsname]) + # assert hasattr(module, clsname) + # assert getattr(module, clsname) == cls + + +def test_schema_dataclasses_dataschema(): + for cls in compas_dataclasses(): + assert dataclass_dataschema(cls) == cls.DATASCHEMA + + +def test_schema_dataclasses_jsonschema(): + for cls in compas_dataclasses(): + schema = dataclass_jsonschema(cls) + assert schema["$schema"] == "https://json-schema.org/draft/2020-12/schema" + assert schema["$id"] == "{}.json".format(cls.__name__) + assert schema["$compas"] == "{}".format(compas.__version__) + assert schema["type"] == "object" + assert schema["properties"]["dtype"] == dataclass_typeschema(cls) + assert schema["properties"]["data"] == dataclass_dataschema(cls) diff --git a/tests/compas/datastructures/test_graph.py b/tests/compas/datastructures/test_graph.py index 5ad2ccb31a9..fe8fcbba79a 100644 --- a/tests/compas/datastructures/test_graph.py +++ b/tests/compas/datastructures/test_graph.py @@ -1,6 +1,7 @@ import pytest - +import json import compas + from compas.datastructures import Graph @@ -19,38 +20,81 @@ def graph(): # ============================================================================== -# Tests - Schema & jsonschema +# Basics +# ============================================================================== + # ============================================================================== +# Data +# ============================================================================== + + +def test_graph_data(graph): + other = Graph.from_data(json.loads(json.dumps(graph.data))) + assert graph.data == other.data + assert graph.default_node_attributes == other.default_node_attributes + assert graph.default_edge_attributes == other.default_edge_attributes + assert graph.number_of_nodes() == other.number_of_nodes() + assert graph.number_of_edges() == other.number_of_edges() -# def test_edgedata_directionality(graph): -# graph.update_default_edge_attributes({'index': 0}) -# for index, (u, v) in enumerate(graph.edges()): -# graph.edge_attribute((u, v), 'index', index) -# assert all(graph.edge_attribute((u, v), 'index') != graph.edge_attribute((v, u), 'index') for u, v in graph.edges()) + if not compas.IPY: + assert Graph.validate_data(graph.data) + assert Graph.validate_data(other.data) -def test_edgedata_io(graph): - graph.update_default_edge_attributes({"index": 0}) - for index, edge in enumerate(graph.edges()): - graph.edge_attribute(edge, "index", index) - other = Graph.from_data(graph.data) - assert all(other.edge_attribute(edge, "index") == index for index, edge in enumerate(other.edges())) +# ============================================================================== +# Constructors +# ============================================================================== + +# ============================================================================== +# Properties +# ============================================================================== + +# ============================================================================== +# Accessors +# ============================================================================== +# ============================================================================== +# Builders +# ============================================================================== # ============================================================================== -# Tests - Samples +# Modifiers # ============================================================================== -def test_node_sample(graph): +def test_graph_invalid_edge_delete(): + graph = Graph() + node = graph.add_node() + edge = graph.add_edge(node, node) + graph.delete_edge(edge) + assert graph.has_edge(edge) is False + + +def test_graph_opposite_direction_edge_delete(): + graph = Graph() + node_a = graph.add_node() + node_b = graph.add_node() + edge_a = graph.add_edge(node_a, node_b) + edge_b = graph.add_edge(node_b, node_a) + graph.delete_edge(edge_a) + assert graph.has_edge(edge_a) is False + assert graph.has_edge(edge_b) is True + + +# ============================================================================== +# Samples +# ============================================================================== + + +def test_graph_node_sample(graph): for node in graph.node_sample(): assert graph.has_node(node) for node in graph.node_sample(size=graph.number_of_nodes()): assert graph.has_node(node) -def test_edge_sample(graph): +def test_graph_edge_sample(graph): for edge in graph.edge_sample(): assert graph.has_edge(edge) for edge in graph.edge_sample(size=graph.number_of_edges()): @@ -58,11 +102,11 @@ def test_edge_sample(graph): # ============================================================================== -# Tests - Attributes +# Attributes # ============================================================================== -def test_default_node_attributes(): +def test_graph_default_node_attributes(): graph = Graph(name="test", default_node_attributes={"a": 1, "b": 2}) for node in graph.nodes(): assert graph.node_attribute(node, name="a") == 1 @@ -71,7 +115,7 @@ def test_default_node_attributes(): assert graph.node_attribute(node, name="a") == 3 -def test_default_edge_attributes(): +def test_graph_default_edge_attributes(): graph = Graph(name="test", default_edge_attributes={"a": 1, "b": 2}) for edge in graph.edges(): assert graph.edge_attribute(edge, name="a") == 1 @@ -81,11 +125,11 @@ def test_default_edge_attributes(): # ============================================================================== -# Tests - Conversion +# Conversion # ============================================================================== -def test_graph_networkx_conversion(): +def test_graph_to_networkx(): if compas.IPY: return @@ -101,8 +145,8 @@ def test_graph_networkx_conversion(): nxg = g.to_networkx() - assert nxg.graph["name"] == "DiGraph", "Graph attributes must be preserved" - assert nxg.graph["val"] == (0, 0, 0), "Graph attributes must be preserved" + assert nxg.graph["name"] == "DiGraph", "Graph attributes must be preserved" # type: ignore + assert nxg.graph["val"] == (0, 0, 0), "Graph attributes must be preserved" # type: ignore assert set(nxg.nodes()) == set(g.nodes()), "Node sets must match" assert nxg.nodes[1]["weight"] == 1.2, "Node attributes must be preserved" assert nxg.nodes[1]["height"] == "test", "Node attributes must be preserved" @@ -118,22 +162,3 @@ def test_graph_networkx_conversion(): assert g2.edge_attribute((0, 1), "attr_value") == 10 assert g2.attributes["name"] == "DiGraph", "Graph attributes must be preserved" assert g2.attributes["val"] == (0, 0, 0), "Graph attributes must be preserved" - - -def test_invalid_edge_delete(): - graph = Graph() - node = graph.add_node() - edge = graph.add_edge(node, node) - graph.delete_edge(edge) - assert graph.has_edge(edge) is False - - -def test_opposite_direction_edge_delete(): - graph = Graph() - node_a = graph.add_node() - node_b = graph.add_node() - edge_a = graph.add_edge(node_a, node_b) - edge_b = graph.add_edge(node_b, node_a) - graph.delete_edge(edge_a) - assert graph.has_edge(edge_a) is False - assert graph.has_edge(edge_b) is True diff --git a/tests/compas/datastructures/test_halfedge.py b/tests/compas/datastructures/test_halfedge.py index 1534477850f..7dcebc57d02 100644 --- a/tests/compas/datastructures/test_halfedge.py +++ b/tests/compas/datastructures/test_halfedge.py @@ -1,5 +1,7 @@ import pytest import random +import json +import compas from compas.geometry import Sphere from compas.geometry import Box @@ -61,7 +63,7 @@ def grid(): # ============================================================================== -# Tests - Schema & jsonschema +# ??? # ============================================================================== @@ -81,7 +83,52 @@ def test_edgedata_io(mesh): # ============================================================================== -# Tests - Samples +# Basics +# ============================================================================== + +# ============================================================================== +# Data +# ============================================================================== + + +def test_halfedge_data(mesh): + other = HalfEdge.from_data(json.loads(json.dumps(mesh.data))) + + assert mesh.data == other.data + assert mesh.default_vertex_attributes == other.default_vertex_attributes + assert mesh.default_edge_attributes == other.default_edge_attributes + assert mesh.default_face_attributes == other.default_face_attributes + assert mesh.number_of_vertices() == other.number_of_vertices() + assert mesh.number_of_edges() == other.number_of_edges() + assert mesh.number_of_faces() == other.number_of_faces() + + if not compas.IPY: + assert HalfEdge.validate_data(mesh.data) + assert HalfEdge.validate_data(other.data) + + +# ============================================================================== +# Constructors +# ============================================================================== + +# ============================================================================== +# Properties +# ============================================================================== + +# ============================================================================== +# Accessors +# ============================================================================== + +# ============================================================================== +# Builders +# ============================================================================== + +# ============================================================================== +# Modifiers +# ============================================================================== + +# ============================================================================== +# Samples # ============================================================================== @@ -107,7 +154,7 @@ def test_face_sample(mesh): # ============================================================================== -# Tests - Vertex Attributes +# Vertex Attributes # ============================================================================== @@ -158,7 +205,7 @@ def test_del_vertex_attribute_in_view(mesh, vertex_key): # ============================================================================== -# Tests - Face Attributes +# Face Attributes # ============================================================================== @@ -208,7 +255,7 @@ def test_del_face_attribute_in_view(mesh, face_key): # ============================================================================== -# Tests - Edge Attributes +# Edge Attributes # ============================================================================== @@ -258,7 +305,7 @@ def test_del_edge_attribute_in_view(mesh, edge_key): # ============================================================================== -# Tests - Halfedges Before/After +# Halfedges Before/After # ============================================================================== @@ -285,7 +332,7 @@ def test_halfedge_before_on_boundary(grid): # ============================================================================== -# Tests - Loops & Strip +# Loops & Strip # ============================================================================== diff --git a/tests/compas/datastructures/test_halfface.py b/tests/compas/datastructures/test_halfface.py index 97cedbd8669..2c6676a69f5 100644 --- a/tests/compas/datastructures/test_halfface.py +++ b/tests/compas/datastructures/test_halfface.py @@ -1,16 +1,74 @@ +import pytest +import json +import compas + from compas.datastructures import HalfFace +from compas.datastructures import VolMesh # ============================================================================== # Fixtures # ============================================================================== + +@pytest.fixture +def halfface(): + return VolMesh.from_meshgrid(1, 1, 1, 2, 2, 2) + + +# ============================================================================== +# Basics +# ============================================================================== + +# ============================================================================== +# Data +# ============================================================================== + + +def test_halfface_data(halfface): + other = HalfFace.from_data(json.loads(json.dumps(halfface.data))) + + assert halfface.data == other.data + assert halfface.default_vertex_attributes == other.default_vertex_attributes + assert halfface.default_edge_attributes == other.default_edge_attributes + assert halfface.default_face_attributes == other.default_face_attributes + assert halfface.default_cell_attributes == other.default_cell_attributes + assert halfface.number_of_vertices() == other.number_of_vertices() + assert halfface.number_of_edges() == other.number_of_edges() + assert halfface.number_of_faces() == other.number_of_faces() + assert halfface.number_of_cells() == other.number_of_cells() + + if not compas.IPY: + assert HalfFace.validate_data(halfface.data) + assert HalfFace.validate_data(other.data) + + +# ============================================================================== +# Constructors +# ============================================================================== + +# ============================================================================== +# Properties +# ============================================================================== + +# ============================================================================== +# Accessors +# ============================================================================== + +# ============================================================================== +# Builders +# ============================================================================== + +# ============================================================================== +# Modifiers +# ============================================================================== + # ============================================================================== -# Tests - Schema & jsonschema +# Samples # ============================================================================== # ============================================================================== -# Tests - Vertex Attributes +# Vertex Attributes # ============================================================================== @@ -24,7 +82,7 @@ def test_default_vertex_attributes(): # ============================================================================== -# Tests - Face Attributes +# Face Attributes # ============================================================================== @@ -38,7 +96,7 @@ def test_default_face_attributes(): # ============================================================================== -# Tests - Edge Attributes +# Edge Attributes # ============================================================================== @@ -52,7 +110,7 @@ def test_default_edge_attributes(): # ============================================================================== -# Tests - Cell Attributes +# Cell Attributes # ============================================================================== @@ -66,7 +124,7 @@ def test_default_cell_attributes(): # ============================================================================== -# Tests - Vertex Queries +# Vertex Queries # ============================================================================== @@ -88,7 +146,7 @@ def test_vertices_where_predicate(): # ============================================================================== -# Tests - Edge Queries +# Edge Queries # ============================================================================== @@ -111,7 +169,7 @@ def test_edges_where_predicate(): # ============================================================================== -# Tests - Face Queries +# Face Queries # ============================================================================== @@ -136,7 +194,7 @@ def test_faces_where_predicate(): # ============================================================================== -# Tests - Cell Queries +# Cell Queries # ============================================================================== diff --git a/tests/compas/datastructures/test_mesh.py b/tests/compas/datastructures/test_mesh.py index a69d9c39fd0..065d86aa1c5 100644 --- a/tests/compas/datastructures/test_mesh.py +++ b/tests/compas/datastructures/test_mesh.py @@ -1,8 +1,9 @@ import tempfile - import pytest - +import json import compas + + from compas.datastructures import Mesh from compas.datastructures import meshes_join_and_weld from compas.geometry import Box @@ -12,24 +13,35 @@ from compas.geometry import allclose -@pytest.fixture -def tet(): +def _tet(): return Mesh.from_polyhedron(4) @pytest.fixture -def cube(): +def tet(): + return _tet() + + +def _cube(): return Mesh.from_polyhedron(6) @pytest.fixture -def box(): +def cube(): + return _cube() + + +def _box(): box = Box.from_width_height_depth(2, 2, 2) return Mesh.from_shape(box) @pytest.fixture -def hexagon(): +def box(): + return _box() + + +def _hexagon(): polygon = Polygon.from_sides_and_radius_xy(6, 1) vertices = polygon.points vertices.append(polygon.centroid) @@ -38,7 +50,11 @@ def hexagon(): @pytest.fixture -def hexagongrid(): +def hexagon(): + return _hexagon() + + +def _hexagongrid(): polygon = Polygon.from_sides_and_radius_xy(6, 1) vertices = polygon.points vertices.append(polygon.centroid) @@ -61,7 +77,11 @@ def hexagongrid(): @pytest.fixture -def biohazard(): +def hexagongrid(): + return _hexagongrid() + + +def _biohazard(): polygon = Polygon.from_sides_and_radius_xy(6, 1) vertices = polygon.points vertices.append(polygon.centroid) @@ -70,7 +90,11 @@ def biohazard(): @pytest.fixture -def triangleboundarychain(): +def biohazard(): + return _biohazard() + + +def _triangleboundarychain(): mesh = Mesh.from_obj(compas.get("faces.obj")) faces = mesh.faces_on_boundaries()[0] for face in faces: @@ -78,6 +102,11 @@ def triangleboundarychain(): return mesh +@pytest.fixture +def triangleboundarychain(): + return _triangleboundarychain() + + # -------------------------------------------------------------------------- # constructors # -------------------------------------------------------------------------- @@ -173,6 +202,38 @@ def test_from_ploygons(): assert mesh.number_of_edges() == 5 +# -------------------------------------------------------------------------- +# data +# -------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "mesh", + [ + _tet(), + _cube(), + _box(), + _hexagon(), + _hexagongrid(), + _triangleboundarychain(), + ], +) +def test_mesh_data(mesh): + other = Mesh.from_data(json.loads(json.dumps(mesh.data))) + + assert mesh.data == other.data + assert mesh.default_vertex_attributes == other.default_vertex_attributes + assert mesh.default_edge_attributes == other.default_edge_attributes + assert mesh.default_face_attributes == other.default_face_attributes + assert mesh.number_of_vertices() == other.number_of_vertices() + assert mesh.number_of_edges() == other.number_of_edges() + assert mesh.number_of_faces() == other.number_of_faces() + + if not compas.IPY: + assert Mesh.validate_data(mesh.data) + assert Mesh.validate_data(other.data) + + # -------------------------------------------------------------------------- # converters # -------------------------------------------------------------------------- diff --git a/tests/compas/datastructures/test_network.py b/tests/compas/datastructures/test_network.py index 6c6c38e55a3..469846fe808 100644 --- a/tests/compas/datastructures/test_network.py +++ b/tests/compas/datastructures/test_network.py @@ -1,6 +1,15 @@ import pytest +import compas +import json +from random import random, randint from compas.datastructures import Network +from compas.geometry import Pointcloud + + +# ============================================================================== +# Fixtures +# ============================================================================== @pytest.fixture @@ -23,6 +32,68 @@ def k5_network(): return network +# ============================================================================== +# Basics +# ============================================================================== + +# ============================================================================== +# Constructors +# ============================================================================== + + +@pytest.mark.parametrize( + "filepath", + [ + compas.get("lines.obj"), + compas.get("grid_irregular.obj"), + ], +) +def test_network_from_obj(filepath): + network = Network.from_obj(filepath) + assert network.number_of_nodes() > 0 + assert network.number_of_edges() > 0 + assert len(list(network.nodes())) == network._max_node + 1 + assert network.is_connected() + + +def test_network_from_pointcloud(): + cloud = Pointcloud.from_bounds(random(), random(), random(), randint(10, 100)) + network = Network.from_pointcloud(cloud=cloud, degree=3) + assert network.number_of_nodes() == len(cloud) + for node in network.nodes(): + assert network.degree(node) >= 3 + + +# ============================================================================== +# Data +# ============================================================================== + + +def test_network_data(): + cloud = Pointcloud.from_bounds(random(), random(), random(), randint(10, 100)) + network = Network.from_pointcloud(cloud=cloud, degree=3) + other = Network.from_data(json.loads(json.dumps(network.data))) + + assert network.data == other.data + + if not compas.IPY: + assert Network.validate_data(network.data) + assert Network.validate_data(other.data) + + +# ============================================================================== +# Properties +# ============================================================================== + +# ============================================================================== +# Accessors +# ============================================================================== + +# ============================================================================== +# Builders +# ============================================================================== + + def test_add_node(): network = Network() assert network.add_node(1) == 1 @@ -31,6 +102,27 @@ def test_add_node(): assert network.add_node(0, x=1) == 0 +# ============================================================================== +# Modifiers +# ============================================================================== + +# ============================================================================== +# Samples +# ============================================================================== + +# ============================================================================== +# Attributes +# ============================================================================== + +# ============================================================================== +# Conversion +# ============================================================================== + +# ============================================================================== +# Methods +# ============================================================================== + + def test_non_planar(k5_network): try: import planarity # noqa: F401 diff --git a/tests/compas/datastructures/test_volmesh.py b/tests/compas/datastructures/test_volmesh.py index ad4abb338f9..456ede4f05c 100644 --- a/tests/compas/datastructures/test_volmesh.py +++ b/tests/compas/datastructures/test_volmesh.py @@ -1,19 +1,67 @@ +import json import compas from compas.datastructures import VolMesh +# ============================================================================== +# Fixtures +# ============================================================================== + +# ============================================================================== +# Basics +# ============================================================================== + +# ============================================================================== +# Constructors +# ============================================================================== + +# ============================================================================== +# Data +# ============================================================================== + def test_volmesh_data(): - vmesh1 = VolMesh.from_obj(compas.get("boxes.obj")) + vmesh = VolMesh.from_obj(compas.get("boxes.obj")) + other = VolMesh.from_data(json.loads(json.dumps(vmesh.data))) + + assert vmesh.data == other.data + assert vmesh.number_of_vertices() == other.number_of_vertices() + assert vmesh.number_of_edges() == other.number_of_edges() + assert vmesh.number_of_faces() == other.number_of_faces() + assert vmesh.number_of_cells() == other.number_of_cells() + + if not compas.IPY: + assert VolMesh.validate_data(vmesh.data) + assert VolMesh.validate_data(other.data) + + +# ============================================================================== +# Properties +# ============================================================================== + +# ============================================================================== +# Accessors +# ============================================================================== + +# ============================================================================== +# Builders +# ============================================================================== - data1 = vmesh1.to_data() - data1_ = vmesh1.to_data() +# ============================================================================== +# Modifiers +# ============================================================================== - assert data1 == data1_ +# ============================================================================== +# Samples +# ============================================================================== - vmesh2 = VolMesh.from_data(data1_) +# ============================================================================== +# Attributes +# ============================================================================== - data2 = vmesh2.to_data() - data2_ = vmesh2.to_data() +# ============================================================================== +# Conversion +# ============================================================================== - assert data2 == data2_ - assert data1 == data2 +# ============================================================================== +# Methods +# ============================================================================== diff --git a/tests/compas/geometry/predicates/test_predicates_2.py b/tests/compas/geometry/test_core_predicates_2.py similarity index 100% rename from tests/compas/geometry/predicates/test_predicates_2.py rename to tests/compas/geometry/test_core_predicates_2.py diff --git a/tests/compas/geometry/test_curves_arc.py b/tests/compas/geometry/test_curves_arc.py index 673a62237db..4d997bb6f7d 100644 --- a/tests/compas/geometry/test_curves_arc.py +++ b/tests/compas/geometry/test_curves_arc.py @@ -1,7 +1,11 @@ import math +import json import pytest +import compas from compas.geometry import Arc +from compas.geometry import Point # noqa: F401 +from compas.geometry import Vector # noqa: F401 from compas.geometry import Frame from compas.geometry import Circle from compas.geometry import close, allclose @@ -12,7 +16,7 @@ def frame(): return Frame([1.23, 0.44, -4.02], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]) -def test_create_arc(): +def test_arc_create(): arc = Arc(radius=1.0, start_angle=0.0, end_angle=math.pi) assert close(arc.radius, 1.0) @@ -28,8 +32,16 @@ def test_create_arc(): assert allclose(arc.point_at(0.5, world=True), arc.point_at(0.5, world=False), tol=1e-12) assert allclose(arc.point_at(1.0, world=True), arc.point_at(1.0, world=False), tol=1e-12) + other = eval(repr(arc)) + assert arc.radius == other.radius + assert close(arc.start_angle, other.start_angle, tol=1e-12) + assert close(arc.end_angle, other.end_angle, tol=1e-12) + assert arc.frame.point == other.frame.point + assert allclose(arc.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(arc.frame.yaxis, other.frame.yaxis, tol=1e-12) -def test_create_arc_frame(frame): + +def test_arc_create_with_frame(frame): arc = Arc(radius=0.2, start_angle=0.0, end_angle=1.14, frame=frame) assert close(arc.radius, 0.2) @@ -38,6 +50,14 @@ def test_create_arc_frame(frame): assert close(arc.end_angle, 1.14) assert not arc.is_circle + other = eval(repr(arc)) + assert arc.radius == other.radius + assert close(arc.start_angle, other.start_angle, tol=1e-12) + assert close(arc.end_angle, other.end_angle, tol=1e-12) + assert arc.frame.point == other.frame.point + assert allclose(arc.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(arc.frame.yaxis, other.frame.yaxis, tol=1e-12) + assert not allclose( arc.point_at(0.0, world=True), arc.point_at(0.0, world=False), @@ -71,17 +91,38 @@ def test_create_arc_frame(frame): ) -def test_create_arc_invalid(): +def test_arc_create_invalid(): with pytest.raises(ValueError): Arc(radius=1.0, start_angle=0.2314, end_angle=7.14) +# ============================================================================= +# Data +# ============================================================================= + + +def test_arc_data(): + arc = Arc(radius=1.0, start_angle=0.0, end_angle=math.pi) + other = Arc.from_data(json.loads(json.dumps(arc.data))) + + assert arc.radius == other.radius + assert close(arc.start_angle, other.start_angle, tol=1e-12) + assert close(arc.end_angle, other.end_angle, tol=1e-12) + assert arc.frame.point == other.frame.point + assert allclose(arc.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(arc.frame.yaxis, other.frame.yaxis, tol=1e-12) + + if not compas.IPY: + assert Arc.validate_data(arc.data) + assert Arc.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= -def test_create_from_circle(frame): +def test_arc_create_from_circle(frame): circle = Circle(radius=34.222, frame=frame) arc = Arc.from_circle(circle, 0.1, 0.443) @@ -97,7 +138,7 @@ def test_create_from_circle(frame): assert allclose(arc.frame, circle.frame) -def test_create_from_full_circle(frame): +def test_arc_create_from_full_circle(frame): circle = Circle(radius=34.222, frame=frame) arc = Arc.from_circle(circle, 0.0, 2.0 * math.pi) diff --git a/tests/compas/geometry/test_curves_bezier.py b/tests/compas/geometry/test_curves_bezier.py index a7cc8b5ac45..87d9a06a540 100644 --- a/tests/compas/geometry/test_curves_bezier.py +++ b/tests/compas/geometry/test_curves_bezier.py @@ -1,10 +1,13 @@ import pytest +import json +import compas + from compas.geometry import allclose from compas.geometry import Frame from compas.geometry import Bezier -def test_create_bezier(): +def test_bezier_create(): curve = Bezier([[-1, 0, 0], [0, 1, 0], [+1, 0, 0]]) assert allclose(curve.points[0], [-1, 0, 0], tol=1e-12) @@ -16,11 +19,30 @@ def test_create_bezier(): assert allclose(curve.point_at(1.0), [+1, 0, 0], tol=1e-12) -def test_create_bezier_frame(): +def test_bezier_create_with_frame(): with pytest.raises(Exception): Bezier([[-1, 0, 0], [0, 1, 0], [+1, 0, 0]], frame=Frame.worldXY()) +# ============================================================================= +# Data +# ============================================================================= + + +def test_bezier_data(): + curve = Bezier([[-1, 0, 0], [0, 1, 0], [+1, 0, 0]]) + other = Bezier.from_data(json.loads(json.dumps(curve.data))) + + assert curve.points == other.points + assert curve.frame.point == other.frame.point + assert allclose(curve.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(curve.frame.yaxis, other.frame.yaxis, tol=1e-12) + + if not compas.IPY: + assert Bezier.validate_data(curve.data) + assert Bezier.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= diff --git a/tests/compas/geometry/test_curves_circle.py b/tests/compas/geometry/test_curves_circle.py index 256eecbc63f..495d4d667d3 100644 --- a/tests/compas/geometry/test_curves_circle.py +++ b/tests/compas/geometry/test_curves_circle.py @@ -1,3 +1,6 @@ +import json +import compas + from compas.geometry import close from compas.geometry import allclose from compas.geometry import Circle @@ -5,7 +8,7 @@ from compas.geometry import Plane -def test_create_circle(): +def test_circle_create(): circle = Circle(radius=1.0) assert close(circle.radius, 1.0, tol=1e-12) @@ -25,7 +28,7 @@ def test_create_circle(): assert allclose(circle.point_at(1.0), [1.0, 0.0, 0.0], tol=1e-12) -def test_create_circle_frame(): +def test_circle_create_with_frame(): circle = Circle(radius=1.0, frame=Frame.worldZX()) assert close(circle.radius, 1.0, tol=1e-12) @@ -77,12 +80,31 @@ def test_create_circle_frame(): ) +# ============================================================================= +# Data +# ============================================================================= + + +def test_circle_data(): + circle = Circle(radius=1.0) + other = Circle.from_data(json.loads(json.dumps(circle.data))) + + assert circle.radius == other.radius + assert circle.frame.point == other.frame.point + assert allclose(circle.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(circle.frame.yaxis, other.frame.yaxis, tol=1e-12) + + if not compas.IPY: + assert Circle.validate_data(circle.data) + assert Circle.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= -def test_create_circle_from_point_and_radius(): +def test_circle_create_from_point_and_radius(): circle = Circle.from_point_and_radius([1.0, 2.0, 3.0], 1.0) assert close(circle.radius, 1.0, tol=1e-12) @@ -99,7 +121,7 @@ def test_create_circle_from_point_and_radius(): assert allclose(circle.frame.zaxis, Frame.worldXY().zaxis, tol=1e-12) -def test_create_circle_from_plane_and_radius(): +def test_circle_create_from_plane_and_radius(): plane = Plane([1.0, 2.0, 3.0], [0.0, 0.0, 1.0]) frame = Frame.from_plane(plane) circle = Circle.from_plane_and_radius(plane, 1.0) @@ -118,11 +140,11 @@ def test_create_circle_from_plane_and_radius(): assert allclose(circle.frame.zaxis, frame.zaxis, tol=1e-12) -def test_create_circle_from_three_points(): +def test_circle_create_from_three_points(): pass -def test_create_circle_from_points(): +def test_circle_create_from_points(): pass diff --git a/tests/compas/geometry/test_curves_ellipse.py b/tests/compas/geometry/test_curves_ellipse.py index cd53fa7e7df..00b752ab8aa 100644 --- a/tests/compas/geometry/test_curves_ellipse.py +++ b/tests/compas/geometry/test_curves_ellipse.py @@ -1,4 +1,7 @@ import pytest +import json +import compas + from compas.geometry import close from compas.geometry import allclose from compas.geometry import Frame @@ -6,13 +9,12 @@ from compas.geometry import Plane -def test_create_ellipse(): +def test_ellipse_create(): ellipse = Ellipse(major=1.0, minor=0.5) assert close(ellipse.major, 1.0, tol=1e-12) assert close(ellipse.minor, 0.5, tol=1e-12) assert close(ellipse.area, 1.5707963267948966, tol=1e-12) - # assert close(ellipse.circumference, 4.442882938158366, tol=1e-12) assert close(ellipse.semifocal, 0.8660254037844386, tol=1e-12) assert close(ellipse.eccentricity, 0.8660254037844386, tol=1e-12) assert close(ellipse.focal, 1.7320508075688772, tol=1e-12) @@ -35,13 +37,12 @@ def test_create_ellipse(): assert allclose(ellipse.point_at(1.0), ellipse.point_at(1.0, world=False), tol=1e-12) -def test_create_ellipse_frame(): +def test_ellipse_create_with_frame(): ellipse = Ellipse(major=1.0, minor=0.5, frame=Frame.worldZX()) assert close(ellipse.major, 1.0, tol=1e-12) assert close(ellipse.minor, 0.5, tol=1e-12) assert close(ellipse.area, 1.5707963267948966, tol=1e-12) - # assert close(ellipse.circumference, 4.442882938158366, tol=1e-12) assert close(ellipse.semifocal, 0.8660254037844386, tol=1e-12) assert close(ellipse.eccentricity, 0.8660254037844386, tol=1e-12) assert close(ellipse.focal, 1.7320508075688772, tol=1e-12) @@ -90,18 +91,37 @@ def test_create_ellipse_frame(): ) +# ============================================================================= +# Data +# ============================================================================= + + +def test_ellipse_data(): + ellipse = Ellipse(major=1.0, minor=0.5) + other = Ellipse.from_data(json.loads(json.dumps(ellipse.data))) + + assert ellipse.major == other.major + assert ellipse.minor == other.minor + assert ellipse.frame.point == other.frame.point + assert allclose(ellipse.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(ellipse.frame.yaxis, other.frame.yaxis, tol=1e-12) + + if not compas.IPY: + assert Ellipse.validate_data(ellipse.data) + assert Ellipse.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= -def test_create_ellipse_from_point_major_minor(): +def test_ellipse_create_from_point_major_minor(): ellipse = Ellipse.from_point_major_minor([1.0, 2.0, 3.0], 1.0, 0.5) assert close(ellipse.major, 1.0, tol=1e-12) assert close(ellipse.minor, 0.5, tol=1e-12) assert close(ellipse.area, 1.5707963267948966, tol=1e-12) - # assert close(ellipse.circumference, 4.442882938158366, tol=1e-12) assert close(ellipse.semifocal, 0.8660254037844386, tol=1e-12) assert close(ellipse.eccentricity, 0.8660254037844386, tol=1e-12) assert close(ellipse.focal, 1.7320508075688772, tol=1e-12) @@ -115,7 +135,7 @@ def test_create_ellipse_from_point_major_minor(): assert allclose(ellipse.frame.zaxis, Frame.worldXY().zaxis, tol=1e-12) -def test_create_ellipse_from_plane_major_minor(): +def test_ellipse_create_from_plane_major_minor(): plane = Plane([1.0, 2.0, 3.0], [0.0, 0.0, 1.0]) frame = Frame.from_plane(plane) ellipse = Ellipse.from_plane_major_minor(plane, 1.0, 0.5) @@ -123,7 +143,6 @@ def test_create_ellipse_from_plane_major_minor(): assert close(ellipse.major, 1.0, tol=1e-12) assert close(ellipse.minor, 0.5, tol=1e-12) assert close(ellipse.area, 1.5707963267948966, tol=1e-12) - # assert close(ellipse.circumference, 4.442882938158366, tol=1e-12) assert close(ellipse.semifocal, 0.8660254037844386, tol=1e-12) assert close(ellipse.eccentricity, 0.8660254037844386, tol=1e-12) assert close(ellipse.focal, 1.7320508075688772, tol=1e-12) diff --git a/tests/compas/geometry/test_curves_hyperbola.py b/tests/compas/geometry/test_curves_hyperbola.py index 8207a3684a0..72165f46c53 100644 --- a/tests/compas/geometry/test_curves_hyperbola.py +++ b/tests/compas/geometry/test_curves_hyperbola.py @@ -1,11 +1,14 @@ import pytest +import json +import compas + from compas.geometry import close from compas.geometry import allclose from compas.geometry import Frame from compas.geometry import Hyperbola -def test_create_hyperbola(): +def test_hyperbola_create(): hyperbola = Hyperbola(major=1.0, minor=0.5) assert close(hyperbola.major, 1.0, tol=1e-12) @@ -26,7 +29,7 @@ def test_create_hyperbola(): assert allclose(hyperbola.point_at(1.0), hyperbola.point_at(1.0, world=False), tol=1e-12) -def test_create_hyperbola_frame(): +def test_hyperbola_create_with_frame(): hyperbola = Hyperbola(major=1.0, minor=0.5, frame=Frame.worldZX()) assert close(hyperbola.major, 1.0, tol=1e-12) @@ -67,6 +70,26 @@ def test_create_hyperbola_frame(): ) +# ============================================================================= +# Data +# ============================================================================= + + +def test_hyperbola_data(): + hyperbola = Hyperbola(major=1.0, minor=0.5) + other = Hyperbola.from_data(json.loads(json.dumps(hyperbola.data))) + + assert hyperbola.major == other.major + assert hyperbola.minor == other.minor + assert hyperbola.frame.point == other.frame.point + assert allclose(hyperbola.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(hyperbola.frame.yaxis, other.frame.yaxis, tol=1e-12) + + if not compas.IPY: + assert Hyperbola.validate_data(hyperbola.data) + assert Hyperbola.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= diff --git a/tests/compas/geometry/test_curves_line.py b/tests/compas/geometry/test_curves_line.py index 980826d072c..4a26b6324d1 100644 --- a/tests/compas/geometry/test_curves_line.py +++ b/tests/compas/geometry/test_curves_line.py @@ -1,4 +1,7 @@ import pytest +import json +import compas + from compas.geometry import add_vectors from compas.geometry import scale_vector from compas.geometry import normalize_vector @@ -27,11 +30,34 @@ ([0, 0, 0], Point(1, 2, 3)), ], ) -def test_create_line(p1, p2): +def test_line_create(p1, p2): line = Line(p1, p2) assert line.start == p1 assert line.end == p2 + assert line.frame == Frame.worldXY() + + +def test_line_create_with_frame(): + with pytest.raises(AttributeError): + Line([0, 0, 0], [1, 0, 0], frame=Frame.worldXY()) + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_line_data(): + line = Line([0, 0, 0], [1, 0, 0]) + other = Line.from_data(json.loads(json.dumps(line.data))) + + assert line.start == other.start + assert line.end == other.end + + if not compas.IPY: + assert Line.validate_data(line.data) + assert Line.validate_data(other.data) # ============================================================================= @@ -50,7 +76,7 @@ def test_create_line(p1, p2): (Point(0, 0, 0), Vector(1, 2, 3)), ], ) -def test_create_line_from_point_and_vector(point, vector): +def test_line_create_from_point_and_vector(point, vector): line = Line.from_point_and_vector(point, vector) assert line.start == point @@ -68,7 +94,7 @@ def test_create_line_from_point_and_vector(point, vector): (Point(0, 0, 0), Vector(1, 2, 3), 3.0), ], ) -def test_create_line_from_point_direction_length(point, direction, length): +def test_line_create_from_point_direction_length(point, direction, length): line = Line.from_point_direction_length(point, direction, length) assert line.start == point diff --git a/tests/compas/geometry/test_curves_parabola.py b/tests/compas/geometry/test_curves_parabola.py index c41407f632a..40e7bf6eeb8 100644 --- a/tests/compas/geometry/test_curves_parabola.py +++ b/tests/compas/geometry/test_curves_parabola.py @@ -1,11 +1,13 @@ import pytest +import json +import compas from compas.geometry import allclose from compas.geometry import Frame from compas.geometry import Parabola -def test_create_parabola(): +def test_parabola_create(): parabola = Parabola(focal=1) assert parabola.focal == 1 @@ -16,7 +18,7 @@ def test_create_parabola(): assert allclose(parabola.point_at(1.0), parabola.point_at(1.0, world=False), tol=1e-12) -def test_create_parabola_frame(): +def test_parabola_create_with_frame(): frame = Frame.worldZX() parabola = Parabola(focal=1, frame=frame) @@ -44,6 +46,25 @@ def test_create_parabola_frame(): ) +# ============================================================================= +# Data +# ============================================================================= + + +def test_parabola_data(): + parabola = Parabola(focal=1) + other = Parabola.from_data(json.loads(json.dumps(parabola.data))) + + assert parabola.focal == other.focal + assert parabola.frame.point == other.frame.point + assert allclose(parabola.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(parabola.frame.yaxis, other.frame.yaxis, tol=1e-12) + + if not compas.IPY: + assert Parabola.validate_data(parabola.data) + assert Parabola.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= diff --git a/tests/compas/geometry/test_curves_polyline.py b/tests/compas/geometry/test_curves_polyline.py index 7872e61007d..8747759e31e 100644 --- a/tests/compas/geometry/test_curves_polyline.py +++ b/tests/compas/geometry/test_curves_polyline.py @@ -1,5 +1,7 @@ import pytest import math +import json +import compas from compas.geometry import Frame from compas.geometry import Polyline @@ -14,17 +16,33 @@ [[0, 0, 0], [1, 0, 0], [2, 0, 0]], ], ) -def test_create_polyline(points): +def test_polyline_create(points): curve = Polyline(points) assert curve.frame == Frame.worldXY() -def test_create_polyline_frame(): +def test_polyline_create_with_frame(): with pytest.raises(AttributeError): Polyline([], frame=Frame.worldXY()) +# ============================================================================= +# Data +# ============================================================================= + + +def test_polyline_data(): + curve = Polyline([[0, 0, 0], [1, 0, 0]]) + other = Polyline.from_data(json.loads(json.dumps(curve.data))) + + assert curve.points == other.points + + if not compas.IPY: + assert Polyline.validate_data(curve.data) + assert Polyline.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= diff --git a/tests/compas/geometry/test_frame.py b/tests/compas/geometry/test_frame.py index 77d96adf2df..f9782e4690d 100644 --- a/tests/compas/geometry/test_frame.py +++ b/tests/compas/geometry/test_frame.py @@ -1,10 +1,70 @@ +from __future__ import division +import pytest +import json +import compas +from random import random +from compas.geometry import allclose +from compas.geometry import close +from compas.geometry import Point +from compas.geometry import Vector from compas.geometry import Frame -def test_axes_are_orthonormal(): - pt = [1, 2, 3] - vec1 = [1, 0, 0] - vec2 = [0, 0.9, 0] +@pytest.mark.parametrize( + "point,xaxis,yaxis", + [ + ([0, 0, 0], [1, 0, 0], [0, 1, 0]), + ([0, 0, 0], [1, 0, 0], [1, 1, 0]), + ([0, 0, 0], [1, 0, 0], [0, 1, 1]), + ([0, 0, 0], [1, 0, 0], [1, 1, 1]), + ([random(), random(), random()], [random(), random(), random()], [random(), random(), random()]), + ], +) +def test_frame(point, xaxis, yaxis): + frame = Frame(point, xaxis, yaxis) + assert frame.point == Point(*point) + assert frame.xaxis == Vector(*xaxis).unitized() + assert close(frame.zaxis.dot(xaxis), 0, tol=1e-12) + assert close(frame.zaxis.dot(yaxis), 0, tol=1e-12) + assert close(frame.xaxis.length, 1, tol=1e-12) + assert close(frame.yaxis.length, 1, tol=1e-12) + assert close(frame.zaxis.length, 1, tol=1e-12) - frame = Frame(pt, vec1, vec2) - assert frame == [[1, 2, 3], [1, 0, 0], [0, 1, 0]] + other = eval(repr(frame)) + assert allclose(frame.point, other.point, tol=1e-12) + assert allclose(frame.xaxis, other.xaxis, tol=1e-12) + assert allclose(frame.yaxis, other.yaxis, tol=1e-12) + + +def test_frame_data(): + point = [random(), random(), random()] + xaxis = [random(), random(), random()] + yaxis = [random(), random(), random()] + frame = Frame(point, xaxis, yaxis) + other = Frame.from_data(json.loads(json.dumps(frame.data))) + + assert allclose(frame.point, other.point, tol=1e-12) + assert allclose(frame.xaxis, other.xaxis, tol=1e-12) + assert allclose(frame.yaxis, other.yaxis, tol=1e-12) + assert frame.guid != other.guid + + if not compas.IPY: + assert Frame.validate_data(frame.data) + assert Frame.validate_data(other.data) + + +def test_frame_predefined(): + frame = Frame.worldXY() + assert frame.point == Point(0, 0, 0) + assert frame.xaxis == Vector(1, 0, 0) + assert frame.yaxis == Vector(0, 1, 0) + + frame = Frame.worldYZ() + assert frame.point == Point(0, 0, 0) + assert frame.xaxis == Vector(0, 1, 0) + assert frame.yaxis == Vector(0, 0, 1) + + frame = Frame.worldZX() + assert frame.point == Point(0, 0, 0) + assert frame.xaxis == Vector(0, 0, 1) + assert frame.yaxis == Vector(1, 0, 0) diff --git a/tests/compas/geometry/test_plane.py b/tests/compas/geometry/test_plane.py index ec84e9bbf08..622b85a7568 100644 --- a/tests/compas/geometry/test_plane.py +++ b/tests/compas/geometry/test_plane.py @@ -1,7 +1,67 @@ +import pytest +import json +import compas +from random import random +from compas.geometry import close +from compas.geometry import allclose +from compas.geometry import Point +from compas.geometry import Vector from compas.geometry import Plane -def test_from_point_and_two_vectors(): +@pytest.mark.parametrize( + "point,vector", + [ + ([1, 2, 3], [0, 0, 1]), + (Point(1.0, 2.0, 3.0), [0.0, 0.0, 1.0]), + (Point(1.0, 2.0, 3.0), Vector(0.0, 0.0, 1.0)), + ([1.0, 2.0, 3.0], Vector(0.0, 0.0, 1.0)), + ([random(), random(), random()], [random(), random(), random()]), + ], +) +def test_plane(point, vector): + plane = Plane(point, vector) + assert plane.point == Point(*point) + assert plane.normal == Vector(*vector).unitized() + assert isinstance(plane.point, Point) + assert isinstance(plane.normal, Vector) + assert close(plane.normal.length, 1.0, tol=1e-12) + + other = eval(repr(plane)) + assert allclose(other.point, plane.point, tol=1e-12) + assert allclose(other.normal, plane.normal, tol=1e-12) + + +def test_plane_data(): + point = Point(random(), random(), random()) + vector = Vector(random(), random(), random()) + plane = Plane(point, vector) + other = Plane.from_data(json.loads(json.dumps(plane.data))) + + assert allclose(other.point, plane.point, tol=1e-12) + assert allclose(other.normal, plane.normal, tol=1e-12) + assert plane.guid != other.guid + + if not compas.IPY: + assert Plane.validate_data(plane.data) + assert Plane.validate_data(other.data) + + +def test_plane_predefined(): + plane = Plane.worldXY() + assert plane.point == Point(0, 0, 0) + assert plane.normal == Vector(0, 0, 1) + + plane = Plane.worldYZ() + assert plane.point == Point(0, 0, 0) + assert plane.normal == Vector(1, 0, 0) + + plane = Plane.worldZX() + assert plane.point == Point(0, 0, 0) + assert plane.normal == Vector(0, 1, 0) + + +def test_plane_from_point_and_two_vectors(): pt = [1, 2, 3] vec1 = [1, 0, 0] vec2 = [0, 1, 0] @@ -10,7 +70,7 @@ def test_from_point_and_two_vectors(): assert result == [[1, 2, 3], [0, 0, 1]] -def test_from_three_points(): +def test_plane_from_three_points(): pt1 = [0, 0, 0] pt2 = [1, 0, 0] pt3 = [0, 1, 0] diff --git a/tests/compas/geometry/test_point.py b/tests/compas/geometry/test_point.py index a487d855089..4d21e61c21e 100644 --- a/tests/compas/geometry/test_point.py +++ b/tests/compas/geometry/test_point.py @@ -1,16 +1,57 @@ +from __future__ import division +import pytest +import json +import compas +from random import random from compas.geometry import Point -def test_point(): - p = Point(1, 0, "0") - assert p.x == 1.0 and p.y == 0.0 and p.z == 0.0 - assert p[0] == 1.0 and p[1] == 0.0 and p[2] == 0.0 - assert p == [1.0, 0.0, 0.0] - assert repr(p) == "Point(1.000, 0.000, 0.000)" +@pytest.mark.parametrize( + "x,y,z", + [ + (1, 2, 3), + (1.0, 2.0, 3.0), + ("1.0", "2", 3.0), + (random(), random(), random()), + ], +) +def test_point(x, y, z): + p = Point(x, y, z) + x, y, z = float(x), float(y), float(z) + assert p.x == x and p.y == y and p.z == z + assert p[0] == x and p[1] == y and p[2] == z + + if not compas.IPY: + assert eval(repr(p)) == p + + +@pytest.mark.parametrize( + "x,y", + [ + (1, 2), + (1.0, 2.0), + ("1.0", "2"), + (random(), random()), + ], +) +def test_point2(x, y): + p = Point(x, y) + x, y, z = float(x), float(y), 0.0 + assert p.x == x and p.y == y and p.z == z + assert p[0] == x and p[1] == y and p[2] == z + + if not compas.IPY: + assert eval(repr(p)) == p def test_point_operators(): - pass + a = Point(random(), random(), random()) + b = Point(random(), random(), random()) + assert a + b == [a.x + b.x, a.y + b.y, a.z + b.z] + assert a - b == [a.x - b.x, a.y - b.y, a.z - b.z] + assert a * 2 == [a.x * 2, a.y * 2, a.z * 2] + assert a / 2 == [a.x / 2, a.y / 2, a.z / 2] + assert a**3 == [a.x**3, a.y**3, a.z**3] def test_point_equality(): @@ -27,6 +68,19 @@ def test_point_inplace_operators(): pass +def test_point_data(): + point = Point(random(), random(), random()) + other = Point.from_data(json.loads(json.dumps(point.data))) + + assert point == other + assert point.data == other.data + assert point.guid != other.guid + + if not compas.IPY: + assert Point.validate_data(point.data) + assert Point.validate_data(other.data) + + def test_point_distance_to_point(): pass diff --git a/tests/compas/geometry/test_pointcloud.py b/tests/compas/geometry/test_pointcloud.py index 63de3128374..7e884361fd9 100644 --- a/tests/compas/geometry/test_pointcloud.py +++ b/tests/compas/geometry/test_pointcloud.py @@ -1,16 +1,52 @@ -import random +import pytest +import json +import compas +from random import random, shuffle +from compas.geometry import Point # noqa: F401 from compas.geometry import Pointcloud -def test_equality(): +@pytest.mark.parametrize( + "points", + [ + [[0, 0, 0], [1, 0, 0]], + [[0, 0, 0], [1, 0, 0], [1, 1, 0]], + [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], + [[0, 0, x] for x in range(5)], + [[random(), random(), random()] for i in range(10)], + ], +) +def test_pointcloud(points): + pointcloud = Pointcloud(points) + assert pointcloud.points == points + + if not compas.IPY: + assert pointcloud == eval(repr(pointcloud)) + + +def test_pointcloud_data(): + points = [[random(), random(), random()] for i in range(10)] + pointcloud = Pointcloud(points) + other = Pointcloud.from_data(json.loads(json.dumps(pointcloud.to_data()))) + + assert pointcloud == other + assert pointcloud.points == other.points + assert pointcloud.data == other.data + + if not compas.IPY: + assert Pointcloud.validate_data(pointcloud.data) + assert Pointcloud.validate_data(other.data) + + +def test_pointcloud__eq__(): a = Pointcloud.from_bounds(10, 10, 10, 10) points = a.points[:] - random.shuffle(points) + shuffle(points) b = Pointcloud(points) assert a == b -def test_inequality(): +def test_pointcloud__neq__(): a = Pointcloud.from_bounds(10, 10, 10, 10) b = Pointcloud.from_bounds(10, 10, 10, 11) assert a != b diff --git a/tests/compas/geometry/test_polygon.py b/tests/compas/geometry/test_polygon.py index f35c0b6a159..81cc5bfdaf6 100644 --- a/tests/compas/geometry/test_polygon.py +++ b/tests/compas/geometry/test_polygon.py @@ -1,18 +1,36 @@ import pytest - +import json +import compas +from random import random from compas.geometry import Point from compas.geometry import Polygon from compas.utilities import pairwise -def test_polygon(): - points = [[0, 0, x] for x in range(5)] +@pytest.mark.parametrize( + "points", + [ + [[0, 0, 0], [1, 0, 0]], + [[0, 0, 0], [1, 0, 0], [1, 1, 0]], + [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], + [[0, 0, x] for x in range(5)], + [[random(), random(), random()] for i in range(10)], + ], +) +def test_polygon(points): polygon = Polygon(points) assert polygon.points == points assert polygon.lines == [(a, b) for a, b in pairwise(points + points[:1])] + assert polygon.points[-1] != polygon.points[0] + assert polygon.lines[0][0] == polygon.points[0] + assert polygon.lines[-1][1] == polygon.points[0] + assert polygon.lines[-1][0] == polygon.points[-1] + + if not compas.IPY: + assert polygon == eval(repr(polygon)) -def test_ctor_does_not_modify_input_params(): +def test_polygon_constructor_does_not_modify_input_params(): pts = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 0]] polygon = Polygon(pts) @@ -20,7 +38,21 @@ def test_ctor_does_not_modify_input_params(): assert len(polygon.points) == 4, "The last point (matching the first) should have been removed" -def test_equality(): +def test_polygon_data(): + points = [[random(), random(), random()] for i in range(10)] + polygon = Polygon(points) + other = Polygon.from_data(json.loads(json.dumps(polygon.to_data()))) + + assert polygon == other + assert polygon.points == other.points + assert polygon.data == other.data + + if not compas.IPY: + assert Polygon.validate_data(polygon.data) + assert Polygon.validate_data(other.data) + + +def test_polygon__eq__(): points1 = [[0, 0, x] for x in range(5)] polygon1 = Polygon(points1) points2 = [[0, 0, x] for x in range(6)] @@ -38,13 +70,7 @@ def test_equality(): assert polygon1 == polygon3 -def test___repr__(): - points = [[0, 0, x] for x in range(5)] - polygon = Polygon(points) - assert polygon == eval(repr(polygon)) - - -def test___getitem__(): +def test_polygon__getitem__(): points = [[0, 0, x] for x in range(5)] polygon = Polygon(points) for x in range(5): @@ -53,7 +79,7 @@ def test___getitem__(): polygon[6] = [0, 0, 6] -def test___setitem__(): +def test_polygon__setitem__(): points = [[0, 0, x] for x in range(5)] polygon = Polygon(points) point = [1, 1, 4] diff --git a/tests/compas/geometry/test_quaternion.py b/tests/compas/geometry/test_quaternion.py new file mode 100644 index 00000000000..c42c057f11d --- /dev/null +++ b/tests/compas/geometry/test_quaternion.py @@ -0,0 +1,84 @@ +import pytest +import json +import compas +from random import random + +from compas.geometry import Quaternion +from compas.geometry import close + + +@pytest.mark.parametrize( + "x,y,z,w", + [ + (0.0, 0.0, 0.0, 0.0), + (0.0, 0.0, 0.0, 1.0), + (1.0, 0.0, 0.0, 0.0), + (1.0, 0.0, 0.0, 1.0), + (0.0, 1.0, 0.0, 0.0), + (0.0, 1.0, 0.0, 1.0), + (0.0, 0.0, 1.0, 0.0), + (0.0, 0.0, 1.0, 1.0), + (1.0, 1.0, 1.0, 0.0), + (1.0, 1.0, 1.0, 1.0), + (random(), random(), random(), random()), + ], +) +def test_quaternion(w, x, y, z): + quaternion = Quaternion(w, x, y, z) + + assert quaternion.w == w + assert quaternion.x == x + assert quaternion.y == y + assert quaternion.z == z + + other = eval(repr(quaternion)) + + assert close(quaternion.w, other.w, tol=1e-12) + assert close(quaternion.x, other.x, tol=1e-12) + assert close(quaternion.y, other.y, tol=1e-12) + assert close(quaternion.z, other.z, tol=1e-12) + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_quaternion_data(): + x = random() + y = random() + z = random() + w = random() + + quaternion = Quaternion(w, x, y, z) + other = Quaternion.from_data(json.loads(json.dumps(quaternion.data))) + + assert quaternion.w == other.w + assert quaternion.x == other.x + assert quaternion.y == other.y + assert quaternion.z == other.z + + if not compas.IPY: + assert Quaternion.validate_data(quaternion.data) + assert Quaternion.validate_data(other.data) + + +# ============================================================================= +# Constructors +# ============================================================================= + +# ============================================================================= +# Properties and Geometry +# ============================================================================= + +# ============================================================================= +# Accessors +# ============================================================================= + +# ============================================================================= +# Comparison +# ============================================================================= + +# ============================================================================= +# Other Methods +# ============================================================================= diff --git a/tests/compas/geometry/test_surfaces_cone.py b/tests/compas/geometry/test_surfaces_cone.py index 3427383961b..305cbd40369 100644 --- a/tests/compas/geometry/test_surfaces_cone.py +++ b/tests/compas/geometry/test_surfaces_cone.py @@ -1,3 +1,93 @@ +import pytest +import json +import compas +from random import random + +from compas.geometry import Point # noqa: F401 +from compas.geometry import Vector # noqa: F401 +from compas.geometry import Frame +from compas.geometry import ConicalSurface +from compas.geometry import close +from compas.utilities import linspace + + +@pytest.mark.parametrize( + "radius,height", + [ + (0, 0), + (1, 0), + (0, 1), + (1, 1), + (random(), random()), + ], +) +def test_cone(radius, height): + cone = ConicalSurface(radius=radius, height=height) + + assert cone.radius == radius + assert cone.height == height + assert cone.frame == Frame.worldXY() + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert cone.point_at(u, v) == cone.point_at(u, v, world=False) + + other = eval(repr(cone)) + + assert close(cone.radius, other.radius, tol=1e-12) + assert close(cone.height, other.height, tol=1e-12) + assert cone.frame == other.frame + + +@pytest.mark.parametrize( + "frame", + [ + Frame.worldXY(), + Frame.worldZX(), + Frame.worldYZ(), + ], +) +def test_cone_frame(frame): + radius = random() + height = random() + cone = ConicalSurface(radius=radius, height=height, frame=frame) + + assert cone.radius == radius + assert cone.height == height + assert cone.frame == frame + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert cone.point_at(u, v) == cone.point_at(u, v, world=False).transformed(cone.transformation) + + other = eval(repr(cone)) + + assert close(cone.radius, other.radius, tol=1e-12) + assert close(cone.height, other.height, tol=1e-12) + assert cone.frame == other.frame + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_cone_data(): + radius = random() + height = random() + cone = ConicalSurface(radius=radius, height=height) + other = ConicalSurface.from_data(json.loads(json.dumps(cone.data))) + + assert cone.data == other.data + assert cone.radius == radius + assert cone.height == height + assert cone.frame == Frame.worldXY() + + if not compas.IPY: + assert ConicalSurface.validate_data(cone.data) + assert ConicalSurface.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= diff --git a/tests/compas/geometry/test_surfaces_cylinder.py b/tests/compas/geometry/test_surfaces_cylinder.py index 3427383961b..9a0b5441d05 100644 --- a/tests/compas/geometry/test_surfaces_cylinder.py +++ b/tests/compas/geometry/test_surfaces_cylinder.py @@ -1,3 +1,84 @@ +import pytest +import json +import compas +from random import random + +from compas.geometry import Point # noqa: F401 +from compas.geometry import Vector # noqa: F401 +from compas.geometry import Frame +from compas.geometry import CylindricalSurface +from compas.geometry import close +from compas.utilities import linspace + + +@pytest.mark.parametrize( + "radius", + [ + 0, + 1, + random(), + ], +) +def test_cylinder(radius): + cylinder = CylindricalSurface(radius) + + assert cylinder.radius == radius + assert cylinder.frame == Frame.worldXY() + + other = eval(repr(cylinder)) + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert cylinder.point_at(u, v) == cylinder.point_at(u, v, world=False) + + assert close(cylinder.radius, other.radius, tol=1e-12) + assert cylinder.frame == other.frame + + +@pytest.mark.parametrize( + "frame", + [ + Frame.worldXY(), + Frame.worldZX(), + Frame.worldYZ(), + ], +) +def test_cylinder_frame(frame): + radius = random() + cylinder = CylindricalSurface(radius, frame) + + assert cylinder.radius == radius + assert cylinder.frame == frame + + other = eval(repr(cylinder)) + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert cylinder.point_at(u, v) == cylinder.point_at(u, v, world=False).transformed(cylinder.transformation) + + assert close(cylinder.radius, other.radius, tol=1e-12) + assert cylinder.frame == other.frame + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_cylinder_data(): + radius = random() + cylinder = CylindricalSurface(radius=radius) + other = CylindricalSurface.from_data(json.loads(json.dumps(cylinder.data))) + + assert cylinder.data == other.data + assert cylinder.radius == radius + assert cylinder.frame == Frame.worldXY() + + if not compas.IPY: + assert CylindricalSurface.validate_data(cylinder.data) + assert CylindricalSurface.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= diff --git a/tests/compas/geometry/test_surfaces_plane.py b/tests/compas/geometry/test_surfaces_plane.py index 3427383961b..ed2903c954b 100644 --- a/tests/compas/geometry/test_surfaces_plane.py +++ b/tests/compas/geometry/test_surfaces_plane.py @@ -1,3 +1,93 @@ +import pytest +import json +import compas +from random import random + +from compas.geometry import Point # noqa: F401 +from compas.geometry import Vector # noqa: F401 +from compas.geometry import Frame +from compas.geometry import PlanarSurface +from compas.geometry import close +from compas.utilities import linspace + + +@pytest.mark.parametrize( + "xsize,ysize", + [ + (0, 0), + (1, 0), + (0, 1), + (1, 1), + (random(), random()), + ], +) +def test_plane(xsize, ysize): + plane = PlanarSurface(xsize=xsize, ysize=ysize) + + assert plane.xsize == xsize + assert plane.ysize == ysize + assert plane.frame == Frame.worldXY() + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert plane.point_at(u, v) == plane.point_at(u, v, world=False) + + other = eval(repr(plane)) + + assert close(plane.xsize, other.xsize, tol=1e-12) + assert close(plane.ysize, other.ysize, tol=1e-12) + assert plane.frame == other.frame + + +@pytest.mark.parametrize( + "frame", + [ + Frame.worldXY(), + Frame.worldZX(), + Frame.worldYZ(), + ], +) +def test_plane_frame(frame): + xsize = random() + ysize = random() + plane = PlanarSurface(xsize=xsize, ysize=ysize, frame=frame) + + assert plane.xsize == xsize + assert plane.ysize == ysize + assert plane.frame == frame + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert plane.point_at(u, v) == plane.point_at(u, v, world=False).transformed(plane.transformation) + + other = eval(repr(plane)) + + assert close(plane.xsize, other.xsize, tol=1e-12) + assert close(plane.ysize, other.ysize, tol=1e-12) + assert plane.frame == other.frame + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_plane_data(): + xsize = random() + ysize = random() + plane = PlanarSurface(xsize=xsize, ysize=ysize) + other = PlanarSurface.from_data(json.loads(json.dumps(plane.data))) + + assert plane.data == other.data + assert plane.xsize == xsize + assert plane.ysize == ysize + assert plane.frame == Frame.worldXY() + + if not compas.IPY: + assert PlanarSurface.validate_data(plane.data) + assert PlanarSurface.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= diff --git a/tests/compas/geometry/test_surfaces_sphere.py b/tests/compas/geometry/test_surfaces_sphere.py index ba37d0b2a93..9e9f9a471ad 100644 --- a/tests/compas/geometry/test_surfaces_sphere.py +++ b/tests/compas/geometry/test_surfaces_sphere.py @@ -1,20 +1,39 @@ import pytest +import json +import compas +from random import random from compas.utilities import linspace +from compas.geometry import Point # noqa: F401 +from compas.geometry import Vector # noqa: F401 from compas.geometry import Frame from compas.geometry import SphericalSurface +from compas.geometry import close -def test_create_spherical_surface(): - surf = SphericalSurface(radius=1.0) +@pytest.mark.parametrize( + "radius", + [ + 0, + 1, + random(), + ], +) +def test_spherical_surface(radius): + surf = SphericalSurface(radius) - assert surf.radius == 1.0 + assert surf.radius == radius assert surf.frame == Frame.worldXY() for u in linspace(0.0, 1.0, num=100): for v in linspace(0.0, 1.0, num=100): assert surf.point_at(u, v) == surf.point_at(u, v, world=False) + other = eval(repr(surf)) + + assert close(surf.radius, other.radius, tol=1e-12) + assert surf.frame == other.frame + @pytest.mark.parametrize( "frame", @@ -24,7 +43,7 @@ def test_create_spherical_surface(): Frame.worldYZ(), ], ) -def test_create_spherical_surface_frame(frame): +def test_spherical_surface_with_frame(frame): surf = SphericalSurface(radius=1.0, frame=frame) assert surf.radius == 1.0 @@ -34,6 +53,30 @@ def test_create_spherical_surface_frame(frame): for v in linspace(0.0, 1.0, num=100): assert surf.point_at(u, v) == surf.point_at(u, v, world=False).transformed(surf.transformation) + other = eval(repr(surf)) + + assert close(surf.radius, other.radius, tol=1e-12) + assert surf.frame == other.frame + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_spherical_surface_data(): + radius = random() + surf = SphericalSurface(radius=radius) + other = SphericalSurface.from_data(json.loads(json.dumps(surf.data))) + + assert surf.data == other.data + assert surf.radius == radius + assert surf.frame == Frame.worldXY() + + if not compas.IPY: + assert SphericalSurface.validate_data(surf.data) + assert SphericalSurface.validate_data(other.data) + # ============================================================================= # Constructors diff --git a/tests/compas/geometry/test_surfaces_torus.py b/tests/compas/geometry/test_surfaces_torus.py index 3427383961b..306d1aa9912 100644 --- a/tests/compas/geometry/test_surfaces_torus.py +++ b/tests/compas/geometry/test_surfaces_torus.py @@ -1,3 +1,92 @@ +import pytest +import json +import compas +from random import random + +from compas.utilities import linspace +from compas.geometry import Point # noqa: F401 +from compas.geometry import Vector # noqa: F401 +from compas.geometry import Frame +from compas.geometry import ToroidalSurface +from compas.geometry import close + + +@pytest.mark.parametrize( + "radius_axis,radius_pipe", + [ + (0, 0), + (1, 0), + (0, 1), + (1, 1), + (random(), random()), + ], +) +def test_torus(radius_axis, radius_pipe): + torus = ToroidalSurface(radius_axis=radius_axis, radius_pipe=radius_pipe) + + assert torus.radius_axis == radius_axis + assert torus.radius_pipe == radius_pipe + assert torus.frame == Frame.worldXY() + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert torus.point_at(u, v) == torus.point_at(u, v, world=False) + + other = eval(repr(torus)) + + assert close(torus.radius_axis, other.radius_axis, tol=1e-12) + assert close(torus.radius_pipe, other.radius_pipe, tol=1e-12) + assert torus.frame == other.frame + + +@pytest.mark.parametrize( + "frame", + [ + Frame.worldXY(), + Frame.worldZX(), + Frame.worldYZ(), + ], +) +def test_torus_with_frame(frame): + torus = ToroidalSurface(radius_axis=1.0, radius_pipe=1.0, frame=frame) + + assert torus.radius_axis == 1.0 + assert torus.radius_pipe == 1.0 + assert torus.frame == frame + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert torus.point_at(u, v) == torus.point_at(u, v, world=False).transformed(torus.transformation) + + other = eval(repr(torus)) + + assert close(torus.radius_axis, other.radius_axis, tol=1e-12) + assert close(torus.radius_pipe, other.radius_pipe, tol=1e-12) + assert torus.frame == other.frame + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_torus_data(): + radius_axis = random() + radius_pipe = random() + frame = Frame.worldXY() + + torus = ToroidalSurface(radius_axis=radius_axis, radius_pipe=radius_pipe, frame=frame) + other = ToroidalSurface.from_data(json.loads(json.dumps(torus.data))) + + assert torus.radius_axis == other.radius_axis + assert torus.radius_pipe == other.radius_pipe + assert torus.frame == frame + + if not compas.IPY: + assert ToroidalSurface.validate_data(torus.data) + assert ToroidalSurface.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= diff --git a/tests/compas/geometry/test_vector.py b/tests/compas/geometry/test_vector.py index e617a7500db..e86bf2a8afe 100644 --- a/tests/compas/geometry/test_vector.py +++ b/tests/compas/geometry/test_vector.py @@ -1,6 +1,86 @@ +from __future__ import division +import pytest +import json +import compas +from random import random from compas.geometry import Vector +@pytest.mark.parametrize( + "x,y,z", + [ + (1, 2, 3), + (1.0, 2.0, 3.0), + ("1.0", "2", 3.0), + (random(), random(), random()), + ], +) +def test_vector(x, y, z): + v = Vector(x, y, z) + x, y, z = float(x), float(y), float(z) + assert v.x == x and v.y == y and v.z == z + assert v[0] == x and v[1] == y and v[2] == z + + if not compas.IPY: + assert eval(repr(v)) == v + + +@pytest.mark.parametrize( + "x,y", + [ + (1, 2), + (1.0, 2.0), + ("1.0", "2"), + (random(), random()), + ], +) +def test_vector2(x, y): + v = Vector(x, y) + x, y, z = float(x), float(y), 0.0 + assert v.x == x and v.y == y and v.z == z + assert v[0] == x and v[1] == y and v[2] == z + + if not compas.IPY: + assert eval(repr(v)) == v + + +def test_vector_operators(): + a = Vector(random(), random(), random()) + b = Vector(random(), random(), random()) + assert a + b == [a.x + b.x, a.y + b.y, a.z + b.z] + assert a - b == [a.x - b.x, a.y - b.y, a.z - b.z] + assert a * 2 == [a.x * 2, a.y * 2, a.z * 2] + assert a / 2 == [a.x / 2, a.y / 2, a.z / 2] + assert a**3 == [a.x**3, a.y**3, a.z**3] + + +def test_vector_equality(): + p1 = Vector(1, 1, 1) + p2 = Vector(1, 1, 1) + p3 = Vector(0, 0, 0) + assert p1 == p2 + assert not (p1 != p2) + assert p1 != p3 + assert not (p1 == p3) + + +def test_vector_inplace_operators(): + pass + + +def test_vector_data(): + vector = Vector(random(), random(), random()) + other = Vector.from_data(json.loads(json.dumps(vector.data))) + + assert vector == other + assert vector.data == other.data + assert vector.guid != other.guid + + if not compas.IPY: + assert Vector.validate_data(vector.data) + assert Vector.validate_data(other.data) + + def test_cross_vectors(): vec_list1 = [[1, 2, 3], [7, 8, 9]] vec_list2 = [[2, 3, 4], [5, 6, 7]]