luminarycloud 0.15.0__py3-none-any.whl → 0.15.2__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.
- luminarycloud/_client/client.py +4 -0
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.py +81 -81
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.pyi +4 -1
- luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.py +61 -0
- luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.pyi +76 -0
- luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2_grpc.py +67 -0
- luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2_grpc.pyi +26 -0
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.py +29 -27
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.pyi +5 -1
- luminarycloud/_proto/api/v0/luminarycloud/named_variable_set/named_variable_set_pb2.py +50 -28
- luminarycloud/_proto/api/v0/luminarycloud/named_variable_set/named_variable_set_pb2.pyi +38 -2
- luminarycloud/_proto/api/v0/luminarycloud/named_variable_set/named_variable_set_pb2_grpc.py +34 -0
- luminarycloud/_proto/api/v0/luminarycloud/named_variable_set/named_variable_set_pb2_grpc.pyi +12 -0
- luminarycloud/_proto/assistant/assistant_pb2.py +74 -74
- luminarycloud/_proto/assistant/assistant_pb2.pyi +22 -30
- luminarycloud/_proto/geometry/geometry_pb2.py +8 -1
- luminarycloud/_proto/geometry/geometry_pb2.pyi +19 -0
- luminarycloud/_proto/hexmesh/hexmesh_pb2.py +37 -37
- luminarycloud/_proto/hexmesh/hexmesh_pb2.pyi +4 -4
- luminarycloud/_proto/inferenceservice/inferenceservice_pb2.py +69 -0
- luminarycloud/_proto/inferenceservice/inferenceservice_pb2.pyi +80 -0
- luminarycloud/_proto/parametricworker/parametricworker_pb2.py +59 -26
- luminarycloud/_proto/parametricworker/parametricworker_pb2.pyi +58 -3
- luminarycloud/feature_modification.py +909 -0
- luminarycloud/meshing/mesh_generation_params.py +1 -1
- luminarycloud/meshing/sizing_strategy/sizing_strategies.py +2 -2
- luminarycloud/named_variable_set.py +10 -4
- luminarycloud/physics_ai/inference.py +55 -0
- luminarycloud/simulation_template.py +4 -4
- luminarycloud/types/adfloat.py +19 -1
- luminarycloud/vis/interactive_scene.py +30 -6
- luminarycloud/vis/visualization.py +57 -0
- {luminarycloud-0.15.0.dist-info → luminarycloud-0.15.2.dist-info}/METADATA +1 -1
- {luminarycloud-0.15.0.dist-info → luminarycloud-0.15.2.dist-info}/RECORD +35 -27
- {luminarycloud-0.15.0.dist-info → luminarycloud-0.15.2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,909 @@
|
|
|
1
|
+
# Copyright 2025 Luminary Cloud, Inc. All Rights Reserved.
|
|
2
|
+
from enum import Enum, auto
|
|
3
|
+
from typing import Dict, Iterable, List, Optional, cast
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from ._proto.geometry import geometry_pb2 as gpb
|
|
6
|
+
from .types import Vector3Like
|
|
7
|
+
from .types.vector3 import _to_vector3
|
|
8
|
+
from .params.geometry import Shape, Sphere, Cube, Cylinder, Torus, Cone, HalfSphere, Volume
|
|
9
|
+
from google.protobuf.internal.containers import RepeatedScalarFieldContainer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FeatureOperationType(Enum):
|
|
13
|
+
"""Enum representing the type of operation in a Feature."""
|
|
14
|
+
|
|
15
|
+
IMPORT = auto()
|
|
16
|
+
CREATE = auto()
|
|
17
|
+
DELETE = auto()
|
|
18
|
+
UNION = auto()
|
|
19
|
+
SUBTRACTION = auto()
|
|
20
|
+
INTERSECTION = auto()
|
|
21
|
+
CHOP = auto()
|
|
22
|
+
IMPRINT = auto()
|
|
23
|
+
TRANSLATE = auto()
|
|
24
|
+
ROTATE = auto()
|
|
25
|
+
SCALE = auto()
|
|
26
|
+
SHRINKWRAP = auto()
|
|
27
|
+
FARFIELD = auto()
|
|
28
|
+
PATTERN_LINEAR = auto()
|
|
29
|
+
PATTERN_CIRCULAR = auto()
|
|
30
|
+
CONFIGURATIONS = auto()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _volumes_to_int_list(volumes: Iterable[Volume | int]) -> List[int]:
|
|
34
|
+
"""
|
|
35
|
+
Convert a collection of Volume objects or integer IDs to a list of integer IDs.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
volumes: Collection of volumes or volume IDs
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of integer IDs
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
TypeError: If any item in volumes is not a Volume object or integer
|
|
45
|
+
"""
|
|
46
|
+
result = []
|
|
47
|
+
for v in volumes:
|
|
48
|
+
if isinstance(v, Volume):
|
|
49
|
+
result.append(int(v.id))
|
|
50
|
+
elif isinstance(v, int):
|
|
51
|
+
result.append(v)
|
|
52
|
+
else:
|
|
53
|
+
raise TypeError(f"Unsupported type for volume: {type(v)}")
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_operation_type(feature: gpb.Feature) -> FeatureOperationType:
|
|
58
|
+
"""
|
|
59
|
+
Determine which type of operation is active in a gpb.Feature object.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
feature: A gpb.Feature object
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The FeatureOperationType enum value representing the active operation
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: If the feature has no operation set or has an unknown operation type
|
|
69
|
+
"""
|
|
70
|
+
operation = feature.WhichOneof("operation")
|
|
71
|
+
if operation is None:
|
|
72
|
+
raise ValueError("No operation set on feature")
|
|
73
|
+
|
|
74
|
+
# Base operation map for simple one-to-one mappings
|
|
75
|
+
base_operation_map = {
|
|
76
|
+
"import": FeatureOperationType.IMPORT,
|
|
77
|
+
"create": FeatureOperationType.CREATE,
|
|
78
|
+
"delete": FeatureOperationType.DELETE,
|
|
79
|
+
"imprint": FeatureOperationType.IMPRINT,
|
|
80
|
+
"shrinkwrap": FeatureOperationType.SHRINKWRAP,
|
|
81
|
+
"farfield": FeatureOperationType.FARFIELD,
|
|
82
|
+
"configurations": FeatureOperationType.CONFIGURATIONS,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Handle boolean operations
|
|
86
|
+
if operation == "boolean":
|
|
87
|
+
bool_op = feature.boolean.WhichOneof("op")
|
|
88
|
+
if bool_op == "reg_union":
|
|
89
|
+
return FeatureOperationType.UNION
|
|
90
|
+
elif bool_op == "reg_subtraction":
|
|
91
|
+
return FeatureOperationType.SUBTRACTION
|
|
92
|
+
elif bool_op == "reg_intersection":
|
|
93
|
+
return FeatureOperationType.INTERSECTION
|
|
94
|
+
elif bool_op == "reg_chop":
|
|
95
|
+
return FeatureOperationType.CHOP
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError(f"Unknown boolean operation type: {bool_op}")
|
|
98
|
+
|
|
99
|
+
# Handle transform operations
|
|
100
|
+
elif operation == "transform":
|
|
101
|
+
transform_op = feature.transform.WhichOneof("t")
|
|
102
|
+
if transform_op == "translation":
|
|
103
|
+
return FeatureOperationType.TRANSLATE
|
|
104
|
+
elif transform_op == "rotation":
|
|
105
|
+
return FeatureOperationType.ROTATE
|
|
106
|
+
elif transform_op == "scaling":
|
|
107
|
+
return FeatureOperationType.SCALE
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError(f"Unknown transform operation type: {transform_op}")
|
|
110
|
+
|
|
111
|
+
# Handle pattern operations
|
|
112
|
+
elif operation == "pattern":
|
|
113
|
+
pattern_type = feature.pattern.direction.WhichOneof("type")
|
|
114
|
+
if pattern_type == "linear_spacing":
|
|
115
|
+
return FeatureOperationType.PATTERN_LINEAR
|
|
116
|
+
elif pattern_type == "circular_distribution":
|
|
117
|
+
return FeatureOperationType.PATTERN_CIRCULAR
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError(f"Unknown pattern type: {pattern_type}")
|
|
120
|
+
|
|
121
|
+
# Handle simple operations
|
|
122
|
+
elif operation in base_operation_map:
|
|
123
|
+
return base_operation_map[operation]
|
|
124
|
+
|
|
125
|
+
# If we get here, it's an unknown operation type
|
|
126
|
+
raise ValueError(f"Unknown operation type: {operation}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _update_repeated_field(
|
|
130
|
+
target_field: RepeatedScalarFieldContainer[int], new_values: list[int]
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Helper function to update a repeated field without using clear().
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
target_field: The RepeatedScalarFieldContainer[int] protobuf repeated field
|
|
136
|
+
new_values: The new values to set
|
|
137
|
+
|
|
138
|
+
This is needed because some versions of protobuf don't support clear() on RepeatedScalarContainer.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
# Delete all existing elements
|
|
142
|
+
while len(target_field) > 0:
|
|
143
|
+
del target_field[0]
|
|
144
|
+
|
|
145
|
+
# Add new elements
|
|
146
|
+
target_field.extend(new_values)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def modify_import(
|
|
150
|
+
feature: gpb.Feature,
|
|
151
|
+
geometry_url: Optional[str] = None,
|
|
152
|
+
scaling: Optional[float] = None,
|
|
153
|
+
force_discrete: Optional[bool] = None,
|
|
154
|
+
) -> gpb.Modification:
|
|
155
|
+
"""
|
|
156
|
+
Modify an import feature with optional new values.
|
|
157
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
feature: A gpb.Feature object
|
|
161
|
+
geometry_url: The new geometry URL
|
|
162
|
+
scaling: New scaling factor
|
|
163
|
+
force_discrete: Whether to force discrete geometry
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
A gpb.Modification object
|
|
167
|
+
"""
|
|
168
|
+
if get_operation_type(feature) != FeatureOperationType.IMPORT:
|
|
169
|
+
raise ValueError("Feature is not an import operation")
|
|
170
|
+
|
|
171
|
+
feature_copy = deepcopy(feature)
|
|
172
|
+
import_op = getattr(feature_copy, "import")
|
|
173
|
+
|
|
174
|
+
if geometry_url is not None:
|
|
175
|
+
# TODO(chiodi): Handle upload of file here.
|
|
176
|
+
import_op.geometry_url = geometry_url
|
|
177
|
+
|
|
178
|
+
if scaling is not None:
|
|
179
|
+
import_op.scaling = scaling
|
|
180
|
+
|
|
181
|
+
if force_discrete is not None:
|
|
182
|
+
import_op.force_discrete = force_discrete
|
|
183
|
+
|
|
184
|
+
return gpb.Modification(
|
|
185
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
186
|
+
feature=feature_copy,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def modify_create(
|
|
191
|
+
feature: gpb.Feature,
|
|
192
|
+
shape: Optional[Shape] = None,
|
|
193
|
+
) -> gpb.Modification:
|
|
194
|
+
"""
|
|
195
|
+
Modify a create feature with a new shape.
|
|
196
|
+
If shape is None, the existing shape is kept.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
feature: A gpb.Feature object
|
|
200
|
+
shape: New shape to create (Sphere, Cube, Cylinder, etc.)
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
A gpb.Modification object
|
|
204
|
+
"""
|
|
205
|
+
if get_operation_type(feature) != FeatureOperationType.CREATE:
|
|
206
|
+
raise ValueError("Feature is not a create operation")
|
|
207
|
+
|
|
208
|
+
feature_copy = deepcopy(feature)
|
|
209
|
+
create_op = feature_copy.create
|
|
210
|
+
|
|
211
|
+
if shape is not None:
|
|
212
|
+
create_op.ClearField("shape")
|
|
213
|
+
|
|
214
|
+
if isinstance(shape, Sphere):
|
|
215
|
+
create_op.sphere.CopyFrom(shape._to_proto()) # type: ignore
|
|
216
|
+
elif isinstance(shape, Cube):
|
|
217
|
+
create_op.box.CopyFrom(shape._to_proto()) # type: ignore
|
|
218
|
+
elif isinstance(shape, Cylinder):
|
|
219
|
+
create_op.cylinder.CopyFrom(shape._to_proto()) # type: ignore
|
|
220
|
+
elif isinstance(shape, Torus):
|
|
221
|
+
create_op.torus.CopyFrom(shape._to_proto()) # type: ignore
|
|
222
|
+
elif isinstance(shape, Cone):
|
|
223
|
+
create_op.cone.CopyFrom(shape._to_proto()) # type: ignore
|
|
224
|
+
elif isinstance(shape, HalfSphere):
|
|
225
|
+
create_op.half_sphere.CopyFrom(shape._to_proto()) # type: ignore
|
|
226
|
+
else:
|
|
227
|
+
raise TypeError(f"Unsupported shape type: {type(shape)}")
|
|
228
|
+
|
|
229
|
+
return gpb.Modification(
|
|
230
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
231
|
+
feature=feature_copy,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def modify_delete(
|
|
236
|
+
feature: gpb.Feature,
|
|
237
|
+
volumes: Optional[List[Volume | int]] = None,
|
|
238
|
+
) -> gpb.Modification:
|
|
239
|
+
"""
|
|
240
|
+
Modify a delete feature with optional new values.
|
|
241
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
feature: A gpb.Feature object
|
|
245
|
+
volumes: List of volumes or volume IDs to delete
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
A gpb.Modification object
|
|
249
|
+
"""
|
|
250
|
+
if get_operation_type(feature) != FeatureOperationType.DELETE:
|
|
251
|
+
raise ValueError("Feature is not a delete operation")
|
|
252
|
+
|
|
253
|
+
feature_copy = deepcopy(feature)
|
|
254
|
+
delete_op = feature_copy.delete
|
|
255
|
+
|
|
256
|
+
if delete_op.type != gpb.EntityType.BODY:
|
|
257
|
+
raise ValueError("Only body delete operations currently supported")
|
|
258
|
+
|
|
259
|
+
if volumes is not None:
|
|
260
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
261
|
+
_update_repeated_field(delete_op.ids, vol_ids)
|
|
262
|
+
|
|
263
|
+
return gpb.Modification(
|
|
264
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
265
|
+
feature=feature_copy,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def modify_union(
|
|
270
|
+
feature: gpb.Feature,
|
|
271
|
+
volumes: Optional[List[Volume | int]] = None,
|
|
272
|
+
) -> gpb.Modification:
|
|
273
|
+
"""
|
|
274
|
+
Modify a boolean union feature with optional new body IDs.
|
|
275
|
+
If volumes is None, the existing body IDs are kept.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
feature: A gpb.Feature object
|
|
279
|
+
volumes: List of volumes or volume IDs to union
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
A gpb.Modification object
|
|
283
|
+
"""
|
|
284
|
+
if get_operation_type(feature) != FeatureOperationType.UNION:
|
|
285
|
+
raise ValueError("Feature is not a boolean union operation")
|
|
286
|
+
|
|
287
|
+
feature_copy = deepcopy(feature)
|
|
288
|
+
boolean_op = feature_copy.boolean
|
|
289
|
+
|
|
290
|
+
if volumes is not None:
|
|
291
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
292
|
+
_update_repeated_field(boolean_op.reg_union.bodies, vol_ids)
|
|
293
|
+
|
|
294
|
+
return gpb.Modification(
|
|
295
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
296
|
+
feature=feature_copy,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def modify_subtraction(
|
|
301
|
+
feature: gpb.Feature,
|
|
302
|
+
volumes: Optional[List[Volume | int]] = None,
|
|
303
|
+
tool_volumes: Optional[List[Volume | int]] = None,
|
|
304
|
+
propagate_tool_tags: Optional[bool] = None,
|
|
305
|
+
) -> gpb.Modification:
|
|
306
|
+
"""
|
|
307
|
+
Modify a boolean subtraction feature with optional new values.
|
|
308
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
feature: A gpb.Feature object
|
|
312
|
+
volumes: List of volumes or volume IDs to subtract from
|
|
313
|
+
tool_volumes: List of volumes or volume IDs to use for subtraction
|
|
314
|
+
propagate_tool_tags: Whether to propagate tool tags
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
A gpb.Modification object
|
|
318
|
+
"""
|
|
319
|
+
if get_operation_type(feature) != FeatureOperationType.SUBTRACTION:
|
|
320
|
+
raise ValueError("Feature is not a boolean subtraction operation")
|
|
321
|
+
|
|
322
|
+
feature_copy = deepcopy(feature)
|
|
323
|
+
boolean_op = feature_copy.boolean
|
|
324
|
+
if volumes is not None:
|
|
325
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
326
|
+
_update_repeated_field(boolean_op.reg_subtraction.bodies, vol_ids)
|
|
327
|
+
|
|
328
|
+
if tool_volumes is not None:
|
|
329
|
+
tool_ids = _volumes_to_int_list(tool_volumes)
|
|
330
|
+
_update_repeated_field(boolean_op.reg_subtraction.tools, tool_ids)
|
|
331
|
+
|
|
332
|
+
if propagate_tool_tags is not None:
|
|
333
|
+
boolean_op.reg_subtraction.propagate_tool_tags = propagate_tool_tags
|
|
334
|
+
|
|
335
|
+
return gpb.Modification(
|
|
336
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
337
|
+
feature=feature_copy,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def modify_intersection(
|
|
342
|
+
feature: gpb.Feature, volumes: Optional[List[Volume | int]] = None
|
|
343
|
+
) -> gpb.Modification:
|
|
344
|
+
"""
|
|
345
|
+
Modify a boolean intersection feature with optional new volumes.
|
|
346
|
+
If volumes is None, the existing volumes are kept.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
feature: A gpb.Feature object
|
|
350
|
+
volumes: List of volumes or volume IDs to intersect
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
A gpb.Modification object
|
|
354
|
+
"""
|
|
355
|
+
if get_operation_type(feature) != FeatureOperationType.INTERSECTION:
|
|
356
|
+
raise ValueError("Feature is not a boolean intersection operation")
|
|
357
|
+
|
|
358
|
+
feature_copy = deepcopy(feature)
|
|
359
|
+
boolean_op = feature_copy.boolean
|
|
360
|
+
|
|
361
|
+
if volumes is not None:
|
|
362
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
363
|
+
_update_repeated_field(boolean_op.reg_intersection.bodies, vol_ids)
|
|
364
|
+
|
|
365
|
+
return gpb.Modification(
|
|
366
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
367
|
+
feature=feature_copy,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def modify_chop(
|
|
372
|
+
feature: gpb.Feature,
|
|
373
|
+
volumes: Optional[List[Volume | int]] = None,
|
|
374
|
+
tool_volumes: Optional[List[Volume | int]] = None,
|
|
375
|
+
propagate_tool_tags: Optional[bool] = None,
|
|
376
|
+
) -> gpb.Modification:
|
|
377
|
+
"""
|
|
378
|
+
Modify a boolean chop feature with optional new values.
|
|
379
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
feature: A gpb.Feature object
|
|
383
|
+
volumes: List of volumes or volume IDs to chop
|
|
384
|
+
tool_volumes: List of volumes or volume IDs to use for chopping
|
|
385
|
+
propagate_tool_tags: Whether to propagate tool tags
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
A gpb.Modification object
|
|
389
|
+
"""
|
|
390
|
+
if get_operation_type(feature) != FeatureOperationType.CHOP:
|
|
391
|
+
raise ValueError("Feature is not a boolean chop operation")
|
|
392
|
+
|
|
393
|
+
feature_copy = deepcopy(feature)
|
|
394
|
+
boolean_op = feature_copy.boolean
|
|
395
|
+
|
|
396
|
+
if volumes is not None:
|
|
397
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
398
|
+
_update_repeated_field(boolean_op.reg_chop.bodies, vol_ids)
|
|
399
|
+
|
|
400
|
+
if tool_volumes is not None:
|
|
401
|
+
tool_ids = _volumes_to_int_list(tool_volumes)
|
|
402
|
+
_update_repeated_field(boolean_op.reg_chop.tools, tool_ids)
|
|
403
|
+
|
|
404
|
+
if propagate_tool_tags is not None:
|
|
405
|
+
boolean_op.reg_chop.propagate_tool_tags = propagate_tool_tags
|
|
406
|
+
|
|
407
|
+
return gpb.Modification(
|
|
408
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
409
|
+
feature=feature_copy,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def modify_imprint(
|
|
414
|
+
feature: gpb.Feature,
|
|
415
|
+
behavior: Optional[str] = None,
|
|
416
|
+
volumes: Optional[List[Volume | int]] = None,
|
|
417
|
+
) -> gpb.Modification:
|
|
418
|
+
"""
|
|
419
|
+
Modify an imprint feature with optional new values.
|
|
420
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
feature: A gpb.Feature object
|
|
424
|
+
behavior: Imprint behavior ("IMPRINT_ALL" or "IMPRINT_SELECTED")
|
|
425
|
+
volumes: List of volumes or volume IDs to imprint. Not needed for an Imprint behavior of IMPRINT_ALL
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
A gpb.Modification object
|
|
429
|
+
"""
|
|
430
|
+
if get_operation_type(feature) != FeatureOperationType.IMPRINT:
|
|
431
|
+
raise ValueError("Feature is not an imprint operation")
|
|
432
|
+
|
|
433
|
+
feature_copy = deepcopy(feature)
|
|
434
|
+
imprint_op = feature_copy.imprint
|
|
435
|
+
|
|
436
|
+
if behavior is not None:
|
|
437
|
+
behavior_map = {
|
|
438
|
+
"IMPRINT_ALL": gpb.Imprint.ImprintBehavior.IMPRINT_ALL,
|
|
439
|
+
"IMPRINT_SELECTED": gpb.Imprint.ImprintBehavior.IMPRINT_SELECTED,
|
|
440
|
+
}
|
|
441
|
+
if behavior not in behavior_map:
|
|
442
|
+
raise ValueError(
|
|
443
|
+
f"Invalid imprint behavior: {behavior}. Expected one of: {', '.join(behavior_map.keys())}"
|
|
444
|
+
)
|
|
445
|
+
imprint_op.behavior = behavior_map[behavior]
|
|
446
|
+
|
|
447
|
+
if volumes is not None and imprint_op.behavior != gpb.Imprint.ImprintBehavior.IMPRINT_ALL:
|
|
448
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
449
|
+
_update_repeated_field(imprint_op.body, vol_ids)
|
|
450
|
+
|
|
451
|
+
if imprint_op.behavior != gpb.Imprint.ImprintBehavior.IMPRINT_ALL and len(imprint_op.body) == 0:
|
|
452
|
+
raise ValueError("No volumes provided for imprint operation")
|
|
453
|
+
|
|
454
|
+
return gpb.Modification(
|
|
455
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
456
|
+
feature=feature_copy,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def modify_translate(
|
|
461
|
+
feature: gpb.Feature,
|
|
462
|
+
volumes: Optional[List[Volume | int]] = None,
|
|
463
|
+
displacement: Optional[Vector3Like] = None,
|
|
464
|
+
keep: Optional[bool] = None,
|
|
465
|
+
) -> gpb.Modification:
|
|
466
|
+
"""
|
|
467
|
+
Modify a transform translate feature with optional new values.
|
|
468
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
feature: A gpb.Feature object
|
|
472
|
+
volumes: List of volumes or volume IDs to transform
|
|
473
|
+
displacement: The displacement vector [x, y, z]
|
|
474
|
+
keep: Whether to keep the original bodies
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
A gpb.Modification object
|
|
478
|
+
"""
|
|
479
|
+
if get_operation_type(feature) != FeatureOperationType.TRANSLATE:
|
|
480
|
+
raise ValueError("Feature is not a translate transform operation")
|
|
481
|
+
|
|
482
|
+
feature_copy = deepcopy(feature)
|
|
483
|
+
transform_op = feature_copy.transform
|
|
484
|
+
|
|
485
|
+
if volumes is not None:
|
|
486
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
487
|
+
_update_repeated_field(transform_op.body, vol_ids)
|
|
488
|
+
|
|
489
|
+
if displacement is not None:
|
|
490
|
+
vec = _to_vector3(displacement)
|
|
491
|
+
transform_op.translation.vector.x = vec.x
|
|
492
|
+
transform_op.translation.vector.y = vec.y
|
|
493
|
+
transform_op.translation.vector.z = vec.z
|
|
494
|
+
|
|
495
|
+
if keep is not None:
|
|
496
|
+
transform_op.keep = keep
|
|
497
|
+
|
|
498
|
+
return gpb.Modification(
|
|
499
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
500
|
+
feature=feature_copy,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def modify_rotate(
|
|
505
|
+
feature: gpb.Feature,
|
|
506
|
+
volumes: Optional[List[Volume | int]] = None,
|
|
507
|
+
angle: Optional[float] = None,
|
|
508
|
+
axis: Optional[Vector3Like] = None,
|
|
509
|
+
origin: Optional[Vector3Like] = None,
|
|
510
|
+
keep: Optional[bool] = None,
|
|
511
|
+
) -> gpb.Modification:
|
|
512
|
+
"""
|
|
513
|
+
Modify a transform rotate feature with optional new values.
|
|
514
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
feature: A gpb.Feature object
|
|
518
|
+
volumes: List of volumes or volume IDs to transform
|
|
519
|
+
angle: Rotation angle in degrees
|
|
520
|
+
axis: Rotation axis vector [x, y, z]
|
|
521
|
+
origin: Rotation origin point [x, y, z]
|
|
522
|
+
keep: Whether to keep the original bodies
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
A gpb.Modification object
|
|
526
|
+
"""
|
|
527
|
+
if get_operation_type(feature) != FeatureOperationType.ROTATE:
|
|
528
|
+
raise ValueError("Feature is not a rotate transform operation")
|
|
529
|
+
|
|
530
|
+
feature_copy = deepcopy(feature)
|
|
531
|
+
transform_op = feature_copy.transform
|
|
532
|
+
|
|
533
|
+
if volumes is not None:
|
|
534
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
535
|
+
_update_repeated_field(transform_op.body, vol_ids)
|
|
536
|
+
|
|
537
|
+
# Update existing rotation
|
|
538
|
+
if angle is not None:
|
|
539
|
+
transform_op.rotation.angle = angle
|
|
540
|
+
|
|
541
|
+
if axis is not None:
|
|
542
|
+
axis_vec = _to_vector3(axis)
|
|
543
|
+
transform_op.rotation.arbitrary.direction.x = axis_vec.x
|
|
544
|
+
transform_op.rotation.arbitrary.direction.y = axis_vec.y
|
|
545
|
+
transform_op.rotation.arbitrary.direction.z = axis_vec.z
|
|
546
|
+
|
|
547
|
+
if origin is not None:
|
|
548
|
+
origin_vec = _to_vector3(origin)
|
|
549
|
+
transform_op.rotation.arbitrary.origin.x = origin_vec.x
|
|
550
|
+
transform_op.rotation.arbitrary.origin.y = origin_vec.y
|
|
551
|
+
transform_op.rotation.arbitrary.origin.z = origin_vec.z
|
|
552
|
+
|
|
553
|
+
if keep is not None:
|
|
554
|
+
transform_op.keep = keep
|
|
555
|
+
|
|
556
|
+
return gpb.Modification(
|
|
557
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
558
|
+
feature=feature_copy,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def modify_scale(
|
|
563
|
+
feature: gpb.Feature,
|
|
564
|
+
volumes: Optional[List[Volume | int]] = None,
|
|
565
|
+
scale_factor: Optional[float] = None,
|
|
566
|
+
origin: Optional[Vector3Like] = None,
|
|
567
|
+
keep: Optional[bool] = None,
|
|
568
|
+
) -> gpb.Modification:
|
|
569
|
+
"""
|
|
570
|
+
Modify a transform scale feature with optional new values.
|
|
571
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
feature: A gpb.Feature object
|
|
575
|
+
volumes: List of volumes or volume IDs to transform
|
|
576
|
+
scale_factor: Uniform scaling factor
|
|
577
|
+
origin: Scaling origin point [x, y, z]
|
|
578
|
+
keep: Whether to keep the original bodies
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
A gpb.Modification object
|
|
582
|
+
"""
|
|
583
|
+
if get_operation_type(feature) != FeatureOperationType.SCALE:
|
|
584
|
+
raise ValueError("Feature is not a scale transform operation")
|
|
585
|
+
|
|
586
|
+
feature_copy = deepcopy(feature)
|
|
587
|
+
transform_op = feature_copy.transform
|
|
588
|
+
|
|
589
|
+
if volumes is not None:
|
|
590
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
591
|
+
_update_repeated_field(transform_op.body, vol_ids)
|
|
592
|
+
|
|
593
|
+
if scale_factor is not None:
|
|
594
|
+
transform_op.scaling.isotropic = scale_factor
|
|
595
|
+
|
|
596
|
+
if origin is not None:
|
|
597
|
+
origin_vec = _to_vector3(origin)
|
|
598
|
+
transform_op.scaling.arbitrary.x = origin_vec.x
|
|
599
|
+
transform_op.scaling.arbitrary.y = origin_vec.y
|
|
600
|
+
transform_op.scaling.arbitrary.z = origin_vec.z
|
|
601
|
+
|
|
602
|
+
if keep is not None:
|
|
603
|
+
transform_op.keep = keep
|
|
604
|
+
|
|
605
|
+
return gpb.Modification(
|
|
606
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
607
|
+
feature=feature_copy,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def modify_shrinkwrap(
|
|
612
|
+
feature: gpb.Feature,
|
|
613
|
+
volumes: Optional[List[Volume | int]] = None,
|
|
614
|
+
tool_volumes: Optional[List[Volume | int]] = None,
|
|
615
|
+
mode: Optional[str] = None,
|
|
616
|
+
resolution_min: Optional[float] = None,
|
|
617
|
+
resolution_max: Optional[float] = None,
|
|
618
|
+
resolution_uniform: Optional[float] = None,
|
|
619
|
+
) -> gpb.Modification:
|
|
620
|
+
"""
|
|
621
|
+
Modify a shrinkwrap feature with optional new values.
|
|
622
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
feature: A gpb.Feature object
|
|
626
|
+
volumes: List of volumes or volume IDs to shrinkwrap
|
|
627
|
+
tool_volumes: List of volumes or volume IDs to use as tools
|
|
628
|
+
mode: Shrinkwrap mode ("AUTOMATIC", "MINMAX", or "UNIFORM")
|
|
629
|
+
resolution_min: Minimum resolution
|
|
630
|
+
resolution_max: Maximum resolution
|
|
631
|
+
resolution_uniform: Uniform resolution
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
A gpb.Modification object
|
|
635
|
+
"""
|
|
636
|
+
if get_operation_type(feature) != FeatureOperationType.SHRINKWRAP:
|
|
637
|
+
raise ValueError("Feature is not a shrinkwrap operation")
|
|
638
|
+
|
|
639
|
+
feature_copy = deepcopy(feature)
|
|
640
|
+
shrinkwrap_op = feature_copy.shrinkwrap
|
|
641
|
+
|
|
642
|
+
if volumes is not None:
|
|
643
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
644
|
+
_update_repeated_field(shrinkwrap_op.body, vol_ids)
|
|
645
|
+
|
|
646
|
+
if tool_volumes is not None:
|
|
647
|
+
tool_ids = _volumes_to_int_list(tool_volumes)
|
|
648
|
+
_update_repeated_field(shrinkwrap_op.tool, tool_ids)
|
|
649
|
+
|
|
650
|
+
if mode is not None:
|
|
651
|
+
mode_map = {
|
|
652
|
+
"AUTOMATIC": gpb.ShrinkwrapMode.AUTOMATIC,
|
|
653
|
+
"MINMAX": gpb.ShrinkwrapMode.MINMAX,
|
|
654
|
+
"UNIFORM": gpb.ShrinkwrapMode.UNIFORM,
|
|
655
|
+
}
|
|
656
|
+
if mode not in mode_map:
|
|
657
|
+
raise ValueError(
|
|
658
|
+
f"Invalid shrinkwrap mode: {mode}. Expected one of: {', '.join(mode_map.keys())}"
|
|
659
|
+
)
|
|
660
|
+
shrinkwrap_op.mode = mode_map[mode]
|
|
661
|
+
|
|
662
|
+
if resolution_min is not None:
|
|
663
|
+
shrinkwrap_op.resolution_min = resolution_min
|
|
664
|
+
|
|
665
|
+
if resolution_max is not None:
|
|
666
|
+
shrinkwrap_op.resolution_max = resolution_max
|
|
667
|
+
|
|
668
|
+
if resolution_uniform is not None:
|
|
669
|
+
shrinkwrap_op.resolution_uniform = resolution_uniform
|
|
670
|
+
|
|
671
|
+
return gpb.Modification(
|
|
672
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
673
|
+
feature=feature_copy,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def modify_farfield(
|
|
678
|
+
feature: gpb.Feature,
|
|
679
|
+
shape: Optional[Shape] = None,
|
|
680
|
+
volumes: Optional[List[Volume | int]] = None,
|
|
681
|
+
keep_source_bodies: Optional[bool] = None,
|
|
682
|
+
keep_tool_bodies: Optional[bool] = None,
|
|
683
|
+
propagate_tool_tags: Optional[bool] = None,
|
|
684
|
+
) -> gpb.Modification:
|
|
685
|
+
"""
|
|
686
|
+
Modify a farfield feature with optional new values.
|
|
687
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
feature: A gpb.Feature object
|
|
691
|
+
shape: The shape to create for the farfield
|
|
692
|
+
volumes: List of volumes or volume IDs to subtract
|
|
693
|
+
keep_source_bodies: Whether to keep source bodies
|
|
694
|
+
keep_tool_bodies: Whether to keep tool bodies
|
|
695
|
+
propagate_tool_tags: Whether to propagate tool tags
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
A gpb.Modification object
|
|
699
|
+
"""
|
|
700
|
+
if get_operation_type(feature) != FeatureOperationType.FARFIELD:
|
|
701
|
+
raise ValueError("Feature is not a farfield operation")
|
|
702
|
+
|
|
703
|
+
feature_copy = deepcopy(feature)
|
|
704
|
+
farfield_op = feature_copy.farfield
|
|
705
|
+
|
|
706
|
+
if shape is not None:
|
|
707
|
+
create_op = gpb.Create()
|
|
708
|
+
if isinstance(shape, Sphere):
|
|
709
|
+
create_op.sphere.CopyFrom(shape._to_proto()) # type: ignore
|
|
710
|
+
elif isinstance(shape, Cube):
|
|
711
|
+
create_op.box.CopyFrom(shape._to_proto()) # type: ignore
|
|
712
|
+
elif isinstance(shape, Cylinder):
|
|
713
|
+
create_op.cylinder.CopyFrom(shape._to_proto()) # type: ignore
|
|
714
|
+
elif isinstance(shape, Torus):
|
|
715
|
+
create_op.torus.CopyFrom(shape._to_proto()) # type: ignore
|
|
716
|
+
elif isinstance(shape, Cone):
|
|
717
|
+
create_op.cone.CopyFrom(shape._to_proto()) # type: ignore
|
|
718
|
+
elif isinstance(shape, HalfSphere):
|
|
719
|
+
create_op.half_sphere.CopyFrom(shape._to_proto()) # type: ignore
|
|
720
|
+
else:
|
|
721
|
+
raise TypeError(f"Unsupported shape type: {type(shape)}")
|
|
722
|
+
farfield_op.create.CopyFrom(create_op)
|
|
723
|
+
|
|
724
|
+
if volumes is not None:
|
|
725
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
726
|
+
_update_repeated_field(farfield_op.bodies, vol_ids)
|
|
727
|
+
|
|
728
|
+
if keep_source_bodies is not None:
|
|
729
|
+
farfield_op.keep_source_bodies = keep_source_bodies
|
|
730
|
+
|
|
731
|
+
if keep_tool_bodies is not None:
|
|
732
|
+
farfield_op.keep_tool_bodies = keep_tool_bodies
|
|
733
|
+
|
|
734
|
+
if propagate_tool_tags is not None:
|
|
735
|
+
farfield_op.propagate_tool_tags = propagate_tool_tags
|
|
736
|
+
|
|
737
|
+
return gpb.Modification(
|
|
738
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
739
|
+
feature=feature_copy,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def modify_linear_pattern(
|
|
744
|
+
feature: gpb.Feature,
|
|
745
|
+
volumes: Optional[List[Volume | int]] = None,
|
|
746
|
+
direction: Optional[Vector3Like] = None,
|
|
747
|
+
quantity: Optional[int] = None,
|
|
748
|
+
symmetric: Optional[bool] = None,
|
|
749
|
+
) -> gpb.Modification:
|
|
750
|
+
"""
|
|
751
|
+
Modify a linear pattern feature with optional new values.
|
|
752
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
feature: A gpb.Feature object
|
|
756
|
+
volumes: List of volumes or volume IDs to pattern
|
|
757
|
+
direction: Direction vector [x, y, z]
|
|
758
|
+
quantity: Number of instances
|
|
759
|
+
symmetric: Whether the pattern is symmetric
|
|
760
|
+
|
|
761
|
+
Returns:
|
|
762
|
+
A gpb.Modification object
|
|
763
|
+
"""
|
|
764
|
+
if get_operation_type(feature) != FeatureOperationType.PATTERN_LINEAR:
|
|
765
|
+
raise ValueError("Feature is not a linear pattern operation")
|
|
766
|
+
|
|
767
|
+
feature_copy = deepcopy(feature)
|
|
768
|
+
pattern_op = feature_copy.pattern
|
|
769
|
+
|
|
770
|
+
if volumes is not None:
|
|
771
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
772
|
+
_update_repeated_field(pattern_op.body, vol_ids)
|
|
773
|
+
|
|
774
|
+
if direction is not None:
|
|
775
|
+
# Update existing linear pattern direction
|
|
776
|
+
dir_vec = _to_vector3(direction)
|
|
777
|
+
pattern_op.direction.linear_spacing.vector.x = dir_vec.x
|
|
778
|
+
pattern_op.direction.linear_spacing.vector.y = dir_vec.y
|
|
779
|
+
pattern_op.direction.linear_spacing.vector.z = dir_vec.z
|
|
780
|
+
|
|
781
|
+
if quantity is not None:
|
|
782
|
+
pattern_op.direction.quantity = quantity
|
|
783
|
+
|
|
784
|
+
if symmetric is not None:
|
|
785
|
+
pattern_op.direction.symmetric = symmetric
|
|
786
|
+
|
|
787
|
+
return gpb.Modification(
|
|
788
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
789
|
+
feature=feature_copy,
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def modify_circular_pattern(
|
|
794
|
+
feature: gpb.Feature,
|
|
795
|
+
volumes: Optional[List[Volume | int]] = None,
|
|
796
|
+
angle: Optional[float] = None,
|
|
797
|
+
axis: Optional[Vector3Like] = None,
|
|
798
|
+
origin: Optional[Vector3Like] = None,
|
|
799
|
+
quantity: Optional[int] = None,
|
|
800
|
+
symmetric: Optional[bool] = None,
|
|
801
|
+
full_rotation: Optional[bool] = None,
|
|
802
|
+
) -> gpb.Modification:
|
|
803
|
+
"""
|
|
804
|
+
Modify a circular pattern feature with optional new values.
|
|
805
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
feature: A gpb.Feature object
|
|
809
|
+
volumes: List of volumes or volume IDs to pattern
|
|
810
|
+
angle: Rotation angle in degrees
|
|
811
|
+
axis: Rotation axis vector [x, y, z]
|
|
812
|
+
origin: Rotation origin point [x, y, z]
|
|
813
|
+
quantity: Number of instances
|
|
814
|
+
symmetric: Whether the pattern is symmetric
|
|
815
|
+
full_rotation: Whether it's a full 360-degree rotation
|
|
816
|
+
|
|
817
|
+
Returns:
|
|
818
|
+
A gpb.Modification object
|
|
819
|
+
"""
|
|
820
|
+
if get_operation_type(feature) != FeatureOperationType.PATTERN_CIRCULAR:
|
|
821
|
+
raise ValueError("Feature is not a circular pattern operation")
|
|
822
|
+
|
|
823
|
+
feature_copy = deepcopy(feature)
|
|
824
|
+
pattern_op = feature_copy.pattern
|
|
825
|
+
|
|
826
|
+
if volumes is not None:
|
|
827
|
+
vol_ids = _volumes_to_int_list(volumes)
|
|
828
|
+
_update_repeated_field(pattern_op.body, vol_ids)
|
|
829
|
+
|
|
830
|
+
if quantity is not None:
|
|
831
|
+
pattern_op.direction.quantity = quantity
|
|
832
|
+
|
|
833
|
+
if symmetric is not None:
|
|
834
|
+
pattern_op.direction.symmetric = symmetric
|
|
835
|
+
|
|
836
|
+
circular = pattern_op.direction.circular_distribution
|
|
837
|
+
|
|
838
|
+
if angle is not None:
|
|
839
|
+
circular.rotation.angle = angle
|
|
840
|
+
|
|
841
|
+
if axis is not None:
|
|
842
|
+
axis_vec = _to_vector3(axis)
|
|
843
|
+
circular.rotation.arbitrary.direction.x = axis_vec.x
|
|
844
|
+
circular.rotation.arbitrary.direction.y = axis_vec.y
|
|
845
|
+
circular.rotation.arbitrary.direction.z = axis_vec.z
|
|
846
|
+
|
|
847
|
+
if origin is not None:
|
|
848
|
+
origin_vec = _to_vector3(origin)
|
|
849
|
+
circular.rotation.arbitrary.origin.x = origin_vec.x
|
|
850
|
+
circular.rotation.arbitrary.origin.y = origin_vec.y
|
|
851
|
+
circular.rotation.arbitrary.origin.z = origin_vec.z
|
|
852
|
+
|
|
853
|
+
if full_rotation is not None:
|
|
854
|
+
circular.full = full_rotation
|
|
855
|
+
|
|
856
|
+
return gpb.Modification(
|
|
857
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
858
|
+
feature=feature_copy,
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def modify_configurations(
|
|
863
|
+
feature: gpb.Feature,
|
|
864
|
+
configurations: Optional[Dict[str, List[Volume | int]]] = None,
|
|
865
|
+
active: Optional[str] = None,
|
|
866
|
+
) -> gpb.Modification:
|
|
867
|
+
"""
|
|
868
|
+
Modify a configurations feature with optional new values.
|
|
869
|
+
For any parameter, if None is passed, the existing value is kept.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
feature: A gpb.Feature object
|
|
873
|
+
configurations: Dictionary mapping configuration names to lists of volumes or volume IDs
|
|
874
|
+
active: Name of the active configuration
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
A gpb.Modification object
|
|
878
|
+
"""
|
|
879
|
+
if get_operation_type(feature) != FeatureOperationType.CONFIGURATIONS:
|
|
880
|
+
raise ValueError("Feature is not a configurations operation")
|
|
881
|
+
|
|
882
|
+
feature_copy = deepcopy(feature)
|
|
883
|
+
configurations_op = feature_copy.configurations
|
|
884
|
+
# Replace only if there were new configs
|
|
885
|
+
if configurations is not None and len(configurations) > 0:
|
|
886
|
+
existing_keys = list(configurations_op.configuration.keys())
|
|
887
|
+
# Delete all old configurations
|
|
888
|
+
for key in existing_keys:
|
|
889
|
+
configurations_op.configuration.pop(key)
|
|
890
|
+
|
|
891
|
+
# Add new configurations
|
|
892
|
+
for name, vols in configurations.items():
|
|
893
|
+
config = gpb.Configurations.Configuration()
|
|
894
|
+
vol_ids = _volumes_to_int_list(vols)
|
|
895
|
+
config.body.extend(vol_ids)
|
|
896
|
+
configurations_op.configuration[name].CopyFrom(config)
|
|
897
|
+
|
|
898
|
+
available_configs = set(configurations_op.configuration.keys())
|
|
899
|
+
if active is not None:
|
|
900
|
+
if active not in available_configs:
|
|
901
|
+
raise ValueError(
|
|
902
|
+
f"Active configuration '{active}' not found in provided configurations"
|
|
903
|
+
)
|
|
904
|
+
configurations_op.active = active
|
|
905
|
+
|
|
906
|
+
return gpb.Modification(
|
|
907
|
+
mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
|
|
908
|
+
feature=feature_copy,
|
|
909
|
+
)
|