Skip to content

Commit 152aedf

Browse files
committed
Fixed typing in operations_part.py
1 parent 8a94a9f commit 152aedf

File tree

2 files changed

+79
-55
lines changed

2 files changed

+79
-55
lines changed

src/build123d/build_common.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,10 @@ def _is_point(obj):
136136
T = TypeVar("T", Any, list[Any])
137137

138138

139-
def flatten_sequence(*obj: T) -> list[Any]:
139+
def flatten_sequence(*obj: T) -> ShapeList[Any]:
140140
"""Convert a sequence of object potentially containing iterables into a flat list"""
141141

142-
flat_list = []
142+
flat_list = ShapeList()
143143
for item in obj:
144144
# Note: an Iterable can't be used here as it will match with Vector & Vertex
145145
# and break them into a list of floats.
@@ -316,10 +316,14 @@ def _add_to_pending(self, *objects: Edge | Face, face_plane: Plane | None = None
316316
"""Integrate a sequence of objects into existing builder object"""
317317
return NotImplementedError # pragma: no cover
318318

319+
T = TypeVar("T", bound="Builder")
320+
319321
@classmethod
320322
def _get_context(
321-
cls, caller: Builder | Shape | Joint | str | None = None, log: bool = True
322-
) -> Builder | None:
323+
cls: Type[T],
324+
caller: Builder | Shape | Joint | str | None = None,
325+
log: bool = True,
326+
) -> T | None:
323327
"""Return the instance of the current builder"""
324328
result = cls._current.get(None)
325329
context_name = "None" if result is None else type(result).__name__
@@ -818,7 +822,7 @@ def __getattr__(self, name):
818822

819823

820824
def validate_inputs(
821-
context: Builder, validating_class, objects: Iterable[Shape] | None = None
825+
context: Builder | None, validating_class, objects: Iterable[Shape] | None = None
822826
):
823827
"""A function to wrap the method when used outside of a Builder context"""
824828
if context is None:

src/build123d/operations_part.py

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"""
2828

2929
from __future__ import annotations
30-
from typing import Union
30+
from typing import cast
3131

3232
from collections.abc import Iterable
3333
from build123d.build_enums import Mode, Until, Kind, Side
@@ -56,11 +56,11 @@
5656

5757

5858
def extrude(
59-
to_extrude: Face | Sketch = None,
60-
amount: float = None,
61-
dir: VectorLike = None, # pylint: disable=redefined-builtin
62-
until: Until = None,
63-
target: Compound | Solid = None,
59+
to_extrude: Face | Sketch | None = None,
60+
amount: float | None = None,
61+
dir: VectorLike | None = None, # pylint: disable=redefined-builtin
62+
until: Until | None = None,
63+
target: Compound | Solid | None = None,
6464
both: bool = False,
6565
taper: float = 0.0,
6666
clean: bool = True,
@@ -89,7 +89,7 @@ def extrude(
8989
Part: extruded object
9090
"""
9191
# pylint: disable=too-many-locals, too-many-branches
92-
context: BuildPart = BuildPart._get_context("extrude")
92+
context: BuildPart | None = BuildPart._get_context("extrude")
9393
validate_inputs(context, "extrude", to_extrude)
9494

