Source code for layerview.visualization.nodes.model

"""Model (print) node generation."""
import math
from typing import Dict, Optional, Tuple

from panda3d.core import (
    Geom,
    GeomNode,
    GeomTristrips,
    GeomVertexData,
    GeomVertexFormat,
    GeomVertexWriter,
    LVecBase2d,
    LVecBase3d,
    LVector2d,
    NodePath,
)

from layerview.visualization.linalg import angle_signed, vec_rotated
from layerview.visualization.nodes.builder import NodeBuilder
from layerview.visualization.point_cloud.layer import Layer as Layer
from layerview.visualization.point_cloud.model import Model as Model
from layerview.visualization.point_cloud.model import ModelInfo
from layerview.visualization.point_cloud.path import Path


[docs]class ModelManager: """Model manager.""" _model_node: NodePath index_to_layer_node: Dict[int, NodePath] priming_layer_node: Optional[NodePath] _model_info: Optional[ModelInfo] def __init__(self): self._model_node: Optional[NodePath] = None self.index_to_layer_node: Dict[int, NodePath] = {} self.priming_layer_node: Optional[NodePath] = None self._model_info: Optional[ModelInfo] = None def __del__(self): # noinspection PyArgumentList self.model_node.remove_node() @property def model_node(self) -> Optional[NodePath]: """Returns NodePath for the managed Model.""" return self._model_node @property def model_info(self) -> Optional[ModelInfo]: """Returns ModelInfo for the managed Model.""" return self._model_info
[docs] def set_layer_color(self, index: int, color: Tuple[float, float, float, float]): """Set color of layer at specified index. Parameters ---------- index : int Target layer index. color : Tuple[float, float, float, float] RGBA color to set. """ self.index_to_layer_node[index].setColor(color)
[docs] def show_layer(self, index: int): """Show layer at specified index. Parameters ---------- index : int Target layer index. """ # noinspection PyArgumentList self.index_to_layer_node[index].show()
[docs] def hide_layer(self, index: int): """Hide layer at specified index. Parameters ---------- index : int Target layer index. """ # noinspection PyArgumentList self.index_to_layer_node[index].hide()
[docs] def show_all_layers(self): """Show all layers.""" for layer_node in self.index_to_layer_node.values(): # noinspection PyArgumentList layer_node.show()
[docs] def hide_all_layers(self): """Hide all layers.""" for layer_node in self.index_to_layer_node.values(): # noinspection PyArgumentList layer_node.hide()
[docs] def show_layer_range_only(self, start: int, end: int): """Show specified layer range, exclusively. Range [start; end] is inclusive. All layers in specified range are shown, others are hidden. Parameters ---------- start : int Range start layer number, inclusive. end : int Range end layer number, inclusive. """ to_show = [i for i in self.index_to_layer_node.keys() if start <= i <= end] to_hide = [i for i in self.index_to_layer_node.keys() if i not in to_show] for i in to_hide: self.hide_layer(i) for i in to_show: self.show_layer(i)
[docs]class ModelManagerBuilder: """Creates root G-code model node."""
[docs] @staticmethod def build_manager(model: Model, name: Optional[str] = "") -> ModelManager: """Get new ModelManager. Parameters ---------- model : Model name : Optional[str] Model NodePath name. Returns ------- model_node_manager : ModelManager """ model_node_manager = ModelManager() model_node: NodePath = NodePath(top_node_name=name) model_node_manager._model_node = model_node model_node_manager._model_info = model.info for index, layer in model.index_to_layer.items(): layer_node: NodePath = LayerNodeBuilder.build_node( layer=layer, nozzle_diam=model.nozzle_diam, height=model.get_layer_height(index), name=f"{name}L{index}", ) layer_node.reparentTo(model_node) layer_node.setPos(0, 0, model.get_layer_z(index)) model_node_manager.index_to_layer_node[index] = layer_node if model.priming_layer: priming_layer_node: NodePath = LayerNodeBuilder.build_node( layer=model.priming_layer, nozzle_diam=model.nozzle_diam, height=model.priming_layer_z, name=f"{name}_priming", ) priming_layer_node.reparentTo(model_node) priming_layer_node.setPos(0, 0, model.priming_layer_z) model_node_manager.priming_layer_node = priming_layer_node return model_node_manager
[docs]class LayerNodeBuilder(NodeBuilder): """Layer node builder""" # noinspection PyArgumentList _GEOM_VERTEX_FORMAT = GeomVertexFormat.get_v3n3c4() # coords + normals + RGBA _COLOR_DEFAULT = (0, 60 / 255, 200 / 255, 1)
[docs] @classmethod def build_node( cls, layer: Layer, height: float, nozzle_diam: float, name: str, ) -> NodePath: """Build layer node. Parameters ---------- layer : Layer Layer for which the data should be generated and written. nozzle_diam : float Nozzle diameter. height : float Layer height, thickness. name : str Name for generated NodePath. Returns ------- NodePath Generated NodePath. """ geom_data = cls._get_geom_vertex_data( layer=layer, nozzle_diam=nozzle_diam, height=height, name=name ) primitive = cls._get_primitive(layer=layer) # Prepare Geom geom = Geom(geom_data) geom.addPrimitive(primitive) # Prepare GeomNode geom_node = GeomNode(name) geom_node.addGeom(geom) # Create NodePath from GeomNode node_path = NodePath(geom_node) return node_path
[docs] @classmethod def _get_geom_vertex_data( cls, layer: Layer, nozzle_diam: float, height: float, name: str ) -> GeomVertexData: """Generate GeomVertexData for the provided Layer. Parameters ---------- layer : Layer Layer for which the data should be generated and written. nozzle_diam : float Nozzle diameter. height : float Layer height, thickness. name : str Name for generated GeomVertexData Returns ------- GeomVertexData Generated GeomVertexData. """ geom_vertex_count = sum([len(path) * 4 for path in layer.paths]) geom_data = GeomVertexData(name, cls._GEOM_VERTEX_FORMAT, Geom.UHStatic) geom_data.setNumRows(geom_vertex_count) cls._write_geom_data( geom_data=geom_data, layer=layer, width=nozzle_diam, height=height ) return geom_data
[docs] @classmethod def _write_geom_data( cls, geom_data: GeomVertexData, layer: Layer, width: float, height: float, ): """Write layer geom data into the provided GeomVertexData. Parameters ---------- geom_data : GeomVertexData GeomVertexData to write data into. layer : Layer Layer for which the data should be generated and written. width : float Segment width (nozzle diameter). height : float Segment height (layer height, thickness). """ # Write colors cls._write_geom_data_colors( geom_data=geom_data, layer_vertex_count=cls._get_layer_vertex_count(layer), ) # Write vertex coords and normals cls._write_geom_data_coords_and_normals( geom_data=geom_data, layer=layer, width=width, height=height )
[docs] @classmethod def _write_geom_data_coords_and_normals( cls, geom_data: GeomVertexData, layer: Layer, width: float, height: float, ): """Write vertex coords and normals into the provided GeomVertexData. Parameters ---------- geom_data : GeomVertexData GeomVertexData to write coords and normals into. layer : Layer Layer for which the coords and normals should be generated and written. width : float Segment width (nozzle diameter). height : float Segment height (layer height, thickness) """ writer_vertex = GeomVertexWriter(geom_data, "vertex") writer_normal = GeomVertexWriter(geom_data, "normal") for toolpath in layer.paths: for index, path_point in enumerate(toolpath): wall_tilt_angle = cls._get_wall_tilt_angle(toolpath, index) width_scale_factor = cls._get_wall_width_scale_factor(toolpath, index) # VERTEX COORDS # Calculate 2D points, rotated x_shift = LVector2d(width / 2, 0) * width_scale_factor v0_2d = path_point v1_2d = vec_rotated( vector=v0_2d - x_shift, pivot=v0_2d, angle=wall_tilt_angle ) v3_2d = vec_rotated( vector=v0_2d + x_shift, pivot=v0_2d, angle=wall_tilt_angle ) # Create 3D vertices. Take Z into account. v0_3d = LVecBase3d(*v0_2d, 0) # v0 3D v1_3d = LVecBase3d(*v1_2d, -height / 2) # v1 3D v2_3d = LVecBase3d(*v0_2d, -height) # v2 3D v3_3d = LVecBase3d(*v3_2d, -height / 2) # v3_3D for vertex_coords in [v0_3d, v1_3d, v2_3d, v3_3d]: writer_vertex.addData3d(vertex_coords) # VERTEX NORMALS if 0 < index < len(toolpath) - 1: # Intermediate wall n0 = LVecBase3d(0, 0, 1) n1 = LVecBase3d( *( (toolpath[index] - toolpath[index - 1]).normalized() + (toolpath[index] - toolpath[index + 1]).normalized() ), 0, ).normalized() n2 = -n0 n3 = -n1 else: # Start or end wall if index == 0: # Start wall # v[0] - v[1] ref_vec: LVecBase2d = LVecBase2d(*(toolpath[1] - toolpath[0])) normal_rot_angle = math.pi / -4 else: # index == toolpath_point_count-1: # End wall # v[-1] - v[-2] ref_vec: LVecBase2d = LVecBase2d(*(toolpath[-1] - toolpath[-2])) normal_rot_angle = math.pi / 4 ref_vec.normalize() # Top and bottom n0 = LVecBase3d(*ref_vec, 1).normalized() n2 = LVecBase3d(*ref_vec, -1).normalized() # Sides n1 = LVecBase3d( *vec_rotated( vector=ref_vec, pivot=v1_2d, angle=normal_rot_angle ), 0, ) n3 = LVecBase3d( *vec_rotated( vector=ref_vec, pivot=v1_2d, angle=normal_rot_angle ), 0, ) for normal in [n0, n1, n2, n3]: writer_normal.addData3d(normal)
[docs] @classmethod def _write_geom_data_colors( cls, geom_data: GeomVertexData, layer_vertex_count: int, color: Tuple[float, float, float, float] = _COLOR_DEFAULT, ): """Write color data into the provided GeomVertexData. Parameters ---------- geom_data : GeomVertexData GeomVertexData to write color data into. layer_vertex_count : int Total vertex count in layer. color : Tuple[float, float, float, float] Target vertex color. """ writer_color = GeomVertexWriter(geom_data, "color") for _ in range(layer_vertex_count): writer_color.addData4(color)
[docs] @classmethod def _get_primitive(cls, layer: Layer) -> GeomTristrips: """Generate GeomTristrips primitive for provided layer.""" primitive = GeomTristrips(Geom.UHStatic) cur_point_num_in_layer = 0 # Iterate over paths in layer for path in layer.paths: segment_count = len(path) - 1 path_start_point_index = ( cur_point_num_in_layer * 4 ) # Relative to first point in first path in layer # Start cap primitive.addVertices(*[path_start_point_index + i for i in [0, 1, 3, 2]]) primitive.closePrimitive() # Segment walls for segment_index in range(segment_count): v_start_index = path_start_point_index + segment_index * 4 # Zigzag around segment walls: 0, 4, 1, 5, 2, 6, 3, 7, 0, 4 for i in range(v_start_index, v_start_index + 4): primitive.addVertex(i) primitive.addVertex(i + 4) primitive.addVertex(v_start_index) primitive.addVertex(v_start_index + 4) primitive.closePrimitive() # End cap end_wall_start_index = (cur_point_num_in_layer + segment_count) * 4 primitive.addVertices(*[end_wall_start_index + i for i in [0, 3, 1, 2]]) primitive.closePrimitive() cur_point_num_in_layer += len(path) return primitive
[docs] @classmethod def _get_wall_width_scale_factor(cls, toolpath: Path, i: int) -> float: """Return wall width scale factor. Parameters ---------- toolpath : Path Toolpath. i : int Index of wall in toolpath. Returns ------- float Wall width scale factor for i-th wall in toolpath. """ if 0 < i < len(toolpath) - 1: # Intermediate point angle_delta = angle_signed( toolpath[i] - toolpath[i - 1], toolpath[i + 1] - toolpath[i], ) return 1.0 + abs(math.sin(angle_delta)) * (math.sqrt(2) - 1) return 1.0
[docs] @classmethod def _get_wall_tilt_angle(cls, toolpath: Path, i: int) -> float: """Returns wall tilt angle for specified point_cloud point. Positive angles are counterclockwise, negative are clockwise. Parameters ---------- toolpath : Path 2D point_cloud i : int Wall index (path vertex index). Returns ------- rotation_angle : float Wall rotation angle for segment wall corresponding to vertex v. """ # Aliases v_count = len(toolpath) v = toolpath if 0 < i < v_count - 1: # Intermediate point, most probable case target_vec = (v[i] - v[i - 1]).normalized() + (v[i + 1] - v[i]).normalized() elif i == 0: # Start point target_vec = v[1] - v[0] elif i == v_count - 1: # End point target_vec = v[i] - v[i - 1] else: # Out of range raise IndexError("Index is out of vertices range.") base_vec = LVecBase2d(0, 1) tilt_angle = angle_signed(base_vec, target_vec) return tilt_angle
[docs] @classmethod def _get_layer_point_count(cls, layer: Layer) -> int: """Return total point count in provided layer.""" return sum([len(path) for path in layer.paths])
[docs] @classmethod def _get_layer_vertex_count(cls, layer: Layer) -> int: """Return expected total vertex count in provided layer.""" return cls._get_layer_point_count(layer) * 4