from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import Rhino
from Rhino.Geometry import Point3d
import compas_rhino
from compas.geometry import Point
from compas.geometry import Scale
from compas.geometry import Translation
from compas.geometry import Rotation
from compas.geometry import subtract_vectors
from compas.geometry import add_vectors
from compas.geometry import scale_vector
from compas_rhino.objects._object import BaseObject
from compas_rhino.objects.modify import mesh_update_attributes
from compas_rhino.objects.modify import mesh_update_vertex_attributes
from compas_rhino.objects.modify import mesh_update_face_attributes
from compas_rhino.objects.modify import mesh_update_edge_attributes
from compas_rhino.objects.modify import mesh_move_vertex
from compas_rhino.objects.modify import mesh_move_vertices
from compas_rhino.objects.modify import mesh_move_face
__all__ = ['MeshObject']
[docs]class MeshObject(BaseObject):
"""Class for representing COMPAS meshes in Rhino.
Parameters
----------
mesh : :class:`compas.datastructures.Mesh`
A mesh data structure.
scene : :class:`compas.scenes.Scene`, optional
A scene object.
name : str, optional
The name of the object.
layer : str, optional
The layer for drawing.
visible : bool, optional
Toggle for the visibility of the object.
settings : dict, optional
A dictionary of settings.
"""
SETTINGS = {
'color.vertices': (255, 255, 255),
'color.edges': (0, 0, 0),
'color.faces': (0, 0, 0),
'color.mesh': (0, 0, 0),
'show.mesh': True,
'show.vertices': True,
'show.edges': True,
'show.faces': False,
'show.vertexlabels': False,
'show.facelabels': False,
'show.edgelabels': False,
'show.vertexnormals': False,
'show.facenormals': False,
}
modify = mesh_update_attributes
modify_vertices = mesh_update_vertex_attributes
modify_faces = mesh_update_face_attributes
modify_edges = mesh_update_edge_attributes
def __init__(self, mesh, scene=None, name=None, layer=None, visible=True, settings=None):
super(MeshObject, self).__init__(mesh, scene, name, layer, visible)
self._guids = []
self._guid_vertex = {}
self._guid_edge = {}
self._guid_face = {}
self._guid_vertexnormal = {}
self._guid_facenormal = {}
self._guid_vertexlabel = {}
self._guid_edgelabel = {}
self._guid_facelabel = {}
self._anchor = None
self._location = None
self._scale = None
self._rotation = None
self.settings.update(type(self).SETTINGS)
if settings:
self.settings.update(settings)
@property
def mesh(self):
return self.item
@mesh.setter
def mesh(self, mesh):
self.item = mesh
self._guids = []
self._guid_vertex = {}
self._guid_edge = {}
self._guid_face = {}
self._guid_vertexnormal = {}
self._guid_facenormal = {}
self._guid_vertexlabel = {}
self._guid_edgelabel = {}
self._guid_facelabel = {}
# def __getstate__(self):
# pass
# def __setstate__(self, state):
# pass
@property
def anchor(self):
"""The vertex of the mesh that is anchored to the location of the object."""
return self._anchor
@anchor.setter
def anchor(self, vertex):
if self.mesh.has_vertex(vertex):
self._anchor = vertex
@property
def location(self):
""":class:`compas.geometry.Point`:
The location of the object.
Default is the origin of the world coordinate system.
The object transformation is applied relative to this location.
Setting this location will make a copy of the provided point object.
Moving the original point will thus not affect the object's location.
"""
if not self._location:
self._location = Point(0, 0, 0)
return self._location
@location.setter
def location(self, location):
self._location = Point(*location)
@property
def scale(self):
"""float:
A uniform scaling factor for the object in the scene.
The scale is applied relative to the location of the object in the scene.
"""
if not self._scale:
self._scale = 1.0
return self._scale
@scale.setter
def scale(self, scale):
self._scale = scale
@property
def rotation(self):
"""list of float:
The rotation angles around the 3 axis of the coordinate system
with the origin placed at the location of the object in the scene.
"""
if not self._rotation:
self._rotation = [0, 0, 0]
return self._rotation
@rotation.setter
def rotation(self, rotation):
self._rotation = rotation
@property
def vertex_xyz(self):
"""dict : The view coordinates of the mesh object."""
origin = Point(0, 0, 0)
if self.anchor is not None:
xyz = self.mesh.vertex_attributes(self.anchor, 'xyz')
point = Point(* xyz)
T1 = Translation.from_vector(origin - point)
S = Scale.from_factors([self.scale] * 3)
R = Rotation.from_euler_angles(self.rotation)
T2 = Translation.from_vector(self.location)
X = T2 * R * S * T1
else:
S = Scale.from_factors([self.scale] * 3)
R = Rotation.from_euler_angles(self.rotation)
T = Translation.from_vector(self.location)
X = T * R * S
mesh = self.mesh.transformed(X)
vertex_xyz = {vertex: mesh.vertex_attributes(vertex, 'xyz') for vertex in mesh.vertices()}
return vertex_xyz
@property
def guid_vertex(self):
"""dict: Map between Rhino object GUIDs and mesh vertex identifiers."""
return self._guid_vertex
@guid_vertex.setter
def guid_vertex(self, values):
self._guid_vertex = dict(values)
@property
def guid_edge(self):
"""dict: Map between Rhino object GUIDs and mesh edge identifiers."""
return self._guid_edge
@guid_edge.setter
def guid_edge(self, values):
self._guid_edge = dict(values)
@property
def guid_face(self):
"""dict: Map between Rhino object GUIDs and mesh face identifiers."""
return self._guid_face
@guid_face.setter
def guid_face(self, values):
self._guid_face = dict(values)
@property
def guid_vertexnormal(self):
"""dict: Map between Rhino object GUIDs and mesh vertexnormal identifiers."""
return self._guid_vertexnormal
@guid_vertexnormal.setter
def guid_vertexnormal(self, values):
self._guid_vertexnormal = dict(values)
@property
def guid_facenormal(self):
"""dict: Map between Rhino object GUIDs and mesh facenormal identifiers."""
return self._guid_facenormal
@guid_facenormal.setter
def guid_facenormal(self, values):
self._guid_facenormal = dict(values)
@property
def guid_vertexlabel(self):
"""dict: Map between Rhino object GUIDs and mesh vertexlabel identifiers."""
return self._guid_vertexlabel
@guid_vertexlabel.setter
def guid_vertexlabel(self, values):
self._guid_vertexlabel = dict(values)
@property
def guid_facelabel(self):
"""dict: Map between Rhino object GUIDs and mesh facelabel identifiers."""
return self._guid_facelabel
@guid_facelabel.setter
def guid_facelabel(self, values):
self._guid_facelabel = dict(values)
@property
def guid_edgelabel(self):
"""dict: Map between Rhino object GUIDs and mesh edgelabel identifiers."""
return self._guid_edgelabel
@guid_edgelabel.setter
def guid_edgelabel(self, values):
self._guid_edgelabel = dict(values)
@property
def guids(self):
"""list: The GUIDs of all Rhino objects created by this artist."""
guids = self._guids
guids += list(self.guid_vertex.keys())
guids += list(self.guid_edge.keys())
guids += list(self.guid_face.keys())
guids += list(self.guid_vertexnormal.keys())
guids += list(self.guid_facenormal.keys())
guids += list(self.guid_vertexlabel.keys())
guids += list(self.guid_edgelabel.keys())
guids += list(self.guid_facelabel.keys())
return guids
[docs] def clear(self):
"""Clear all Rhino objects associated with this object.
"""
compas_rhino.delete_objects(self.guids, purge=True)
self._guids = []
self._guid_vertex = {}
self._guid_edge = {}
self._guid_face = {}
self._guid_vertexnormal = {}
self._guid_facenormal = {}
self._guid_vertexlabel = {}
self._guid_edgelabel = {}
self._guid_facelabel = {}
[docs] def draw(self):
"""Draw the object representing the mesh.
"""
self.clear()
if not self.visible:
return
self.artist.vertex_xyz = self.vertex_xyz
if self.settings['show.vertices']:
vertices = list(self.mesh.vertices())
guids = self.artist.draw_vertices(vertices=vertices, color=self.settings['color.vertices'])
self.guid_vertex = zip(guids, vertices)
if self.settings['show.vertexlabels']:
text = {vertex: str(vertex) for vertex in vertices}
guids = self.artist.draw_vertexlabels(text=text, color=self.settings['color.vertices'])
self.guid_vertexlabel = zip(guids, vertices)
if self.settings['show.vertexnormals']:
guids = self.artist.draw_vertexnormals(vertices=vertices, color=self.settings['color.vertices'])
self.guid_vertexnormal = zip(guids, vertices)
if self.settings['show.mesh']:
guids = self.artist.draw_mesh(color=self.settings['color.mesh'], disjoint=True)
self._guids = guids
else:
if self.settings['show.faces']:
faces = list(self.mesh.faces())
guids = self.artist.draw_faces(faces=faces, color=self.settings['color.faces'])
self.guid_face = zip(guids, faces)
if self.settings['show.facelabels']:
text = {face: str(face) for face in faces}
guids = self.artist.draw_facelabels(text=text, color=self.settings['color.faces'])
self.guid_facelabel = zip(guids, faces)
if self.settings['show.facenormals']:
guids = self.artist.draw_facenormals(faces=faces, color=self.settings['color.faces'])
self.guid_face = zip(guids, faces)
if self.settings['show.edges']:
edges = list(self.mesh.edges())
guids = self.artist.draw_edges(edges=edges, color=self.settings['color.edges'])
self.guid_edge = zip(guids, edges)
if self.settings['show.edgelabels']:
text = {edge: "{}-{}".format(*edge) for edge in edges}
guids = self.artist.draw_edgelabels(text=text, color=self.settings['color.edges'])
self.guid_edgelabel = zip(guids, edges)
self.redraw()
def select(self):
# there is currently no "general" selection method
# for the entire mesh object
raise NotImplementedError
def select_vertex(self, message="Select one vertex."):
"""Select one vertex of the mesh.
Returns
-------
int
A vertex identifier.
"""
guid = compas_rhino.select_point(message=message)
if guid and guid in self.guid_vertex:
return self.guid_vertex[guid]
[docs] def select_vertices(self, message="Select vertices."):
"""Select vertices of the mesh.
Returns
-------
list
A list of vertex identifiers.
"""
guids = compas_rhino.select_points(message=message)
vertices = [self.guid_vertex[guid] for guid in guids if guid in self.guid_vertex]
return vertices
[docs] def select_faces(self, message="Select faces."):
"""Select faces of the mesh.
Returns
-------
list
A list of face identifiers.
"""
guids = compas_rhino.select_meshes(message=message)
faces = [self.guid_face[guid] for guid in guids if guid in self.guid_face]
return faces
[docs] def select_edges(self, message="Select edges."):
"""Select edges of the mesh.
Returns
-------
list
A list of edge identifiers.
"""
guids = compas_rhino.select_lines(message=message)
edges = [self.guid_edge[guid] for guid in guids if guid in self.guid_edge]
return edges
# not clear if this is now about the location or the data
def move(self):
"""Move the entire mesh object to a different location."""
raise NotImplementedError
def move_vertex(self, vertex):
"""Move a single vertex of the mesh object and update the data structure accordingly.
Parameters
----------
vertex : int
The identifier of the vertex.
Returns
-------
bool
True if the operation was successful.
False otherwise.
"""
return mesh_move_vertex(self.mesh, vertex)
def move_vertices(self, vertices):
"""Move a multiple vertices of the mesh object and update the data structure accordingly.
Parameters
----------
vertices : list of int
The identifiers of the vertices.
Returns
-------
bool
True if the operation was successful.
False otherwise.
"""
return mesh_move_vertices(self.mesh, vertices)
def move_face(self, face):
"""Move a single face of the mesh object and update the data structure accordingly.
Parameters
----------
face : int
The identifier of the face.
Returns
-------
bool
True if the operation was successful.
False otherwise.
"""
return mesh_move_face(self.mesh, face)
def scale_from_3_points(self, message="Select the base vertex for the scaling operation."):
"""Scale the mesh object from 3 reference points.
Note that this does not scale the underlying data structure,
but only the scale of the representation in Rhino.
The first reference point of the scaling operation must be a vertex of the mesh.
This vertex will become the new anchor of the object.
Returns
-------
bool
True if the operation was successful.
False otherwise.
"""
def OnDynamicDraw(sender, e):
d1 = p0.DistanceTo(p1)
d2 = p0.DistanceTo(e.CurrentPoint)
ratio = d2 / d1
DrawLine = e.Display.DrawDottedLine
for vertex in self.diagram.vertices():
if vertex == anchor:
continue
vector = vertex_vector[vertex]
vertex_xyz[vertex] = add_vectors(origin, scale_vector(vector, ratio))
for u, v in iter(edges):
DrawLine(Point3d(* vertex_xyz[u]), Point3d(* vertex_xyz[v]), color)
anchor = self.select_vertex(message=message)
if anchor is None:
return
color = Rhino.ApplicationSettings.AppearanceSettings.FeedbackColor
edges = list(self.diagram.edges())
vertex_xyz = self.artist.vertex_xyz
vertex_xyz0 = {vertex: vertex_xyz[vertex][:] for vertex in vertex_xyz}
origin = vertex_xyz0[anchor]
vertex_vector = {vertex: subtract_vectors(vertex_xyz0[vertex], origin) for vertex in vertex_xyz}
p0 = Point3d(* origin)
gp = Rhino.Input.Custom.GetPoint()
gp.SetCommandPrompt('Select the 1st reference point.')
gp.Get()
if gp.CommandResult() != Rhino.Commands.Result.Success:
return False
p1 = gp.Point()
gp.SetCommandPrompt('Select the 2nd reference point.')
gp.DynamicDraw += OnDynamicDraw
gp.Get()
if gp.CommandResult() != Rhino.Commands.Result.Success:
return False
p2 = gp.Point()
d1 = p0.DistanceTo(p1)
d2 = p0.DistanceTo(p2)
ratio = d2 / d1
self.scale *= ratio
self.anchor = anchor
self.location = vertex_xyz[anchor]
return True
# ============================================================================
# Main
# ============================================================================
if __name__ == "__main__":
pass