Source code for session_py.plane

import uuid
import math
from .point import Point
from .vector import Vector
from .tolerance import Tolerance


[docs] class Plane: """A 3D plane defined by origin and coordinate axes. Parameters ---------- origin : Point, optional Origin point of the plane. Defaults to Point(0, 0, 0). x_axis : Vector, optional X-axis direction. Defaults to Vector(1, 0, 0). y_axis : Vector, optional Y-axis direction. Defaults to Vector(0, 1, 0). name : str, optional Name of the plane. Defaults to "my_plane". Attributes ---------- guid : str The unique identifier of the plane. name : str The name of the plane. origin : Point The origin point of the plane. x_axis : Vector The X-axis direction vector. y_axis : Vector The Y-axis direction vector. z_axis : Vector The Z-axis direction vector (normal). a : float Plane equation coefficient (normal x-component). b : float Plane equation coefficient (normal y-component). c : float Plane equation coefficient (normal z-component). d : float Plane equation coefficient (distance from origin). """
[docs] def __init__(self, origin=None, x_axis=None, y_axis=None, name="my_plane"): self.guid = str(uuid.uuid4()) self.name = name if origin is None: self._origin = Point(0.0, 0.0, 0.0) else: self._origin = origin if x_axis is None: self._x_axis = Vector.x_axis() else: self._x_axis = x_axis self._x_axis.normalize_self() if y_axis is None: self._y_axis = Vector.y_axis() else: self._y_axis = y_axis - x_axis * (y_axis.dot(self._x_axis)) self._y_axis.normalize_self() self._z_axis = self._x_axis.cross(self._y_axis) self._z_axis.normalize_self() self._update_equation()
def _update_equation(self): """Update plane equation coefficients from z_axis and origin.""" self._a = self._z_axis.x self._b = self._z_axis.y self._c = self._z_axis.z self._d = -( self._a * self._origin.x + self._b * self._origin.y + self._c * self._origin.z ) @property def origin(self): """Get the origin point.""" return self._origin @property def x_axis(self): """Get the X-axis vector.""" return self._x_axis @property def y_axis(self): """Get the Y-axis vector.""" return self._y_axis @property def z_axis(self): """Get the Z-axis vector (normal).""" return self._z_axis @property def a(self): """Get plane equation coefficient a.""" return self._a @property def b(self): """Get plane equation coefficient b.""" return self._b @property def c(self): """Get plane equation coefficient c.""" return self._c @property def d(self): """Get plane equation coefficient d.""" return self._d
[docs] @staticmethod def from_point_normal(point, normal): """Create a plane from a point and normal vector. Parameters ---------- point : Point Point on the plane. normal : Vector Normal vector of the plane. Returns ------- Plane The constructed plane. """ plane = Plane.__new__(Plane) plane.guid = str(uuid.uuid4()) plane.name = "my_plane" plane._origin = point plane._z_axis = Vector(normal.x, normal.y, normal.z) plane._z_axis.normalize_self() plane._x_axis = Vector() plane._x_axis.perpendicular_to(plane._z_axis) plane._x_axis.normalize_self() plane._y_axis = plane._z_axis.cross(plane._x_axis) plane._y_axis.normalize_self() plane._update_equation() return plane
[docs] @staticmethod def from_points(points): """Create a plane from three or more points. Parameters ---------- points : list of Point List of at least 3 points. Returns ------- Plane The constructed plane. """ if len(points) < 3: return Plane() plane = Plane.__new__(Plane) plane.guid = str(uuid.uuid4()) plane.name = "my_plane" plane._origin = points[0] v1 = points[1] - points[0] v2 = points[2] - points[0] plane._z_axis = v1.cross(v2) plane._z_axis.normalize_self() plane._x_axis = Vector(v1.x, v1.y, v1.z) plane._x_axis.normalize_self() plane._y_axis = plane._z_axis.cross(plane._x_axis) plane._y_axis.normalize_self() plane._update_equation() return plane
[docs] @staticmethod def from_two_points(point1, point2): """Create a plane from two points. Parameters ---------- point1 : Point First point. point2 : Point Second point. Returns ------- Plane The constructed plane. """ plane = Plane.__new__(Plane) plane.guid = str(uuid.uuid4()) plane.name = "my_plane" plane._origin = point1 direction = point2 - point1 direction.normalize_self() plane._z_axis = Vector() plane._z_axis.perpendicular_to(direction) plane._z_axis.normalize_self() plane._x_axis = direction plane._y_axis = plane._z_axis.cross(plane._x_axis) plane._y_axis.normalize_self() plane._update_equation() return plane
[docs] @staticmethod def xy_plane(): """Create the XY plane. Returns ------- Plane XY plane at origin. """ plane = Plane.__new__(Plane) plane.guid = str(uuid.uuid4()) plane.name = "xy_plane" plane._origin = Point(0.0, 0.0, 0.0) plane._x_axis = Vector.x_axis() plane._y_axis = Vector.y_axis() plane._z_axis = Vector.z_axis() plane._a = 0.0 plane._b = 0.0 plane._c = 1.0 plane._d = 0.0 return plane
[docs] @staticmethod def yz_plane(): """Create the YZ plane. Returns ------- Plane YZ plane at origin. """ plane = Plane.__new__(Plane) plane.guid = str(uuid.uuid4()) plane.name = "yz_plane" plane._origin = Point(0.0, 0.0, 0.0) plane._x_axis = Vector.y_axis() plane._y_axis = Vector.z_axis() plane._z_axis = Vector.x_axis() plane._a = 1.0 plane._b = 0.0 plane._c = 0.0 plane._d = 0.0 return plane
[docs] @staticmethod def xz_plane(): """Create the XZ plane. Returns ------- Plane XZ plane at origin. """ plane = Plane.__new__(Plane) plane.guid = str(uuid.uuid4()) plane.name = "xz_plane" plane._origin = Point(0.0, 0.0, 0.0) plane._x_axis = Vector.x_axis() plane._y_axis = Vector(0.0, 0.0, -1.0) plane._z_axis = Vector(0.0, 1.0, 0.0) plane._a = 0.0 plane._b = 1.0 plane._c = 0.0 plane._d = 0.0 return plane
########################################################################################### # Operators ########################################################################################### def __str__(self): return f"Plane(origin={self._origin}, x_axis={self._x_axis}, y_axis={self._y_axis}, z_axis={self._z_axis}, guid={self.guid}, name={self.name})" def __repr__(self): return self.__str__() def __eq__(self, other): if isinstance(other, Point): return self._origin == other return False def __ne__(self, other): return not self.__eq__(other) def __getitem__(self, index): """Get axis by index (0=x, 1=y, 2=z).""" if index == 0: return self._x_axis elif index == 1: return self._y_axis elif index == 2: return self._z_axis raise IndexError("Plane index out of range (0-2)") ########################################################################################### # No-copy Operators ########################################################################################### def __iadd__(self, other): """Translate plane by vector (in-place).""" if isinstance(other, Vector): self._origin += other self._update_equation() return self def __isub__(self, other): """Translate plane by negative vector (in-place).""" if isinstance(other, Vector): self._origin -= other self._update_equation() return self ########################################################################################### # Copy Operators ########################################################################################### def __add__(self, other): """Translate plane by vector (copy).""" if isinstance(other, Vector): result = Plane.__new__(Plane) result.guid = self.guid result.name = self.name result._origin = self._origin + other result._x_axis = Vector(self._x_axis.x, self._x_axis.y, self._x_axis.z) result._y_axis = Vector(self._y_axis.x, self._y_axis.y, self._y_axis.z) result._z_axis = Vector(self._z_axis.x, self._z_axis.y, self._z_axis.z) result._update_equation() return result return NotImplemented def __sub__(self, other): """Translate plane by negative vector (copy).""" if isinstance(other, Vector): result = Plane.__new__(Plane) result.guid = self.guid result.name = self.name result._origin = self._origin - other result._x_axis = Vector(self._x_axis.x, self._x_axis.y, self._x_axis.z) result._y_axis = Vector(self._y_axis.x, self._y_axis.y, self._y_axis.z) result._z_axis = Vector(self._z_axis.x, self._z_axis.y, self._z_axis.z) result._update_equation() return result return NotImplemented ########################################################################################### # Details ###########################################################################################
[docs] def reverse(self): """Reverse the plane's normal direction.""" temp = self._x_axis self._x_axis = self._y_axis self._y_axis = temp self._z_axis.reverse() self._update_equation()
[docs] def rotate(self, angles_in_radians): """Rotate the plane around its normal. Parameters ---------- angles_in_radians : float Rotation angle in radians. """ cos_angle = math.cos(angles_in_radians) sin_angle = math.sin(angles_in_radians) new_x = self._x_axis * cos_angle + self._y_axis * sin_angle new_y = self._y_axis * cos_angle - self._x_axis * sin_angle self._x_axis = new_x self._y_axis = new_y self._update_equation()
[docs] def is_right_hand(self): """Check if the plane follows the right-hand rule. Returns ------- bool True if x_axis × y_axis = z_axis (right-handed). """ cross = self._x_axis.cross(self._y_axis) dot_product = cross.dot(self._z_axis) return dot_product > 0.999
[docs] @staticmethod def is_same_direction(plane0, plane1, can_be_flipped=True): """Check if two planes have the same or flipped normal. Parameters ---------- plane0 : Plane First plane. plane1 : Plane Second plane. can_be_flipped : bool, optional Allow flipped normals. Defaults to True. Returns ------- bool True if normals are parallel or antiparallel. """ n0 = plane0._z_axis n1 = plane1._z_axis parallel = n0.is_parallel_to(n1) if can_be_flipped: return parallel != 0 else: return parallel == 1
[docs] @staticmethod def is_same_position(plane0, plane1): """Check if two planes are in the same position. Parameters ---------- plane0 : Plane First plane. plane1 : Plane Second plane. Returns ------- bool True if origins are very close. """ dist0 = abs( plane0._a * plane1._origin.x + plane0._b * plane1._origin.y + plane0._c * plane1._origin.z + plane0._d ) dist1 = abs( plane1._a * plane0._origin.x + plane1._b * plane0._origin.y + plane1._c * plane0._origin.z + plane1._d ) tolerance = Tolerance.ZERO_TOLERANCE return dist0 < tolerance and dist1 < tolerance
[docs] @staticmethod def is_coplanar(plane0, plane1, can_be_flipped=True): """Check if two planes are coplanar. Parameters ---------- plane0 : Plane First plane. plane1 : Plane Second plane. can_be_flipped : bool, optional Allow flipped normals. Defaults to True. Returns ------- bool True if planes are coplanar. """ return Plane.is_same_direction( plane0, plane1, can_be_flipped ) and Plane.is_same_position(plane0, plane1)
[docs] def translate_by_normal(self, distance): """Translate (move) a plane along its normal direction by a specified distance. Parameters ---------- distance : float Distance to move the plane along its normal (positive = normal direction, negative = opposite). Returns ------- Plane New plane translated by the specified distance. """ normal = Vector(self._z_axis.x, self._z_axis.y, self._z_axis.z) normal.normalize_self() new_origin = self._origin + (normal * distance) return Plane(new_origin, self._x_axis, self._y_axis)
########################################################################################### # Polymorphic JSON Serialization ########################################################################################### def __jsondump__(self): """Serialize to polymorphic JSON format with type field. Returns ------- dict Dictionary with 'type', 'guid', 'name', and object fields. """ return { "type": f"{self.__class__.__name__}", "guid": self.guid, "name": self.name, "origin": self.origin.__jsondump__(), "x_axis": self.x_axis.__jsondump__(), "y_axis": self.y_axis.__jsondump__(), "z_axis": self.z_axis.__jsondump__(), "a": self.a, "b": self.b, "c": self.c, "d": self.d, } @classmethod def __jsonload__(cls, data, guid=None, name=None): """Deserialize from polymorphic JSON format. Parameters ---------- data : dict Dictionary containing plane data. guid : str, optional GUID for the plane. name : str, optional Name for the plane. Returns ------- :class:`Plane` Reconstructed plane instance. """ from .encoders import decode_node origin = decode_node(data["origin"]) x_axis = decode_node(data["x_axis"]) y_axis = decode_node(data["y_axis"]) plane = cls(origin, x_axis, y_axis) plane.guid = guid if guid is not None else data.get("guid", plane.guid) plane.name = name if name is not None else data.get("name", plane.name) # z_axis, a, b, c, d are computed automatically, but verify if provided if "z_axis" in data: z_axis_loaded = decode_node(data["z_axis"]) # z_axis is already computed from cross product, just verify consistency if "xform" in data: obj.xform = decode_node(data["xform"]) return plane