9595
to_extrude_faces: list[Face]
@@ -130,12 +130,6 @@ def extrude(
130130
if len(face_planes) != len(to_extrude_faces):
131131
raise ValueError("dir must be provided when extruding non-planar faces")
132132

133-
if until is not None:
134-
if target is None and context is None:
135-
raise ValueError("A target object must be provided")
136-
if target is None:
137-
target = context.part
138-
139133
logger.info(
140134
"%d face(s) to extrude on %d face plane(s)",
141135
len(to_extrude_faces),
@@ -144,7 +138,7 @@ def extrude(
144138

145139
for face, plane in zip(to_extrude_faces, face_planes):
146140
for direction in [1, -1] if both else [1]:
147-
if amount:
141+
if amount is not None:
148142
if taper == 0:
149143
new_solids.append(
150144
Solid.extrude(
@@ -162,10 +156,19 @@ def extrude(
162156
)
163157

164158
else:
159+
if until is None:
160+
raise ValueError("Either amount or until must be provided")
161+
if target is None:
162+
if context is None:
163+
raise ValueError("A target object must be provided")
164+
target_object = context.part
165+
else:
166+
target_object = target
167+
165168
new_solids.append(
166169
Solid.extrude_until(
167170
section=face,
168-
target_object=target,
171+
target_object=target_object,
169172
direction=plane.z_dir * direction,
170173
until=until,
171174
)
@@ -186,7 +189,7 @@ def extrude(
186189

187190

188191
def loft(
189-
sections: Face | Sketch | Iterable[Vertex | Face | Sketch] = None,
192+
sections: Face | Sketch | Iterable[Vertex | Face | Sketch] | None = None,
190193
ruled: bool = False,
191194
clean: bool = True,
192195
mode: Mode = Mode.ADD,
@@ -203,7 +206,7 @@ def loft(
203206
clean (bool, optional): Remove extraneous internal structure. Defaults to True.
204207
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
205208
"""
206-
context: BuildPart = BuildPart._get_context("loft")
209+
context: BuildPart | None = BuildPart._get_context("loft")
207210

208211
section_list = flatten_sequence(sections)
209212
validate_inputs(context, "loft", section_list)
@@ -235,10 +238,11 @@ def loft(
235238
elif isinstance(s, Face):
236239
loft_wires.append(s.outer_wire())
237240
elif isinstance(s, Sketch):
238-
loft_wires.append(s.face().outer_wire())
241+
loft_wires.extend([f.outer_wire() for f in s.faces()])
239242
elif all(isinstance(s, Vertex) for s in section_list):
240243
raise ValueError(
241-
"At least one face/sketch is required if vertices are the first, last, or first and last elements"
244+
"At least one face/sketch is required if vertices are the first, last, "
245+
"or first and last elements"
242246
)
243247

244248
new_solid = Solid.make_loft(loft_wires, ruled)
@@ -262,7 +266,7 @@ def loft(
262266
def make_brake_formed(
263267
thickness: float,
264268
station_widths: float | Iterable[float],
265-
line: Edge | Wire | Curve = None,
269+
line: Edge | Wire | Curve | None = None,
266270
side: Side = Side.LEFT,
267271
kind: Kind = Kind.ARC,
268272
clean: bool = True,
@@ -298,7 +302,7 @@ def make_brake_formed(
298302
Part: sheet metal part
299303
"""
300304
# pylint: disable=too-many-locals, too-many-branches
301-
context: BuildPart = BuildPart._get_context("make_brake_formed")
305+
context: BuildPart | None = BuildPart._get_context("make_brake_formed")
302306
validate_inputs(context, "make_brake_formed")
303307

304308
if line is not None:
@@ -321,8 +325,16 @@ def make_brake_formed(
321325
raise ValueError("line not suitable - probably straight") from exc
322326

323327
# Make edge pairs
324-
station_edges = ShapeList()
328+
station_edges: ShapeList[Edge] = ShapeList()
325329
line_vertices = line.vertices()
330+
331+
if isinstance(station_widths, (float, int)):
332+
station_widths_list = [station_widths] * len(line_vertices)
333+
elif isinstance(station_widths, Iterable):
334+
station_widths_list = list(station_widths)
335+
else:
336+
raise TypeError("station_widths must be either a single number or an iterable")
337+
326338
for vertex in line_vertices:
327339
others = offset_vertices.sort_by_distance(Vector(vertex.X, vertex.Y, vertex.Z))
328340
for other in others[1:]:
@@ -333,27 +345,25 @@ def make_brake_formed(
333345
break
334346
station_edges = station_edges.sort_by(line)
335347

336-
if isinstance(station_widths, (float, int)):
337-
station_widths = [station_widths] * len(line_vertices)
338-
if len(station_widths) != len(line_vertices):
348+
if len(station_widths_list) != len(line_vertices):
339349
raise ValueError(
340350
f"widths must either be a single number or an iterable with "
341351
f"a length of the # vertices in line ({len(line_vertices)})"
342352
)
343353
station_faces = [
344354
Face.extrude(obj=e, direction=plane.z_dir * w)
345-
for e, w in zip(station_edges, station_widths)
355+
for e, w in zip(station_edges, station_widths_list)
346356
]
347357
sweep_paths = line.edges().sort_by(line)
348-
sections = []
358+
sections: list[Solid] = []
349359
for i in range(len(station_faces) - 1):
350360
sections.append(
351361
Solid.sweep_multi(
352362
[station_faces[i], station_faces[i + 1]], path=sweep_paths[i]
353363
)
354364
)
355365
if len(sections) > 1:
356-
new_solid = sections.pop().fuse(*sections)
366+
new_solid = cast(Part, Part.fuse(*sections))
357367
else:
358368
new_solid = sections[0]
359369

@@ -391,7 +401,7 @@ def project_workplane(
391401
Returns:
392402
Plane: workplane aligned for projection
393403
"""
394-
context: BuildPart = BuildPart._get_context("project_workplane")
404+
context: BuildPart | None = BuildPart._get_context("project_workplane")
395405

396406
if context is not None and not isinstance(context, BuildPart):
397407
raise RuntimeError(
@@ -422,7 +432,7 @@ def project_workplane(
422432

423433

424434
def revolve(
425-
profiles: Face | Iterable[Face] = None,
435+
profiles: Face | Iterable[Face] | None = None,
426436
axis: Axis = Axis.Z,
427437
revolution_arc: float = 360.0,
428438
clean: bool = True,
@@ -444,7 +454,7 @@ def revolve(
444454
Raises:
445455
ValueError: Invalid axis of revolution
446456
"""
447-
context: BuildPart = BuildPart._get_context("revolve")
457+
context: BuildPart | None = BuildPart._get_context("revolve")
448458

449459
profile_list = flatten_sequence(profiles)
450460

@@ -458,16 +468,13 @@ def revolve(
458468
if all([s is None for s in profile_list]):
459469
if context is None or (context is not None and not context.pending_faces):
460470
raise ValueError("No profiles provided")
461-
profile_list = context.pending_faces
471+
profile_faces = context.pending_faces
462472
context.pending_faces = []
463473
context.pending_face_planes = []
464474
else:
465-
p_list = []
466-
for profile in profile_list:
467-
p_list.extend(profile.faces())
468-
profile_list = p_list
475+
profile_faces = profile_list.faces()
469476

470-
new_solids = [Solid.revolve(profile, angle, axis) for profile in profile_list]
477+
new_solids = [Solid.revolve(profile, angle, axis) for profile in profile_faces]
471478

472479
new_solid = Compound(new_solids)
473480
if context is not None:
@@ -479,7 +486,7 @@ def revolve(
479486

480487

481488
def section(
482-
obj: Part = None,
489+
obj: Part | None = None,
483490
section_by: Plane | Iterable[Plane] = Plane.XZ,
484491
height: float = 0.0,
485492
clean: bool = True,
@@ -497,13 +504,17 @@ def section(
497504
clean (bool, optional): Remove extraneous internal structure. Defaults to True.
498505
mode (Mode, optional): combination mode. Defaults to Mode.INTERSECT.
499506
"""
500-
context: BuildPart = BuildPart._get_context("section")
507+
context: BuildPart | None = BuildPart._get_context("section")
501508
validate_inputs(context, "section", None)
502509

503-
if context is not None and obj is None:
504-
max_size = context.part.bounding_box(optimal=False).diagonal
510+
if obj is not None:
511+
to_section = obj
512+
elif context is not None:
513+
to_section = context.part
505514
else:
506-
max_size = obj.bounding_box(optimal=False).diagonal
515+
raise ValueError("No object to section")
516+
517+
max_size = to_section.bounding_box(optimal=False).diagonal
507518

508519
if section_by is not None:
509520
section_planes = (
@@ -528,7 +539,13 @@ def section(
528539
else:
529540
raise ValueError("obj must be provided")
530541

531-
new_objects = [obj.intersect(plane) for plane in planes]
542+
new_objects: list[Face | Shell] = []
543+
for plane in planes:
544+
intersection = to_section.intersect(plane)
545+
if isinstance(intersection, ShapeList):
546+
new_objects.extend(intersection)
547+
elif intersection is not None:
548+
new_objects.append(intersection)
532549

533550
if context is not None:
534551
context._add_to_context(
@@ -542,9 +559,9 @@ def section(
542559

543560

544561
def thicken(
545-
to_thicken: Face | Sketch = None,
546-
amount: float = None,
547-
normal_override: VectorLike = None,
562+
to_thicken: Face | Sketch | None = None,
563+
amount: float | None = None,
564+
normal_override: VectorLike | None = None,
548565
both: bool = False,
549566
clean: bool = True,
550567
mode: Mode = Mode.ADD,
@@ -555,7 +572,7 @@ def thicken(
555572
556573
Args:
557574
to_thicken (Union[Face, Sketch], optional): object to thicken. Defaults to None.
558-
amount (float, optional): distance to extrude, sign controls direction. Defaults to None.
575+
amount (float): distance to extrude, sign controls direction.
559576
normal_override (Vector, optional): The normal_override vector can be used to
560577
indicate which way is 'up', potentially flipping the face normal direction
561578
such that many faces with different normals all go in the same direction
@@ -571,11 +588,14 @@ def thicken(
571588
Returns:
572589
Part: extruded object
573590
"""
574-
context: BuildPart = BuildPart._get_context("thicken")
591+
context: BuildPart | None = BuildPart._get_context("thicken")
575592
validate_inputs(context, "thicken", to_thicken)
576593

577594
to_thicken_faces: list[Face]
578595

596+
if amount is None:
597+
raise ValueError("An amount must be provided")
598+
579599
if to_thicken is None:
580600
if context is not None and context.pending_faces:
581601
# Get pending faces and face planes
@@ -603,15 +623,15 @@ def thicken(
603623
for direction in [1, -1] if both else [1]:
604624
new_solids.append(
605625
Solid.thicken(
606-
face, depth=amount, normal_override=face_normal * direction
626+
face, depth=amount, normal_override=Vector(face_normal) * direction
607627
)
608628
)
609629

610630
if context is not None:
611631
context._add_to_context(*new_solids, clean=clean, mode=mode)
612632
else:
613633
if len(new_solids) > 1:
614-
new_solids = [new_solids.pop().fuse(*new_solids)]
634+
new_solids = [cast(Part, Part.fuse(*new_solids))]
615635
if clean:
616636
new_solids = [solid.clean() for solid in new_solids]
617637

0 commit comments

Comments
 (0)