diff --git a/CHANGELOG.md b/CHANGELOG.md index 32bdd6d2044..230fe58f6ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `VolMesh.vertex_edges`. * Added `VolMesh.from_meshes`. * Added `VolMesh.from_polyhedrons`. +* Added pluggable `Surface.from_native`. +* Added deprecation warning on `RhinoSurface.from_rhino`. +* Added `Surface.native_surface`. +* Added `RhinoSurface.native_surface`. +* Added plugin `RhinoSurface.from_native`. +* Added parameters `frame`, `domain_u` and `domain_v` to `RhinoNurbs.from_parameters`. +* Added `nurbssurface_to_compas` to `compas_rhino.conversions`. +* Added `is_closed_u` and `is_closed_v` to `compas.geometry.Surface`. ### Changed @@ -54,11 +62,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Updated `compas.geometry.vector.__truediv__` to allow element-wise division with another vector. * Fixed bug in registration `shapely` boolean plugins. * Temporarily restrict `numpy` to versions lower than `2.x`. +* Protected call to `compas.geometry.Surface()`. `Surface` should be instantiated using one of the alternative constructors. +* Protected call to `compas.geometry.NurbsSurface()`. `NurbsSurface` should be instantiated using one of the alternative constructors. ### Removed * Removed `System` dependency in `compas_ghpython/utilities/drawing.py`. * Removed GH plugin for `compas.scene.clear` since it clashed with the Rhino version. +* Removed pluggable `new_nurbssurface`. +* Removed pluggable `new_surface`. +* Removed `compas.geometry.Surface.is_closed`. ## [2.1.1] 2024-05-14 diff --git a/src/compas/geometry/surfaces/nurbs.py b/src/compas/geometry/surfaces/nurbs.py index 23f7d9a8f86..a399763a4b7 100644 --- a/src/compas/geometry/surfaces/nurbs.py +++ b/src/compas/geometry/surfaces/nurbs.py @@ -2,6 +2,7 @@ from __future__ import division from __future__ import print_function +from compas.geometry import Frame from compas.geometry import Point from compas.itertools import linspace from compas.itertools import meshgrid @@ -12,17 +13,17 @@ @pluggable(category="factories") -def new_nurbssurface(cls, *args, **kwargs): +def new_nurbssurface_from_native(cls, *args, **kwargs): raise PluginNotInstalledError @pluggable(category="factories") -def new_nurbssurface_from_native(cls, *args, **kwargs): +def new_nurbssurface_from_parameters(cls, *args, **kwargs): raise PluginNotInstalledError @pluggable(category="factories") -def new_nurbssurface_from_parameters(cls, *args, **kwargs): +def new_nurbssurface_from_plane(cls, *args, **kwargs): raise PluginNotInstalledError @@ -94,36 +95,66 @@ def __dtype__(self): @property def __data__(self): - return { - "points": [point.__data__ for point in self.points], - "weights": self.weights, - "knots_u": self.knots_u, - "knots_v": self.knots_v, - "mults_u": self.mults_u, - "mults_v": self.mults_v, - "degree_u": self.degree_u, - "degree_v": self.degree_v, - "is_periodic_u": self.is_periodic_u, - "is_periodic_v": self.is_periodic_v, - } + data = super(NurbsSurface, self).__data__ + data["points"] = [[point.__data__ for point in row] for row in self.points] # type: ignore + data["weights"] = self.weights + data["knots_u"] = self.knots_u + data["knots_v"] = self.knots_v + data["mults_u"] = self.mults_u + data["mults_v"] = self.mults_v + data["degree_u"] = self.degree_u + data["degree_v"] = self.degree_v + data["is_periodic_u"] = self.is_periodic_u + data["is_periodic_v"] = self.is_periodic_v + return data @classmethod def __from_data__(cls, data): + """Construct a BSpline surface from its data representation. + + Parameters + ---------- + data : dict + The data dictionary. + + Returns + ------- + :class:`compas_rhino.geometry.RhinoNurbsSurface` + The constructed surface. + + """ + frame = Frame.__from_data__(data["frame"]) + domain_u = data["domain_u"] + domain_v = data["domain_v"] + points = [[Point.__from_data__(point) for point in row] for row in data["points"]] + weights = data["weights"] + knots_u = data["knots_u"] + knots_v = data["knots_v"] + mults_u = data["mults_u"] + mults_v = data["mults_v"] + degree_u = data["degree_u"] + degree_v = data["degree_v"] + is_periodic_u = data["is_periodic_u"] + is_periodic_v = data["is_periodic_v"] + return cls.from_parameters( - data["points"], - data["weights"], - data["knots_u"], - data["knots_v"], - data["mults_u"], - data["mults_v"], - data["degree_u"], - data["degree_v"], - data["is_periodic_u"], - data["is_periodic_v"], + frame, + domain_u, + domain_v, + points, + weights, + knots_u, + knots_v, + mults_u, + mults_v, + degree_u, + degree_v, + is_periodic_u, + is_periodic_v, ) def __new__(cls, *args, **kwargs): - return new_nurbssurface(cls, *args, **kwargs) + raise AssertionError("NurbsSurface() is protected. Use NurbsSurface.from_...() to construct a NurbsSurface object.") def __init__(self, name=None): super(NurbsSurface, self).__init__(name=name) @@ -211,6 +242,9 @@ def from_native(cls, surface): @classmethod def from_parameters( cls, + frame, + domain_u, + domain_v, points, weights, knots_u, @@ -250,6 +284,9 @@ def from_parameters( """ return new_nurbssurface_from_parameters( cls, + frame, + domain_u, + domain_v, points, weights, knots_u, @@ -350,6 +387,10 @@ def from_fill(cls, curve1, curve2, curve3=None, curve4=None, style="stretch"): """ return new_nurbssurface_from_fill(cls, curve1, curve2, curve3, curve4, style) + @classmethod + def from_plane(cls, plane, u_degree=1, v_degree=1): + return new_nurbssurface_from_plane(cls, plane, u_degree, v_degree) + # ============================================================================== # Conversions # ============================================================================== diff --git a/src/compas/geometry/surfaces/surface.py b/src/compas/geometry/surfaces/surface.py index b8b77ac75a5..8bbb5edad5e 100644 --- a/src/compas/geometry/surfaces/surface.py +++ b/src/compas/geometry/surfaces/surface.py @@ -13,10 +13,8 @@ @pluggable(category="factories") -def new_surface(cls, *args, **kwargs): - surface = object.__new__(cls) - surface.__init__(*args, **kwargs) - return surface +def new_surface_from_native(cls, *args, **kwargs): + raise NotImplementedError @pluggable(category="factories") @@ -43,15 +41,41 @@ class Surface(Geometry): The parameter domain of the surface in the U direction. domain_v : tuple[float, float], read-only The parameter domain of the surface in the V direction. + is_closed_u : bool, read-only + Flag indicating if the surface is closed in the U direction. + is_closed_v : bool, read-only + Flag indicating if the surface is closed in the V direction. is_periodic_u : bool, read-only Flag indicating if the surface is periodic in the U direction. is_periodic_v : bool, read-only Flag indicating if the surface is periodic in the V direction. + native_surface : Any, read-only + The CAD native surface object. """ + @property + def __data__(self): + return { + "frame": self._frame.__data__, + "domain_u": list(self.domain_u), + "domain_v": list(self.domain_v), + } + + @classmethod + def __from_data__(cls, data): + frame = Frame.__from_data__(data["frame"]) + domain_u = tuple(data["domain_u"]) + domain_v = tuple(data["domain_v"]) + return cls.from_plane(frame, domain_u, domain_v) + + @property + def __dtype__(self): + # make serialization CAD agnostic + return "compas.geometry/Surface" + def __new__(cls, *args, **kwargs): - return new_surface(cls, *args, **kwargs) + raise AssertionError("Surface() is protected. Use Surface.from_...() to construct a Surface object.") def __init__(self, frame=None, name=None): super(Surface, self).__init__(name=name) @@ -95,6 +119,10 @@ def transformation(self): self._transformation = Transformation.from_frame(self.frame) return self._transformation + @property + def native_surface(self): + raise NotImplementedError + @property def point(self): if not self._point: @@ -134,7 +162,11 @@ def domain_v(self): return self._domain_v @property - def is_closed(self): + def is_closed_u(self): + raise NotImplementedError + + @property + def is_closed_v(self): raise NotImplementedError @property @@ -197,6 +229,22 @@ def from_plane(cls, plane, *args, **kwargs): """ return new_surface_from_plane(cls, plane, *args, **kwargs) + @classmethod + def from_native(cls, surface): + """Construct a surface from a CAD native surface object. + + Parameters + ---------- + surface : Any + A native surface object. + + Returns + ------- + :class:`compas.geometry.Surface` + + """ + return new_surface_from_native(cls, surface) + # ============================================================================== # Conversions # ============================================================================== diff --git a/src/compas_rhino/conversions/__init__.py b/src/compas_rhino/conversions/__init__.py index ee610f2a17d..076a7ee39ba 100644 --- a/src/compas_rhino/conversions/__init__.py +++ b/src/compas_rhino/conversions/__init__.py @@ -43,6 +43,7 @@ ) from .surfaces import ( surface_to_rhino, + nurbssurface_to_compas, data_to_rhino_surface, surface_to_compas_data, surface_to_compas, @@ -127,6 +128,7 @@ "curve_to_compas", # surfaces "surface_to_rhino", + "nurbssurface_to_compas", "surface_to_compas_data", "data_to_rhino_surface", "surface_to_compas", diff --git a/src/compas_rhino/conversions/surfaces.py b/src/compas_rhino/conversions/surfaces.py index fbb84010aed..c869a928700 100644 --- a/src/compas_rhino/conversions/surfaces.py +++ b/src/compas_rhino/conversions/surfaces.py @@ -7,6 +7,7 @@ from compas.datastructures import Mesh from compas.geometry import NurbsSurface from compas.geometry import Point +from compas.geometry import Surface from compas.tolerance import TOL from compas.utilities import memoize @@ -32,12 +33,14 @@ def surface_to_rhino(surface): Rhino.Geometry.Surface """ - return surface.rhino_surface + return surface.native_surface def data_to_rhino_surface(data): """Convert a COMPAS surface to a Rhino surface. + TODO: still needed? this is basically RhinoSurface.from_parameters() + Parameters ---------- data: dict @@ -85,6 +88,8 @@ def data_to_rhino_surface(data): def surface_to_compas_data(surface): """Convert a Rhino surface to a COMPAS surface. + TODO: still needed? this is basically RhinoSurface.__data__ + Parameters ---------- surface: :rhino:`Rhino.Geometry.Surface` @@ -147,24 +152,37 @@ def surface_to_compas(surface): Parameters ---------- surface: :rhino:`Rhino.Geometry.Surface` + A Rhino surface object. Returns ------- - :class:`compas.geometry.Surface` + :class:`compas_rhino.geometry.RhinoSurface` """ - if isinstance(surface, Rhino.DocObjects.RhinoObject): - surface = surface.Geometry + if not isinstance(surface, Rhino.Geometry.Surface): + raise ConversionError("Expected a Rhino Surface object but got: {}".format(type(surface))) + + return Surface.from_native(surface) - if not isinstance(surface, Rhino.Geometry.Brep): - brep = Rhino.Geometry.Brep.TryConvertBrep(surface) - else: - brep = surface - if brep.Surfaces.Count > 1: # type: ignore - raise ConversionError("Conversion of a Brep with multiple underlying surface is currently not supported.") +def nurbssurface_to_compas(nurbssurface): + """Convert a Rhino NurbsSurface to a COMPAS NurbsSurface. + + Parameters + ---------- + nurbssurface : :class:`Rhino.Geometry.NurbsSurface` + A Rhino NurbsSurface object. + + Returns + ------- + :class:`compas_rhino.geometry.RhinoNurbsSurface` + A COMPAS NurbsSurface object. + + """ + if not isinstance(nurbssurface, Rhino.Geometry.NurbsSurface): + raise ConversionError("Expected a Rhino NurbsSurface object but got: {}".format(type(nurbssurface))) - return NurbsSurface.from_native(brep.Surfaces[0]) + return NurbsSurface.from_native(nurbssurface) def surface_to_compas_mesh(surface, facefilter=None, cleanup=False, cls=None): diff --git a/src/compas_rhino/geometry/__init__.py b/src/compas_rhino/geometry/__init__.py index 2481a1bfb8f..90b64f1abab 100644 --- a/src/compas_rhino/geometry/__init__.py +++ b/src/compas_rhino/geometry/__init__.py @@ -5,6 +5,7 @@ from __future__ import absolute_import from .curves.nurbs import RhinoNurbsCurve +from .surfaces import RhinoSurface from .surfaces.nurbs import RhinoNurbsSurface from .brep.brep import RhinoBrep @@ -34,6 +35,7 @@ "trimesh_principal_curvature", "trimesh_slice", "RhinoNurbsCurve", + "RhinoSurface", "RhinoNurbsSurface", "RhinoBrep", "RhinoBrepVertex", diff --git a/src/compas_rhino/geometry/surfaces/__init__.py b/src/compas_rhino/geometry/surfaces/__init__.py index 30081543e44..54c7e453070 100644 --- a/src/compas_rhino/geometry/surfaces/__init__.py +++ b/src/compas_rhino/geometry/surfaces/__init__.py @@ -1,28 +1,22 @@ from .surface import RhinoSurface # noqa : F401 from .nurbs import RhinoNurbsSurface -from compas.geometry import Surface -from compas.geometry import NurbsSurface from compas.plugins import plugin @plugin(category="factories", requires=["Rhino"]) -def new_surface(cls, *args, **kwargs): - surface = super(Surface, cls).__new__(cls) - surface.__init__(*args, **kwargs) - return surface +def new_surface_from_native(cls, *args, **kwargs): + return RhinoSurface.from_native(*args, **kwargs) @plugin(category="factories", requires=["Rhino"]) -def new_nurbssurface(cls, *args, **kwargs): - surface = super(NurbsSurface, cls).__new__(cls) - surface.__init__(*args, **kwargs) - return surface +def new_surface_from_plane(cls, *args, **kwargs): + return RhinoSurface.from_plane(*args, **kwargs) @plugin(category="factories", requires=["Rhino"]) def new_nurbssurface_from_native(cls, *args, **kwargs): - return RhinoNurbsSurface.from_rhino(*args, **kwargs) + return RhinoNurbsSurface.from_native(*args, **kwargs) @plugin(category="factories", requires=["Rhino"]) diff --git a/src/compas_rhino/geometry/surfaces/nurbs.py b/src/compas_rhino/geometry/surfaces/nurbs.py index 037263cff99..fa407b446d1 100644 --- a/src/compas_rhino/geometry/surfaces/nurbs.py +++ b/src/compas_rhino/geometry/surfaces/nurbs.py @@ -7,9 +7,9 @@ import Rhino.Geometry # type: ignore from compas.geometry import NurbsSurface -from compas.geometry import Point from compas.geometry import knots_and_mults_to_knotvector from compas.itertools import flatten +from compas_rhino.conversions import plane_to_rhino from compas_rhino.conversions import point_to_compas from compas_rhino.conversions import point_to_rhino @@ -51,6 +51,8 @@ def __iter__(self): def rhino_surface_from_parameters( + domain_u, + domain_v, points, weights, knots_u, @@ -89,6 +91,9 @@ def rhino_surface_from_parameters( ) raise ValueError("Failed to create NurbsSurface with params:\n{}".format(message)) + rhino_surface.SetDomain(0, Rhino.Geometry.Interval(domain_u[0], domain_u[1])) + rhino_surface.SetDomain(1, Rhino.Geometry.Interval(domain_v[0], domain_v[1])) + knotvector_u = knots_and_mults_to_knotvector(knots_u, mults_u) knotvector_v = knots_and_mults_to_knotvector(knots_v, mults_v) # account for superfluous knots @@ -152,56 +157,19 @@ def __data__(self): mults_u[-1] += 1 mults_v[0] += 1 mults_v[-1] += 1 - return { - "points": [[point.__data__ for point in row] for row in self.points], # type: ignore - "weights": self.weights, - "knots_u": self.knots_u, - "knots_v": self.knots_v, - "mults_u": mults_u, - "mults_v": mults_v, - "degree_u": self.degree_u, - "degree_v": self.degree_v, - "is_periodic_u": self.is_periodic_u, - "is_periodic_v": self.is_periodic_v, - } - @classmethod - def __from_data__(cls, data): - """Construct a BSpline surface from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`compas_rhino.geometry.RhinoNurbsSurface` - The constructed surface. - - """ - points = [[Point.__from_data__(point) for point in row] for row in data["points"]] - weights = data["weights"] - knots_u = data["knots_u"] - knots_v = data["knots_v"] - mults_u = data["mults_u"] - mults_v = data["mults_v"] - degree_u = data["degree_u"] - degree_v = data["degree_v"] - is_periodic_u = data["is_periodic_u"] - is_periodic_v = data["is_periodic_v"] - return cls.from_parameters( - points, - weights, - knots_u, - knots_v, - mults_u, - mults_v, - degree_u, - degree_v, - is_periodic_u, - is_periodic_v, - ) + data = super(NurbsSurface, self).__data__ + data["points"] = [[point.__data__ for point in row] for row in self.points] # type: ignore + data["weights"] = self.weights + data["knots_u"] = self.knots_u + data["knots_v"] = self.knots_v + data["mults_u"] = mults_u + data["mults_v"] = mults_v + data["degree_u"] = self.degree_u + data["degree_v"] = self.degree_v + data["is_periodic_u"] = self.is_periodic_u + data["is_periodic_v"] = self.is_periodic_v + return data # ============================================================================== # Properties @@ -272,6 +240,9 @@ def degree_v(self): @classmethod def from_parameters( cls, + frame, + domain_u, + domain_v, points, weights, knots_u, @@ -287,6 +258,12 @@ def from_parameters( Parameters ---------- + frame : :class:`compas.geometry.Frame` + The frame of the surface. + domain_u : tuple(float) + The domain of the surface in the U direction. + domain_v : tuple(float) + The domain of the surface in the V direction. points : list[list[:class:`compas.geometry.Point`]] The control points. weights : list[list[float]] @@ -310,7 +287,10 @@ def from_parameters( """ surface = cls() - surface.rhino_surface = rhino_surface_from_parameters(points, weights, knots_u, knots_v, mults_u, mults_v, degree_u, degree_v) + surface.rhino_surface = rhino_surface_from_parameters(domain_u, domain_v, points, weights, knots_u, knots_v, mults_u, mults_v, degree_u, degree_v) + surface.frame = frame + surface._domain_u = domain_u + surface._domain_v = domain_v return surface @classmethod @@ -361,6 +341,45 @@ def from_fill(cls, curve1, curve2): surface.rhino_surface = Rhino.Geometry.NurbsSurface.CreateRuledSurface(curve1, curve2) return surface + @classmethod + def from_plane(cls, plane, u_degree=1, v_degree=1, u_domain=(0.0, 1.0), v_domain=(0.0, 1.0)): + """Construct a NURBS surface from a plane. + + Parameters + ---------- + plane : :class:`compas.geometry.Plane` + The plane from which to construct the surface. + u_degree : int, optional + The degree of the surface in the U direction. + Default is ``1``. + v_degree : int, optional + The degree of the surface in the V direction. + Default is ``1``. + u_domain : tuple(float), optional + The domain of the surface in the U direction. + Default is ``(0, 1)``. + v_domain : tuple(float), optional + The domain of the surface in the V direction. + Default is ``(0, 1)``. + + Returns + ------- + :class:`compas_rhino.geometry.RhinoNurbsSurface` + + """ + plane = plane_to_rhino(plane) + surface = cls() + surface.rhino_surface = Rhino.Geometry.NurbsSurface.CreateFromPlane( + plane, + Rhino.Geometry.Interval(*u_domain), + Rhino.Geometry.Interval(*v_domain), + u_degree, + v_degree, + u_degree + 1, + v_degree + 1, + ) + return surface + # ============================================================================== # Conversions # ============================================================================== diff --git a/src/compas_rhino/geometry/surfaces/surface.py b/src/compas_rhino/geometry/surfaces/surface.py index 36fa127a0fa..f5ee87cb96c 100644 --- a/src/compas_rhino/geometry/surfaces/surface.py +++ b/src/compas_rhino/geometry/surfaces/surface.py @@ -4,10 +4,16 @@ import Rhino.Geometry # type: ignore +import compas + +if not compas.IPY: + from typing import Tuple # noqa: F401 + +from compas.geometry import Frame +from compas.geometry import Plane # noqa: F401 from compas.geometry import Surface from compas_rhino.conversions import box_to_compas from compas_rhino.conversions import cylinder_to_rhino -from compas_rhino.conversions import frame_to_rhino_plane from compas_rhino.conversions import plane_to_compas_frame from compas_rhino.conversions import plane_to_rhino from compas_rhino.conversions import point_to_compas @@ -27,42 +33,56 @@ class RhinoSurface(Surface): The parameter domain in the U direction. domain_v: tuple[float, float] The parameter domain in the V direction. + frame: :class:`compas.geometry.Frame` + The frame of the surface at the parametric origin. is_periodic_u: bool True if the surface is periodic in the U direction. is_periodic_v: bool True if the surface is periodic in the V direction. + native_surface: :class:`Rhino.Geometry.Surface` + The underlying Rhino surface object. """ + def __new__(cls, *args, **kwargs): + # needed because the Surface.__new__ now enforces creation via alternative constructors only + # cls is the class that is being instantiated + return super(Surface, cls).__new__(cls, *args, **kwargs) + def __init__(self, name=None): super(RhinoSurface, self).__init__(name=name) self._rhino_surface = None + def _get_frame_from_planesurface(self): + u_start = self.domain_u[0] + v_start = self.domain_v[0] + success, frame = self.native_surface.FrameAt(u_start, v_start) + if not success: + raise ValueError("Failed to get frame at u={} v={}".format(u_start, v_start)) + return plane_to_compas_frame(frame) + + # ============================================================================== + # Implementation of abstract properties + # ============================================================================== + + @property + def native_surface(self): + return self._rhino_surface + @property def rhino_surface(self): + # TODO: Deprecate. replace with native_surface return self._rhino_surface @rhino_surface.setter def rhino_surface(self, surface): + # TODO: Deprecate. should be read-only self._rhino_surface = surface - # ============================================================================== - # Data - # ============================================================================== - - # ============================================================================== - # Properties - # ============================================================================== - - @property - def domain_u(self): - if self.rhino_surface: - return self.rhino_surface.Domain(0) - @property - def domain_v(self): - if self.rhino_surface: - return self.rhino_surface.Domain(1) + def is_closed(self): + # TODO: implement this properly + raise NotImplementedError @property def is_periodic_u(self): @@ -95,7 +115,7 @@ def from_corners(cls, corners): """ rhino_points = [Rhino.Geometry.Point3d(corner.x, corner.y, corner.z) for corner in corners] - return cls.from_rhino(Rhino.Geometry.NurbsSurface.CreateFromCorners(*rhino_points)) + return cls.from_native(Rhino.Geometry.NurbsSurface.CreateFromCorners(*rhino_points)) @classmethod def from_sphere(cls, sphere): @@ -113,7 +133,7 @@ def from_sphere(cls, sphere): """ sphere = sphere_to_rhino(sphere) surface = Rhino.Geometry.NurbsSurface.CreateFromSphere(sphere) - return cls.from_rhino(surface) + return cls.from_native(surface) @classmethod def from_cylinder(cls, cylinder): @@ -131,7 +151,7 @@ def from_cylinder(cls, cylinder): """ cylinder = cylinder_to_rhino(cylinder) surface = Rhino.Geometry.NurbsSurface.CreateFromCylinder(cylinder) - return cls.from_rhino(surface) + return cls.from_native(surface) @classmethod def from_torus(cls, torus): @@ -153,6 +173,9 @@ def from_torus(cls, torus): def from_rhino(cls, rhino_surface): """Construct a NURBS surface from an existing Rhino surface. + .. deprecated:: 2.1.1 + ``from_rhino`` will be removed in the future. Use ``from_native`` instead. + Parameters ---------- rhino_surface : :rhino:`Rhino.Geometry.Surface` @@ -163,57 +186,64 @@ def from_rhino(cls, rhino_surface): :class:`compas_rhino.geometry.RhinoSurface` """ - curve = cls() - curve.rhino_surface = rhino_surface - return curve + from warnings import warn + + warn("RhinoSurface.from_rhino will be removed in the future. Use RhinoSurface.from_native instead.", DeprecationWarning) + return cls.from_native(rhino_surface) @classmethod - def from_plane(cls, plane, box): - """Construct a surface from a plane. + def from_native(cls, native_surface): + """Construct a surface from an existing Rhino surface. Parameters ---------- - plane : :class:`compas.geometry.Plane` - The plane. + native_surface : :rhino:`Rhino.Geometry.Surface` + A Rhino surface. Returns ------- :class:`compas_rhino.geometry.RhinoSurface` """ - plane = plane_to_rhino(plane) - box = Rhino.Geometry.BoundingBox(box.xmin, box.ymin, box.zmin, box.xmax, box.ymax, box.zmax) - rhino_surface = Rhino.Geometry.PlaneSurface.CreateThroughBox(plane, box) - return cls.from_rhino(rhino_surface) + instance = cls() + instance._rhino_surface = native_surface + instance.frame = instance._get_frame_from_planesurface() + instance._domain_u = native_surface.Domain(0)[0], native_surface.Domain(0)[1] + instance._domain_v = native_surface.Domain(1)[0], native_surface.Domain(1)[1] + return instance @classmethod - def from_frame(cls, frame, u_interval, v_interval): - """Creates a planar surface from a frame and parametric domain information. + def from_plane(cls, plane, u_domain=(0.0, 1.0), v_domain=(0.0, 1.0)): + # type: (Plane, Tuple[float, float], Tuple[float, float]) -> RhinoSurface + """Construct a surface from a plane. 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]. + plane : :class:`compas.geometry.Plane` or :class:`compas.geometry.Frame` + The plane or frame to use as origin and orientation of the surface. + u_domain : tuple(float, float), optional + The parametric domain of the U parameter. u_domain[0] => u_domain[1]. + Default is ``(0.0, 1.0)``. + v_domain : tuple(float, float), optional + The parametric domain of the V parameter. v_domain[0] => v_domain[1]. + Default is ``(0.0, 1.0)``. + + Note + ---- + While the plane's origin is its center, the surface's parametric origin is at the surface's corner. + For the plane to overlap with the surface, the plane's origin should be first shifted by half it's domain. + Alternatively, the surface's domain can be adjusted to match the plane's origin. Returns ------- - :class:`compas_rhino.geometry.surface.RhinoSurface` + :class:`compas_rhino.geometry.RhinoSurface` """ - surface = Rhino.Geometry.PlaneSurface( - frame_to_rhino_plane(frame), - Rhino.Geometry.Interval(*u_interval), - Rhino.Geometry.Interval(*v_interval), - ) - if not surface: - msg = "Failed creating PlaneSurface from frame:{} u_interval:{} v_interval:{}" - raise ValueError(msg.format(frame, u_interval, v_interval)) - return cls.from_rhino(surface) + if isinstance(plane, Frame): + plane = Plane.from_frame(plane) + plane = plane_to_rhino(plane) + rhino_surface = Rhino.Geometry.PlaneSurface(plane, Rhino.Geometry.Interval(*u_domain), Rhino.Geometry.Interval(*v_domain)) + return cls.from_native(rhino_surface) # ============================================================================== # Conversions