import subprocess import re import json import os from .message import bright, info, error from .processor import Processor from .config import Config from .robot import Robot from .geometry import Box, Cylinder, Sphere, Shape import numpy as np class ProcessorScad(Processor): """ Scad processor. This processor will parse OpenSCAD files to create pure shapes when available. The code is a naive parser of the intermediate CSG file produced by OpenSCAD, gathering Box, Sphere and Cylinders. """ is_safe: bool = False def __init__(self, config: Config): super().__init__(config) # OpenSCAD pure shapes self.use_scads: bool = config.get("use_scads", False) self.pure_shape_dilatation: float = config.get("pure_shape_dilatation", 0.0) if self.use_scads: self.check_openscad() def check_openscad(self): if self.use_scads: print(bright("* Checking OpenSCAD presence...")) try: subprocess.run(["openscad", "-v"]) except FileNotFoundError: print(bright("Can't run openscad -v, disabling OpenSCAD support")) print(info("TIP: consider installing openscad:")) print(info("Linux:")) print(info("sudo add-apt-repository ppa:openscad/releases")) print(info("sudo apt-get update")) print(info("sudo apt-get install openscad")) print(info("Windows:")) print(info("go to: https://openscad.org/downloads.html ")) self.use_scads = False def process(self, robot: Robot): if self.use_scads: getcwd = os.getcwd() os.chdir(self.config.output_directory) print(info("+ Parsing OpenSCAD files...")) for link in robot.links: for part in link.parts: converted_meshes = [] for mesh in part.meshes: if mesh.collision: scad_file = mesh.filename.replace(".stl", ".scad") if os.path.exists(scad_file): part.shapes += self.parse_scad(scad_file, mesh.color) converted_meshes.append(mesh) for converted_mesh in converted_meshes: converted_mesh.collision = False part.prune_unused_geometry() os.chdir(getcwd) def multmatrix_parse(self, parameters: str): matrix = np.array(json.loads(parameters), dtype=float) matrix[0, 3] /= 1000.0 matrix[1, 3] /= 1000.0 matrix[2, 3] /= 1000.0 return matrix def cube_parse(self, parameters: str): results = re.findall(r"^size = (.+), center = (.+)$", parameters) if len(results) != 1: raise Exception(f"! Can't parse CSG cube parameters: {parameters}") extra = np.array([self.pure_shape_dilatation] * 3) return ( extra + np.array(json.loads(results[0][0]), dtype=float) / 1000.0 ), results[0][1] == "true" def cylinder_parse(self, parameters: str): results = re.findall( r"h = (.+), r1 = (.+), r2 = (.+), center = (.+)", parameters ) if len(results) != 1: raise Exception(f"! Can't parse CSG cylinder parameters: {parameters}") result = results[0] extra = np.array([self.pure_shape_dilatation / 2, self.pure_shape_dilatation]) return (extra + np.array([result[0], result[1]], dtype=float) / 1000.0), result[ 3 ] == "true" def sphere_parse(self, parameters: str): results = re.findall(r"r = (.+)$", parameters) if len(results) != 1: raise Exception(f"! Can't parse CSG sphere parameters: {parameters}") return self.pure_shape_dilatation + float(results[0]) / 1000.0 def extract_node_parameters(self, line: str): line = line.strip() parts = line.split("(", 1) node = parts[0] parameters = parts[1] if parameters[-1] == ";": parameters = parameters[:-2] if parameters[-1] == "{": parameters = parameters[:-3] return node, parameters def translation(self, x: float, y: float, z: float): m = np.eye(4) m[:3, 3] = [x, y, z] return m def parse_csg(self, csg_data: str, color): shapes: list[Shape] = [] lines = csg_data.split("\n") matrices = [] for line in lines: line = line.strip() if line != "": if line[-1] == "{": node, parameters = self.extract_node_parameters(line) if node == "multmatrix": matrix = self.multmatrix_parse(parameters) else: matrix = np.eye(4) matrices.append(matrix) elif line[-1] == "}": matrices.pop() else: node, parameters = self.extract_node_parameters(line) transform = np.eye(4) for matrix in matrices: transform = transform @ matrix if node == "cube": size, center = self.cube_parse(parameters) if not center: transform = transform @ self.translation( size[0] / 2.0, size[1] / 2.0, size[2] / 2.0 ) shapes.append( Box(transform, size, color, visual=False, collision=True) ) if node == "cylinder": size, center = self.cylinder_parse(parameters) if not center: transform = transform @ self.translation( 0, 0, size[0] / 2.0 ) shapes.append( Cylinder( transform, size[0], size[1], color, visual=False, collision=True, ) ) if node == "sphere": shapes.append( Sphere( transform, self.sphere_parse(parameters), color, visual=False, collision=True, ) ) return shapes def parse_scad(self, scad_file: str, color: np.ndarray): tmp_data = os.getcwd() + "/_tmp_data.csg" os.system("openscad " + scad_file + " -o " + tmp_data) with open(tmp_data, "r", encoding="utf-8") as stream: data = stream.read() os.system("rm " + tmp_data) return self.parse_csg(data, color)