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)