diff --git a/CHANGELOG.md b/CHANGELOG.md index 05b4c1931044..b08f8ae2b28d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added conversion function `frame_to_rhino_plane` to `compas_rhino.conversions`. +* Added `RhinoSurface.from_frame` to `compas_rhino.geometry`. +* Added representation for trims with `compas.geometry.BrepTrim`. + ### Changed ### Removed @@ -24,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Fixed strange point values in RhinoNurbsCurve caused by conversion `ControlPoint` to COMPAS instead of `ControlPoint.Location`. * Fixed flipped order of NURBS point count values when creating RhinoNurbsSurface from parameters. +* Changed serialization format and reconstruction procedure of `RhinoBrep`. ### Removed diff --git a/src/compas/geometry/__init__.py b/src/compas/geometry/__init__.py index 8ea134b45fa4..2ee8957d603c 100644 --- a/src/compas/geometry/__init__.py +++ b/src/compas/geometry/__init__.py @@ -86,6 +86,8 @@ BrepEdge BrepLoop BrepFace + BrepTrim + BrepTrimIsoStatus BrepType BrepOrientation @@ -892,6 +894,8 @@ BrepFace, BrepLoop, BrepEdge, + BrepTrim, + BrepTrimIsoStatus, BrepType, BrepOrientation, BrepError, @@ -1202,6 +1206,8 @@ "BrepEdge", "BrepVertex", "BrepFace", + "BrepTrim", + "BrepTrimIsoStatus", "BrepType", "BrepOrientation", "BrepError", diff --git a/src/compas/geometry/brep/__init__.py b/src/compas/geometry/brep/__init__.py index 0e47dd38d2d7..63c4272235f4 100644 --- a/src/compas/geometry/brep/__init__.py +++ b/src/compas/geometry/brep/__init__.py @@ -5,6 +5,8 @@ from .loop import BrepLoop from .face import BrepFace from .vertex import BrepVertex +from .trim import BrepTrim +from .trim import BrepTrimIsoStatus class BrepError(Exception): @@ -31,6 +33,8 @@ class BrepTrimmingError(BrepError): "BrepLoop", "BrepFace", "BrepVertex", + "BrepTrim", + "BrepTrimIsoStatus", "BrepOrientation", "BrepType", "BrepError", diff --git a/src/compas/geometry/brep/brep.py b/src/compas/geometry/brep/brep.py index 905ad57fc12e..1f2b58d1006b 100644 --- a/src/compas/geometry/brep/brep.py +++ b/src/compas/geometry/brep/brep.py @@ -127,6 +127,8 @@ class Brep(Geometry): The vertices of the Brep. edges : list[:class:`~compas.geometry.BrepEdge`], read-only The edges of the Brep. + trims : list[:class:`~compas.geometry.BrepTrim`], read-only + The trims of the Brep. loops : list[:class:`~compas.geometry.BrepLoop`], read-only The loops of the Brep. faces : list[:class:`~compas.geometry.BrepFace`], read-only @@ -314,6 +316,10 @@ def vertices(self): def edges(self): raise NotImplementedError + @property + def trims(self): + raise NotImplementedError + @property def loops(self): raise NotImplementedError diff --git a/src/compas/geometry/brep/trim.py b/src/compas/geometry/brep/trim.py new file mode 100644 index 000000000000..d2ef2de5ddee --- /dev/null +++ b/src/compas/geometry/brep/trim.py @@ -0,0 +1,40 @@ +from compas.data import Data + + +class BrepTrimIsoStatus(object): + """An enumeration of isoparametric curve direction on the surface.""" + + NONE = 0 + X = 1 + Y = 2 + WEST = 3 + SOUTH = 4 + EAST = 5 + NORTH = 6 + + +class BrepTrim(Data): + """An interface for a Brep Trim + + Attributes + ---------- + curve : :class:`~compas.geometry.NurbsCurve`, read_only + Returns the geometry for this trim as a 2d curve. + iso_status : literal(NONE|X|Y|WEST|SOUTH|EAST|NORTH) + The isoparametric curve direction on the surface. + is_reversed : bool + True if this trim is reversed from its associated edge curve and False otherwise. + + """ + + @property + def curve(self): + raise NotImplementedError + + @property + def iso_status(self): + raise NotImplementedError + + @property + def is_reversed(self): + raise NotImplementedError diff --git a/src/compas_rhino/conversions/__init__.py b/src/compas_rhino/conversions/__init__.py index 6cd84431f074..14de864cc5b9 100644 --- a/src/compas_rhino/conversions/__init__.py +++ b/src/compas_rhino/conversions/__init__.py @@ -58,6 +58,7 @@ line_to_rhino plane_to_rhino frame_to_rhino + frame_to_rhino_plane circle_to_rhino ellipse_to_rhino polyline_to_rhino @@ -134,6 +135,7 @@ line_to_rhino, plane_to_rhino, frame_to_rhino, + frame_to_rhino_plane, circle_to_rhino, ellipse_to_rhino, polyline_to_rhino, @@ -196,6 +198,7 @@ "line_to_rhino", "plane_to_rhino", "frame_to_rhino", + "frame_to_rhino_plane", "circle_to_rhino", "ellipse_to_rhino", "polyline_to_rhino", diff --git a/src/compas_rhino/conversions/_primitives.py b/src/compas_rhino/conversions/_primitives.py index 301107c38854..2f0ede30556b 100644 --- a/src/compas_rhino/conversions/_primitives.py +++ b/src/compas_rhino/conversions/_primitives.py @@ -160,6 +160,21 @@ def plane_to_compas_frame(plane): ) +def frame_to_rhino_plane(frame): + """Convert a COMPAS frame to a Rhino plane. + + Parameters + ---------- + frame : :class:`~compas.geometry.Frame` + + Returns + ------- + :rhino:`Rhino.Geometry.Plane` + + """ + return RhinoPlane(point_to_rhino(frame.point), vector_to_rhino(frame.xaxis), vector_to_rhino(frame.yaxis)) + + def frame_to_rhino(frame): """Convert a COMPAS frame to a Rhino plane. diff --git a/src/compas_rhino/geometry/__init__.py b/src/compas_rhino/geometry/__init__.py index b78886491eff..6b37b1205a33 100644 --- a/src/compas_rhino/geometry/__init__.py +++ b/src/compas_rhino/geometry/__init__.py @@ -21,7 +21,10 @@ RhinoBrepEdge RhinoBrepFace RhinoBrepLoop - + RhinoBrepTrim + RhinoBrepBuilder + RhinoFaceBuilder + RhinoLoopBuilder Plugins ======= @@ -91,7 +94,10 @@ from .brep import RhinoBrepVertex from .brep import RhinoBrepFace from .brep import RhinoBrepEdge - +from .brep import RhinoBrepTrim +from .brep import RhinoBrepBuilder +from .brep import RhinoFaceBuilder +from .brep import RhinoLoopBuilder __all__ = [ "RhinoGeometry", @@ -116,4 +122,8 @@ "RhinoBrepEdge", "RhinoBrepFace", "RhinoBrepLoop", + "RhinoBrepTrim", + "RhinoBrepBuilder", + "RhinoFaceBuilder", + "RhinoLoopBuilder", ] diff --git a/src/compas_rhino/geometry/brep/__init__.py b/src/compas_rhino/geometry/brep/__init__.py index 43416bc31259..0c057d1864f6 100644 --- a/src/compas_rhino/geometry/brep/__init__.py +++ b/src/compas_rhino/geometry/brep/__init__.py @@ -5,6 +5,10 @@ from .edge import RhinoBrepEdge from .vertex import RhinoBrepVertex from .loop import RhinoBrepLoop +from .trim import RhinoBrepTrim +from .builder import RhinoBrepBuilder +from .builder import RhinoFaceBuilder +from .builder import RhinoLoopBuilder import Rhino @@ -15,6 +19,10 @@ "RhinoBrepEdge", "RhinoBrepLoop", "RhinoBrepFace", + "RhinoBrepTrim", + "RhinoBrepBuilder", + "RhinoFaceBuilder", + "RhinoLoopBuilder", "new_brep", "from_native", "from_box", diff --git a/src/compas_rhino/geometry/brep/brep.py b/src/compas_rhino/geometry/brep/brep.py index 917b01826746..11a042e39bc7 100644 --- a/src/compas_rhino/geometry/brep/brep.py +++ b/src/compas_rhino/geometry/brep/brep.py @@ -1,17 +1,16 @@ from compas.geometry import Frame from compas.geometry import Brep -from compas.geometry import BrepInvalidError from compas.geometry import BrepTrimmingError from compas.geometry import Plane from compas_rhino.conversions import box_to_rhino -from compas_rhino.conversions import point_to_rhino from compas_rhino.conversions import xform_to_rhino from compas_rhino.conversions import frame_to_rhino from compas_rhino.conversions import cylinder_to_rhino import Rhino +from .builder import RhinoBrepBuilder from .face import RhinoBrepFace from .edge import RhinoBrepEdge from .vertex import RhinoBrepVertex @@ -36,6 +35,8 @@ class RhinoBrep(Brep): The list of vertex geometries as points in 3D space. edges : list[:class:`~compas_rhino.geometry.RhinoBrepEdge`], read-only The list of edges which comprise this brep. + trims : list[:class:`~compas_rhino.geometry.RhinoBrepTrim`], read-only + The list of trims which comprise this brep. loops : list[:class:`~compas_rhino.geometry.RhinoBrepLoop`], read-only The list of loops which comprise this brep. faces : list[:class:`~compas_rhino.geometry.RhinoBrepFace`], read-only @@ -70,18 +71,22 @@ def __init__(self, brep=None): @property def data(self): - faces = [] - for face in self.faces: - faces.append(face.data) - return {"faces": faces} + return { + "vertices": [v.data for v in self.vertices], + "edges": [e.data for e in self.edges], + "faces": [f.data for f in self.faces], + } @data.setter def data(self, data): - faces = [] - for facedata in data["faces"]: - face = RhinoBrepFace.from_data(facedata) - faces.append(face) - self._create_native_brep(faces) + builder = RhinoBrepBuilder() + for v_data in data["vertices"]: + RhinoBrepVertex.from_data(v_data, builder) + for e_data in data["edges"]: + RhinoBrepEdge.from_data(e_data, builder) + for f_data in data["faces"]: + RhinoBrepFace.from_data(f_data, builder) + self._brep = builder.result # ============================================================================== # Properties @@ -102,6 +107,11 @@ def points(self): @property def edges(self): + if self._brep: + return [RhinoBrepEdge(edge) for edge in self._brep.Edges] + + @property + def trims(self): if self._brep: return [RhinoBrepEdge(trim) for trim in self._brep.Trims] @@ -326,88 +336,3 @@ def split(self, cutter): """ resulting_breps = self._brep.Split(cutter.native_brep, TOLERANCE) return [RhinoBrep.from_native(brep) for brep in resulting_breps] - - # ============================================================================== - # Other Methods - # ============================================================================== - - def _create_native_brep(self, faces): - # Source: https://github.com/mcneel/rhino-developer-samples/blob/3179a8386a64602ee670cc832c77c561d1b0944b/rhinocommon/cs/SampleCsCommands/SampleCsTrimmedPlane.cs - # Things need to be defined in a valid brep: - # 1- Vertices - # 2- 3D Curves (geometry) - # 3- Edges (topology - reference curve geometry) - # 4- Surfaces (geometry) - # 5- Faces (topology - reference surface geometry) - # 6- Loops (2D parameter space of faces) - # 4- Trims and 2D curves (2D parameter space of edges) - self._brep = Rhino.Geometry.Brep() - for face in faces: - rhino_face, rhino_surface = self._create_brep_face(face) - for loop in face.loops: - rhino_loop = self._brep.Loops.Add(Rhino.Geometry.BrepLoopType.Outer, rhino_face) - for edge in loop.edges: - start_vertex, end_vertex = self._add_edge_vertices(edge) - rhino_edge = self._add_edge(edge, start_vertex, end_vertex) - rhino_2d_curve = self._create_trim_curve(rhino_edge, rhino_surface) - self._add_trim(rhino_2d_curve, rhino_edge, rhino_loop) - - self._brep.Repair(TOLERANCE) - self._brep.JoinNakedEdges( - TOLERANCE - ) # without this, Brep.Trim() led to some weird results on de-serialized Breps - self._validate_brep() - - def _validate_brep(self): - if self._brep.IsValid: - return - - error_message = "" - valid_topo, log_topo = self._brep.IsValidTopology() - valid_tol, log_tol = self._brep.IsValidTolerancesAndFlags() - valid_geo, log_geo = self._brep.IsValidGeometry() - if not valid_geo: - error_message += "Invalid geometry:\n{}\n".format(log_geo) - if not valid_topo: - error_message += "Invalid topology:\n{}\n".format(log_topo) - if not valid_tol: - error_message += "Invalid tolerances:\n{}\n".format(log_tol) - - raise BrepInvalidError(error_message) - - def _create_brep_face(self, face): - # Geometry - surface_index = self._brep.AddSurface(face.native_surface) - brep_surface = self._brep.Surfaces.Item[surface_index] - # Topology - brep_face = self._brep.Faces.Add(surface_index) - return brep_face, brep_surface - - def _add_edge_vertices(self, edge): - start_vertex = self._brep.Vertices.Add(point_to_rhino(edge.start_vertex.point), TOLERANCE) - end_vertex = self._brep.Vertices.Add(point_to_rhino(edge.end_vertex.point), TOLERANCE) - return start_vertex, end_vertex - - def _add_edge(self, edge, start_vertex, end_vertex): - # Geometry - curve_index = self._brep.AddEdgeCurve(edge.curve) - # Topology - rhino_edge = self._brep.Edges.Add(start_vertex, end_vertex, curve_index, TOLERANCE) - return rhino_edge - - def _add_trim(self, rhino_trim_curve, rhino_edge, rhino_loop): - # Geometry - trim_curve_index = self._brep.AddTrimCurve(rhino_trim_curve) - # Topology - trim = self._brep.Trims.Add(rhino_edge, True, rhino_loop, trim_curve_index) - trim.IsoStatus = getattr( - Rhino.Geometry.IsoStatus, "None" - ) # IsoStatus.None makes lint, IDE and even Python angry - trim.TrimType = Rhino.Geometry.BrepTrimType.Boundary - trim.SetTolerances(TOLERANCE, TOLERANCE) - - @staticmethod - def _create_trim_curve(rhino_edge, rhino_surface): - curve_2d = rhino_surface.Pullback(rhino_edge.EdgeCurve, TOLERANCE) - curve_2d.Reverse() - return curve_2d diff --git a/src/compas_rhino/geometry/brep/builder.py b/src/compas_rhino/geometry/brep/builder.py new file mode 100644 index 000000000000..d4d2e26c7ff2 --- /dev/null +++ b/src/compas_rhino/geometry/brep/builder.py @@ -0,0 +1,178 @@ +from compas.geometry import BrepInvalidError +from compas_rhino.conversions import point_to_rhino + + +import Rhino + + +TOLERANCE = 1e-6 + + +class RhinoLoopBuilder(object): + """Builds a Brep loop. + + Parameters + ========== + loop : :rhino:`Rhino.Geometry.BrepLoop` + The loop currently being constructed. + brep : :rhino:`Rhino.Geometry.Brep` + The parent brep object. + + Attributes + ========== + result : :rhino: Rhino.Geometry.BrepTrim + The created loop. + + """ + + def __init__(self, loop=None, brep=None): + self._loop = loop + self._brep = brep + + def add_trim(self, curve, edge_index, is_reversed, iso_status): + """Add trim to the new Brep. + + Parameters + ========== + curve : :rhino:`Rhino.Geometry.NurbsCurve` + The curve representing the geometry of this trim. + edge_index : int + The index of the already added edge which corresponds with this trim. + is_reversed : bool + True if this trim is reversed in direction from its associated edge. + iso_status : :rhino:`Rhino.Geometry.IsoStatus` + The iso status of this trim. + + Returns + ======= + :rhino:`Rhino.Geometry.BrepTrim` + The newly added BrepTrim instance. + + """ + c_index = self._brep.AddTrimCurve(curve) + edge = self._brep.Edges[edge_index] + trim = self._brep.Trims.Add(edge, is_reversed, self._loop, c_index) + trim.IsoStatus = iso_status + trim.SetTolerances(TOLERANCE, TOLERANCE) + return trim + + @property + def result(self): + return self._loop + + +class RhinoFaceBuilder(object): + """Builds a BrepFace. + + Serves as context for reconstructing the loop elements associated with this face. + + Parameters + ========== + face : :rhino:`Rhino.Geometry.BrepFace` + The face currently being constructed. + brep : :rhino:`Rhino.Geometry.Brep` + The parent brep. + + Attributes + ========== + result : :rhino:`Rhino.Geometry.BrepFace` + The resulting BrepFace. + + """ + + def __init__(self, face=None, brep=None): + self._face = face + self._brep = brep + + @property + def result(self): + return self._face + + def add_loop(self, loop_type): + """Add a new loop to this face. + + Returns a new builder to be used by all the trims of the newly added loop. + + Parameters + ========== + loop_type : :rhino:`Rhino.Geometry.BrepLoopType` + The enumeration value representing the type of this loop. + + Returns + ======= + :class:`compas_rhino.geometry.RhinoLoopBuilder` + + """ + loop = self._brep.Loops.Add(loop_type, self._face) + return RhinoLoopBuilder(loop, self._brep) + + +class RhinoBrepBuilder(object): + """Reconstructs a Rhino.Geometry.Brep from COMPAS types + + Attributes + ========== + result : :rhino:`Rhino.Geometry.Brep` + The Brep resulting from the reconstruction, if successful. + + """ + + def __init__(self): + self._brep = Rhino.Geometry.Brep() + + @property + def result(self): + is_valid, log = self._brep.IsValidWithLog() + if not is_valid: + raise BrepInvalidError("Brep reconstruction failed!\n{}".format(log)) + return self._brep + + def add_vertex(self, point): + """Add vertext to a new Brep + + point : :class:`~compas.geometry.Point` + + Returns + ------- + :rhino:`Rhino.Geometry.BrepVertex` + + """ + return self._brep.Vertices.Add(point_to_rhino(point), TOLERANCE) + + def add_edge(self, edge_curve, start_vertex, end_vertex): + """Add edge to the new Brep + + edge_curve : :class:`~compas_rhino.geometry.RhinoNurbsCurve` + start_vertex: int + index of the vertex at the start of this edge + end_vertex: int + index of the vertex at the end of this edge + + Returns + ------- + :rhino:`Rhino.Geometry.BrepEdge` + + """ + curve_index = self._brep.AddEdgeCurve(edge_curve) + s_vertex = self._brep.Vertices[start_vertex] + e_vertex = self._brep.Vertices[end_vertex] + return self._brep.Edges.Add(s_vertex, e_vertex, curve_index, TOLERANCE) + + def add_face(self, surface): + """Creates and adds a new face to the brep. + + Returns a new builder to be used by all the loops related to his new face to add themselves. + + Parameters + ========== + surface : :rhino:`Rhino.Geometry.Surface` + The surface of this face. + + Returns + ======= + :class:`compas_rhino.geometry.RhinoFaceBuilder` + + """ + surface_index = self._brep.AddSurface(surface.rhino_surface) + face = self._brep.Faces.Add(surface_index) + return RhinoFaceBuilder(face=face, brep=self._brep) diff --git a/src/compas_rhino/geometry/brep/edge.py b/src/compas_rhino/geometry/brep/edge.py index 159afc66bd9a..e52f51e459af 100644 --- a/src/compas_rhino/geometry/brep/edge.py +++ b/src/compas_rhino/geometry/brep/edge.py @@ -1,12 +1,12 @@ from compas.geometry import BrepEdge from compas.geometry import Line -from compas.geometry import Point from compas.geometry import Circle from compas.geometry import Ellipse from compas_rhino.geometry import RhinoNurbsCurve from compas_rhino.conversions import curve_to_compas_line -from compas_rhino.conversions import curve_to_compas_circle -from compas_rhino.conversions import curve_to_compas_ellipse + +# from compas_rhino.conversions import curve_to_compas_circle +# from compas_rhino.conversions import curve_to_compas_ellipse from compas_rhino.conversions import line_to_rhino_curve from compas_rhino.conversions import circle_to_rhino_curve from compas_rhino.conversions import ellipse_to_rhino_curve @@ -39,20 +39,22 @@ class RhinoBrepEdge(BrepEdge): """ - def __init__(self, rhino_trim=None): + def __init__(self, rhino_edge=None, builder=None): super(RhinoBrepEdge, self).__init__() + self._builder = builder self._edge = None self._curve = None + self._curve_type = None self._start_vertex = None self._end_vertex = None - if rhino_trim: - self._set_edge(rhino_trim) + if rhino_edge: + self._set_edge(rhino_edge) - def _set_edge(self, rhino_trim): - self._edge = rhino_trim.Edge - self._curve = self._edge.EdgeCurve - self._start_vertex = RhinoBrepVertex(rhino_trim.StartVertex) - self._end_vertex = RhinoBrepVertex(rhino_trim.EndVertex) + def _set_edge(self, rhino_edge): + self._edge = rhino_edge + self._curve = RhinoNurbsCurve.from_rhino(rhino_edge.EdgeCurve.ToNurbsCurve()) + self._start_vertex = RhinoBrepVertex(rhino_edge.StartVertex) + self._end_vertex = RhinoBrepVertex(rhino_edge.EndVertex) # ============================================================================== # Data @@ -60,39 +62,40 @@ def _set_edge(self, rhino_trim): @property def data(self): - if self.is_line: - type_ = "line" - curve = curve_to_compas_line(self._curve) - elif self.is_circle: - type_ = "circle" - curve = curve_to_compas_circle(self._curve) - elif self.is_ellipse: - type_ = "ellipse" - curve = curve_to_compas_ellipse(self._curve) - else: - type_ = "nurbs" - curve = RhinoNurbsCurve.from_rhino(self._curve) + curve_type, curve = self._get_curve_geometry() return { - "type": type_, - "value": curve.data, - "points": [self.start_vertex.point.data, self.end_vertex.point.data], + "curve_type": curve_type, + "curve": curve.data, + "start_vertex": self._edge.StartVertex.VertexIndex, + "end_vertex": self._edge.EndVertex.VertexIndex, } @data.setter def data(self, value): - curve_type = value["type"] - if curve_type == "line": - self._curve = line_to_rhino_curve(Line.from_data(value["value"])) # this returns a Nurbs Curve, why? - elif curve_type == "circle": - self._curve = circle_to_rhino_curve(Circle.from_data(value["value"])) # this returns a Nurbs Curve, why? - elif curve_type == "ellipse": - self._curve = ellipse_to_rhino_curve(Ellipse.from_data(value["value"])) - else: - self._curve = RhinoNurbsCurve.from_data(value["value"]).rhino_curve - - self._start_vertex, self._end_vertex = RhinoBrepVertex(), RhinoBrepVertex() - self._start_vertex._point = Point.from_data(value["points"][0]) - self._end_vertex._point = Point.from_data(value["points"][1]) + edge_curve = self._create_curve_from_data(value["curve_type"], value["curve"]) + edge = self._builder.add_edge(edge_curve, value["start_vertex"], value["end_vertex"]) + self._set_edge(edge) + + @classmethod + def from_data(cls, data, builder): + """Construct an object of this type from the provided data. + + Parameters + ---------- + data : dict + The data dictionary. + builder : :class:`~compas_rhino.geometry.BrepBuilder` + The object reconstructing the current Brep. + + Returns + ------- + :class:`~compas.data.Data` + An instance of this object type if the data contained in the dict has the correct schema. + + """ + obj = cls(builder=builder) + obj.data = data + return obj # ============================================================================== # Properties @@ -116,12 +119,40 @@ def vertices(self): @property def is_circle(self): - return self._curve.IsCircle() + return self._edge.EdgeCurve.IsCircle() @property def is_line(self): - return self._curve.IsLinear() + return self._edge.EdgeCurve.IsLinear() @property def is_ellipse(self): - return self._curve.IsEllipse() + return self._edge.EdgeCurve.IsEllipse() + + def _get_curve_geometry(self): + curve = self._edge.EdgeCurve + if self.is_line: + type_ = "line" + curve = curve_to_compas_line(curve) + # TODO: there is an edge/trim direction issue when creating and edge from circle + # elif self.is_circle: + # type_ = "circle" + # curve = curve_to_compas_circle(curve) + # elif self.is_ellipse: + # type_ = "ellipse" + # curve = curve_to_compas_ellipse(curve) + else: + type_ = "nurbs" + curve = self._curve + return type_, curve + + @staticmethod + def _create_curve_from_data(curve_type, curve_data): + if curve_type == "line": + return line_to_rhino_curve(Line.from_data(curve_data)) + elif curve_type == "circle": + return circle_to_rhino_curve(Circle.from_data(curve_data)) + elif curve_type == "ellipse": + return ellipse_to_rhino_curve(Ellipse.from_data(curve_data)) + else: + return RhinoNurbsCurve.from_data(curve_data).rhino_curve diff --git a/src/compas_rhino/geometry/brep/face.py b/src/compas_rhino/geometry/brep/face.py index aa2b973a3b8b..7034980bd45c 100644 --- a/src/compas_rhino/geometry/brep/face.py +++ b/src/compas_rhino/geometry/brep/face.py @@ -1,11 +1,14 @@ from compas.geometry import BrepFace from compas.geometry import Sphere from compas.geometry import Cylinder +from compas.geometry import Frame from compas_rhino.geometry import RhinoNurbsSurface -from compas_rhino.conversions import plane_to_compas +from compas_rhino.conversions import plane_to_compas_frame from compas_rhino.conversions import sphere_to_compas from compas_rhino.conversions import cylinder_to_compas +from Rhino.Geometry import Interval + from .loop import RhinoBrepLoop @@ -29,8 +32,9 @@ class RhinoBrepFace(BrepFace): """ - def __init__(self, rhino_face=None): + def __init__(self, rhino_face=None, builder=None): super(RhinoBrepFace, self).__init__() + self._builder = builder self._loops = None self._surface = None self._face = None @@ -39,8 +43,8 @@ def __init__(self, rhino_face=None): def _set_face(self, native_face): self._face = native_face - self._loops = [RhinoBrepLoop(loop) for loop in self._face.Loops] - self._surface = self._face.UnderlyingSurface() + self._loops = [RhinoBrepLoop(loop) for loop in native_face.Loops] + self._surface = RhinoNurbsSurface.from_rhino(self._face.UnderlyingSurface().ToNurbsSurface()) # ============================================================================== # Data @@ -48,34 +52,45 @@ def _set_face(self, native_face): @property def data(self): - boundary = self._loops[0].data - holes = [loop.data for loop in self._loops[1:]] - surface_type, surface = self._get_surface_geometry(self._surface) - surface_data = {"value": surface.data, "type": surface_type} - return {"boundary": boundary, "surface": surface_data, "holes": holes} + surface_type, surface, uv_domain = self._get_surface_geometry(self._face.UnderlyingSurface()) + return { + "surface_type": surface_type, + "surface": surface.data, + "uv_domain": uv_domain, + "loops": [loop.data for loop in self._loops], + } @data.setter def data(self, value): - boundary = RhinoBrepLoop.from_data(value["boundary"]) - holes = [RhinoBrepLoop.from_data(loop) for loop in value["holes"]] - self._loops = [boundary] + holes - # TODO: using the new serialization mechanism, surface.to_nurbs() should replace all this branching.. # TODO: given that Plane, Sphere, Cylinder etc. all implement to_nurbs() - surface_data = value["surface"] - type_ = surface_data["type"] - surface = surface_data["value"] - if type_ == "plane": - surface = self._make_surface_from_loop(boundary) - elif type_ == "sphere": - surface = RhinoNurbsSurface.from_sphere(Sphere.from_data(surface)) - elif type_ == "cylinder": - surface = RhinoNurbsSurface.from_cylinder(Cylinder.from_data(surface)) - elif type_ == "nurbs": - surface = RhinoNurbsSurface.from_data(surface) - elif type_ == "torus": - raise NotImplementedError("Support for torus surface is not yet implemented!") - self._surface = surface.rhino_surface + self._surface = self._make_surface_from_data(value["surface_type"], value["surface"], value["uv_domain"]) + face_builder = self._builder.add_face(self._surface) + for loop_data in value["loops"]: + RhinoBrepLoop.from_data(loop_data, face_builder) + self._set_face(face_builder.result) + + @classmethod + def from_data(cls, data, builder): + """Construct an object of this type from the provided data. + + Parameters + ---------- + data : dict + The data dictionary. + builder : :class:`~compas_rhino.geometry.BrepBuilder` + The object reconstructing the current Brep. + + Returns + ------- + :class:`~compas.data.Data` + An instance of this object type if the data contained in the dict has the correct schema. + + """ + + obj = cls(builder=builder) + obj.data = data + return obj # ============================================================================== # Properties @@ -107,23 +122,35 @@ def is_plane(self): @staticmethod def _get_surface_geometry(surface): - success, cast_surface = surface.TryGetPlane() - if success: - return "plane", plane_to_compas(cast_surface) + uv_domain = [[surface.Domain(0)[0], surface.Domain(0)[1]], [surface.Domain(1)[0], surface.Domain(1)[1]]] success, cast_surface = surface.TryGetSphere() if success: - return "sphere", sphere_to_compas(cast_surface) + return "sphere", sphere_to_compas(cast_surface), uv_domain success, cast_surface = surface.TryGetCylinder() if success: - return "cylinder", cylinder_to_compas(cast_surface) + return "cylinder", cylinder_to_compas(cast_surface), uv_domain success, cast_surface = surface.TryGetTorus() if success: raise NotImplementedError("Support for torus surface is not yet implemented!") - return "nurbs", RhinoNurbsSurface.from_rhino(surface.ToNurbsSurface()) + success, cast_surface = surface.TryGetPlane() + if success: + return "plane", plane_to_compas_frame(cast_surface), uv_domain + return "nurbs", RhinoNurbsSurface.from_rhino(surface.ToNurbsSurface()), uv_domain @staticmethod - def _make_surface_from_loop(loop): - # order of corners determines the normal of the resulting surface - corners = [loop.edges[i].start_vertex.point for i in range(4)] - surface = RhinoNurbsSurface.from_corners(corners) + def _make_surface_from_data(surface_type, surface_data, uv_domain): + u_domain, v_domain = uv_domain + if surface_type == "plane": + frame = Frame.from_data(surface_data) + surface = RhinoNurbsSurface.from_frame(frame, u_domain, v_domain) + elif surface_type == "sphere": + surface = RhinoNurbsSurface.from_sphere(Sphere.from_data(surface_data)) + elif surface_type == "cylinder": + surface = RhinoNurbsSurface.from_cylinder(Cylinder.from_data(surface_data)) + elif surface_type == "nurbs": + surface = RhinoNurbsSurface.from_data(surface_data) + elif surface_type == "torus": + raise NotImplementedError("Support for torus surface is not yet implemented!") + surface.rhino_surface.SetDomain(0, Interval(*u_domain)) + surface.rhino_surface.SetDomain(1, Interval(*v_domain)) return surface diff --git a/src/compas_rhino/geometry/brep/loop.py b/src/compas_rhino/geometry/brep/loop.py index 2317246f4a0a..6c3ff5a30386 100644 --- a/src/compas_rhino/geometry/brep/loop.py +++ b/src/compas_rhino/geometry/brep/loop.py @@ -2,7 +2,7 @@ import Rhino -from .edge import RhinoBrepEdge +from .trim import RhinoBrepTrim class LoopType(object): @@ -43,18 +43,19 @@ class RhinoBrepLoop(BrepLoop): """ - def __init__(self, rhino_loop=None): + def __init__(self, rhino_loop=None, builder=None): super(RhinoBrepLoop, self).__init__() + self._builder = builder self._loop = None - self._edges = None self._type = LoopType.UNKNOWN + self._trims = None if rhino_loop: self._set_loop(rhino_loop) def _set_loop(self, native_loop): self._loop = native_loop self._type = int(self._loop.LoopType) - self._edges = [RhinoBrepEdge(trim) for trim in self._loop.Trims] + self._trims = [RhinoBrepTrim(trim) for trim in self._loop.Trims] # ============================================================================== # Data @@ -62,11 +63,38 @@ def _set_loop(self, native_loop): @property def data(self): - return [e.data for e in self._edges] + return {"type": str(self._loop.LoopType), "trims": [t.data for t in self._trims]} @data.setter def data(self, value): - self._edges = [RhinoBrepEdge.from_data(e_data) for e_data in value] + self._type = ( + Rhino.Geometry.BrepLoopType.Outer if value["type"] == "Outer" else Rhino.Geometry.BrepLoopType.Inner + ) + loop_builder = self._builder.add_loop(self._type) + for trim_data in value["trims"]: + RhinoBrepTrim.from_data(trim_data, loop_builder) + self._set_loop(loop_builder.result) + + @classmethod + def from_data(cls, data, builder): + """Construct an object of this type from the provided data. + + Parameters + ---------- + data : dict + The data dictionary. + builder : :class:`~compas_rhino.geometry.BrepFaceBuilder` + The object reconstructing the current BrepFace. + + Returns + ------- + :class:`~compas.data.Data` + An instance of this object type if the data contained in the dict has the correct schema. + + """ + obj = cls(builder=builder) + obj.data = data + return obj # ============================================================================== # Properties diff --git a/src/compas_rhino/geometry/brep/trim.py b/src/compas_rhino/geometry/brep/trim.py new file mode 100644 index 000000000000..442afd1157c1 --- /dev/null +++ b/src/compas_rhino/geometry/brep/trim.py @@ -0,0 +1,87 @@ +from compas.geometry import BrepTrim +from compas_rhino.geometry import RhinoNurbsCurve + + +import Rhino + + +class RhinoBrepTrim(BrepTrim): + """An interface for a Brep Trim + + Attributes + ---------- + curve : :class:`~compas.geometry.NurbsCurve`, read_only + Returns the geometry for this trim as a 2d curve. + iso_status : literal(NONE|X|Y|West|South|East|North) + The isoparametric curve direction on the surface. + is_reversed : bool + True if this trim is reversed from its associated edge curve and False otherwise. + + """ + + def __init__(self, rhino_trim=None, builder=None): + + super(RhinoBrepTrim, self).__init__() + self._builder = builder + self._trim = None + self._curve = None + self._is_reversed = None + self._iso_type = None + if rhino_trim: + self._set_trim(rhino_trim) + + def _set_trim(self, rhino_trim): + self._trim = rhino_trim + self._curve = RhinoNurbsCurve.from_rhino(rhino_trim.TrimCurve.ToNurbsCurve()) + self._is_reversed = rhino_trim.IsReversed() + self._iso_type = int(rhino_trim.IsoStatus) + + @property + def data(self): + return { + "edge": self._trim.Edge.EdgeIndex, + "curve": RhinoNurbsCurve.from_rhino(self._trim.TrimCurve.ToNurbsCurve()).data, + "iso": str(self._trim.IsoStatus), + "is_reversed": "true" if self._trim.IsReversed() else "false", + } + + @data.setter + def data(self, value): + curve = RhinoNurbsCurve.from_data(value["curve"]).rhino_curve + iso_status = getattr(Rhino.Geometry.IsoStatus, value["iso"]) + is_reversed = True if value["is_reversed"] == "true" else False + trim = self._builder.add_trim(curve, value["edge"], is_reversed, iso_status) + self._set_trim(trim) + + @classmethod + def from_data(cls, data, builder): + """Construct an object of this type from the provided data. + + Parameters + ---------- + data : dict + The data dictionary. + builder : :class:`~compas_rhino.geometry.BrepLoopBuilder` + The object reconstructing the current BrepLoop. + + Returns + ------- + :class:`~compas.data.Data` + An instance of this object type if the data contained in the dict has the correct schema. + + """ + obj = cls(builder=builder) + obj.data = data + return obj + + @property + def curve(self): + return self._curve + + @property + def is_reverse(self): + return self._curve + + @property + def iso_status(self): + return self._iso_type diff --git a/src/compas_rhino/geometry/brep/vertex.py b/src/compas_rhino/geometry/brep/vertex.py index 204846e39161..7ba92dcb2597 100644 --- a/src/compas_rhino/geometry/brep/vertex.py +++ b/src/compas_rhino/geometry/brep/vertex.py @@ -13,8 +13,9 @@ class RhinoBrepVertex(BrepVertex): """ - def __init__(self, rhino_vertex=None): + def __init__(self, rhino_vertex=None, builder=None): super(RhinoBrepVertex, self).__init__() + self._builder = builder self._vertex = None self._point = None if rhino_vertex: @@ -36,9 +37,29 @@ def data(self): @data.setter def data(self, data): - # Rhino.BrepVertex has no public constructor - # Vertex creation is via Brep.Vertices.Add(Rhino.Point3D) self._point = Point.from_data(data["point"]) + self._vertex = self._builder.add_vertex(self._point) + + @classmethod + def from_data(cls, data, builder): + """Construct an object of this type from the provided data. + + Parameters + ---------- + data : dict + The data dictionary. + builder : :class:`~compas_rhino.geometry.BrepBuilder` + The object reconstructing the current Brep. + + Returns + ------- + :class:`~compas.data.Data` + An instance of this object type if the data contained in the dict has the correct schema. + + """ + obj = cls(builder=builder) + obj.data = data + return obj # ============================================================================== # Properties diff --git a/src/compas_rhino/geometry/surfaces/surface.py b/src/compas_rhino/geometry/surfaces/surface.py index 8f1aad8183d4..07f03a44f1ac 100644 --- a/src/compas_rhino/geometry/surfaces/surface.py +++ b/src/compas_rhino/geometry/surfaces/surface.py @@ -8,6 +8,7 @@ from compas_rhino.conversions import point_to_compas from compas_rhino.conversions import vector_to_compas from compas_rhino.conversions import plane_to_compas_frame +from compas_rhino.conversions import frame_to_rhino_plane from compas_rhino.conversions import plane_to_rhino from compas_rhino.conversions import box_to_compas from compas_rhino.conversions import xform_to_rhino @@ -187,6 +188,55 @@ def from_plane(cls, plane, box): rhino_surface = Rhino.Geometry.PlaneSurface.CreateThroughBox(plane, box) return cls.from_rhino(rhino_surface) + @classmethod + def from_frame(cls, frame, u_interval, v_interval, u_degree=1, v_degree=1, u_point_count=2, v_point_count=2): + """Creates a NURBS surface from a frame and parametric domain information. + + Parameters + ---------- + frame : :class:`~compas.geometry.Frame` + A frame with point at the center of the wanted plannar surface and + x and y axes the direction of u and v respectively. + u_interval : tuple(float, float) + The parametric domain of the U parameter. u_interval[0] => u_interval[1]. + v_interval : tuple(float, float) + The parametric domain of the V parameter. v_interval[0] => v_interval[1]. + u_degree : int + Degree of the U parameter. Default is 1 in both directions for a simple planar surface. + v_degree : int + Degree of the V parameter. Default is 1 in both directions for a simple planar surface. + u_point_count : int + Number of control points in the U direction. Default is 2 in both directions for a simple planar surface. + v_point_count : int + Number of control points in the V direction. Default is 2 in both directions for a simple planar surface. + + Returns + ------- + :rhino:`Rhino.Geometry.NurbsSurface` + + """ + # so that parameteric surface starts correctly at corner of the wanted plane section + rhino_plane = frame_to_rhino_plane(frame) + u_size = abs(u_interval[1] - u_interval[0]) + v_size = abs(v_interval[1] - v_interval[0]) + rhino_plane.Origin = rhino_plane.PointAt(-u_size / 2.0, -v_size / 2.0) # TODO: shift to plane corner + surface = Rhino.Geometry.NurbsSurface.CreateFromPlane( + rhino_plane, + Rhino.Geometry.Interval(*u_interval), + Rhino.Geometry.Interval(*v_interval), + v_degree, + u_degree, + v_point_count, + u_point_count, + ) + if not surface: + msg = "Failed creating NurbsSurface from " + msg += "frame:{} u_interval:{} v_interval:{} u_degree:{} v_degree:{} u_point_count:{} v_point_count:{}" + raise ValueError( + msg.format(frame, u_interval, v_interval, u_degree, v_degree, u_point_count, v_point_count) + ) + return cls.from_rhino(surface) + # ============================================================================== # Conversions # ==============================================================================