Source code for pyvcad_rendering.vtk_export

import os
import pyvcad as pv
from vtkmodules.vtkIOXML import vtkXMLPolyDataWriter, vtkXMLImageDataWriter

from .vtk_utils import (
    compute_voxel_size_for_quality_profile,
    create_sdf_from_object,
    colors_for_points
)

def _get_attribute_type_from_sample(vcad_object, attr_name):
    min_box, max_box = vcad_object.bounding_box()
    if min_box is None or max_box is None:
        return None
    
    center_x = (min_box.x + max_box.x) / 2.0
    center_y = (min_box.y + max_box.y) / 2.0
    center_z = (min_box.z + max_box.z) / 2.0
    
    candidates = [
        (center_x, center_y, center_z),
        (min_box.x, min_box.y, min_box.z),
        (max_box.x, max_box.y, max_box.z),
        (min_box.x, max_box.y, min_box.z),
        (max_box.x, min_box.y, center_z)
    ]
    
    for px, py, pz in candidates:
        result = vcad_object.sample(px, py, pz)
        if len(result) == 2 and result[1] is not None:
            attrs = result[1]
            if attrs.has_sample(attr_name):
                return attrs.get_type(attr_name)
                
    return None

[docs] def export_iso_surface_vtk(vcad_object, file_path: str, quality="high", use_blending=True, progress_callback=None): """ Exports the colored iso-surface of a given OpenVCAD object to a .vtp file. It computes the colors for all defined attributes and adds them as arrays in the PointData. Args: vcad_object: The OpenVCAD node to export. file_path (str): The destination file path (.vtp is recommended). quality (str, optional): The sampling quality profile ("low", "medium", "high", "ultra"). Defaults to "high". use_blending (bool, optional): Whether to use alpha blending. Defaults to True. progress_callback (callable, optional): Callback for progress updates taking a float (0-100). """ materials = None if pv.DefaultAttributes.VOLUME_FRACTIONS in vcad_object.attribute_list(): vf_attr = vcad_object.get_attribute(pv.DefaultAttributes.VOLUME_FRACTIONS) materials = vf_attr.material_defs if materials is None: raise RuntimeError("Volume fractions attribute requires material definitions to export.") voxel_size = compute_voxel_size_for_quality_profile(vcad_object, quality) tree_sampler = pv.TreeSampler(vcad_object, voxel_size, materials) # Generate the SDF sdf_volume_data = create_sdf_from_object( vcad_object, voxel_size, tree_sampler, progress_callback=lambda p: progress_callback(p * 0.33) if progress_callback else None ) from vtkmodules.vtkFiltersCore import vtkContourFilter contour = vtkContourFilter() contour.SetInputData(sdf_volume_data) contour.SetValue(0, 0.0) # Pass along contouring progress if progress_callback is not None: def contour_progress(caller, event): p = caller.GetProgress() if p % 0.05 < 0.001: progress_callback(33.0 + p * 33.0) contour.AddObserver("ProgressEvent", contour_progress) contour.Update() poly_data = contour.GetOutput() points = poly_data.GetPoints() if points: attributes = ["none", "Signed Distance"] + vcad_object.attribute_list() color_map = pv.ColorMap.create_viridis() num_attrs = len(attributes) for i, attr in enumerate(attributes): # Compute partial progress def attr_callback(p): if progress_callback: base = 66.0 + (i / num_attrs) * 33.0 frac = p / 100.0 * (33.0 / num_attrs) progress_callback(base + frac) # Always extract the mapped color representation for visualization if attr != "Signed Distance": colors = colors_for_points( vcad_object, materials, points, voxel_size, tree_sampler, attr, color_map, use_blending, attr_callback ) colors.SetName(f"Color_{attr}") poly_data.GetPointData().AddArray(colors) # Export raw numerical values if applicable if attr != "none": import vtkmodules.util.numpy_support as vtk_np from vtkmodules.vtkCommonCore import VTK_FLOAT pv_points = [pv.Vec3(*points.GetPoint(idx)) for idx in range(points.GetNumberOfPoints())] if attr == "Signed Distance": raw_data = tree_sampler.sample_points_to_float_array(pv_points, attr, None) num_components = 1 attr_type = pv.Attribute.ReturnType.dbl else: attr_type = _get_attribute_type_from_sample(vcad_object, attr) if attr_type is None: continue raw_data = None num_components = 1 if attr_type == pv.Attribute.ReturnType.dbl: raw_data = tree_sampler.sample_points_to_float_array(pv_points, attr, None) num_components = 1 elif attr_type == pv.Attribute.ReturnType.vec3: raw_data = tree_sampler.sample_points_to_vec3_array(pv_points, attr, None) num_components = 3 elif attr_type == pv.Attribute.ReturnType.vec4: raw_data = tree_sampler.sample_points_to_vec4_array(pv_points, attr, None) num_components = 4 if raw_data is not None: import numpy as np if isinstance(raw_data, np.ndarray): np_data = raw_data.astype(np.float32) elif attr_type == pv.Attribute.ReturnType.vec3: np_data = np.array([[v.x, v.y, v.z] for v in raw_data], dtype=np.float32) elif attr_type == pv.Attribute.ReturnType.vec4: np_data = np.array([[v.r, v.g, v.b, v.a] for v in raw_data], dtype=np.float32) else: np_data = np.array(raw_data, dtype=np.float32) vtk_raw_array = vtk_np.numpy_to_vtk(num_array=np_data, deep=True, array_type=VTK_FLOAT) vtk_raw_array.SetNumberOfComponents(num_components) vtk_raw_array.SetName(attr) poly_data.GetPointData().AddArray(vtk_raw_array) writer = vtkXMLPolyDataWriter() writer.SetFileName(file_path) writer.SetInputData(poly_data) writer.Write() if progress_callback: progress_callback(100.0)
[docs] def export_volume_vtk(vcad_object, file_path: str, quality="high", use_blending=True, progress_callback=None): """ Exports the multi-material colored volume of a given OpenVCAD object to a .vti file. It computes the colors for all defined attributes and adds them as arrays in the PointData. Args: vcad_object: The OpenVCAD node to export. file_path (str): The destination file path (.vti is recommended). quality (str, optional): The sampling quality profile ("low", "medium", "high", "ultra"). Defaults to "high". use_blending (bool, optional): Whether to use alpha blending. Defaults to True. progress_callback (callable, optional): Callback for progress updates taking a float (0-100). """ materials = None if pv.DefaultAttributes.VOLUME_FRACTIONS in vcad_object.attribute_list(): vf_attr = vcad_object.get_attribute(pv.DefaultAttributes.VOLUME_FRACTIONS) materials = vf_attr.material_defs if materials is None: raise RuntimeError("Volume fractions attribute requires material definitions to export.") voxel_size = compute_voxel_size_for_quality_profile(vcad_object, quality) tree_sampler = pv.TreeSampler(vcad_object, voxel_size, materials) from vtkmodules.vtkCommonDataModel import vtkImageData import vtkmodules.util.numpy_support as vtk_np from vtkmodules.vtkCommonCore import VTK_UNSIGNED_CHAR import numpy as np attributes = ["none", "Signed Distance"] + vcad_object.attribute_list() color_map = pv.ColorMap.create_viridis() num_attrs = len(attributes) volume_data = vtkImageData() nx, ny, nz = tree_sampler.sample_dimensions() volume_data.SetDimensions(nx, ny, nz) volume_data.SetSpacing(voxel_size.x, voxel_size.y, voxel_size.z) min_bbox, max_bbox = vcad_object.bounding_box() # Expand by two voxels to match the TreeSampler's internal bounding box expansion volume_data.SetOrigin( min_bbox.x - (voxel_size.x * 2.0), min_bbox.y - (voxel_size.y * 2.0), min_bbox.z - (voxel_size.z * 2.0), ) refs = [] # Keep alive numpy arrays from vtkmodules.vtkCommonCore import VTK_FLOAT for i, attr in enumerate(attributes): def attr_callback(p): if progress_callback: base = (i / num_attrs) * 100.0 frac = (p / 100.0) * (100.0 / num_attrs) progress_callback(base + frac) # Always extract the mapped color representation for visualization if attr != "Signed Distance": color_data = tree_sampler.as_rgba_array(attr, color_map, use_blending, attr_callback) vtk_color_array = vtk_np.numpy_to_vtk( num_array=color_data, deep=True, array_type=VTK_UNSIGNED_CHAR ) vtk_color_array.SetNumberOfComponents(4) vtk_color_array.SetName(f"Color_{attr}") volume_data.GetPointData().AddArray(vtk_color_array) refs.append(color_data) # Export raw numerical values if applicable if attr != "none": if attr == "Signed Distance": raw_data = tree_sampler.as_signed_distance_array(None) attr_type = pv.Attribute.ReturnType.dbl else: attr_type = _get_attribute_type_from_sample(vcad_object, attr) if attr_type is None: continue raw_data = None if attr_type == pv.Attribute.ReturnType.dbl: raw_data = tree_sampler.as_float_array(attr, None) elif attr_type == pv.Attribute.ReturnType.vec3: raw_data = tree_sampler.as_vec3_array(attr, None) elif attr_type == pv.Attribute.ReturnType.vec4: raw_data = tree_sampler.as_vec4_array(attr, None) if raw_data is not None: import numpy as np if isinstance(raw_data, np.ndarray): np_data = raw_data.astype(np.float32) elif attr_type == pv.Attribute.ReturnType.vec3: np_data = np.array([[v.x, v.y, v.z] for v in raw_data], dtype=np.float32) elif attr_type == pv.Attribute.ReturnType.vec4: np_data = np.array([[v.r, v.g, v.b, v.a] for v in raw_data], dtype=np.float32) else: np_data = np.array(raw_data, dtype=np.float32) vtk_raw_array = vtk_np.numpy_to_vtk(num_array=np_data, deep=False, array_type=VTK_FLOAT) # Number of components is automatically determined by the shape of the numpy array vtk_raw_array.SetName(attr) volume_data.GetPointData().AddArray(vtk_raw_array) refs.append(np_data) # Prevent garbage collection while vtk operates on them setattr(volume_data, "_np_refs", refs) writer = vtkXMLImageDataWriter() writer.SetFileName(file_path) writer.SetInputData(volume_data) writer.Write() if progress_callback: progress_callback(100.0)