import vtk import numpy as np import tempfile def arc_length(cl): s = np.zeros(len(cl)) for i in range(1, len(cl)): s[i] = s[i-1] + np.linalg.norm(cl[i] - cl[i-1]) return s def compute_parallel_frames(centerline): frames = [] t0 = centerline[1] - centerline[0] t0 /= np.linalg.norm(t0) ref = np.array([0, 0, 1]) if abs(np.dot(t0, ref)) > 0.9: ref = np.array([1, 0, 0]) n0 = np.cross(t0, ref) n0 /= np.linalg.norm(n0) b0 = np.cross(t0, n0) frames.append((t0, n0, b0)) for i in range(1, len(centerline)): ti = centerline[i] - centerline[i-1] ti /= np.linalg.norm(ti) t_prev, n_prev, b_prev = frames[-1] v = np.cross(t_prev, ti) if np.linalg.norm(v) < 1e-6: frames.append((ti, n_prev, b_prev)) continue v /= np.linalg.norm(v) angle = np.arccos(np.clip(np.dot(t_prev, ti), -1, 1)) def rot(vec): return (vec * np.cos(angle) + np.cross(v, vec) * np.sin(angle) + v * np.dot(v, vec) * (1 - np.cos(angle))) frames.append((ti, rot(n_prev), rot(b_prev))) return frames def project_point_to_surface(p, surface_poly): locator = vtk.vtkCellLocator() locator.SetDataSet(surface_poly) locator.BuildLocator() closest = [0.0, 0.0, 0.0] cellId = vtk.mutable(0) subId = vtk.mutable(0) dist2 = vtk.mutable(0.0) locator.FindClosestPoint(p, closest, cellId, subId, dist2) return np.array(closest) def sample_centerline_by_s(centerline, s_cl, frames, s): idx = np.searchsorted(s_cl, s) if idx <= 0: return centerline[0], frames[0] if idx >= len(centerline): return centerline[-1], frames[-1] s0, s1 = s_cl[idx-1], s_cl[idx] P0, P1 = centerline[idx-1], centerline[idx] t = (s - s0) / (s1 - s0 + 1e-12) P = (1 - t) * P0 + t * P1 t0, N0, B0 = frames[idx-1] t1, N1, B1 = frames[idx] t_vec = (1 - t) * t0 + t * t1 N_vec = (1 - t) * N0 + t * N1 B_vec = (1 - t) * B0 + t * B1 t_vec /= np.linalg.norm(t_vec) N_vec /= np.linalg.norm(N_vec) B_vec /= np.linalg.norm(B_vec) return P, (t_vec, N_vec, B_vec) # Fabric def create_component_fabric(centerline, frames, start_s, length, diameter, start_amp_pp=2.0, waves=8): # shift centerline sampling shifted_centerline = centerline.copy() s_cl = arc_length(centerline) pts = vtk.vtkPoints() polys = vtk.vtkCellArray() rings = [] A = start_amp_pp / 2.0 n_sections = 180 n_theta = 64 for i in range(n_sections): s_base = start_s + length * i / (n_sections - 1) P_base, (t, N, B) = sample_centerline_by_s(centerline, s_cl, frames, s_base) ring = [] for j in range(n_theta): theta = 2 * np.pi * j / n_theta axial_offset = 0.0 if i == 0 and start_amp_pp > 0: axial_offset = A * np.sin(waves * theta) - A P = P_base + axial_offset * t r = diameter / 2.0 pos = P + r * (np.cos(theta) * N + np.sin(theta) * B) ring.append(pts.InsertNextPoint(pos)) rings.append(ring) for i in range(len(rings)-1): for j in range(n_theta): q = vtk.vtkQuad() q.GetPointIds().SetId(0, rings[i][j]) q.GetPointIds().SetId(1, rings[i][(j+1)%n_theta]) q.GetPointIds().SetId(2, rings[i+1][(j+1)%n_theta]) q.GetPointIds().SetId(3, rings[i+1][j]) polys.InsertNextCell(q) poly = vtk.vtkPolyData() poly.SetPoints(pts) poly.SetPolys(polys) return poly # Struts def create_proximal_bare_stent(centerline, frames, fabric_start_s, main_diam_mm, length, main_fabric, peak_to_peak=15.0, peak_spacing=7.0, component_type="proximal", wire_radius=0.35, waves=8, samples=240 ): s_cl = arc_length(centerline) append = vtk.vtkAppendPolyData() # --- build fabric normals & locator ONCE --- normal_gen = vtk.vtkPolyDataNormals() normal_gen.SetInputData(main_fabric) normal_gen.ComputePointNormalsOn() normal_gen.SplittingOff() normal_gen.ConsistencyOn() normal_gen.Update() fabric_normals = normal_gen.GetOutput().GetPointData().GetNormals() locator = vtk.vtkPointLocator() locator.SetDataSet(main_fabric) locator.BuildLocator() # ----------------------------------------- A = peak_to_peak # peak-peak amplitude = 15 r = main_diam_mm / 2.0 gap = peak_spacing step = A + gap n_crowns = int(np.ceil(length / step)) s_peaks = [fabric_start_s + 1.0 + i * step for i in range(n_crowns)] if ((component_type == "distal" or component_type == "extension") and (len(s_peaks) > 4)): s_peaks = s_peaks[3:] # ========================================================= # ===== EXTRA CLIPPED HALF sin ================= # ========================================================= A_extra = peak_to_peak / 2.0 # 7.5 mm height extra_top = -1e-10 # peak at -7.5 if component_type == "proximal": P0 = centerline[0] t0, N0, B0 = frames[0] P = P0 + extra_top * t0 # extrapolate backward t, N, B = t0, N0, B0 elif component_type == "distal": P, (t, N, B) = sample_centerline_by_s(centerline, s_cl, frames, fabric_start_s + length - extra_top) else: P, (t, N, B) = sample_centerline_by_s(centerline, s_cl, frames, extra_top) pts = vtk.vtkPoints() lines = vtk.vtkCellArray() for i in range(samples + 1): theta = 2*np.pi*i/samples # keep ONLY negative or positive half of sine if component_type == "proximal": axial_offset = A_extra * min(0.0, np.sin(waves * theta)) raw_pos = (P + (r + wire_radius) * (np.cos(theta)*N + np.sin(theta)*B) + axial_offset * t) elif (component_type == "distal" and length != 0): axial_offset = A_extra * max(0.0, np.sin(waves * theta)) raw_pos = (P + (r + wire_radius) * (np.cos(theta)*N + np.sin(theta)*B) + axial_offset * t) else: continue radial = (np.cos(theta)*N + np.sin(theta)*B) radial /= np.linalg.norm(radial) pos = P + (r + wire_radius) * radial + axial_offset * t pts.InsertNextPoint(pos) if i > 0: l = vtk.vtkLine() l.GetPointIds().SetId(0, i-1) l.GetPointIds().SetId(1, i) lines.InsertNextCell(l) poly = vtk.vtkPolyData() poly.SetPoints(pts) poly.SetLines(lines) tube = vtk.vtkTubeFilter() tube.SetInputData(poly) tube.SetRadius(wire_radius) tube.SetNumberOfSides(18) tube.CappingOff() tube.Update() append.AddInputData(tube.GetOutput()) # ========================================================= # ================= NORMAL FULL struts ==================== # ========================================================= for s0 in s_peaks: P, (t, N, B) = sample_centerline_by_s(centerline, s_cl, frames, s0) pts = vtk.vtkPoints() lines = vtk.vtkCellArray() for i in range(samples + 1): theta = 2*np.pi*i/samples axial_offset = A * (1 + np.sin(waves * theta)) / 2.0 raw_pos = (P + (r + wire_radius) * (np.cos(theta)*N + np.sin(theta)*B) + axial_offset * t) surf_p = project_point_to_surface(raw_pos, main_fabric) pid = locator.FindClosestPoint(surf_p) n = np.array(fabric_normals.GetTuple(pid)) n /= np.linalg.norm(n) pos = surf_p + wire_radius * n pts.InsertNextPoint(pos) if i > 0: l = vtk.vtkLine() l.GetPointIds().SetId(0, i-1) l.GetPointIds().SetId(1, i) lines.InsertNextCell(l) poly = vtk.vtkPolyData() poly.SetPoints(pts) poly.SetLines(lines) tube = vtk.vtkTubeFilter() tube.SetInputData(poly) tube.SetRadius(wire_radius) tube.SetNumberOfSides(18) tube.CappingOff() tube.Update() append.AddInputData(tube.GetOutput()) append.Update() return append.GetOutput() def create_component_stents(centerline, frames, fabric_start_s, length, diameter, fabric, peak_to_peak=15.0, peak_spacing=7.0, component_type="proximal"): stents = create_proximal_bare_stent(centerline, frames, fabric_start_s, diameter, length, fabric, peak_to_peak, peak_spacing, component_type) return stents def merge_components(proximal, extension, distal): merged_components = vtk.vtkAppendPolyData() merged_components.AddInputData(proximal) merged_components.AddInputData(extension) merged_components.AddInputData(distal) merged_components.Update() clean = vtk.vtkCleanPolyData() clean.SetInputData(merged_components.GetOutput()) clean.Update() return clean def create_endograft(centerline, diameter, prox_length,extension_length, distal_length, prox_gap, extension_gap, distal_gap): frames = compute_parallel_frames(centerline) peak_to_peak = 15.0 # ------------------------- # PROXIMAL COMPONENT # ------------------------- prox_start = 0.0 prox_fabric = create_component_fabric(centerline, frames, prox_start, prox_length, diameter, start_amp_pp=2.0) prox_stents = create_component_stents(centerline, frames, prox_start, prox_length, diameter, prox_fabric, peak_spacing=prox_gap, component_type="proximal") # ------------------------- # EXTENSION COMPONENT # ------------------------- if extension_length > 97: prox_extension_overlap = 3*peak_to_peak + 2*extension_gap else: prox_extension_overlap = 2*peak_to_peak + extension_gap extension_start = prox_length - prox_extension_overlap extension_fabric = create_component_fabric(centerline, frames, extension_start, extension_length, diameter, start_amp_pp=0.0) extension_stents = create_component_stents(centerline, frames, extension_start, extension_length, diameter, extension_fabric, peak_spacing=extension_gap, component_type="extension") # ------------------------- # DISTAL COMPONENT # ------------------------- distal_overlap = 3*peak_to_peak + 2*distal_gap distal_start = prox_length + extension_length - prox_extension_overlap - distal_overlap distal_fabric = create_component_fabric(centerline, frames, distal_start, distal_length, diameter, start_amp_pp=0.0) distal_stents = create_component_stents(centerline, frames, distal_start, distal_length, diameter, distal_fabric, peak_spacing=distal_gap, component_type="distal") # ------------------------- # MERGE COMPONENTS # ------------------------- fabric_part = merge_components(prox_fabric, extension_fabric, distal_fabric) stent_part = merge_components(prox_stents, extension_stents, distal_stents) endograft = vtk.vtkAppendPolyData() endograft.AddInputData(fabric_part.GetOutput()) endograft.AddInputData(stent_part.GetOutput()) endograft.Update() clean = vtk.vtkCleanPolyData() clean.SetInputData(endograft.GetOutput()) clean.Update() return fabric_part.GetOutput(), stent_part.GetOutput(), clean.GetOutput() def normalize_centerline_input(centerline): if len(centerline) > 0 and isinstance(centerline[0], dict): centerline = [[p["x"], p["y"], p["z"]] for p in centerline] arr = np.asarray(centerline, dtype=np.float64) if arr.ndim != 2 or arr.shape[1] != 3: raise ValueError("centerline must have shape [N, 3]") if arr.shape[0] < 2: raise ValueError("centerline must contain at least 2 points") return arr # Returns generated VTK polydata parts as standalone objects def build_endograft_parts(centerline, diameter=22.0, prox_length=105.0, extension_length=0.0, distal_length=0.0, prox_gap=7.0, extension_gap=0.0, distal_gap=0.0): centerline_np = normalize_centerline_input(centerline) endograft_poly, struts_poly, combined_poly = create_endograft(centerline_np, diameter, prox_length, extension_length, distal_length, prox_gap, extension_gap, distal_gap) return {"endograft": endograft_poly, "struts": struts_poly, "combined": combined_poly,} def polydata_to_stl_bytes(polydata): tmp = tempfile.NamedTemporaryFile(suffix=".stl", delete=False) tmp_path = tmp.name tmp.close() try: writer = vtk.vtkSTLWriter() writer.SetFileName(tmp_path) writer.SetInputData(polydata) writer.Write() with open(tmp_path, "rb") as f: return f.read() finally: try: import os os.unlink(tmp_path) except OSError: pass def build_endograft_stl_payload(centerline, **params): parts = build_endograft_parts(centerline, **params) return {"endograft": polydata_to_stl_bytes(parts["endograft"]), "struts": polydata_to_stl_bytes(parts["struts"]), "combined": polydata_to_stl_bytes(parts["combined"])} def generate_stl_files(centerline, output_prefix="endograft", **params): parts = build_endograft_parts(centerline, **params) outputs = {"endograft": f"{output_prefix}-endograft.stl", "struts": f"{output_prefix}-struts.stl", "combined": f"{output_prefix}-combined.stl"} for key, path in outputs.items(): writer = vtk.vtkSTLWriter() writer.SetFileName(path) writer.SetInputData(parts[key]) writer.Write() return outputs