10
10
11
11
class VoxelModel :
12
12
"""
13
- Represents a 3D model composed of voxels aligned to a grid .
13
+ Represents a 3D model composed of voxels.
14
14
Each voxel can have independent dimensions (width, height, depth).
15
15
16
16
Allows adding voxels based on coordinates and anchor points, and exporting
@@ -24,20 +24,20 @@ def __init__(self, voxel_dimensions=(1.0, 1.0, 1.0)):
24
24
Args:
25
25
voxel_dimensions (tuple): A tuple of three positive numbers
26
26
(width, height, depth) representing the
27
- size of each voxel in the X, Y, and Z
28
- dimensions, respectively. Defaults to (1.0, 1.0, 1.0).
27
+ default size of each voxel.
29
28
"""
30
29
if not (isinstance (voxel_dimensions , (tuple , list )) and
31
30
len (voxel_dimensions ) == 3 and
32
31
all (isinstance (dim , (int , float )) and dim > 0 for dim in voxel_dimensions )):
33
32
raise ValueError ("voxel_dimensions must be a tuple or list of three positive numbers (width, height, depth)." )
34
33
self .voxel_dimensions = tuple (float (dim ) for dim in voxel_dimensions )
35
- # Stores the integer grid coordinates (ix, iy, iz) of each voxel.
36
- # The actual position depends on the grid coordinates and voxel_dimensions.
37
- self ._voxels = set ()
38
- logger .info (f"VoxelModel initialized with voxel_dimensions={ self .voxel_dimensions } " )
34
+ # Stores voxel data as a dictionary:
35
+ # key: integer grid coordinate (ix, iy, iz)
36
+ # value: tuple of dimensions (width, height, depth) for that voxel
37
+ self ._voxels = {}
38
+ logger .info (f"VoxelModel initialized with default voxel_dimensions={ self .voxel_dimensions } " )
39
39
40
- def _calculate_min_corner (self , x , y , z , anchor ):
40
+ def _calculate_min_corner (self , x , y , z , anchor , dimensions ):
41
41
"""
42
42
Calculates the minimum corner coordinates based on anchor point and voxel dimensions.
43
43
@@ -48,14 +48,15 @@ def _calculate_min_corner(self, x, y, z, anchor):
48
48
y (float): Y-coordinate of the anchor point.
49
49
z (float): Z-coordinate of the anchor point.
50
50
anchor (CubeAnchor): The anchor type.
51
+ dimensions (tuple): The (width, height, depth) of the voxel.
51
52
52
53
Returns:
53
54
tuple: (min_x, min_y, min_z) coordinates of the voxel's minimum corner.
54
55
55
56
Raises:
56
57
ValueError: If an invalid anchor point is provided.
57
58
"""
58
- size_x , size_y , size_z = self . voxel_dimensions
59
+ size_x , size_y , size_z = dimensions
59
60
half_x , half_y , half_z = size_x / 2.0 , size_y / 2.0 , size_z / 2.0
60
61
61
62
if anchor == CubeAnchor .CORNER_NEG :
@@ -73,7 +74,7 @@ def _calculate_min_corner(self, x, y, z, anchor):
73
74
74
75
return min_x , min_y , min_z
75
76
76
- def add_voxel (self , x , y , z , anchor = CubeAnchor .CORNER_NEG ):
77
+ def add_voxel (self , x , y , z , anchor = CubeAnchor .CORNER_NEG , dimensions = None ):
77
78
"""
78
79
Adds a voxel to the model. Replaces add_cube.
79
80
@@ -84,33 +85,44 @@ def add_voxel(self, x, y, z, anchor=CubeAnchor.CORNER_NEG):
84
85
anchor (CubeAnchor): The reference point within the voxel that
85
86
(x, y, z) corresponds to. Defaults to
86
87
CubeAnchor.CORNER_NEG.
88
+ dimensions (tuple, optional): The (width, height, depth) for this
89
+ specific voxel. If None, the model's
90
+ default dimensions are used.
87
91
"""
88
- min_x , min_y , min_z = self ._calculate_min_corner (x , y , z , anchor )
92
+ voxel_dims = self .voxel_dimensions if dimensions is None else tuple (float (d ) for d in dimensions )
93
+ if dimensions is not None and not (isinstance (voxel_dims , (tuple , list )) and
94
+ len (voxel_dims ) == 3 and
95
+ all (isinstance (d , (int , float )) and d > 0 for d in voxel_dims )):
96
+ raise ValueError ("Custom dimensions must be a tuple or list of three positive numbers." )
89
97
90
- # Calculate grid coordinates based on minimum corner and dimensions
91
- # Using round to snap to the nearest grid point.
98
+ min_x , min_y , min_z = self ._calculate_min_corner (x , y , z , anchor , voxel_dims )
99
+
100
+ # Calculate grid coordinates based on minimum corner and *default* dimensions
101
+ # This ensures voxels snap to a consistent grid.
92
102
grid_x = round (min_x / self .voxel_dimensions [0 ])
93
103
grid_y = round (min_y / self .voxel_dimensions [1 ])
94
104
grid_z = round (min_z / self .voxel_dimensions [2 ])
95
105
96
106
grid_coord = (grid_x , grid_y , grid_z )
97
- self ._voxels . add ( grid_coord )
107
+ self ._voxels [ grid_coord ] = voxel_dims
98
108
# logger.debug(f"Added voxel at grid {grid_coord} (from anchor {anchor} at ({x},{y},{z}))")
99
109
100
110
# Alias add_cube to add_voxel for backward compatibility (optional, but can be helpful)
101
111
add_cube = add_voxel
102
112
103
- def add_voxels (self , coordinates , anchor = CubeAnchor .CORNER_NEG ):
113
+ def add_voxels (self , coordinates , anchor = CubeAnchor .CORNER_NEG , dimensions = None ):
104
114
"""
105
115
Adds multiple voxels from an iterable. Replaces add_cubes.
106
116
107
117
Args:
108
118
coordinates (iterable): An iterable of (x, y, z) tuples or lists.
109
119
anchor (CubeAnchor): The anchor point to use for all voxels added
110
120
in this call.
121
+ dimensions (tuple, optional): The dimensions to apply to all voxels
122
+ in this call. If None, defaults are used.
111
123
"""
112
124
for x_coord , y_coord , z_coord in coordinates :
113
- self .add_voxel (x_coord , y_coord , z_coord , anchor )
125
+ self .add_voxel (x_coord , y_coord , z_coord , anchor , dimensions )
114
126
115
127
# Alias add_cubes to add_voxels
116
128
add_cubes = add_voxels
@@ -126,14 +138,17 @@ def remove_voxel(self, x, y, z, anchor=CubeAnchor.CORNER_NEG):
126
138
anchor (CubeAnchor): The reference point within the voxel that
127
139
(x, y, z) corresponds to.
128
140
"""
129
- min_x , min_y , min_z = self ._calculate_min_corner (x , y , z , anchor )
141
+ # Note: Removal does not need custom dimensions, as it identifies the
142
+ # voxel by its position on the grid, which is calculated using default dimensions.
143
+ min_x , min_y , min_z = self ._calculate_min_corner (x , y , z , anchor , self .voxel_dimensions )
130
144
131
145
grid_x = round (min_x / self .voxel_dimensions [0 ])
132
146
grid_y = round (min_y / self .voxel_dimensions [1 ])
133
147
grid_z = round (min_z / self .voxel_dimensions [2 ])
134
148
135
149
grid_coord = (grid_x , grid_y , grid_z )
136
- self ._voxels .discard (grid_coord )
150
+ if grid_coord in self ._voxels :
151
+ del self ._voxels [grid_coord ]
137
152
# logger.debug(f"Attempted removal at grid {grid_coord}")
138
153
139
154
# Alias remove_cube to remove_voxel
@@ -162,7 +177,7 @@ def generate_mesh(self):
162
177
163
178
logger .info (f"Generating mesh for { len (self ._voxels )} voxels..." )
164
179
triangles = []
165
- size_x , size_y , size_z = self . voxel_dimensions # Use specific dimensions
180
+ # Voxel dimensions are now fetched per-voxel, so size_x, etc. are defined inside the loop.
166
181
167
182
# Define faces by normal, neighbor offset, and vertex indices (0-7)
168
183
# Vertex indices correspond to relative positions scaled by dimensions:
@@ -180,22 +195,27 @@ def generate_mesh(self):
180
195
]
181
196
182
197
processed_faces = 0
183
- for gx , gy , gz in self ._voxels :
184
- # Calculate the minimum corner based on grid coordinates and dimensions
185
- min_cx = gx * size_x
186
- min_cy = gy * size_y
187
- min_cz = gz * size_z
198
+ for ( gx , gy , gz ), ( size_x , size_y , size_z ) in self ._voxels . items () :
199
+ # Calculate the minimum corner based on grid coordinates and *default* dimensions
200
+ min_cx = gx * self . voxel_dimensions [ 0 ]
201
+ min_cy = gy * self . voxel_dimensions [ 1 ]
202
+ min_cz = gz * self . voxel_dimensions [ 2 ]
188
203
189
- # Calculate the 8 absolute vertex coordinates for this voxel
204
+ # Calculate the 8 absolute vertex coordinates for this voxel using its specific dimensions
190
205
verts = [
191
206
(min_cx + (i % 2 ) * size_x , min_cy + ((i // 2 ) % 2 ) * size_y , min_cz + (i // 4 ) * size_z )
192
207
for i in range (8 )
193
208
]
194
209
195
210
for normal , offset , indices in faces_data :
196
211
neighbor_coord = (gx + offset [0 ], gy + offset [1 ], gz + offset [2 ])
212
+ neighbor_dims = self ._voxels .get (neighbor_coord )
197
213
198
- if neighbor_coord not in self ._voxels : # Exposed face
214
+ # A face is exposed if there is no neighbor, OR if the neighbor
215
+ # has different dimensions, which would create a complex partial
216
+ # surface. For simplicity and correctness of the outer shell,
217
+ # we will generate the face in the latter case.
218
+ if not neighbor_dims or neighbor_dims != (size_x , size_y , size_z ): # Exposed face
199
219
processed_faces += 1
200
220
# Get the four vertices for this face using the indices
201
221
v0 = verts [indices [0 ]]
@@ -215,7 +235,7 @@ def save_mesh(self, filename, format='stl_binary', **kwargs):
215
235
216
236
Args:
217
237
filename (str): The path to the output file.
218
- format (str): The desired output format identifier (e.g. ,
238
+ format (str): The desired output format identifier (e.g/ ,
219
239
'stl_binary', 'stl_ascii'). Case-insensitive.
220
240
Defaults to 'stl_binary'.
221
241
**kwargs: Additional arguments passed directly to the specific
0 commit comments