diff --git a/rocolib/api/composables/GraphComposable.py b/rocolib/api/composables/GraphComposable.py index b788889f39e7091aa5728e04b5fdc25d4ce6545d..e7d79e401da6d62bcf76f8f3a97b709d29b72836 100644 --- a/rocolib/api/composables/GraphComposable.py +++ b/rocolib/api/composables/GraphComposable.py @@ -92,23 +92,17 @@ class GraphComposable(Composable, BaseGraph): handle("silhouette", "Silhouette cut-and-fold", d.toDXF, "silhouette.dxf", mode="silhouette") handle("autofolding", "autofolding", d.toDXF, "autofold-default.dxf", mode="autofold") handle("autofolding", " -- (graph)", d.toDXF, "autofold-graph.dxf") - handle("3D", "stl", self.to3D, "model.stl", format="stl", **kwargs) - handle("3D", "webots", self.to3D, "model.wbo", format="webots", **kwargs) - + handle("stl", "3D", self.toSTL, "model.stl", **kwargs) if kw("display3D", False) or kw("png"): from rocolib.utils.display import display3D if basename: if kw("stl"): - try: - stl_handle = open(basename+"model.stl", 'rb') - except: - log.info("No Stl found") - return + stl_handle = open(basename+"model.stl", 'rb') else: log.info("STL wasn't selected, making now...") - handle("png", "stl", self.to3D, None, format="stl") + handle("png", "3D", self.toSTL, None, **kwargs) stl_handle = bufs['png'] stl_handle.seek(0) if kw("png"): diff --git a/rocolib/api/composables/graph/Graph.py b/rocolib/api/composables/graph/Graph.py index e3c391abad6169aa91553a5257615b4e72822576..46dafa9dee591a9f348754930893ae94b37e2b53 100644 --- a/rocolib/api/composables/graph/Graph.py +++ b/rocolib/api/composables/graph/Graph.py @@ -2,9 +2,7 @@ import logging from rocolib.api.composables.graph.HyperEdge import HyperEdge from rocolib.utils.utils import prefix as prefixString -from rocolib.utils.roco2sim.Node import ShapeNode import rocolib.utils.numsym as np -from rocolib.utils.roco2sim.format_3d import format_wrl log = logging.getLogger(__name__) @@ -25,78 +23,54 @@ def inflate(face, thickness=.1, edges=False): return faces +def STLWrite(faces, fp, **kwargs): + scale = .001 # roco units : mm ; STL units m -def _triangulate(faces, **kwargs): - """Create triangulated faces in 3D space with thickness""" - scale = .001 # roco units : mm ; STL units m + from .stlwriter import ASCII_STL_Writer as STL_Writer import triangle - import numpy as np - - triangles_coord_index = [] - triangles_facet_normal = [] - facets_index = np.empty([1, 3]) - B_add = 0 - k = 0 # index initiated + shells = [] + triangles = [] for i, f in enumerate(faces): + r = f[0] + A = f[1] - r = f[0] # coord transformation matrix - A = f[1] # vertices info - - facets_coord_index = [] - facets_normal = [] facets = [] B = triangle.triangulate(A, opts='p') - if not 'triangles' in B: log.warning("No triangles in " + f[2]) continue thickness = "thickness" in kwargs and kwargs["thickness"] if thickness: - for t in [np.transpose(np.array([list(B['vertices'][x]) + [0, 1] for x in (face[0], face[1], face[2])])) for - face in B['triangles']]: - for x in inflate(t, thickness=thickness): - facets_coord_index.extend(np.transpose(np.dot(r, x) * scale)) - facets_normal.extend(np.transpose(np.dot(r, x) * scale)) - index_current = [[k, k + 1, k + 2]] - k = k + 3 - facets_index = np.concatenate((facets_index, index_current), 0) - for t in [np.transpose(np.array([list(A['vertices'][x]) + [0, 1] for x in (edge[0], edge[1])])) for edge in - A['segments']]: - for x in inflate(t, thickness=thickness, edges=True): - facets_coord_index.extend(np.transpose(np.dot(r, x) * scale)) - facets_normal.extend(np.transpose(np.dot(r, x) * scale)) - index_current = [[k, k + 1, k + 2]] - k = k + 3 - facets_index = np.concatenate((facets_index, index_current), 0) + for t in [np.transpose(np.array([list(B['vertices'][x]) + [0,1] for x in (face[0], face[1], face[2])])) for face in B['triangles']]: + facets.extend([np.dot(r, x) * scale for x in inflate(t, thickness=thickness)]) + for t in [np.transpose(np.array([list(A['vertices'][x]) + [0,1] for x in (edge[0], edge[1])])) for edge in A['segments']]: + facets.extend([np.dot(r, x) * scale for x in inflate(t, thickness=thickness, edges=True)]) else: - for t in [np.transpose(np.array([list(B['vertices'][x]) + [0, 1] for x in (face[0], face[1], face[2])])) for - face in B['triangles']]: + for t in [np.transpose(np.array([list(B['vertices'][x]) + [0,1] for x in (face[0], face[1], face[2])])) for face in B['triangles']]: facets.append(np.dot(r, t) * scale) - facets_coord_index.extend(np.transpose(np.dot(r, t)) * scale) - facets_normal.extend(np.transpose(np.dot(r, t)) * scale) - #B_triangle = B['triangles'] + B_add - #B_add = np.amax(B_triangle) + 1 - index_current = [[k, k + 1, k + 2]] - k = k + 3 - facets_index = np.concatenate((facets_index, index_current), 0) - - - triangles_coord_index.extend(facets_coord_index) - triangles_facet_normal.extend(facets_normal) - - facets_index_return = np.delete(facets_index, 0, 0) - - return format_wrl(triangles_coord_index, triangles_facet_normal, facets_index_return) + triangles.extend(facets) + ''' + # Output each face to its own STL file + if filename: + with open(filename.replace(".stl", "_%02d.stl" % i), 'wb') as fp: + writer = STL_Writer(fp) + writer.add_faces(facets) + writer.close() + ''' + + faces = triangles + writer = STL_Writer(fp) + writer.add_faces(faces) + writer.close() class Graph(): def __init__(self): self.faces = [] self.facelists = [] self.edges = [] - self.shapeNodes=None def addFace(self, f, prefix=None, root=False, faceEdges=None, faceAngles=None, faceFlips=None): if prefix: @@ -329,42 +303,15 @@ class Graph(): e.pts2D = None e.pts3D = None - def toShapeNodes(self, **kwargs): - """Create a shape node list""" + def toSTL(self, fp, **kwargs): self.place() - shapeNodes = [] - for i, facesInFacelists in enumerate(self.facelists): - faces = [] - for f in facesInFacelists: - if f.area > 0: - faces.append([f.transform3D, f.getTriangleDict(), f.name]) - else: - log.info(f"Omitting face {f.name} with area {f.area} from wbo") - # Crate 1 shape node per set of faces. - wrl = _triangulate(faces, **kwargs) - shapeNodes.append(ShapeNode(f.name, wrl, f.transform3D)) - - self.shapeNodes = shapeNodes - - def to3D(self, fp, **kwargs): - """stl or webots expects fp instance from handle. if None, not creating it""" - if self.shapeNodes is None: - self.toShapeNodes(**kwargs) - - if kwargs.get('format') == 'stl': - f = [] - for sn in self.shapeNodes: - f.extend(sn.getSTL().face) - from .stlwriter import ASCII_STL_Writer as STL_Writer - writer = STL_Writer(fp) - writer.add_faces(f) - writer.close() - - if kwargs.get('format') == 'webots': - from rocolib.utils.roco2sim.wbo_nodes import wboConverter - fp.write(wboConverter(self.shapeNodes)) - fp.close() - + stlFaces = [] + for face in self.faces: + if face.area > 0: + stlFaces.append([face.transform3D, face.getTriangleDict(), face.name]) + else: + log.info(f"Omitting face {face.name} with area {face.area} from STL") + return STLWrite(stlFaces, fp, **kwargs) ''' @staticmethod diff --git a/rocolib/utils/roco2sim/Node.py b/rocolib/utils/roco2sim/Node.py deleted file mode 100644 index e905bde778aa66bb771aedc07af370c7e7114e7b..0000000000000000000000000000000000000000 --- a/rocolib/utils/roco2sim/Node.py +++ /dev/null @@ -1,141 +0,0 @@ -import numpy as np - -from rocolib.utils.roco2sim.material_data import Material as mat, MaterialConst as mc -from rocolib.utils.roco2sim.material_data import materialData as md -from rocolib.utils.roco2sim.format_3d import format_wrl as wrl -from rocolib.utils.roco2sim.format_3d import wrl2stl - -"""BaseNodes (No children allowed)""" - -class AppearanceNode: - """appearance""" - - def __init__(self, material: mat = md[mc.MAT_DEFAULT]): - # if no material specified, use default material - self._material = material - - def getMaterial(self): - return self._material - - def getRGB(self): - return self._material.rgb - - def getRoughness(self): - return self._material.roughness - - def getReflection(self): - return self._material.reflection - - def getName(self): - return self._material.name - - def getDescription(self): - return self._material.description - - -class PhysicsNode: - def __init__(self, material: mat = md[mc.MAT_DEFAULT], mass=0.3, contact=None): - self.material = material - self.mass = mass - self.contact = contact - - def isMassSpecified(self): - return self.mass is not None - - - -class DeviceNode: - def __int__(self, name=None, type=None, options=None): - if options is None: - options = {} - self.options = options - self.name = name - self.type = type - - -"""Composable Nodes (can have children)""" -class BaseComposableNode: - def __init__(self, name, children=None): - self.name = name - if children is None: - children = [] - self.children = children - - -class ShapeNode(BaseComposableNode): - """Shape node holds faces and appearance information. - If solidified, then includes physics""" - - def __init__(self, name, wrl: wrl, trans, appearance: AppearanceNode = None, solidify: PhysicsNode = None, - children=None): - super().__init__(name, children) - self.trans = trans - self.wrl = wrl - self.solid = False - - - # if no appearance specified, default - if appearance is None: - self.appearance = AppearanceNode() - else: - self.appearance = appearance - - # physics is None, then inherit, if Physics node is given, then use it. - # otherwise not physics - self.physics = PhysicsNode - if solidify is None: - self.solidify() - elif isinstance(solidify, PhysicsNode): - self.solidifyWithNewPhysics(solidify) - - def solidify(self): - """Add physics property to this shape based on appearance material""" - self.physics = PhysicsNode(self.appearance.getMaterial()) - self.solid = True - - def solidifyWithNewPhysics(self, physics: PhysicsNode): - """Add physics property to this shape by specified physics nodes""" - self.physics = physics - self.solid = True - - def gasify(self): - self.solid = False - - def isSolid(self): - return self.solid - - def getXyz(self): - return self.trans - def getRotationVector(self): - R = self.trans[0:3,0:3] - v = np.array([R[2,1]-R[1,2],R[0,2]-R[2,0],R[1,0]-R[0,1]]) - return np.append(v/np.linalg.norm(v), np.arccos((np.trace(R)-1)/2)) - - def getPhysicsNode(self): - return self.physics - - def getWRL(self): - return self.wrl - - def getSTL(self): - return wrl2stl(self.wrl) - - # def getFaces(self): - # return self.faces - # def getAppearance(self): - # return self.appearance - - -class ModuleNode(BaseComposableNode): - def __init__(self, name, shape: ShapeNode, device: DeviceNode = None, children=None): - super().__init__(name, children) - self.shape = shape - self.device = device - - def getDeviceOptions(self, optionName): - if optionName in self.device.options.items(): - return self.device.options[optionName] - return "" - - def getPhysicsNode(self): - return self.shape.physics diff --git a/rocolib/utils/roco2sim/format_3d.py b/rocolib/utils/roco2sim/format_3d.py deleted file mode 100644 index 78fa78ca5f5de3bfca6fb959bd8d6846bf1a25fc..0000000000000000000000000000000000000000 --- a/rocolib/utils/roco2sim/format_3d.py +++ /dev/null @@ -1,19 +0,0 @@ -from dataclasses import dataclass -import numpy as np - -@dataclass -class format_wrl: - coord: list - normal: list - index: list - -@dataclass -class format_stl: - face: list - -def wrl2stl(wrl: format_wrl): - faces = [] - for (index) in wrl.index: - faces.append(np.transpose(np.vstack((wrl.coord[int(index[0])],wrl.coord[int(index[1])],wrl.coord[int(index[2])])))) - - return format_stl(faces) diff --git a/rocolib/utils/roco2sim/material_data.py b/rocolib/utils/roco2sim/material_data.py deleted file mode 100644 index c80db6db9f599a8f9d587d4dc9a91aa893eb2a7a..0000000000000000000000000000000000000000 --- a/rocolib/utils/roco2sim/material_data.py +++ /dev/null @@ -1,27 +0,0 @@ -from dataclasses import dataclass, field -import numpy as np - -@dataclass -class Material: - name: str - density: float = 1 # kg/m3 - rgb: np.ndarray = np.array([0, 0, 0]) - roughness: float = 1.0 - reflection: float = 0. - optional: dict = field(default_factory=dict) - description: str = """""" - -@dataclass(frozen=True) -class MaterialConst: - MAT_DEFAULT: str = 'defaultMaterial' - - - -mc = MaterialConst() -materialData = {} - -materialData[mc.MAT_DEFAULT] \ - = Material('generic_material', description="""This is a default material.""") - -materialData['paperThick'] \ - = Material('Thick Paper', 833.3, description="""Business card paper""") diff --git a/rocolib/utils/roco2sim/wbo_nodes.py b/rocolib/utils/roco2sim/wbo_nodes.py deleted file mode 100644 index ebc6a02517add235cc39f85601a4ab19847c232f..0000000000000000000000000000000000000000 --- a/rocolib/utils/roco2sim/wbo_nodes.py +++ /dev/null @@ -1,132 +0,0 @@ -from typing import List -from rocolib.utils.roco2sim.Node import * - - -scale = 0.001 - -class WrlFormatter: - def __init__(self): - self.COORD_FACET = """{face[0]:g} {face[1]:g} {face[2]:g}, """ - self.COORD_FACET_INDEX = """{face_pt_num[0]}, {face_pt_num[1]}, {face_pt_num[2]}, -1, """ - self.FACET_NORMAL = """0 0 0, """ - - def coordPointsFormatter(self, p): - self.coordPoints = [] - for x in p: - self.coordPoints.append(self.COORD_FACET.format(face=x.astype(float)+0)) - return ''.join(self.coordPoints).strip(', ') - - def normalFormatter(self,n): - self.normal = [] - for x in n: - self.normal.append(self.FACET_NORMAL.format(face=x)) - return ''.join(self.normal).strip(', ') - - def coordIndexFormatter(self,c): - self.coordIndex = [] - for x in c: - self.coordIndex.append(self.COORD_FACET_INDEX.format(face_pt_num = [int(y) for y in x])) - return ''.join(self.coordIndex).strip(', ') - - -def wboShape(sNode: ShapeNode): - appearance = wboAppearance(sNode.appearance) - name = wboName(sNode.name) - geometry = wboGeometryIndexedFaceSet(sNode) - node = """DEF %(name)s Shape {%(appearance)s %(geometry)s}""" % locals() - return node - - -def wboGeometryIndexedFaceSet(sNode: ShapeNode): - wrl = sNode.wrl - if not wrl.coord: - return "" - - wf = WrlFormatter() - normal = wf.coordPointsFormatter(wrl.normal) - points = wf.coordPointsFormatter(wrl.coord) - coordIndex = wf.coordIndexFormatter(wrl.index) - - node = """geometry IndexedFaceSet {coord Coordinate {point [%(points)s]} normal Normal {vector [%(normal)s]} coordIndex [%(coordIndex)s]}""" % locals() - return node - - -def wboAppearance(aNode: AppearanceNode): - baseColor = str(aNode.getRGB()).strip('[ ]') - roughness = str(aNode.getRoughness()) - metalness = str(aNode.getReflection()) - name = wboName(aNode.getName()) - node = """appearance PBRAppearance { baseColor %(baseColor)s roughness %(roughness)s metalness %(metalness)s name "%(name)s"}""" % locals() - return node - - -def wboPhysics(pNode: PhysicsNode): - if pNode is None: - return """ """ - density = pNode.material.density - mass = pNode.mass - node = """physics Physics {density %(density)s mass %(mass)s}""" % locals() - return node - - -def wboXyz(sNode: ShapeNode): - return str(scale * np.array(sNode.getXyz()).astype(int)[0:3, 3]).strip('[ ]') - - -def wboSolid(sNode: ShapeNode): - if not sNode.isSolid(): - return wboShape(sNode) - shape = wboShape(sNode) - contactMaterial = wboContact(sNode) - physics = wboPhysics(sNode.getPhysicsNode()) - name = wboName(sNode.name) - node = """DEF %(name)s Solid {children [%(shape)s] %(contactMaterial)s boundingObject USE %(name)s %(physics)s}""" % locals() - return node - -def wboContact(sNode: ShapeNode): - contactMaterial = sNode.getPhysicsNode().contact - if contactMaterial is None: - return """contactMaterial "default" """ - return """contactMaterial "%(contactMaterial)s" """ - - -def wboRobot(mNode: ShapeNode): - # create local var to fill the nodes - name = wboName(mNode.name) - mNode.gasify() - solid = wboSolid(mNode) - mNode.children.append(solid) - childrenNode = " ".join(mNode.children) - physics = wboPhysics(mNode.getPhysicsNode()) - contactMaterial = wboContact(mNode) - rotation = str(mNode.getRotationVector()).strip('[ ]') - # battery = mNode.getDeviceOptions('battery') - # cpuConsumption = mNode.getDeviceOptions('cpuConsumption') - node = """DEF %(name)s Robot { - rotation %(rotation)s - children [%(childrenNode)s] - name "%(name)s" - %(contactMaterial)s - boundingObject USE %(name)s - %(physics)s - controller "%(name)s" - supervisor TRUE -}""" % locals() - return node - - -def wboHeader(version="R2021b"): - return """#VRML_OBJ R2021b utf8 -""" - - -def wboConverter(sNodes: List[ShapeNode]): - robot = sNodes[0] - for sNode in sNodes[1:len(sNodes)]: - robot.children.append(wboSolid(sNode)) - node = wboHeader() + wboRobot(robot) - return node - - -def wboName(name): - return name.replace('.', '_')