morph-spines-visualizer 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- morph_spines_visualizer/__init__.py +33 -0
- morph_spines_visualizer/core/__init__.py +8 -0
- morph_spines_visualizer/core/data_loading.py +146 -0
- morph_spines_visualizer/core/geometry.py +99 -0
- morph_spines_visualizer/core/k3d_core.py +171 -0
- morph_spines_visualizer/core/k3d_visualization.py +392 -0
- morph_spines_visualizer/core/spines.py +172 -0
- morph_spines_visualizer/utils/mesh_loading.py +64 -0
- morph_spines_visualizer/utils/supress.py +20 -0
- morph_spines_visualizer-0.2.4.dist-info/METADATA +39 -0
- morph_spines_visualizer-0.2.4.dist-info/RECORD +14 -0
- morph_spines_visualizer-0.2.4.dist-info/WHEEL +5 -0
- morph_spines_visualizer-0.2.4.dist-info/licenses/LICENSE +201 -0
- morph_spines_visualizer-0.2.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""morph_spines."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
__version__ = version(__package__)
|
|
6
|
+
|
|
7
|
+
from morph_spines_visualizer.core.k3d_core import (
|
|
8
|
+
k3d_version,
|
|
9
|
+
add_mesh_to_plot,
|
|
10
|
+
add_mesh_point_cloud_to_plot,
|
|
11
|
+
add_morphology_to_plot
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from morph_spines_visualizer.core.data_loading import (
|
|
15
|
+
load_spiny_morphology
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from morph_spines_visualizer.core.spines import (
|
|
19
|
+
get_spine_ids_by_section_id,
|
|
20
|
+
get_section_ids_for_sections_with_spines,
|
|
21
|
+
get_section_ids_with_spine_counts_for_sections_with_spines,
|
|
22
|
+
get_spine_counts_for_sections_with_spines
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from morph_spines_visualizer.core.k3d_visualization import (
|
|
26
|
+
visualize_morphology_with_point_cloud,
|
|
27
|
+
visualization_morphology_with_synapses
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from morph_spines_visualizer.utils.mesh_loading import (
|
|
31
|
+
load_mesh_vertices_and_faces,
|
|
32
|
+
load_mesh_vertices
|
|
33
|
+
)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import pyvista as pv
|
|
2
|
+
import numpy as np
|
|
3
|
+
import trimesh
|
|
4
|
+
import os
|
|
5
|
+
import morph_spines
|
|
6
|
+
from morph_spines_visualizer.utils import supress
|
|
7
|
+
|
|
8
|
+
def load_mesh_vertices_and_faces_trimesh(mesh_path: str, scale_factor: float = 1.0):
|
|
9
|
+
"""
|
|
10
|
+
Load a triangular mesh using trimesh and return its vertices and faces.
|
|
11
|
+
|
|
12
|
+
This function reads a mesh file (e.g., .obj, .ply, .stl) using trimesh,
|
|
13
|
+
optionally scales it, and extracts the vertex coordinates and triangle indices
|
|
14
|
+
in NumPy array format.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
mesh_path (str): Path to the mesh file to load.
|
|
18
|
+
scale_factor (float, optional): Scale factor to apply to the mesh coordinates.
|
|
19
|
+
Defaults to 1.0.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
tuple[np.ndarray, np.ndarray]:
|
|
23
|
+
- vertices (np.ndarray of shape (N, 3)): Vertex coordinates (x, y, z)
|
|
24
|
+
- faces (np.ndarray of shape (M, 3)): Triangle vertex indices
|
|
25
|
+
"""
|
|
26
|
+
# Load the mesh using trimesh
|
|
27
|
+
mesh = trimesh.load(mesh_path, process=False)
|
|
28
|
+
|
|
29
|
+
# Scale vertices if needed
|
|
30
|
+
vertices = mesh.vertices * scale_factor
|
|
31
|
+
faces = mesh.faces
|
|
32
|
+
|
|
33
|
+
return vertices.astype(np.float32), faces.astype(np.uint32)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_mesh_vertices_trimesh(mesh_path: str, scale_factor: float = 1.0):
|
|
37
|
+
"""
|
|
38
|
+
Load only the vertices of a mesh using trimesh.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
mesh_path (str): Path to the mesh file.
|
|
42
|
+
scale_factor (float, optional): Scale factor to apply to the mesh coordinates.
|
|
43
|
+
Defaults to 1.0.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
np.ndarray: Vertex coordinates (Nx3) in float32.
|
|
47
|
+
"""
|
|
48
|
+
mesh = trimesh.load(mesh_path, process=False)
|
|
49
|
+
vertices = mesh.vertices * scale_factor
|
|
50
|
+
return vertices.astype(np.float32)
|
|
51
|
+
|
|
52
|
+
def load_mesh_vertices_and_faces_pyvista(mesh_path: str, scale_factor: float = 1.0):
|
|
53
|
+
"""
|
|
54
|
+
Load a triangular mesh using PyVista and return its vertices and faces.
|
|
55
|
+
|
|
56
|
+
This function reads a mesh file (e.g., .obj, .ply, .stl, .vtu, etc.) using PyVista,
|
|
57
|
+
optionally scales it, and extracts the vertex coordinates and triangle indices
|
|
58
|
+
in NumPy array format. It is optimized for performance and simplicity compared
|
|
59
|
+
to K3D’s data loading.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
mesh_path (str): Path to the mesh file to load.
|
|
63
|
+
scale_factor (float, optional): Scale factor to apply to the mesh coordinates.
|
|
64
|
+
Defaults to 1.0.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
tuple[np.ndarray, np.ndarray]:
|
|
68
|
+
- **vertices** (`np.ndarray` of shape `(N, 3)`): Array of vertex coordinates (x, y, z).
|
|
69
|
+
- **faces** (`np.ndarray` of shape `(M, 3)`): Array of triangle vertex indices.
|
|
70
|
+
"""
|
|
71
|
+
# Read mesh using PyVista
|
|
72
|
+
mesh = pv.read(mesh_path)
|
|
73
|
+
|
|
74
|
+
# Scale vertices if needed
|
|
75
|
+
mesh.points *= scale_factor
|
|
76
|
+
|
|
77
|
+
# Extract vertices and faces
|
|
78
|
+
vertices = mesh.points.astype(np.float32)
|
|
79
|
+
|
|
80
|
+
# PyVista stores faces as [n, v0, v1, v2, n, v0, v1, v2, ...]
|
|
81
|
+
# So we reshape and drop the leading 'n' (number of vertices per face)
|
|
82
|
+
faces = mesh.faces.reshape(-1, 4)[:, 1:4].astype(np.uint32)
|
|
83
|
+
|
|
84
|
+
return vertices, faces
|
|
85
|
+
|
|
86
|
+
def load_mesh_vertices_pyvista(mesh_path: str, scale_factor: float = 1.0):
|
|
87
|
+
"""
|
|
88
|
+
Load only the vertices of a mesh using PyVista.
|
|
89
|
+
|
|
90
|
+
This function reads a mesh file (e.g., .obj, .ply, .stl, .vtu, etc.) using PyVista,
|
|
91
|
+
optionally scales its coordinates, and returns the vertex positions as a NumPy array.
|
|
92
|
+
It is lightweight and optimized for when face data is not needed.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
mesh_path (str): Path to the mesh file to load.
|
|
96
|
+
scale_factor (float, optional): Scale factor to apply to the mesh coordinates.
|
|
97
|
+
Defaults to 1.0.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
np.ndarray:
|
|
101
|
+
**vertices** (`np.ndarray` of shape `(N, 3)`): Array of vertex coordinates (x, y, z)
|
|
102
|
+
in single precision (`float32`).
|
|
103
|
+
"""
|
|
104
|
+
# Read mesh using PyVista
|
|
105
|
+
mesh = pv.read(mesh_path)
|
|
106
|
+
|
|
107
|
+
# Scale vertices if needed
|
|
108
|
+
mesh.points *= scale_factor
|
|
109
|
+
|
|
110
|
+
# Extract and convert vertices
|
|
111
|
+
vertices = mesh.points.astype(np.float32)
|
|
112
|
+
|
|
113
|
+
return vertices
|
|
114
|
+
|
|
115
|
+
def load_spiny_morphology(morphology_path: str):
|
|
116
|
+
"""
|
|
117
|
+
Load a spiny neuronal morphology from a file using the morph-spine libaray.
|
|
118
|
+
|
|
119
|
+
This function uses the morph_spines library to load a neuronal morphology
|
|
120
|
+
that includes spines using the morph-spine library.
|
|
121
|
+
The morphology file can be in formats supported by morph_spines (e.g., .swc, .asc).
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
morphology_path (str): Path to the morphology file.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
morphology: A morphology object containing the spines.
|
|
128
|
+
Raises:
|
|
129
|
+
FileNotFoundError: If the morphology file does not exist at `morphology_path`.
|
|
130
|
+
RuntimeError: If the morphology cannot be loaded by `morph_spines`.
|
|
131
|
+
"""
|
|
132
|
+
if not os.path.exists(morphology_path):
|
|
133
|
+
raise FileNotFoundError(f"Morphology file not found: {morphology_path}")
|
|
134
|
+
|
|
135
|
+
# Load morphology using morph_spines
|
|
136
|
+
try:
|
|
137
|
+
import warnings
|
|
138
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
139
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
140
|
+
|
|
141
|
+
# To suppress the stdout/stderr output from morph_spines loading
|
|
142
|
+
with supress.SuppressOutput():
|
|
143
|
+
morphology = morph_spines.load_morphology_with_spines(morphology_path)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
raise RuntimeError(f"Failed to load morphology from {morphology_path}: {e}")
|
|
146
|
+
return morphology
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
|
|
4
|
+
def get_section_points(
|
|
5
|
+
morphology, section_id: int) -> np.ndarray:
|
|
6
|
+
"""
|
|
7
|
+
Retrieve the 3D points of a specific section in the morphology.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
morphology: A morphology object containing sections with 3D points.
|
|
11
|
+
Expected structure: morphology.morphology.sections, where
|
|
12
|
+
each section has a `.points` attribute as an array-like of (x, y, z[, r]).
|
|
13
|
+
section_id (int): The ID of the section to retrieve points from.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
np.ndarray: An array of shape (N, 3) containing the 3D points of the section.
|
|
17
|
+
"""
|
|
18
|
+
for section in morphology.morphology.sections:
|
|
19
|
+
if section.id == section_id:
|
|
20
|
+
pts = np.asarray(section.points)[:, :3].astype(np.float32)
|
|
21
|
+
return pts
|
|
22
|
+
raise ValueError(f"Section ID {section_id} not found in morphology.")
|
|
23
|
+
|
|
24
|
+
def get_sections_points(morphology):
|
|
25
|
+
"""
|
|
26
|
+
Extract 3D point coordinates for each section in a neuron morphology.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
morphology : object
|
|
31
|
+
A morphology object containing a `.morphology.sections` iterable.
|
|
32
|
+
Each section is expected to have:
|
|
33
|
+
- section.points : array-like of shape (N, ≥3)
|
|
34
|
+
- section.id : unique identifier
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
dict
|
|
39
|
+
A dictionary mapping section IDs to Nx3 NumPy arrays of float32
|
|
40
|
+
coordinates. Sections with fewer than 2 points are skipped.
|
|
41
|
+
|
|
42
|
+
Examples
|
|
43
|
+
--------
|
|
44
|
+
>>> sections = get_sections_points(morph)
|
|
45
|
+
>>> sections[12].shape
|
|
46
|
+
(45, 3)
|
|
47
|
+
"""
|
|
48
|
+
sections_points = {}
|
|
49
|
+
|
|
50
|
+
for section in morphology.morphology.sections:
|
|
51
|
+
pts = np.asarray(section.points, dtype=np.float32)[:, :3]
|
|
52
|
+
|
|
53
|
+
# Skip if section does not contain enough points to define a segment
|
|
54
|
+
if pts.shape[0] < 2:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
sections_points[section.id] = pts
|
|
58
|
+
|
|
59
|
+
return sections_points
|
|
60
|
+
|
|
61
|
+
def compute_morphology_bounds(
|
|
62
|
+
morphology) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float]:
|
|
63
|
+
"""
|
|
64
|
+
Compute the bounding box and related metrics for a neuronal morphology.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
morphology: A morphology object containing sections with 3D points.
|
|
68
|
+
Expected structure: morphology.morphology.sections, where
|
|
69
|
+
each section has a `.points` attribute as an array-like of (x, y, z[, r]).
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Tuple containing:
|
|
73
|
+
- min_pt (np.ndarray): Minimum coordinates [x, y, z] of the bounding box.
|
|
74
|
+
- max_pt (np.ndarray): Maximum coordinates [x, y, z] of the bounding box.
|
|
75
|
+
- center (np.ndarray): Center of the bounding box.
|
|
76
|
+
- extent (np.ndarray): Size of the bounding box along each axis (max - min).
|
|
77
|
+
- radius (float): Approximate radius for visualization purposes (0.6 * diagonal length).
|
|
78
|
+
"""
|
|
79
|
+
section_points = {}
|
|
80
|
+
|
|
81
|
+
# Collect 3D points from all sections with at least 2 points
|
|
82
|
+
for section in morphology.morphology.sections:
|
|
83
|
+
pts = np.asarray(section.points)[:, :3].astype(np.float32)
|
|
84
|
+
if pts.shape[0] < 2:
|
|
85
|
+
continue
|
|
86
|
+
section_points[section.id] = pts
|
|
87
|
+
|
|
88
|
+
if not section_points:
|
|
89
|
+
raise ValueError("Morphology contains no sections with valid points.")
|
|
90
|
+
|
|
91
|
+
# Concatenate all vertices to compute bounding box
|
|
92
|
+
all_vertices = np.concatenate(list(section_points.values()), axis=0)
|
|
93
|
+
min_pt = all_vertices.min(axis=0)
|
|
94
|
+
max_pt = all_vertices.max(axis=0)
|
|
95
|
+
center = (min_pt + max_pt) / 2
|
|
96
|
+
extent = max_pt - min_pt
|
|
97
|
+
radius = np.linalg.norm(extent) * 0.6
|
|
98
|
+
|
|
99
|
+
return min_pt, max_pt, center, extent, radius
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import k3d
|
|
2
|
+
import numpy as np
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
def k3d_version():
|
|
6
|
+
"""
|
|
7
|
+
Returns the installed version of the k3d library.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
str: The instaleld version of the k3d library.
|
|
11
|
+
"""
|
|
12
|
+
return k3d.__version__
|
|
13
|
+
|
|
14
|
+
def add_mesh_to_plot(
|
|
15
|
+
mesh_vertices: np.ndarray,
|
|
16
|
+
mesh_faces: np.ndarray,
|
|
17
|
+
plot: k3d.plot,
|
|
18
|
+
color: int = 0x00ffcc
|
|
19
|
+
) -> k3d.mesh:
|
|
20
|
+
"""
|
|
21
|
+
Add a triangular mesh to a K3D plot.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
mesh_vertices (np.ndarray): Nx3 array of vertex coordinates (x, y, z).
|
|
25
|
+
mesh_faces (np.ndarray): Mx3 array of triangle vertex indices.
|
|
26
|
+
plot_ (k3d.plot): The K3D plot object to which the mesh will be added.
|
|
27
|
+
color (int, optional): RGB color of the mesh in 0xRRGGBB format. Defaults to 0x00ffcc.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
k3d.mesh: The K3D mesh object added to the plot.
|
|
31
|
+
"""
|
|
32
|
+
# Flatten faces if necessary (K3D expects a 1D array of indices)
|
|
33
|
+
faces_flat = mesh_faces.flatten()
|
|
34
|
+
|
|
35
|
+
mesh_plot = k3d.mesh(
|
|
36
|
+
mesh_vertices,
|
|
37
|
+
faces_flat,
|
|
38
|
+
color=color,
|
|
39
|
+
opacity=1.0,
|
|
40
|
+
wireframe=False,
|
|
41
|
+
flat_shading=True
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
plot += mesh_plot
|
|
45
|
+
return mesh_plot
|
|
46
|
+
|
|
47
|
+
def add_point_cloud_to_plot(
|
|
48
|
+
points: np.ndarray,
|
|
49
|
+
plot: k3d.plot,
|
|
50
|
+
point_size: float = 0.15,
|
|
51
|
+
opacity: float = 0.15,
|
|
52
|
+
color: int = 0x00ffcc,
|
|
53
|
+
shader: str = 'flat'
|
|
54
|
+
) -> k3d.points:
|
|
55
|
+
"""
|
|
56
|
+
Add a 3D point cloud to a K3D plot.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
points (np.ndarray): Nx3 array of points coordinates (x, y, z).
|
|
60
|
+
plot (k3d.plot): The K3D plot object to which the point cloud will be added.
|
|
61
|
+
point_size (float, optional): Size of the points. Defaults to 0.15.
|
|
62
|
+
opacity (float, optional): Opacity of the points. Defaults to 0.15.
|
|
63
|
+
color (int, optional): RGB color of the points in 0xRRGGBB format. Defaults to 0x00ffcc.
|
|
64
|
+
shader (str, optional): Shader to use for rendering points ('flat', 'mesh', 'sphere', etc.).
|
|
65
|
+
Defaults to 'flat'.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
k3d.points: The K3D point cloud object added to the plot.
|
|
69
|
+
"""
|
|
70
|
+
# Create point cloud object
|
|
71
|
+
point_cloud_plot = k3d.points(
|
|
72
|
+
points,
|
|
73
|
+
point_size=point_size,
|
|
74
|
+
color=color,
|
|
75
|
+
opacity=opacity
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Set shader
|
|
79
|
+
point_cloud_plot.shader = shader
|
|
80
|
+
|
|
81
|
+
# Add to the plot
|
|
82
|
+
plot += point_cloud_plot
|
|
83
|
+
|
|
84
|
+
return point_cloud_plot
|
|
85
|
+
|
|
86
|
+
def add_mesh_point_cloud_to_plot(
|
|
87
|
+
mesh_points: np.ndarray,
|
|
88
|
+
plot: k3d.plot,
|
|
89
|
+
point_size: float = 0.15,
|
|
90
|
+
opacity: float = 0.15,
|
|
91
|
+
color: int = 0x00ffcc,
|
|
92
|
+
shader: str = 'flat'
|
|
93
|
+
) -> k3d.points:
|
|
94
|
+
"""
|
|
95
|
+
Add a 3D point cloud representing mesh vertices to a K3D plot.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
mesh_points (np.ndarray): Nx3 array of vertex coordinates (x, y, z).
|
|
99
|
+
plot (k3d.plot): The K3D plot object to which the point cloud will be added.
|
|
100
|
+
point_size (float, optional): Size of the points. Defaults to 0.15.
|
|
101
|
+
opacity (float, optional): Opacity of the points. Defaults to 0.15.
|
|
102
|
+
color (int, optional): RGB color of the points in 0xRRGGBB format. Defaults to 0x00ffcc.
|
|
103
|
+
shader (str, optional): Shader to use for rendering points ('flat', 'mesh', 'sphere', etc.).
|
|
104
|
+
Defaults to 'flat'.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
k3d.points: The K3D point cloud object added to the plot.
|
|
108
|
+
"""
|
|
109
|
+
# Create point cloud object
|
|
110
|
+
point_cloud_plot = k3d.points(
|
|
111
|
+
mesh_points,
|
|
112
|
+
point_size=point_size,
|
|
113
|
+
color=color,
|
|
114
|
+
opacity=opacity
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Set shader
|
|
118
|
+
point_cloud_plot.shader = shader
|
|
119
|
+
|
|
120
|
+
# Add to the plot
|
|
121
|
+
plot += point_cloud_plot
|
|
122
|
+
|
|
123
|
+
return point_cloud_plot
|
|
124
|
+
|
|
125
|
+
def add_morphology_to_plot(
|
|
126
|
+
morphology,
|
|
127
|
+
plot: k3d.plot,
|
|
128
|
+
line_color: int = 0x0000FF
|
|
129
|
+
) -> Optional[k3d.line]:
|
|
130
|
+
"""
|
|
131
|
+
Add a neuronal morphology to a K3D plot as connected lines.
|
|
132
|
+
|
|
133
|
+
Each section in the morphology is drawn as a line, with NaN separators
|
|
134
|
+
to prevent connecting separate sections.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
morphology: A morphology object containing sections with points.
|
|
138
|
+
Expected structure: morphology.morphology.sections,
|
|
139
|
+
where each section has a `.points` attribute as an array-like of (x, y, z[, r]).
|
|
140
|
+
plot (k3d.plot): The K3D plot object to which the morphology lines will be added.
|
|
141
|
+
line_color (int, optional): RGB color of the morphology lines in 0xRRGGBB format.
|
|
142
|
+
Defaults to 0x0000FF (blue).
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
k3d.line or None: The K3D line object added to the plot, or None if no valid sections exist.
|
|
146
|
+
"""
|
|
147
|
+
all_points = []
|
|
148
|
+
|
|
149
|
+
# Collect all section points, with NaN separators between sections
|
|
150
|
+
for section in morphology.morphology.sections:
|
|
151
|
+
pts = np.asarray(section.points)[:, :3].astype(np.float32)
|
|
152
|
+
if pts.shape[0] < 2: # skip sections with fewer than 2 points
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
all_points.append(pts)
|
|
156
|
+
|
|
157
|
+
# NaN separator to break the line between sections
|
|
158
|
+
all_points.append(np.array([[np.nan, np.nan, np.nan]], dtype=np.float32))
|
|
159
|
+
|
|
160
|
+
# Remove last NaN separator and concatenate
|
|
161
|
+
if not all_points:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
all_points = all_points[:-1]
|
|
165
|
+
vertices = np.vstack(all_points)
|
|
166
|
+
|
|
167
|
+
# Add single line plot for all vertices
|
|
168
|
+
line_plot = k3d.line(vertices, width=1.0, color=line_color, shader='simple')
|
|
169
|
+
plot += line_plot
|
|
170
|
+
|
|
171
|
+
return line_plot
|