"""
This module provides various geometric operations on Geometry objects,
including shifting, mirroring, rotating, union, subtraction, intersection,
stacking, and clipping.
"""
from __future__ import annotations
import numpy as np
from .base import Geometry
from typing import Sequence
[docs]class Shift(Geometry):
"""
Shift geometry by given offsets in x, y, z directions.
Shortest import: `from geoparticle import Shift`
"""
[docs] def __init__(self, geo: Geometry,
x: float | None = None, y: float | None = None, z: float | None = None,
name=None):
"""
Initialize a Shift object that shifts a geometry by specified offsets.
Args:
geo (Geometry): The source geometry to shift.
x (float | None): Shift in the x-direction. Defaults to None.
y (float | None): Shift in the y-direction. Defaults to None.
z (float | None): Shift in the z-direction. Defaults to None.
name (str, optional): Name of the resulting geometry. Defaults to None.
"""
super().__init__(name=name or f'Shift {self.get_counter()}', dimension=geo.dimension)
self.load_from(geo.shift(x, y, z))
[docs]class Mirror(Geometry):
"""
Mirror geometry across a specified plane.
Shortest import: `from geoparticle import Mirror`
"""
[docs] def __init__(self, geo: Geometry, plane_name: str, plane_pos: float, name=None):
"""
Initialize a Mirror object that mirrors the geometry across a specified plane.
Args:
geo (Geometry): The source geometry to mirror.
plane_name (str): Name of the plane ('YOZ', 'XOY', or 'XOZ').
plane_pos (float): Position of the plane.
Raises:
ValueError: If an invalid plane name is provided.
"""
super().__init__(name=name or f'Mirror {self.get_counter()}', dimension=geo.dimension)
self.load_from(geo.mirror(plane_name, plane_pos))
[docs]class Rotate(Geometry):
"""
Rotate geometry around an axis by a given angle.
Shortest import: `from geoparticle import Rotate`
"""
[docs] def __init__(self, geo: Geometry,
angle_deg: float, axis_direction: str | None = None,
axis_point1: Sequence[float] | None = None,
axis_point2: Sequence[float] | None = None,
name=None):
"""
Initialize a Rotate object that rotates a geometry around a specified axis.
Rules:
- If axis_direction is provided, axis_point1 and axis_point2 must not be provided
- If axis_direction is None, both axis_point1 and axis_point2 must be provided
Args:
geo (Geometry): The source geometry to rotate.
angle_deg (float): Rotation angle in degrees.
axis_direction (str | None): Principal axis ('x', 'y', or 'z').
axis_point1 (Sequence[float] | None): First point defining the custom axis.
axis_point2 (Sequence[float] | None): Second point defining the custom axis.
Raises:
ValueError: If invalid axis parameters are provided.
"""
super().__init__(name=name or f'Rotate {self.get_counter()}', dimension=geo.dimension)
self.load_from(geo.rotate(angle_deg, axis_direction, axis_point1, axis_point2))
[docs]class Union(Geometry):
"""
Concatenate multiple geometries.
Shortest import: `from geoparticle import Union`
"""
[docs] def __init__(self, geometries: Sequence[Geometry], name=None):
"""
Initialize a Union object that concatenates multiple geometries.
Users had better ensure no overlapping (too close) points among the geometries.
Args:
geometries (Sequence[Geometry]): Sequence of Geometry objects to concatenate.
name (str, optional): Name of the resulting geometry. Defaults to None.
"""
super().__init__(name=name or f'Union {self.get_counter()}')
if len(geometries) == 1:
self.set_coord(geometries[0].xs, geometries[0].ys, geometries[0].zs)
return
base = geometries[0]
self.load_from(base.union(geometries[1:]))
self.check_overlap()
[docs]class Subtract(Geometry):
"""
Pointwise subtraction: keep points in geo1 farther than `rmax` from any point in geo2.
Shortest import: `from geoparticle import Subtract`
"""
[docs] def __init__(self, geo1: Geometry, geo2: Geometry, rmax: float = 1e-5, name=None):
"""
Initialize a Subtract object that removes points in geo1 close to geo2.
Args:
geo1 (Geometry): The base geometry.
geo2 (Geometry): The geometry to subtract from geo1.
rmax (float, optional): Maximum distance for subtraction. Defaults to 1e-5.
name (str, optional): Name of the resulting geometry. Defaults to None.
"""
super().__init__(name=name or f'Subtract {self.get_counter()}')
self.load_from(geo1.subtract(geo2, rmax=rmax))
self.check_overlap()
[docs]class Intersect(Geometry):
"""
Pointwise intersection of multiple geometries.
Keeps points from the first geometry that are within `rmax` of at least one
point in every other geometry (common intersection under tolerance).
Usage:
- Intersect(g1, g2, g3, ..., rmax=1e-5)
- Intersect([g1, g2, g3, ...], rmax=1e-5)
Shortest import: `from geoparticle import Intersect`
"""
[docs] def __init__(self, geometries: Sequence[Geometry],
rmax: float = 1e-5, name=None):
"""
Initialize an Intersect object that computes the intersection of multiple geometries.
Args:
geometries (Sequence[Geometry]): Geometries to intersect.
rmax (float, optional): Maximum distance for intersection. Defaults to 1e-5.
name (str, optional): Name of the resulting geometry. Defaults to None.
"""
super().__init__(name=name or f'Intersect {self.get_counter()}')
if len(geometries) == 1:
self.set_coord(geometries[0].xs, geometries[0].ys, geometries[0].zs)
return
base = geometries[0]
self.load_from(base.intersect(geometries[1:], rmax=rmax))
self.check_overlap()
[docs]class Stack(Geometry):
"""
Stack a 2D layer along an axis by repeating its points at dl-spacing.
Shortest import: `from geoparticle import Stack`
"""
[docs] def __init__(self, layer: Geometry, axis: str, n_axis: int,
dl: float, dimension: int, name=None):
"""
Initialize a Stack object that stacks a 2D layer along a specified axis.
Args:
layer (Geometry): The 2D layer to stack.
axis (str): Axis along which to stack ('x', 'y', or 'z').
n_axis (int): Number of repetitions along the axis.
dl (float): Spacing between repetitions.
name (str, optional): Name of the resulting geometry. Defaults to None.
"""
super().__init__(name=name or f'Stack {self.get_counter()}')
self.load_from(layer.stack(axis, n_axis, dl, dimension=dimension))
self.check_overlap()
[docs]class Clip(Geometry):
"""
Half-space clipping by a named plane through the origin or an arbitrary plane.
Shortest import: `from geoparticle import Clip`
"""
[docs] def __init__(
self,
geo: Geometry,
*,
keep: str,
plane_name: str | None = None,
plane_normal: Sequence[float] | None = None,
plane_point: Sequence[float] | None = None,
name=None,
):
"""
Initialize a Clip object that clips a geometry by a plane.
Rules:
- If plane_name is given, plane_normal and plane_point must not be provided.
- If plane_name is not given, plane_normal and plane_point must both be provided.
Args:
geo (Geometry): The source geometry to clip.
keep (str): Side to keep ('positive' or 'negative').
plane_name (str, optional): Named plane ('XOY', 'XOZ', 'YOZ'). Defaults to None.
plane_normal (Sequence[float], optional): Normal vector of the plane. Defaults to None.
plane_point (Sequence[float], optional): A point on the plane. Defaults to None.
name (str, optional): Name of the resulting geometry. Defaults to None.
Raises:
ValueError: If invalid arguments are provided.
"""
super().__init__(name=name or f'Clip {self.get_counter()}')
self.load_from(geo.clip(
keep=keep,
plane_name=plane_name,
plane_normal=plane_normal,
plane_point=plane_point,
))
self.check_overlap()