kumiki.joints.workshop.shavings¶
Flat import path
kumiki/__init__.py re-exports everything on this page via from kumiki import *, so every name below is also available directly as kumiki.shavings -- you do not need to import from the submodule path shown in the heading above.
kumiki.joints.workshop.shavings
¶
Joint shavings helpers - re-exports from all shavings submodules.
DEFAULT_FEATURE_TEST_TOLERANCES
module-attribute
¶
DEFAULT_FEATURE_TEST_TOLERANCES = FeatureTestTolerances()
FEATURE_GROUP_PAIRS
module-attribute
¶
FEATURE_GROUP_PAIRS: dict = {FeatureGroup.A: frozenset({FeatureGroup.B1, FeatureGroup.B2}), FeatureGroup.B1: frozenset({FeatureGroup.A}), FeatureGroup.B2: frozenset({FeatureGroup.A, FeatureGroup.B2}), FeatureGroup.C: frozenset({FeatureGroup.C}), FeatureGroup.NONE: frozenset()}
LocatedTimberFeature
module-attribute
¶
TimberLike
module-attribute
¶
TimberLike = Union[Timber, MeshTimber, RoundTimber, RegularPolygonTimber, Board]
IMPERFECT_TIMBER_WARNING
module-attribute
¶
IMPERFECT_TIMBER_WARNING = 'timber is imperfect (does not match perfect timber within), this joint currently does not supporting maknig relief cuts beyond the perfect timber within so the joint may not actually fit'
Ticket
dataclass
¶
Bases: ABC
Base ticket shared by all ticket categories.
The category is represented by the concrete subclass rather than an enum field.
hierarchical identifier using '/' as separator.
e.g. "posts/frontleft", "door/boards/1" The last segment is the display name; preceding segments are folder names.
kumiki_id
class-attribute
instance-attribute
¶
TimberTicket
dataclass
¶
TimberTicket(path: str = UNNAMED_TICKET_PATH, material: Optional[str] = None, reference_faces: Optional[tuple[str, ...]] = None, tags: tuple[TimberTag, ...] = ())
Bases: Ticket
Ticket metadata for physical timber members.
reference_faces
class-attribute
instance-attribute
¶
__post_init__
¶
with_member
¶
with_member(member: Union[Member, str]) -> Self
Return a copy in this member role, replacing whatever role it had.
with_tags cannot do this: a second role is an error, not an addition.
Source code in kumiki/ticket.py
AccessoryTicket
dataclass
¶
JointTicket
dataclass
¶
Drawing
dataclass
¶
Drawing(name: str, timber_paths: Tuple[TimberPath, ...] = (), drawing_id: Optional[DrawingId] = None, measurements: Mapping[str, Tuple[Measure, ...]] = dict())
A drawing the frame asks for: a name, and which timbers it is of.
Timbers are named by path, the same name they carry everywhere else, and by path alone -- which of two timbers sharing a path is not a question a name can answer, and a drawing of "the front left post" should not have to know whether one was made twice. A path naming no timber is not an error either: a drawing of a timber a later edit removed is worth keeping and showing as empty, rather than failing to raise the frame it belongs to.
drawing_id is what an override in the drawings file names, so it has to
survive editing the code around it. It defaults to the name, which is stable
as long as the name is.
measurements
class-attribute
instance-attribute
¶
measurements: Mapping[str, Tuple[Measure, ...]] = field(default_factory=dict)
__post_init__
¶
Source code in kumiki/drawing.py
AssemblyFreedom
dataclass
¶
AssemblyFreedom(translations: Tuple[TranslationDof, ...] = (), rotations: Tuple[RotationDof, ...] = ())
The freedom shape for ONE member within ONE joint.
Currently a set of independent half/full intervals from 0 in R6. Future richer shapes (e.g. requires twisting while moving, or move-then-twist sequences) should extend this class rather than being encoded by callers.
translation
staticmethod
¶
translation(direction: Direction3D, freed_after: Numeric) -> AssemblyFreedom
A single half-interval translational freedom along direction.
Source code in kumiki/assembly.py
bidirectional_translation
staticmethod
¶
bidirectional_translation(direction: Direction3D, freed_after: Numeric) -> AssemblyFreedom
A full-interval translational freedom: two opposite half DOFs.
Source code in kumiki/assembly.py
combine
staticmethod
¶
combine(f1: AssemblyFreedom, f2: AssemblyFreedom) -> AssemblyFreedom
Union of the DOFs of two freedoms (a member that can escape either way).
Source code in kumiki/assembly.py
AssemblyJoint
dataclass
¶
AssemblyJoint(name: str, members: Mapping[int, JointMemberSpec])
One joint in the assembly graph.
members must contain an entry for EVERY member participating in the
joint.
AssemblyMember
dataclass
¶
AssemblyMember(key: int, name: str, position: V3, bbox: Optional[BoundingBox] = None)
AssemblySolution
dataclass
¶
JointMemberSpec
dataclass
¶
JointMemberSpec(freedom: Optional[AssemblyFreedom] = None, ordering: Ordering = Ordering())
One member's participation in one joint.
A None freedom means "unspecified" and the connection is treated as rigid (the member is dragged along whenever the joint moves).
Ordering
dataclass
¶
Extraction position: compared lexicographically; smaller = out earlier.
suborder expresses sequencing required WITHIN a joint (locking
accessories at -1 pop before members at 0) and is authored by the joint
cut functions; order is the frame-level plan set via Joint.with_order.
label
¶
AssemblyBoundingBox
dataclass
¶
AssemblyBoundingBox(min_x: float, max_x: float, min_y: float, max_y: float, min_z: float, max_z: float)
Axis-aligned box in GLOBAL space; used only by the Phase 4 clear-out.
TimberFeature
¶
Bases: Enum
feature
¶
feature() -> TimberFeature
face
¶
face() -> TimberFace
Convert to TimberFace. Values 1-6 map to faces.
Source code in kumiki/timber.py
end
¶
end() -> TimberEnd
Convert to TimberEnd. Values 1-2 map to ends.
Source code in kumiki/timber.py
long_face
¶
long_face() -> TimberLongFace
Convert to TimberLongFace. Values 3-6 map to long faces.
Source code in kumiki/timber.py
edge
¶
edge() -> TimberEdge
Convert to TimberEdge. Values 8-19 map to edges.
Source code in kumiki/timber.py
centerline
¶
centerline() -> TimberCenterline
Convert to TimberCenterline. Value 7 maps to CENTERLINE.
Source code in kumiki/timber.py
long_edge
¶
long_edge() -> TimberLongEdge
Convert to TimberLongEdge. Values 8-11 map to long edges.
Source code in kumiki/timber.py
short_edge
¶
short_edge() -> TimberShortEdge
Convert to TimberShortEdge. Values 12-19 map to short edges.
Source code in kumiki/timber.py
corner
¶
corner() -> TimberCorner
Convert to TimberCorner. Values 20-27 map to corners.
Source code in kumiki/timber.py
TimberCorner
¶
Bases: Enum
TimberEdge
¶
Bases: Enum
canonical_line_from_corner
¶
canonical_line_from_corner() -> Tuple[TimberCorner, TimberFace]
Returns canonical way to express a line from an edge. The line is defined by starting from the TimberCorner and pointing in the direction of the returned TimberFace's outward normal.
For long edges the line starts at the bottom corner and points toward TOP. For short edges the direction follows cross(long_face_normal, end_outward).
Source code in kumiki/timber.py
long_edge
¶
long_edge() -> TimberLongEdge
Convert to TimberLongEdge. Values 8-11 map to long edges.
Source code in kumiki/timber.py
short_edge
¶
short_edge() -> TimberShortEdge
Convert to TimberShortEdge. Values 12-19 map to short edges.
Source code in kumiki/timber.py
TimberLongEdge
¶
Bases: Enum
TimberShortEdge
¶
Bases: Enum
long_face
property
¶
long_face: TimberLongFace
Get the TimberLongFace associated with this short edge.
PerfectTimberWithin
dataclass
¶
PerfectTimberWithin(length: Numeric, size: V2, transform: Transform, ticket: TimberTicket = TimberTicket())
Bases: ABC
Base class for all timber types in the timber framing system (immutable)
This is an abstract base class (ABC) to prevent direct instantiation. All timbers contain a perfect rectangular timber within their rough bounding box.
Note: Use create_timber() factory function to construct timber instances from length_direction and width_direction vectors. Subclasses are frozen to ensure immutability after construction.
Alternatively, if you already have a Transform object, you can construct a timber directly by passing: Timber(length, size, transform, ticket)
Attributes:
| Name | Type | Description |
|---|---|---|
length |
Numeric
|
Length of the timber along its centerline axis |
size |
V2
|
Cross-sectional size (width, height) of the perfect timber within |
transform |
Transform
|
Position and orientation in global coordinates |
ticket |
TimberTicket
|
Ticket for this timber (used for rendering/debugging) |
ticket
class-attribute
instance-attribute
¶
ticket: TimberTicket = field(default_factory=TimberTicket)
__post_init__
¶
get_orientation_global
¶
get_orientation_global() -> Orientation
get_bottom_position_global
¶
get_bottom_position_global() -> V3
Get the bottom position (center of bottom cross-section) in global coordinates from the transform.
get_length_direction_global
¶
get_length_direction_global() -> Direction3D
Get the length direction vector in global coordinates from the orientation matrix
Source code in kumiki/timber.py
get_width_direction_global
¶
get_width_direction_global() -> Direction3D
Get the width direction vector in global coordinates from the orientation matrix
Source code in kumiki/timber.py
get_height_direction_global
¶
get_height_direction_global() -> Direction3D
Get the height direction vector in global coordinates from the orientation matrix
Source code in kumiki/timber.py
get_face_direction_global
¶
get_face_direction_global(face: SomeTimberFace) -> Direction3D
Get the world direction vector for a specific face of this timber.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
face
|
SomeTimberFace
|
The face to get the direction for (can be TimberFace, TimberEnd, or TimberLongFace) |
required |
Returns:
| Type | Description |
|---|---|
Direction3D
|
Direction vector pointing outward from the specified face in world coordinates |
Source code in kumiki/timber.py
get_corner_position_global
¶
get_corner_position_global(corner: TimberCorner) -> V3
Get the position of a corner in global coordinates.
Source code in kumiki/timber.py
get_size_index_in_long_face_normal_axis
¶
get_size_index_in_long_face_normal_axis(face: TimberLongFace) -> int
Get the index of the size in the direction normal to the specified face.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
face
|
TimberLongFace
|
The long face to get the size index for (RIGHT/LEFT or FRONT/BACK) |
required |
Returns:
| Type | Description |
|---|---|
int
|
Index into self.size: 0 (width) for RIGHT/LEFT, 1 (height) for FRONT/BACK |
Source code in kumiki/timber.py
get_size_in_face_normal_axis
¶
get_size_in_face_normal_axis(face: SomeTimberFace) -> Numeric
Get the size of the timber in the direction normal to the specified face.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
face
|
SomeTimberFace
|
The face to get the size for (can be TimberFace, TimberEnd, or TimberLongFace) |
required |
Returns:
| Type | Description |
|---|---|
Numeric
|
The timber's extent along the axis normal to the given face: self.length for |
Numeric
|
TOP/BOTTOM, self.size[0] (width) for RIGHT/LEFT, self.size[1] (height) for FRONT/BACK |
Source code in kumiki/timber.py
get_rough_size_in_face_normal_axis
¶
get_rough_size_in_face_normal_axis(face: SomeTimberFace) -> Numeric
Get the full rough size of the timber in the direction normal to the specified face.
For long faces this returns the sum of the two half-sizes (e.g. right + left for RIGHT or LEFT). For end faces (TOP/BOTTOM) this returns the length.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
face
|
SomeTimberFace
|
The face to get the size for (can be TimberFace, TimberEnd, or TimberLongFace) |
required |
Source code in kumiki/timber.py
get_half_rough_size_in_face_normal_axis
¶
get_half_rough_size_in_face_normal_axis(face: SomeTimberFace) -> Numeric
Get the rough half-size of the timber from the centerline to the specified face.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
face
|
SomeTimberFace
|
A long face (RIGHT, LEFT, FRONT, or BACK). TOP/BOTTOM will raise ValueError since length has no asymmetry concept. |
required |
Returns:
| Type | Description |
|---|---|
Numeric
|
The half-size from centerline to the specified face. |
Source code in kumiki/timber.py
get_nominal_size_in_face_normal_axis
¶
get_nominal_size_in_face_normal_axis(face: SomeTimberFace) -> Numeric
get_half_nominal_size_in_face_normal_axis
¶
get_half_nominal_size_in_face_normal_axis(face: SomeTimberFace) -> Numeric
get_size_in_direction_2d
¶
Get the size of the timber's cross-section measured along an arbitrary 2D direction.
The direction is in the timber's local cross-section plane where x is the width axis and y is the height axis. Returns the total extent (support width) of the rectangular cross-section projected onto that direction.
For axis-aligned directions this matches get_size_in_face_normal_axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
direction
|
V2
|
A 2D direction vector (x=width, y=height) in local cross-section space. Does not need to be normalized. |
required |
Returns:
| Type | Description |
|---|---|
Numeric
|
The size of the cross-section measured along the given direction. |
Source code in kumiki/timber.py
get_size_in_direction_3d
¶
get_size_in_direction_3d(direction: Direction3D) -> Numeric
Get the size of the timber measured along an arbitrary 3D direction in global space.
Transforms the direction into the timber's local frame and computes the total extent (support width) of the rectangular prism projected onto that direction.
For axis-aligned directions this matches get_size_in_face_normal_axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
direction
|
Direction3D
|
A 3D direction vector in global coordinates. Does not need to be normalized. |
required |
Returns:
| Type | Description |
|---|---|
Numeric
|
The size of the timber measured along the given direction. |
Source code in kumiki/timber.py
get_closest_oriented_face_from_global_direction
¶
get_closest_oriented_face_from_global_direction(target_direction: Direction3D) -> TimberFace
Find which face of this timber best aligns with the target direction.
The target_direction should point "outwards" from the desired face (not into it).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target_direction
|
Direction3D
|
Direction vector to match against |
required |
Returns:
| Type | Description |
|---|---|
TimberFace
|
The TimberFace that best aligns with the target direction |
Source code in kumiki/timber.py
get_closest_oriented_long_face_from_global_direction
¶
get_closest_oriented_long_face_from_global_direction(target_direction: Direction3D) -> TimberLongFace
Find which long face of this timber best aligns with the target direction.
The target_direction should point "outwards" from the desired face (not into it).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target_direction
|
Direction3D
|
Direction vector to match against |
required |
Returns:
| Type | Description |
|---|---|
TimberLongFace
|
The TimberLongFace that best aligns with the target direction |
Source code in kumiki/timber.py
get_closest_oriented_end_face_from_global_direction
¶
get_closest_oriented_end_face_from_global_direction(target_direction: Direction3D) -> TimberEnd
Find which end face of this timber best aligns with the target direction.
The target_direction should point "outwards" from the desired end face (not into it).
Returns:
| Type | Description |
|---|---|
TimberEnd
|
The TimberEnd that best aligns with the target direction |
Source code in kumiki/timber.py
get_inside_face_from_footprint
¶
get_inside_face_from_footprint(footprint: Footprint) -> TimberFace
Get the inside face of this timber relative to the footprint.
This method finds which face of the timber is oriented toward the interior of the footprint by: 1. Finding the nearest boundary of the footprint to the timber's centerline 2. Getting the inward normal of that boundary 3. Finding which timber face best aligns with that inward direction
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
footprint
|
Footprint
|
The footprint to determine inside/outside orientation |
required |
Returns:
| Type | Description |
|---|---|
TimberFace
|
The TimberFace that points toward the inside of the footprint |
Source code in kumiki/timber.py
get_outside_face_from_footprint
¶
get_outside_face_from_footprint(footprint: Footprint) -> TimberFace
Get the outside face of this timber relative to the footprint.
This method finds which face of the timber is oriented toward the exterior of the footprint by: 1. Finding the nearest boundary of the footprint to the timber's centerline 2. Getting the inward normal of that boundary 3. Finding which timber face best aligns with the opposite (outward) direction
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
footprint
|
Footprint
|
The footprint to determine inside/outside orientation |
required |
Returns:
| Type | Description |
|---|---|
TimberFace
|
The TimberFace that points toward the outside of the footprint |
Source code in kumiki/timber.py
get_transform_matrix
¶
get_transform_matrix() -> Matrix
Get the 4x4 transformation matrix for this timber
Source code in kumiki/timber.py
project_global_point_onto_timber_face_global
¶
project_global_point_onto_timber_face_global(global_point: V3, face: SomeTimberFace) -> V3
Project a point from global coordinates onto the timber's face and return result in global coordinates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
global_point
|
V3
|
The point to project in global coordinates (3x1 Matrix) |
required |
face
|
SomeTimberFace
|
The face to project onto (can be TimberFace, TimberEnd, or TimberLongFace) |
required |
Source code in kumiki/timber.py
get_perfect_size
¶
get_perfect_size() -> V2
Returns the perfect cross sectional size of the timber.
The perfect size is the cross sectional size of the perfect timber within.
can_be_extended_for_joints
¶
Returns True if the timber can be extended when cutting joints.
Returns:
| Type | Description |
|---|---|
bool
|
True if the timber can be extended when cutting joints. |
get_rough_half_sizes
abstractmethod
¶
Returns the rough half-sizes of the timber measured from the centerline.
The rough bounding box is defined by four half-sizes measured from the centerline in each direction. This allows the rough timber to be non-coaxial with the perfect timber within (useful for square rule layout).
Returns:
| Type | Description |
|---|---|
Tuple[V2, V2]
|
Tuple of two V2s: - width_halves: V2(right_half, left_half) — half-sizes in the width dimension - height_halves: V2(front_half, back_half) — half-sizes in the height dimension |
Source code in kumiki/timber.py
get_nominal_half_sizes
¶
get_rough_size
¶
get_rough_size() -> V2
Returns the rough cross sectional size of the timber.
The rough size is the total cross sectional size defined by the rough half-sizes. For a perfect timber, this matches the perfect size. For an imperfect timber, this may differ and represents the intended bounding box for joint layout and intersection tests.
Source code in kumiki/timber.py
get_perfect_timber_within_csg_local
¶
get_perfect_timber_within_csg_local() -> RectangularPrism
Returns the perfect rectangular prism CSG in local coordinates.
This represents the perfect timber within as a CSG object -- the idealized, finished-dimension bounding box (self.size), not the rough/as-sawn stock boundary. All timber types have a perfect rectangular prism that bounds their actual geometry.
Returns:
| Type | Description |
|---|---|
RectangularPrism
|
RectangularPrism in local coordinates (relative to timber's bottom position) |
Source code in kumiki/timber.py
csg_label_name
classmethod
¶
What this kind of timber is called in a CSG label.
Derived from the class name -- "board", "round_timber" -- so a new timber type names itself without anyone remembering to add it here.
Source code in kumiki/timber.py
csg_label
classmethod
¶
csg_label(*qualifiers: str) -> CutCSGLabel
Label for one of this timber's own CSG shapes.
A classmethod so the name follows the derived class -- a Board's rough extended prism reads "board (rough, extended)", not "timber (...)".
Source code in kumiki/timber.py
get_actual_csg_local
¶
get_actual_csg_local() -> CutCSG
Returns the actual CSG geometry for this timber.
For the base PerfectTimberWithin class, this returns the perfect rectangular prism. Subclasses override this to return different geometries (cylinder, mesh, etc.).
Returns:
| Type | Description |
|---|---|
CutCSG
|
CutCSG representing the actual geometry in local coordinates |
Source code in kumiki/timber.py
get_extended_actual_csg_local
¶
get_extended_actual_csg_local(extend_bot: bool, extend_top: bool) -> CutCSG
Returns the actual CSG geometry extended to infinity at specified ends.
For the base PerfectTimberWithin class, this returns a rectangular prism using the perfect timber within size, optionally extended to infinity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
extend_bot
|
bool
|
If True, extend to -infinity at bottom (z=0) |
required |
extend_top
|
bool
|
If True, extend to +infinity at top (z=length) |
required |
Returns:
| Type | Description |
|---|---|
CutCSG
|
CutCSG representing the extended geometry in local coordinates |
Source code in kumiki/timber.py
get_extended_perfect_csg_local
¶
get_extended_perfect_csg_local(extend_bot: bool, extend_top: bool) -> CutCSG
Returns the PERFECT (finished-dimension) CSG geometry extended to infinity at specified ends -- always self.get_perfect_size(), regardless of any rough/actual sizing a subclass's get_extended_actual_csg_local may use instead. Unlike get_extended_actual_csg_local, this is not overridden per-subclass: every timber type's perfect timber within is a rectangular prism (see get_perfect_timber_within_csg_local), so one implementation suffices for all of them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
extend_bot
|
bool
|
If True, extend to -infinity at bottom (z=0) |
required |
extend_top
|
bool
|
If True, extend to +infinity at top (z=length) |
required |
Returns:
| Type | Description |
|---|---|
CutCSG
|
CutCSG representing the extended geometry in local coordinates |
Source code in kumiki/timber.py
is_face_perfect
¶
is_face_perfect(face: TimberFace) -> bool
Check if the specified face of the timber is perfect (matches the perfect timber within).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
face
|
TimberFace
|
The TimberFace to check |
required |
Source code in kumiki/timber.py
is_perfect_timber
¶
Check if this timber's actual geometry matches its rough bounding box.
Returns True when the rough half-sizes are symmetric and equal to half the perfect timber within size.
Returns:
| Type | Description |
|---|---|
bool
|
True if the timber is a perfect timber, False otherwise |
Source code in kumiki/timber.py
get_imperfect_fringe_csg_local
¶
get_imperfect_fringe_csg_local() -> CutCSG
Returns the CSG (local coordinates) of the region where this timber's actual geometry sticks out beyond its perfect-timber-within boundary, i.e. actual minus perfect.
Source code in kumiki/timber.py
Timber
dataclass
¶
Timber(length: Numeric, size: V2, transform: Transform, ticket: TimberTicket = TimberTicket(), rough_half_sizes: Optional[Tuple[V2, V2]] = None)
Bases: PerfectTimberWithin
Rectangular timber which may or may not be perfect.
Inherits all attributes and methods from PerfectTimberWithin
- length: Length of the timber
- size: Cross-sectional size (width, height)
- transform: Position and orientation
- name: Optional name
rough_half_sizes
class-attribute
instance-attribute
¶
from_perfect_timber_within
staticmethod
¶
from_perfect_timber_within(perfect_timber: PerfectTimberWithin, rough_half_sizes: Optional[Tuple[V2, V2]] = None) -> Timber
Create a Timber instance from a PerfectTimberWithin instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
perfect_timber
|
PerfectTimberWithin
|
An instance of PerfectTimberWithin |
required |
rough_half_sizes
|
Optional[Tuple[V2, V2]]
|
Optional asymmetric half-sizes from centerline |
None
|
Source code in kumiki/timber.py
get_rough_half_sizes
¶
Returns the rough half-sizes of the timber.
If rough_half_sizes is set, returns that. Otherwise returns symmetric half-sizes derived from the perfect timber within size.
Returns:
| Type | Description |
|---|---|
Tuple[V2, V2]
|
Tuple of (V2(right_half, left_half), V2(front_half, back_half)) |
Source code in kumiki/timber.py
get_actual_csg_local
¶
get_actual_csg_local() -> CutCSG
Returns the actual CSG geometry for this timber.
For Timber, this returns a rectangular prism using the rough half-sizes, offset from the centerline when the half-sizes are asymmetric.
Returns:
| Type | Description |
|---|---|
CutCSG
|
RectangularPrism representing the actual geometry in local coordinates |
Source code in kumiki/timber.py
get_extended_actual_csg_local
¶
get_extended_actual_csg_local(extend_bot: bool, extend_top: bool) -> CutCSG
Returns the actual CSG geometry extended to infinity at specified ends.
For Timber, this returns a rectangular prism using the rough half-sizes, offset from the centerline when the half-sizes are asymmetric.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
extend_bot
|
bool
|
If True, extend to -infinity at bottom (z=0) |
required |
extend_top
|
bool
|
If True, extend to +infinity at top (z=length) |
required |
Returns:
| Type | Description |
|---|---|
CutCSG
|
CutCSG representing the extended geometry in local coordinates |
Source code in kumiki/timber.py
Board
dataclass
¶
Board(length: Numeric, size: V2, transform: Transform, ticket: TimberTicket = TimberTicket())
Bases: PerfectTimberWithin
Boards are perfect timbers with board-specific semantics
Boards are structurally identical to perfect timbers but carry additional semantics: - the "length" of the board runs in the Z direction so the TOP and BOTTOM faces are referred to as the "ends" of the board - the "width" of the board runs in the X direction so the LEFT and RIGHT faces are referred to as the "sides" of the board - the "thickness" of the board runs in the Y direction so the FRONT and BACK faces are the same as the "faces" of the board
Like timbers, we assume the grain is always running in the length direction.
Note that you can end cut along the length direction but not in the other directions so you must ensure the board dimensions are large enough to incorporate the cuts
get_rough_half_sizes
¶
Returns the rough half-sizes of the board.
For Board, these are symmetric halves of the perfect timber within size.
Returns:
| Type | Description |
|---|---|
Tuple[V2, V2]
|
Tuple of (V2(right_half, left_half), V2(front_half, back_half)) |
Source code in kumiki/timber.py
get_extended_actual_csg_local
¶
get_extended_actual_csg_local(extend_bot: bool, extend_top: bool) -> CutCSG
Returns the actual CSG geometry extended to infinity at specified ends.
For Board, this returns a rectangular prism using the perfect timber within size.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
extend_bot
|
bool
|
If True, extend to -infinity at bottom (z=0) |
required |
extend_top
|
bool
|
If True, extend to +infinity at top (z=length) |
required |
Returns:
| Type | Description |
|---|---|
CutCSG
|
CutCSG representing the extended geometry in local coordinates |
Source code in kumiki/timber.py
RoundTimber
dataclass
¶
RoundTimber(length: Numeric, size: V2, transform: Transform, ticket: TimberTicket = TimberTicket(), *, diameter: Numeric)
Bases: PerfectTimberWithin
Cylindrical timber (e.g., logs, poles)
Round timbers have a circular cross-section centered on the centerline. The rough bounding box is a square that contains the circle, but the actual geometry is a cylinder.
is_perfect_timber
¶
from_perfect_timber_within
staticmethod
¶
from_perfect_timber_within(perfect_timber: PerfectTimberWithin, diameter: Optional[Numeric] = None) -> RoundTimber
Create a Timber instance from a PerfectTimberWithin instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
perfect_timber
|
PerfectTimberWithin
|
An instance of PerfectTimberWithin |
required |
diameter
|
Optional[Numeric]
|
Optional diameter for the round timber, if None, then the diagonal of the perfect_timber.size is used to compute the diameter. |
None
|
Source code in kumiki/timber.py
get_rough_half_sizes
¶
Returns the rough half-sizes of the round timber.
For round timbers, this is a symmetric square bounding box using the diameter.
Returns:
| Type | Description |
|---|---|
Tuple[V2, V2]
|
Tuple of (V2(d/2, d/2), V2(d/2, d/2)) |
Source code in kumiki/timber.py
get_actual_csg_local
¶
get_actual_csg_local() -> CutCSG
Returns the actual CSG geometry for this timber.
For RoundTimber, this returns a Cylinder with the specified diameter.
Returns:
| Type | Description |
|---|---|
CutCSG
|
Cylinder representing the actual geometry in local coordinates |
Source code in kumiki/timber.py
get_extended_actual_csg_local
¶
get_extended_actual_csg_local(extend_bot: bool, extend_top: bool) -> CutCSG
Returns the actual CSG geometry extended to infinity at specified ends.
For RoundTimber, this returns a Cylinder optionally extended to infinity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
extend_bot
|
bool
|
If True, extend to -infinity at bottom (z=0) |
required |
extend_top
|
bool
|
If True, extend to +infinity at top (z=length) |
required |
Returns:
| Type | Description |
|---|---|
CutCSG
|
Cylinder representing the extended geometry in local coordinates |
Source code in kumiki/timber.py
MeshTimber
dataclass
¶
MeshTimber(length: Numeric, size: V2, transform: Transform, ticket: TimberTicket = TimberTicket())
Bases: PerfectTimberWithin
Timber represented by an arbitrary mesh geometry
This timber type uses a mesh CSG to represent complex or irregular timber geometries that cannot be represented by simple primitives.
TODO: Add mesh_csg field and override get_actual_csg_local()
get_rough_half_sizes
¶
can_be_extended_for_joints
¶
get_extended_actual_csg_local
¶
get_extended_actual_csg_local(extend_bot: bool, extend_top: bool) -> CutCSG
Returns the actual CSG geometry extended to infinity at specified ends.
For MeshTimber, this returns a rectangular prism using the perfect timber within size (the bounding box). Note: MeshTimber cannot be extended for joints.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
extend_bot
|
bool
|
If True, extend to -infinity at bottom (z=0) |
required |
extend_top
|
bool
|
If True, extend to +infinity at top (z=length) |
required |
Returns:
| Type | Description |
|---|---|
CutCSG
|
CutCSG representing the extended geometry in local coordinates |
Source code in kumiki/timber.py
RegularPolygonTimber
dataclass
¶
RegularPolygonTimber(length: Numeric, size: V2, transform: Transform, ticket: TimberTicket = TimberTicket(), *, num_sides: int)
Bases: PerfectTimberWithin
Timber with regular polygonal cross-section
This timber type has a polygonal (non-rectangular) cross-section that is extruded along the length axis. Examples include hexagonal or octagonal timbers.
The polygon is inscribed in a circle with radius equal to half the minimum dimension of the rough bounding box.
is_perfect_timber
¶
get_rough_half_sizes
¶
Returns the rough half-sizes of the polygon timber.
For polygon extrusion timbers, these are symmetric halves of the rectangular bounding box.
Returns:
| Type | Description |
|---|---|
Tuple[V2, V2]
|
Tuple of (V2(w/2, w/2), V2(h/2, h/2)) |
Source code in kumiki/timber.py
get_actual_csg_local
¶
get_actual_csg_local() -> CutCSG
Returns the actual CSG geometry for this timber.
For RegularPolygonTimber, this returns a ConvexPolygonExtrusion with the specified number of sides.
Returns:
| Type | Description |
|---|---|
CutCSG
|
ConvexPolygonExtrusion representing the actual geometry in local coordinates |
Source code in kumiki/timber.py
get_extended_actual_csg_local
¶
get_extended_actual_csg_local(extend_bot: bool, extend_top: bool) -> CutCSG
Returns the actual CSG geometry extended to infinity at specified ends.
For RegularPolygonTimber, this returns a ConvexPolygonExtrusion optionally extended to infinity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
extend_bot
|
bool
|
If True, extend to -infinity at bottom (z=0) |
required |
extend_top
|
bool
|
If True, extend to +infinity at top (z=length) |
required |
Returns:
| Type | Description |
|---|---|
CutCSG
|
ConvexPolygonExtrusion representing the extended geometry in local coordinates |
Source code in kumiki/timber.py
CutTimber
¶
CutTimber(timber: PerfectTimberWithin, cuts: Optional[List[Cutting]] = None, joints: Optional[List[Joint]] = None)
A timber with cuts applied to it.
Create a CutTimber from a Timber.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
PerfectTimberWithin
|
The timber to be cut |
required |
cuts
|
Optional[List[Cutting]]
|
List of cuts to apply (default: empty list) |
None
|
joints
|
Optional[List[Joint]]
|
Joints this timber participates in (default: empty list). Populated by the from_joints constructors. Anything asking "which joint produced this cut?" reads it, so a CutTimber built by hand simply cannot answer that -- which is the honest outcome, since by hand there is no joint to name. |
None
|
Source code in kumiki/timber.py
resolve_joint_path
¶
Which of this timber's joints a name refers to.
A list, for the same reason Frame.resolve_timber_path returns one: two identical joints on one timber -- both ends of a brace -- share a name, and pretending a name means one joint quietly picks whichever was cut first. Where it does match several the reference stops being stable, so it warns.
Counted over this timber's cuts, in order, which is what the cut labels and the CSG paths below them are numbered by.
Source code in kumiki/timber.py
from_joints
classmethod
¶
from_joints(timber: PerfectTimberWithin, joints: List[Joint]) -> CutTimber
Build a CutTimber for timber by collecting every Cutting across joints
whose Cutting.timber is this exact timber (matched by identity -- the same
matching Frame.from_joints uses to merge cuttings for a timber across the
whole frame).
Useful when a joint function needs "this timber's actual body so far" (e.g.
cut_free_house_joint's housed_timbers) but the timber has cuts from more than
one joint (e.g. a corner miter plus a roundover decoration): rather than
manually picking which Joint.cuttings key belongs to which timber (easy to
mix up -- see cuttings["timberA"] vs cuttings["timberB"]), this collects
every relevant cutting automatically, in the order joints are given.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
PerfectTimberWithin
|
The timber to build a CutTimber for |
required |
joints
|
List[Joint]
|
Joints to search for cuttings on |
required |
Returns:
| Type | Description |
|---|---|
CutTimber
|
CutTimber wrapping |
Source code in kumiki/timber.py
render_timber_with_cuts_csg_local
¶
render_timber_with_cuts_csg_local() -> CutCSG
Returns a CSG representation of the timber with all cuts applied.
Returns:
| Type | Description |
|---|---|
CutCSG
|
Difference CSG representing the timber with all cuts subtracted |
Source code in kumiki/timber.py
get_perfect_timber_within_bounding_box_prism
¶
get_perfect_timber_within_bounding_box_prism() -> RectangularPrism
Get the bounding box prism for this timber cropped based on its end cuts if any, otherwise the original perfet timber within box is produced. The bounding box is aligned with the timber's orientation.
Uses PerfectTimberWithin size to determine the cross-sectional size of the bounding box. Uses the end cuts (maybe_top_end_cut and maybe_bottom_end_cut) to determine the extent of the timber along its length. For skewed end cuts, finds where the plane intersects the four long edges of the timber and takes the max/min.
Returns:
| Name | Type | Description |
|---|---|---|
RectangularPrism |
RectangularPrism
|
The bounding box for the cut timber in global coordinates |
Source code in kumiki/timber.py
get_rough_bounding_box_prism
¶
get_rough_bounding_box_prism() -> RectangularPrism
Get the bounding box prism for this timber's ROUGH (as-sawn) cross-section, cropped in length the same way as get_perfect_timber_within_bounding_box_prism (the most restrictive end cut across every Cutting on this timber -- the frame's aggregated outer length trims, not each joint's own internal cut geometry).
Unlike the perfect-timber-within box, the rough box may be off-center from the timber's centerline (see get_rough_half_sizes -- e.g. for square-rule layout) and is generally larger than the perfect/finished size.
Returns:
| Name | Type | Description |
|---|---|---|
RectangularPrism |
RectangularPrism
|
The rough bounding box for the cut timber, in global coordinates |
Source code in kumiki/timber.py
get_bounding_box_prism
¶
get_bounding_box_prism() -> RectangularPrism
DEPRECATED_approximate_bounding_prism
¶
DEPRECATED_approximate_bounding_prism() -> RectangularPrism
TODO someday we want a fully analytical solution for this, but for now this is sufficient for our needs.
Get the bounding box prism for this timber including all its cuts. The bounding box is aligned with the timber's orientation.
Uses a hybrid approach: analytical methods for simple cases (HalfSpace cuts), and sampling for complex CSG operations. Works with all CSG types and orientations.
Returns:
| Name | Type | Description |
|---|---|---|
RectangularPrism |
RectangularPrism
|
The bounding box for the cut timber in global coordinates |
Source code in kumiki/timber.py
2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 | |
Accessory
dataclass
¶
Accessory(*, ticket: AccessoryTicket = AccessoryTicket(), assembly_freedom: Optional[AssemblyFreedom] = None, assembly_ordering: Ordering = Ordering())
Bases: ABC
Base class for joint accessories like wedges, drawbores, etc.
ticket
class-attribute
instance-attribute
¶
ticket: AccessoryTicket = field(default_factory=AccessoryTicket, kw_only=True)
assembly_freedom
class-attribute
instance-attribute
¶
assembly_freedom: Optional[AssemblyFreedom] = field(default=None, kw_only=True)
assembly_ordering
class-attribute
instance-attribute
¶
get_csg_local
abstractmethod
¶
get_csg_local() -> CutCSG
Generate CSG representation of the accessory in local space.
The local space is defined by the accessory's orientation and position, where the CSG is generated at the origin with identity orientation.
Returns:
| Name | Type | Description |
|---|---|---|
CutCSG |
CutCSG
|
The CSG representation of the accessory in local space |
Source code in kumiki/timber.py
PegShape
¶
Peg
dataclass
¶
Peg(transform: Transform, size: Numeric, shape: PegShape, forward_length: Numeric, stickout_length: Numeric, *, ticket: AccessoryTicket = AccessoryTicket(), assembly_freedom: Optional[AssemblyFreedom] = None, assembly_ordering: Ordering = Ordering())
Bases: Accessory
Represents a peg used in timber joinery (e.g., draw bore pegs, komisen).
The peg is stored in GLOBAL SPACE with absolute position and orientation. In identity orientation, the peg points in the +Z direction, with the insertion end at the origin.
By convention, the origin of the peg is on the mortise face that the peg is going into. This is why there are 2 lengths parameters, one for how deep the peg goes past the mortise face, and one for how far the peg sticks out of the mortise face.
Attributes:
| Name | Type | Description |
|---|---|---|
transform |
Transform
|
Transform (position and orientation) of the peg in global space |
size |
Numeric
|
Size/diameter of the peg (for square pegs, this is the side length) |
shape |
PegShape
|
Shape of the peg (SQUARE or ROUND) |
forward_length |
Numeric
|
How far the peg reaches in the forward direction (into the mortise) |
stickout_length |
Numeric
|
How far the peg "sticks out" in the back direction (outside the mortise) |
get_csg_local
¶
get_csg_local() -> CutCSG
Generate CSG representation of the peg in local space.
The peg is centered at the origin with identity orientation, extending from -stickout_length to forward_length along the Z axis.
Returns:
| Name | Type | Description |
|---|---|---|
CutCSG |
CutCSG
|
The CSG representation of the peg |
Source code in kumiki/timber.py
WedgeShape
dataclass
¶
Wedge
dataclass
¶
Wedge(transform: Transform, base_width: Numeric, tip_width: Numeric, height: Numeric, length: Numeric, stickout_length: Numeric = scalar(0), *, ticket: AccessoryTicket = AccessoryTicket(), assembly_freedom: Optional[AssemblyFreedom] = None, assembly_ordering: Ordering = Ordering())
Bases: Accessory
Represents a wedge used in timber joinery (e.g., wedged tenons).
The wedge is stored in local space of a timber. In identity orientation, the pointy end of the wedge goes in the length direction of the timber.
The profile of the wedge (trapezoidal shape) is in the Y axis (height in Y). The width of the wedge is in the X axis. The origin (0,0) is at the bottom center of the longer side of the triangle.
Visual representation (looking at wedge from the side):
+z
_ <- tip width
/ \ \
/ \ \ +y
-x /____\ <- base width
↑
origin
get_csg_local
¶
get_csg_local() -> CutCSG
Generate CSG representation of the wedge in local space.
The wedge is created using a polyline extrusion (ConvexPolygonExtrusion) with a trapezoidal profile in the XZ plane. The base is at z=0 with base_width, and the tip is at z=length with tip_width. The extrusion extends along Y from -height/2 to height/2.
The polygon profile is a trapezoid in the XZ plane: - Base at z=0 with width = base_width (centered at x=0) - Tip at z=length with width = tip_width (centered at x=0)
The transform is rotated so that +Y goes to +Z (rotation around X axis by +90°).
Returns:
| Name | Type | Description |
|---|---|---|
CutCSG |
CutCSG
|
The CSG representation of the wedge |
Source code in kumiki/timber.py
2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 | |
CSGAccessory
dataclass
¶
CSGAccessory(transform: Transform, positive_csg: CutCSG, *, ticket: AccessoryTicket = AccessoryTicket(), assembly_freedom: Optional[AssemblyFreedom] = None, assembly_ordering: Ordering = Ordering())
Bases: Accessory
Generic accessory represented as local-space positive CSG plus a global transform.
Sticker
dataclass
¶
Sticker(transform: Transform, size: Numeric = inches(1), *, ticket: AccessoryTicket = AccessoryTicket(), assembly_freedom: Optional[AssemblyFreedom] = None, assembly_ordering: Ordering = Ordering())
Bases: Accessory
Just a marking used for debugging (ball at center + shaft in local +Z).
get_csg_local
¶
get_csg_local() -> CutCSG
Source code in kumiki/timber.py
Joint
dataclass
¶
Joint(cuttings: Dict[str, Cutting], ticket: JointTicket, jointAccessories: Dict[str, Accessory] = dict())
jointAccessories
class-attribute
instance-attribute
¶
jointAccessories: Dict[str, Accessory] = field(default_factory=dict)
__post_init__
¶
Source code in kumiki/timber.py
is_decorative
¶
with_order
¶
with_order(order: Union[int, Mapping[str, int], Iterable[Tuple[Union[str, PerfectTimberWithin, Accessory], int]]]) -> Joint
Return a copy of this joint with assembly order(s) assigned.
Assembly freedoms and suborders are authored by the cut functions; the order is the frame-level plan and is assigned here, after cutting (smaller order = extracted earlier during disassembly).
with_order(n): sets order=n on every cutting and accessory, keeping their suborders, so intra-joint sequencing (peg pops before the tenon slides) is preserved within step n.
with_order({key: n, ...}) or with_order([(member, n), ...]): sets Ordering(n, 0) on each named member — referenced by cutting/accessory string key, or by the timber / accessory object itself (a timber reference applies to every cutting holding it; use the pair-list form for object references, which are unhashable). Unnamed members keep their current ordering. Raises ValueError for unknown references, or when the new orderings break the strict precedence the cut function expressed via suborders (any member pair previously strictly ordered must remain strictly ordered).
Assign orders BEFORE building the Frame: this rebuilds the member objects (dataclasses.replace, preserving timber references), so a Frame built earlier would still hold the previous orderings.
Source code in kumiki/timber.py
2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 | |
Frame
dataclass
¶
Frame(cut_timbers: List[CutTimber], accessories: List[Accessory] = list(), name: Optional[str] = None, source_joints: Optional[List] = None, footprints: List[Footprint] = list(), drawings: List[Drawing] = list())
Represents a complete timber frame structure with all cut timbers and accessories.
In traditional timber framing, a 'frame' is the complete structure ready for raising. This class encapsulates all the timbers that have been cut with their joints, plus any accessories like pegs, wedges, or drawbores.
Attributes:
| Name | Type | Description |
|---|---|---|
cut_timbers |
List[CutTimber]
|
List of CutTimber objects representing all timbers in the frame |
accessories |
List[Accessory]
|
List of Accessory objects (already in global space) |
name |
Optional[str]
|
Optional name for this frame (e.g., "Oscar's Shed", "Main Frame") |
accessories
class-attribute
instance-attribute
¶
accessories: List[Accessory] = field(default_factory=list)
source_joints
class-attribute
instance-attribute
¶
footprints
class-attribute
instance-attribute
¶
footprints: List[Footprint] = field(default_factory=list)
resolve_timber_path
¶
resolve_timber_path(path: TimberPath) -> List[ResolvedTimberPath]
Which timbers a name refers to, in this frame.
A list, because a name may match several: paths are not required to be unique, and pretending one always means one timber would quietly pick whichever came first. Where it does match several, the reference stops being stable -- each is then told apart by the order the frame built them, so inserting another above them moves every reference below. That is worth saying out loud rather than discovering later, so it warns.
Source code in kumiki/timber.py
timber_paths
¶
timber_paths() -> List[TimberPath]
Every name in the frame, in order, duplicates included.
from_joints
classmethod
¶
from_joints(joints: List[Joint], additional_unjointed_timbers: Optional[List[PerfectTimberWithin]] = None, name: Optional[str] = None) -> Frame
Create a Frame from a list of joints and optional additional unjointed timbers.
This constructor extracts all cut timbers and accessories from the joints, and combines cut timbers that share the same underlying timber reference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
joints
|
List[Joint]
|
List of Joint objects |
required |
additional_unjointed_timbers
|
Optional[List[PerfectTimberWithin]]
|
Optional list of PerfectTimberWithin objects that don't participate in any joints (default: empty list) |
None
|
name
|
Optional[str]
|
Optional name for the frame |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
Frame |
Frame
|
A new Frame object with merged cut timbers and collected accessories |
Raises:
| Type | Description |
|---|---|
ValueError
|
If two timbers with the same name but same underlying timber have different references (indicates a bug) |
Source code in kumiki/timber.py
2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 | |
get_bounding_box
¶
Get the axis-aligned bounding box for the entire frame in global coordinates.
This computes the bounding box by getting the bounding prism for each cut timber and finding the global min/max coordinates that enclose all of them.
Returns:
| Type | Description |
|---|---|
tuple[V3, V3]
|
tuple[V3, V3]: (min_corner, max_corner) where each is a 3x1 Matrix representing the minimum and maximum corners of the axis-aligned bounding box in global coordinates |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the frame contains no cut timbers |
Source code in kumiki/timber.py
2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 | |
KumikiArrangementError
¶
Bases: ValueError
Raised when a timber arrangement or joint parameter fails a validation check.
Unlike AssertionError, this survives python -O and is safe for callers to
catch specifically when handling invalid joint/arrangement configurations.
Footprint
dataclass
¶
A support class representing the footprint of the structure in the XY plane
__post_init__
¶
Validate corners. Args: corners: Tuple of points defining the corners, last point connects to first
Source code in kumiki/footprint.py
sides
¶
Returns a list of sides (line segments) connecting consecutive corners.
Returns:
| Type | Description |
|---|---|
List[Tuple[V2, V2]]
|
List of tuples, each containing two points (start, end) representing a side |
Source code in kumiki/footprint.py
is_valid
¶
Checks if the footprint is valid. A valid footprint has at least 3 corners and no intersecting sides.
Returns:
| Type | Description |
|---|---|
bool
|
True if valid, False otherwise |
Source code in kumiki/footprint.py
contains_point
¶
contains_point(point: V2) -> bool
Check if a point is contained within the footprint boundary using ray casting algorithm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V2
|
2D point to check |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if point is inside or on the boundary, False otherwise |
Source code in kumiki/footprint.py
nearest_corner
¶
Find the nearest corner to a given point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V2
|
2D point to measure from |
required |
Returns:
| Type | Description |
|---|---|
Tuple[int, V2]
|
Tuple of (index, corner) where index is the corner index and corner is the V2 point |
Source code in kumiki/footprint.py
nearest_boundary
¶
Find the nearest side (line segment) to a given point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V2
|
2D point to measure from |
required |
Returns:
| Type | Description |
|---|---|
Tuple[int, Tuple[V2, V2], Numeric]
|
Tuple of (index, side, distance) where: - index is the side index - side is a tuple (start_corner, end_corner) - distance is the perpendicular distance to the side |
Source code in kumiki/footprint.py
get_inward_normal
¶
get_inward_normal(side_index: int) -> Direction3D
Get the inward-pointing normal vector for a boundary side.
The inward normal is perpendicular to the boundary side and points toward the interior of the footprint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
side_index
|
int
|
Index of the boundary side |
required |
Returns:
| Type | Description |
|---|---|
Direction3D
|
Direction3D representing the normalized 3D inward normal vector |
Source code in kumiki/footprint.py
nearest_boundary_from_line
¶
Find the nearest boundary side to a given line segment.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
line_start
|
V2
|
Start point of the line segment (2D) |
required |
line_end
|
V2
|
End point of the line segment (2D) |
required |
Returns:
| Type | Description |
|---|---|
Tuple[int, Tuple[V2, V2], Numeric]
|
Tuple of (index, side, distance) where: - index is the side index - side is a tuple (start_corner, end_corner) - distance is the minimum distance between the line segment and the boundary |
Source code in kumiki/footprint.py
FootprintLocation
¶
Line
dataclass
¶
Line(direction: Direction3D, point: V3)
Represents an oriented, infinite line with origin in 3D space.
BoundingBox
dataclass
¶
Axis-aligned bounding box (AABB) for a CSG object.
Each bound is Optional[Numeric] where None means unbounded in that direction.
When is_empty is True, the CSG object contains no points at all (e.g. EmptyCSG, or a union/intersection that reduces to nothing). The bound fields are meaningless in this case (by convention all set to 0) and must not be treated as a real zero-size box at the origin — check is_empty first.
FeatureCategory
¶
Bases: Enum
What kind of place on a primitive's boundary a default feature names.
One vocabulary across every primitive for simplicity. OK to add primitive specific keys here rather than reuse.
Some will be paired with an index, others may be one offs (index 0)
PrismFace
¶
Bases: Enum
Face of a RectangularPrism, indices match TimberFace.
ExtrusionCap
¶
CylinderPart
¶
Bases: Enum
Which surface of a Cylinder a feature is on.
BARREL is the curved lateral surface. Unlike a prism's four sides it is a single feature, not four -- there is no non-arbitrary way to cut it up, and nothing in joinery wants to reference "a quarter of a peg hole wall".
CSGFeatureType
¶
Bases: Enum
What kind of geometry a feature names.
The three cases measurement cares about: measuring between two features dispatches on this pair (two parallel faces measure like two parallel planes, a point and a face measure a projected distance, and so on).
Everything nameable on a primitive today is a FACE. EDGE arrives with features derived from intersecting face pairs; POINT with their vertices.
FeatureTestTolerances
dataclass
¶
How close a point must be to count as on a feature, per feature type.
Not epsilons: an epsilon absorbs float error, while these absorb the gap between meshed and analytic geometry and the imprecision of a human click. They are several orders of magnitude larger than EPSILON_GENERIC and are chosen, not derived.
One tolerance does not fit all three, and the reason is about how features get selected rather than about the geometry:
- a FACE you click directly, so the only slack needed is the gap between the analytic surface and the triangulated mesh a raycast actually hits;
- an EDGE or a POINT you cannot click exactly at all. Selecting one means snapping to it, the way any CAD package works, so they want considerably more room -- and a caller driving this from a viewport usually wants to derive theirs from screen space, or a line is unhittable zoomed out and greedy zoomed in.
This replaces the earlier pair of eps / snap_eps parameters, which
keyed the wider tolerance off real instead. Type is the better key: a
real derived edge is just as unclickable as a non-real centre axis.
for_type
¶
for_type(feature_type: CSGFeatureType) -> Numeric
The test tolerance for a feature of feature_type.
Source code in kumiki/cutcsg.py
__mul__
¶
__mul__(factor: Numeric) -> FeatureTestTolerances
Scale every tolerance by factor.
The reason this exists is camera zoom. Selecting an edge or a point is a snap, and how much slack a snap needs is a screen-space question: a fixed 2mm is a comfortable target zoomed in and an invisible one zoomed out. A viewport can hold one FeatureTestTolerances describing the tolerances at some reference zoom and scale it by world-units-per-pixel per query.
Source code in kumiki/cutcsg.py
__rmul__
¶
__rmul__(factor: Numeric) -> FeatureTestTolerances
__truediv__
¶
__truediv__(divisor: Numeric) -> FeatureTestTolerances
uniform
staticmethod
¶
uniform(eps: Numeric) -> FeatureTestTolerances
exact
staticmethod
¶
exact() -> FeatureTestTolerances
FeatureGroup
¶
Bases: Enum
Which other features a feature is allowed to form an edge with.
Deriving edges from every pair of faces in a CSG tree produces mostly nonsense -- a tenon cheek and the far end of the timber do not meet. Groups make the useful pairs declarable instead of searched for:
A intersects with B1 and B2
B1 intersects with A only
B2 intersects with A, and with itself
C intersects with itself only
NONE is the exception to the scheme: it meets nothing, not even itself, and is how a feature says it forms no edges at all. Some geometry is worth naming and pointing at without every face of it turning into an arris.
Defaults today: a timber's perfect-timber-within and rough faces are B2, and every named joint feature is A -- so joint geometry meets the timber body, and the body meets itself, the latter being the timber's own four long arrises, which drawing generation needs. B1 and C are defined but unused until something needs them.
A consequence of the body meeting itself: relief geometry embeds the MATING timber's rough body to scribe against, and its faces carry the same reserved rough.* names (see timber.ROUGH_FACE_PREFIX). Two timbers' faces then pair into an edge that reads as one timber's -- rough.back x rough.back -- since the name says nothing about whose body it is.
FeatureMarkingStatus
¶
Bases: Enum
Whether a feature has to appear on a drawing.
TODO integrate: declared and carried on every feature, but nothing reads it yet -- the drawing generator does not consult it when deciding what to mark.
FeatureMarkingSpec
dataclass
¶
How a feature should be marked, when that differs from the default.
mark_relative_to names the feature a dimension should be measured from, which is how a drawing says "38mm from the shoulder" rather than giving an absolute position. None leaves that to whatever generates the drawing.
TODO integrate: nothing sets marking_override and nothing reads it, so a joint cannot yet say how it wants to be dimensioned.
mark
class-attribute
instance-attribute
¶
FeaturePurpose
¶
Bases: Enum
What purpose the feature serves.
TODO integrate: ROUGH_RELIEF is never set and never tested against, so relief geometry is still indistinguishable from a joint's real surfaces everywhere it matters -- picking, measuring and drawing alike.
FeatureProperties
dataclass
¶
Metadata every feature carries, independent of how it is identified.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group
|
which other features this one may form an edge with. NONE by default, so a feature pairs with nothing unless someone says it should. Deriving an edge is the expensive, noisy thing the feature system does -- every pairing is a line that has to be worth selecting -- so it is opted into rather than out of. Today the only pairing anyone wants is a shoulder plane against the timber's own prism. |
required | |
priority
|
lower wins when several features claim the same point. |
required | |
real
|
False for a feature that names no actual surface (a bore's centre axis, a reference plane). Real features can be cropped away by the CSG tree and so are tested against the triangulated result first; non-real ones are unaffected by boolean operations. |
required | |
marking_override
|
how to mark this feature on a drawing, when the default for its kind is not what is wanted. None means the default. TODO integrate: carried, never read. |
required | |
purpose
|
what the feature is for, where that is worth recording -- relief geometry is not a feature of the joint the way a tenon cheek is. TODO integrate: carried, never read. |
required |
marking_override
class-attribute
instance-attribute
¶
marking_override: Optional[FeatureMarkingSpec] = None
CSGFeatureExtent
dataclass
¶
Roughly where a feature is, for placing annotations against it.
Separate from locate(): that gives the unbounded geometry a measurement is computed on, this says where to actually draw the thing. Approximate is fine -- a dimension line only needs somewhere sensible to attach.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
anchor
|
a representative point -- a face's centre, an edge's midpoint, or the point itself. |
required | |
ends
|
for an edge, its two endpoints. |
required | |
aabb
|
for a face, a rough bounding box. |
required |
CSGFeature
dataclass
¶
Bases: ABC
A named region of a CutCSG's boundary -- a face today, edges and points later.
A feature is stored on the primitive it belongs to and does NOT hold a reference back to it: the owner is passed in to every method that needs geometry. That keeps a feature constructible before its owner exists (which it must be, to be passed to the owner's constructor) and means there is one feature type rather than a stored declaration plus a resolved copy.
Because a feature alone does not know where it lives, queries hand back a OwnedFeatureHit pairing it with the primitive that matched.
Subclasses say how the feature is identified: by an enum member for the simple per-primitive cases, or by an arbitrary predicate for ProgrammableCSGFeature.
properties
class-attribute
instance-attribute
¶
properties: FeatureProperties = field(default_factory=FeatureProperties)
feature_key
¶
feature_key() -> Optional[FeatureKey]
Which default slot this feature occupies, or None if it has none.
This is what lets an authored feature REPLACE the default at the same place rather than sit alongside it. None is the honest answer for anything with no fixed place on a primitive -- a ProgrammableCSGFeature matching a formula, or a derived edge, which exists only as the product of two hits and never occupies a slot of its own.
Source code in kumiki/cutcsg.py
feature_type
abstractmethod
¶
feature_type() -> CSGFeatureType
What kind of geometry this feature names.
A method rather than a field so it cannot be set to something the feature is not: a face feature has no way to claim it is an edge. Subclasses that name one kind by construction return a constant; only a feature whose kind genuinely varies stores one.
Kept off FeatureProperties deliberately -- this says what the feature is, while properties say how it should be treated.
Source code in kumiki/cutcsg.py
locate
¶
locate(owner: CutCSG) -> Optional[LocatedGeometry]
The unbounded geometry this feature lies on, in the owner's space.
A Plane for a planar face, a Line for an edge, a Point for a vertex.
None when the feature names a surface that is not one of those -- a cylinder's barrel, a lofted side, an extrusion side that follows a curved path segment. Those are perfectly good features to select and highlight; there is just no single plane to measure against, so measurement has to decline rather than invent one.
Note the space: the CSG tree is timber-local, so this is too. Anything comparing features across timbers has to lift both through the timber transform first.
Source code in kumiki/cutcsg.py
get_extent
¶
get_extent(owner: CutCSG) -> Optional[CSGFeatureExtent]
Roughly where this feature sits, for placing annotations.
None when the feature has no bounded extent at all (a half-space's plane), or when it is not worked out for this shape yet.
Source code in kumiki/cutcsg.py
test_point_unbounded
abstractmethod
¶
Whether point lies on this feature's SURFACE, unbounded.
Unbounded is in the name because it is half a test and reads as a whole one. A face feature answers for the face's whole PLANE: the RIGHT face of a prism is x == half_width and nothing about y or z, so it says yes a metre off the end of the timber. An edge built from two of these says yes all the way along its line.
Primitive level, and deliberately so: no root node is involved, so this cannot know what the rest of the tree did to owner. The other half comes from collect_feature_hits, which gates every real feature on is_point_on_boundary of the node declaring it, and from each enclosing compound node, which gates again on its own boundary. On the plane AND on the boundary means on the face, so a point query IS bounded -- by composition, at every level of the tree.
CONSIDERED: folding the bound in here, so this stands on its own. Decided against, for now:
- it would not remove the gate. A face buried inside a sibling union is still on its own primitive's boundary, so the compound levels have to keep checking regardless.
- it costs more. The gate is computed once per node and shared by every feature that node declares; bounding each feature separately does the same work per feature -- ten times over for a prism with six faces and four arrises, for the same answer.
- it duplicates the primitive's own extent inside every feature sitting on it, which is the kind of thing that drifts apart.
- it fixes nothing that is broken. The bug this looks like it would fix -- a highlight running past the end of an edge -- is an EXTENT question, and no point test answers that however well bounded. That is what crop_line_to_segments_on_csg is for.
What it would buy is safety for a caller that uses this on its own, which today means one: the mesh-vertex fallback in kigumi's runner, already marked for deletion. If that stops being the only one, revisit.
ALSO CONSIDERED: two more optional arguments, a surface normal and a line, so a caller that knows more about the point can say so. Several features can claim one point -- two coincident parallel faces, or the several that meet at a corner -- and a normal would tell them apart where the point alone cannot.
Not done, because the caller cannot honestly supply either one for the case that wants them most. Selecting an EDGE is the case: you would get the edge's own line only by clicking exactly on a triangle edge of the mesh, which is the one thing a human click never does. Every other click lands on a triangle's face, so what is actually available is that triangle's normal and a line lying on ONE of the two faces forming the edge -- which is not the edge, and answers a slightly different question with total confidence. Better nothing than that.
Worth revisiting if a picker ever hands back the analytic surface it hit rather than the triangle, since then both arguments mean what they say.
Source code in kumiki/cutcsg.py
ProgrammableCSGFeature
dataclass
¶
Bases: CSGFeature
A feature identified by an arbitrary predicate rather than an enum member.
The escape hatch for anything the simple per-primitive classes cannot name: a formula-defined region, half of a face, an edge derived from two other features. Works on any primitive, since the owner is just an argument.
The predicate is called only for points already known to be on the owner's boundary, and receives the same eps the query was made with.
predicate
class-attribute
instance-attribute
¶
declared_type
class-attribute
instance-attribute
¶
declared_type: CSGFeatureType = CSGFeatureType.FACE
feature_type
¶
feature_type() -> CSGFeatureType
test_point_unbounded
¶
DerivedEdgeFeature
dataclass
¶
Bases: CSGFeature
The edge where two face features meet.
Built rather than authored: joints declare faces, and the edges between
them fall out of which faces are allowed to meet (see FeatureGroup). Use
derive() rather than constructing directly -- it applies the group rules,
rejects pairs that form no edge, and names the result deterministically.
The two parents generally live on different primitives (a tenon cheek and
the timber body, say), so each is carried with its own owner. The owner
passed to this feature's own methods is the compound node that contains
both, and is unused here -- the geometry comes from the parents.
feature_type
¶
feature_type() -> CSGFeatureType
test_point_unbounded
¶
Source code in kumiki/cutcsg.py
locate
¶
locate(owner: CutCSG) -> Optional[LocatedGeometry]
Source code in kumiki/cutcsg.py
get_extent
¶
get_extent(owner: CutCSG) -> Optional[CSGFeatureExtent]
Where this edge sits -- only approximately, and deliberately so.
ends is None and anchor is the point on the INFINITE line closest to
the origin, which need not be anywhere near the stretch of edge that
actually exists. Harmless for picking, which only calls
test_point_unbounded, and
not good enough to hang a dimension line off.
Measurement does the cropping instead, a level up where the enclosing timber is known -- this feature cannot see it, since its owner is whichever node derived it. See cropcsg.segment_on_line, called from the runner's _feature_anchor.
Source code in kumiki/cutcsg.py
derive
staticmethod
¶
derive(a: OwnedFeatureHit, b: OwnedFeatureHit) -> Optional[DerivedEdgeFeature]
The edge where a and b meet, or None if they form none.
None when: either is not a face; their groups are not allowed to meet; either names a face that is not THERE; or their planes are parallel (which includes being the same plane -- coincident faces share a whole plane, not a line).
Not planar is a different thing from not there, and only the second stops an edge existing. A cylinder's barrel and a lofted side are real surfaces with no single plane, and the edge where one meets a flat face is real too -- pickable, just not measurable as a line, which is what locate() returning None means for it. The top of a prism extended to infinity is not a surface at all, and an edge against it is nothing.
It takes both questions to tell those apart, because each alone gets one of them wrong. A barrel has no plane but has an extent; a half space has a plane but no extent, being unbounded; a face that is not there has neither. So neither answer on its own means absent -- both do.
Source code in kumiki/cutcsg.py
HalfSpaceFeature
dataclass
¶
Bases: CSGFeature
The entire boundary plane of a HalfSpace.
A half-space has exactly one face, so this needs no key to say which.
feature_key
¶
feature_key() -> Optional[FeatureKey]
feature_type
¶
feature_type() -> CSGFeatureType
locate
¶
locate(owner: CutCSG) -> Optional[LocatedGeometry]
Source code in kumiki/cutcsg.py
test_point_unbounded
¶
SimpleRectangularPrismFeature
dataclass
¶
Bases: CSGFeature
One of the six faces of a RectangularPrism, named by PrismFace.
feature_key
¶
feature_key() -> Optional[FeatureKey]
feature_type
¶
feature_type() -> CSGFeatureType
get_extent
¶
get_extent(owner: CutCSG) -> Optional[CSGFeatureExtent]
Source code in kumiki/cutcsg.py
test_point_unbounded
¶
Source code in kumiki/cutcsg.py
SimpleRectangularPrismEdgeFeature
dataclass
¶
Bases: CSGFeature
An arris of a RectangularPrism, named by the two faces it lies between.
Declared rather than derived, which is the difference that matters. A derived edge exists only as the product of two face hits at a query point, so it cannot be referred to afterwards by name and its identity depends on both parents surviving. An arris a timber simply HAS is a thing to name once, and then to measure to for as long as the timber has it.
The two faces must actually meet: opposite faces are parallel and share no line, and asking for that pair gets None from locate() rather than an invented answer.
faces
class-attribute
instance-attribute
¶
feature_type
¶
feature_type() -> CSGFeatureType
feature_key
¶
feature_key() -> Optional[FeatureKey]
The arris between two adjacent sides, or None between a side and a cap.
The low run of ARRIS is side n against side n+1, so only an adjacent PAIR of sides has one there; a side against a cap lands further along the same run, and opposite sides never meet at all.
Source code in kumiki/cutcsg.py
test_point_unbounded
¶
Source code in kumiki/cutcsg.py
locate
¶
locate(owner: CutCSG) -> Optional[LocatedGeometry]
The line the two faces meet in, or None if they never do.
get_extent
¶
get_extent(owner: CutCSG) -> Optional[CSGFeatureExtent]
Where the arris sits -- only approximately, as for a derived edge.
ends is None and anchor is the point on the INFINITE line closest to
the origin. Cropping it to the timber is measurement's job, a level up
where the enclosing solid is known; see cropcsg.segment_on_line.
Source code in kumiki/cutcsg.py
SimpleCylinderFeature
dataclass
¶
Bases: CSGFeature
One surface of a Cylinder: an end cap, or the barrel.
feature_type
¶
feature_type() -> CSGFeatureType
locate
¶
locate(owner: CutCSG) -> Optional[LocatedGeometry]
Source code in kumiki/cutcsg.py
get_extent
¶
get_extent(owner: CutCSG) -> Optional[CSGFeatureExtent]
Source code in kumiki/cutcsg.py
test_point_unbounded
¶
Source code in kumiki/cutcsg.py
SimpleConvexPolygonExtrusionFeature
dataclass
¶
Bases: CSGFeature
One side face (points[key] -> points[key+1 mod n]) or end cap of a ConvexPolygonExtrusion.
feature_key
¶
feature_key() -> Optional[FeatureKey]
feature_type
¶
feature_type() -> CSGFeatureType
get_extent
¶
get_extent(owner: CutCSG) -> Optional[CSGFeatureExtent]
Source code in kumiki/cutcsg.py
test_point_unbounded
¶
Source code in kumiki/cutcsg.py
SimpleLoftFeature
dataclass
¶
Bases: CSGFeature
One side face or end cap of a ConvexPolygonSimpleLoft.
Side faces are ruled surfaces and are only planar in the special case of a pure per-axis taper, so a named side is a surface, not necessarily a plane. Edge derivation (which assumes planes) has to account for that.
feature_key
¶
feature_key() -> Optional[FeatureKey]
feature_type
¶
feature_type() -> CSGFeatureType
locate
¶
locate(owner: CutCSG) -> Optional[LocatedGeometry]
Source code in kumiki/cutcsg.py
get_extent
¶
get_extent(owner: CutCSG) -> Optional[CSGFeatureExtent]
Source code in kumiki/cutcsg.py
test_point_unbounded
¶
Source code in kumiki/cutcsg.py
FeatureSource
¶
Bases: Flag
Which features a query is asking for.
DEFAULTS are what a primitive names on its own, in FeatureKey slots. OVERRIDES are what an author handed it: a replacement for a default at the same key, or a feature at a slot no default occupies. BOTH is the answer to "what does this shape name", which is what nearly every caller wants.
HasFeatures
dataclass
¶
Storage for the features a primitive names on its own boundary.
A mixin rather than a field on CutCSG, because a compound node names nothing: a SolidUnion, Difference or Intersection has no surface of its own, only the surfaces its children contribute. Only the primitives that have a boundary of their own inherit this.
Six primitives carried an identical copy of the field and its accessor before this existed. That is the whole reason it exists -- feature storage is one idea, and the shapes that have features differ in their geometry, not in how they hold a list.
Two layers, not one. A primitive names its own boundary through
default_features(), keyed by FeatureKey; an author overrides or adds to
that through _features. An authored feature whose key matches a default
REPLACES it, so naming a face does not leave the anonymous one behind to be
found twice.
default_features
¶
default_features() -> Dict[FeatureKey, CSGFeature]
What this primitive names on its own, keyed by where it sits.
Empty here: a shape opts in by overriding this. Whatever it returns must be in FeatureGroup.NONE -- see the note on default_features in RectangularPrism for why that matters more than it looks.
Source code in kumiki/cutcsg.py
get_declared_features
¶
get_declared_features(source: FeatureSource = BOTH) -> List[CSGFeature]
Features this node names on its own boundary, whether or not any point lies on them.
Source code in kumiki/cutcsg.py
OwnedFeatureHit
dataclass
¶
A feature, paired with the primitive it belongs to.
A CSGFeature holds no reference to its owner, so anything handing one around carries both. That covers two jobs with the same shape: what a query hands back, and how a DerivedEdgeFeature refers to the two parents it was built from -- which generally live on different primitives.
Anything needing the feature's geometry -- its plane, its extent -- needs
the owner too, so locate and get_extent are forwarded here.
feature_type
¶
feature_type() -> CSGFeatureType
locate
¶
locate() -> Optional[LocatedGeometry]
get_extent
¶
get_extent() -> Optional[CSGFeatureExtent]
CSGParity
¶
Bases: Enum
Whether a node adds material to the finished solid or takes it away.
ADDITIVE means growing that node grows the result; SUBTRACTIVE means growing it shrinks the result.
Cylinder
dataclass
¶
Bases: HasFeatures, CutCSG
A cylinder with circular cross-section, optionally infinite in one or both ends.
The cylinder is defined by: - A position (translation from origin) - An axis direction - A radius - Start and end distances along the axis from the position
So the center point of the radius cross section is at position and the cylinder extends out in -z by start_distance and +z by end_distance.
Use None for start_distance or end_distance to make the cylinder infinite in that direction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
axis_direction
|
Direction of the cylinder's axis (3x1 Matrix) |
required | |
radius
|
Radius of the cylinder |
required | |
position
|
Position of the cylinder origin in global coordinates (3x1 Matrix, default: origin) |
required | |
start_distance
|
Distance from position to start of cylinder (None = -infinite) |
required | |
end_distance
|
Distance from position to end of cylinder (None = infinite) |
required |
position
class-attribute
instance-attribute
¶
default_features
¶
default_features() -> Dict[FeatureKey, CSGFeature]
Two caps and the barrel. See RectangularPrism for why the group is NONE.
No arrises: a cylinder's rims are circles, and there is no feature class for one yet. Their slots are the two arrises against the caps, which arris_against_cap names for a shape with a single side.
Source code in kumiki/cutcsg.py
__repr__
¶
contains_point
¶
Check if a point is contained within the cylinder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is inside or on the boundary of the cylinder, False otherwise |
Source code in kumiki/cutcsg.py
is_point_on_boundary
¶
Check if a point is on the boundary of the cylinder.
A point is on the boundary if it's either: 1. On the cylindrical surface (at radius distance from axis) 2. On one of the end caps (if finite)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is on the boundary of the cylinder, False otherwise |
Source code in kumiki/cutcsg.py
get_outward_normal
¶
get_outward_normal(point: V3, eps: Optional[Numeric] = None) -> Optional[Direction3D]
Get the outward normal vector at a boundary point.
For a cylinder, the normal depends on which surface the point is on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
A point on the boundary |
required |
Returns:
| Type | Description |
|---|---|
Optional[Direction3D]
|
The outward normal vector at the point |
Source code in kumiki/cutcsg.py
get_aabb
¶
get_aabb() -> BoundingBox
Source code in kumiki/cutcsg.py
ConvexPolygonExtrusion
dataclass
¶
Bases: HasFeatures, CutCSG
An extruded Convex Polygon shape, optionally infinite in one or both ends.
The extrusion is defined by: - A list of ordered (x,y) points in the polygon (must be convex!) - A transform (position and orientation in global coordinates) - Start and end distances along the local Z-axis from the position
The polygon is in the local XY plane at the position, and the extrusion extends out in -z by start_distance and +z by end_distance.
Use None for start_distance or end_distance to make the extrusion infinite in that direction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
List of ordered (x,y) points in the polygon (last connects to first, must be convex) |
required | |
transform
|
Transform (position and orientation) in global coordinates (default: identity) |
required | |
start_distance
|
Distance from position along Z-axis to start of extrusion (None = -infinite) |
required | |
end_distance
|
Distance from position along Z-axis to end of extrusion (None = infinite) |
required |
transform
class-attribute
instance-attribute
¶
default_features
¶
default_features() -> Dict[FeatureKey, CSGFeature]
Two caps and a side per edge of the profile.
No arrises yet: SimpleRectangularPrismEdgeFeature is a prism's, and an extrusion needs its own before ARRIS n can be filled in here.
Source code in kumiki/cutcsg.py
display_name
classmethod
¶
get_bottom_position
¶
get_bottom_position() -> V3
Get the position of the bottom of the extrusion (at start_distance). Only valid for extrusions with finite start_distance.
Returns:
| Type | Description |
|---|---|
V3
|
The 3D position at the bottom of the extrusion |
Raises:
| Type | Description |
|---|---|
ValueError
|
If start_distance is None (infinite extrusion) |
Source code in kumiki/cutcsg.py
get_top_position
¶
get_top_position() -> V3
Get the position of the top of the extrusion (at end_distance). Only valid for extrusions with finite end_distance.
Returns:
| Type | Description |
|---|---|
V3
|
The 3D position at the top of the extrusion |
Raises:
| Type | Description |
|---|---|
ValueError
|
If end_distance is None (infinite extrusion) |
Source code in kumiki/cutcsg.py
__repr__
¶
is_valid
¶
Check if the ConvexPolygonExtrusion is valid
Checks: 1. At least 3 points 2. Valid distance configuration (if both finite, end > start) 3. Polygon is convex (all turns go the same direction)
Returns:
| Type | Description |
|---|---|
bool
|
True if valid, False otherwise |
Source code in kumiki/cutcsg.py
contains_point
¶
Check if a point is contained within the extruded polygon.
A point is inside if: 1. Its Z coordinate (in local space) is between start_distance and end_distance 2. Its XY coordinates (in local space) are inside the convex polygon
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is inside or on the boundary, False otherwise |
Source code in kumiki/cutcsg.py
is_point_on_boundary
¶
Check if a point is on the boundary of the extruded polygon.
A point is on the boundary if it's contained and either: 1. On the top or bottom face (z = start_distance or z = end_distance, if finite) 2. On one of the side faces (on an edge of the polygon)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is on the boundary, False otherwise |
Source code in kumiki/cutcsg.py
2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 | |
get_outward_normal
¶
get_outward_normal(point: V3, eps: Optional[Numeric] = None) -> Optional[Direction3D]
Get the outward normal vector at a boundary point.
For a convex polygon extrusion, the normal depends on which surface.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
A point on the boundary |
required |
Returns:
| Type | Description |
|---|---|
Optional[Direction3D]
|
The outward normal vector at the point |
Source code in kumiki/cutcsg.py
2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 | |
get_aabb
¶
get_aabb() -> BoundingBox
Source code in kumiki/cutcsg.py
StickoutReference
¶
Bases: Enum
Defines how stickout is measured relative to timber connection points.
Stickout measured from centerline of the timber (default)
joined timber | | |||===== created timber | |
Stickout measured from inside face of the timber
joined timber | | | |===== created timber | |
Stickout measured from outside face of the timber
joined timber | | |====== created timber | |
Stickout
¶
Defines how much a timber extends beyond connection points.
For symmetric stickout, set stickout1 = stickout2. For asymmetric stickout, use different values. Default is no stickout (0, 0) from CENTER_LINE.
StickoutReference modes:
Stickout measured from centerline of the joined timber
joined timber | | |||===== created timber | |
Stickout measured from inside face of the joined timber
joined timber | | | |===== created timber | |
Stickout measured from outside face of the joined timber
joined timber | | |====== created timber | |
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stickout1
|
Extension beyond the first connection point (default: 0) |
required | |
stickout2
|
Extension beyond the second connection point (default: 0) |
required | |
stickoutReference1
|
How stickout1 is measured (default: CENTER_LINE) |
required | |
stickoutReference2
|
How stickout2 is measured (default: CENTER_LINE) |
required |
Examples:
Symmetric stickout from centerline¶
s = Stickout.symmetric(scalar(1, 5)) # Both sides extend 0.2m from centerline
No stickout¶
s = Stickout.nostickout() # Both sides are 0
Asymmetric stickout¶
s = Stickout(scalar(1, 10), scalar(2, 5)) # Left extends 0.1m, right extends 0.4m from centerline
Stickout from outside faces¶
s = Stickout(scalar(1, 10), scalar(1, 5), StickoutReference.OUTSIDE, StickoutReference.OUTSIDE)
stickoutReference1
class-attribute
instance-attribute
¶
stickoutReference1: Optional[StickoutReference] = None
stickoutReference2
class-attribute
instance-attribute
¶
stickoutReference2: Optional[StickoutReference] = None
__post_init__
¶
Set default stickout references if not provided.
Source code in kumiki/construction.py
symmetric
classmethod
¶
symmetric(value: Numeric, reference: Optional[StickoutReference] = None) -> Stickout
Create a symmetric stickout where both sides extend by the same amount.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Numeric
|
The stickout distance for both sides |
required |
reference
|
Optional[StickoutReference]
|
How stickout is measured (default: CENTER_LINE) |
None
|
Returns:
| Type | Description |
|---|---|
Stickout
|
Stickout instance with stickout1 = stickout2 = value |
Source code in kumiki/construction.py
DoubleButtJointTimberArrangement
¶
Two butt timbers meeting a single receiving timber.
Arrangements: - Opposing: butt_timber_1 and butt_timber_2 point in opposite cardinal directions (antiparallel, like spokes from either side of the receiving timber). - Orthogonal: butt_timber_1 and butt_timber_2 point in perpendicular cardinal directions (90° apart, like an L at the receiving timber).
front_face_on_butt_timber_1
class-attribute
instance-attribute
¶
front_face_on_butt_timber_1: Optional[TimberLongFace] = None
check_face_aligned
¶
Return None if all timbers are face-aligned with the receiving timber, else an error message.
Source code in kumiki/construction.py
check_face_aligned_cardinal_and_opposing_butts
¶
Return None if: - all timbers are face-aligned, - each butt timber's length direction is orthogonal to the receiving timber (cardinal), - butt_timber_1 and butt_timber_2 are in different cardinal directions, and - the pair approaches from opposite directions (antiparallel), accounting for which end of each timber is used.
Source code in kumiki/construction.py
check_face_aligned_and_orthogonal_butts
¶
Return None if all timbers are face-aligned and the two butt timbers are orthogonal to each other (length directions perpendicular), else an error message.
Source code in kumiki/construction.py
check_perfection
¶
Return None if all timbers are perfect, else an error message.
Source code in kumiki/construction.py
TripleButtJointTimberArrangement
¶
Three butt timbers meeting a single receiving timber.
main_butt_timber_1 and main_butt_timber_2 form an opposing pair (antiparallel). awk_timber is the third butt timber pointing in a third cardinal direction.
check_face_aligned
¶
Return None if all butt timbers are face-aligned with the receiving timber, else an error message.
Source code in kumiki/construction.py
check_face_aligned_cardinal_and_opposing_butts
¶
Return None if: - all timbers are face-aligned, - each butt timber's length direction is orthogonal to the receiving timber (cardinal), - all three butt timbers are in different cardinal directions, and - main_butt_timber_1 and main_butt_timber_2 are antiparallel (pointing towards each other).
Source code in kumiki/construction.py
check_perfection
¶
Return None if all timbers are perfect, else an error message.
Source code in kumiki/construction.py
QuadrupleButtJointTimberArrangement
¶
Four butt timbers meeting a single receiving timber, covering all four cardinal directions.
main_butt_timber_1 and main_butt_timber_2 form one opposing pair (antiparallel). awk_1 and awk_2 form the second opposing pair (antiparallel, on the perpendicular axis).
check_face_aligned
¶
Return None if all butt timbers are face-aligned with the receiving timber, else an error message.
Source code in kumiki/construction.py
check_face_aligned_cardinal_and_opposing_butts
¶
Return None if: - all timbers are face-aligned, - each butt timber's length direction is orthogonal to the receiving timber (cardinal), - all four butt timbers are in different cardinal directions, and - main_butt_timber_1/main_butt_timber_2 are antiparallel and awk_1/awk_2 are antiparallel.
Source code in kumiki/construction.py
check_perfection
¶
Return None if all timbers are perfect, else an error message.
Source code in kumiki/construction.py
CrossCapJointTimberArrangement
¶
A butting post timber "capped" by two crossed timbers.
check_face_aligned_and_orthogonal
¶
Source code in kumiki/construction.py
check_perfection
¶
Return None if all timbers are perfect, else an error message.
Source code in kumiki/construction.py
SpliceJointTimberArrangement
¶
front_face_on_timber1
class-attribute
instance-attribute
¶
front_face_on_timber1: Optional[TimberLongFace] = None
check_face_aligned_and_parallel_axis
¶
Return None if timbers are face-aligned and have parallel length axes, else an error message.
Source code in kumiki/construction.py
check_perfection
¶
Return None if both timbers are perfect, else an error message.
Source code in kumiki/construction.py
CornerJointTimberArrangement
¶
front_face_on_timber1
class-attribute
instance-attribute
¶
front_face_on_timber1: Optional[TimberLongFace] = None
compute_normalized_timber_cross_product
¶
compute_normalized_timber_cross_product() -> Direction3D
Compute the normalized cross product of timber1 and timber2 length directions.
Source code in kumiki/construction.py
is_timber2_left_of_timber1
¶
returns true if timber2 is to the left of timber1 when looking down the length of timber1 and standing on the front face of timber1
Source code in kumiki/construction.py
check_plane_aligned
¶
Return None if timbers are plane-aligned and front face is in plane, else an error message.
Source code in kumiki/construction.py
check_face_aligned_and_orthogonal
¶
Return None if timbers are face-aligned and orthogonal, else an error message.
Source code in kumiki/construction.py
check_perfection
¶
Return None if both timbers are perfect, else an error message.
Source code in kumiki/construction.py
CrossJointTimberArrangement
¶
front_face_on_timber1
class-attribute
instance-attribute
¶
front_face_on_timber1: Optional[TimberLongFace] = None
compute_normalized_timber_cross_product
¶
compute_normalized_timber_cross_product() -> Direction3D
Compute the normalized cross product of timber1 and timber2 length directions.
Source code in kumiki/construction.py
check_plane_aligned
¶
Return None if timbers are plane-aligned and front face is in plane, else an error message.
Source code in kumiki/construction.py
check_face_aligned_and_orthogonal
¶
Return None if timbers are face-aligned and orthogonal, else an error message.
Source code in kumiki/construction.py
check_perfection
¶
Return None if both timbers are perfect, else an error message.
Source code in kumiki/construction.py
BraceJointTimberArrangement
¶
front_face_on_timber1
class-attribute
instance-attribute
¶
front_face_on_timber1: Optional[TimberLongFace] = None
check_perfection
¶
Return None if all timbers are perfect, else an error message.
Source code in kumiki/construction.py
ButtJointBoardArrangement
¶
front_face_on_butt_timber
class-attribute
instance-attribute
¶
front_face_on_butt_timber: Optional[TimberFace] = None
__post_init__
¶
check_orthogonal
¶
Return None if timbers are orthogonal, else an error message. Timbers are orthogonal in this arrangement if butt_timber_face is parallel to some face on the receiving_timber
Source code in kumiki/construction.py
check_face_aligned
¶
Return None if timbers are face-aligned, else an error message.
Source code in kumiki/construction.py
check_perfection
¶
Return None if both timbers are perfect, else an error message.
Source code in kumiki/construction.py
PanelBoardArrangement
¶
check_parallal_coplanar_and_same_thickness
¶
Return None if all boards are parallel (same orientation), share the same thickness, and are coplanar (all measured against boards[0]), else an error message describing the first violation found.
Source code in kumiki/construction.py
ExtendedTimberArrangement
¶
Just a list of timbers, with no real restriction until check functions are called...
check_plane_aligned
¶
check_parallel
¶
Return None if all timbers have parallel length directions (measured against timbers[0]), else an error message.
Source code in kumiki/construction.py
check_face_aligned
¶
Return None if all timbers are face-aligned (measured against timbers[0]), else an error message.
Source code in kumiki/construction.py
check_coaxial_face_aligned_and_same_size
¶
Return None if all timbers share the same centerline (coaxial), are face-aligned, and have the same cross-sectional size, else an error message describing the first violation found (measured against timbers[0]).
"Same size" is checked along matching GLOBAL directions rather than local (width, height) indices: a face-aligned timber may be rotated 90 degrees about the shared centerline relative to timbers[0], in which case its local width/height are swapped relative to global space even though its physical cross-section matches.
Source code in kumiki/construction.py
LineOnPlane
dataclass
¶
LineOnPlane(normal: Direction3D, point_on_line: V3, line_direction: Direction3D)
A line lying in a plane, with a direction to measure in.
Three things, not two: the line itself (a point on it and its direction), and the plane's normal, which says which way is positive for anything measured from the line. What locate_edge_on_face gives back -- an edge as seen ON one of the faces that meets there, which is how a mark gets squared across a piece.
Called LineOnPlane once, which named the wrong thing: a half plane is a 2D REGION bounded by a line, and this is the line and an orientation, with no region at all.
UnsignedPlane
dataclass
¶
UnsignedPlane(normal: Direction3D, point: V3)
Bases: Plane
Same as Plane but the sign on the normal should be ignored.
__repr__
¶
from_transform_and_direction
staticmethod
¶
from_transform_and_direction(transform: Transform, direction: Direction3D) -> UnsignedPlane
Create an unsigned plane from a transform and a direction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transform
|
Transform
|
Transform defining the position and orientation |
required |
direction
|
Direction3D
|
Direction in the transform's local coordinate system |
required |
Returns:
| Type | Description |
|---|---|
UnsignedPlane
|
UnsignedPlane with normal in global coordinates and point at transform position |
Source code in kumiki/geometry.py
Marking
dataclass
¶
Bases: ABC
locate
abstractmethod
¶
locate() -> Union[UnsignedPlane, Plane, Line, Point, LineOnPlane, Space]
DistanceFromFace
dataclass
¶
Bases: Marking
Represents a distance from a face on a timber with + being AWAY from the face.
locate
¶
locate() -> UnsignedPlane
DistanceFromPointIntoFace
dataclass
¶
Bases: Marking
Represents a distance from a point into a face on a timber with + being INTO the timber (that is the negative face normal direction is the + axis of the measurement) If the point is not supplied, the center of the face is used.
locate
¶
locate() -> Point
Convert the distance from a point into a face to a Point
Returns:
| Type | Description |
|---|---|
Point
|
Point at the specified distance from the starting point |
Source code in kumiki/measuring.py
DistanceFromLongEdgeOnFace
dataclass
¶
Bases: Marking
Represents a distance from a long edge on a timber with + being onto the face from the edge.
locate
¶
locate() -> Line
Convert the distance from a long edge to a line on the specified face.
Returns a line parallel to the edge, on the given face, at the specified distance from the edge. The distance is measured along the face plane, perpendicular to the edge direction. Positive distance means moving in the direction of the "other" face's normal (the face that defines the edge together with self.face).
Returns:
| Type | Description |
|---|---|
Line
|
Line parallel to the edge at the specified distance on the face |
Source code in kumiki/measuring.py
PointFromCornerInFaceDirection
dataclass
¶
Bases: Marking
Point on an edge in a given direction.
locate
¶
locate() -> Point
Source code in kumiki/measuring.py
DistanceFromCornerAlongEdge
dataclass
¶
Bases: Marking
Distance along a timber edge from a reference end (corner) to an intersection or closest point. Positive means into the timber from the end.
locate
¶
locate() -> Point
Source code in kumiki/measuring.py
PlaneFromEdgeInDirection
dataclass
¶
ButtJointShoulderResult
¶
Result of computing a butt joint shoulder plane and its associated marking space.
Attributes:
| Name | Type | Description |
|---|---|---|
shoulder_plane |
Plane
|
The shoulder plane (normal points from mortise centerline toward tenon). |
butt_direction |
Direction3D
|
Direction the butt timber is pointing into the receiving timber. |
marking_space |
Space
|
Located where tenon centerline intersects the shoulder plane, oriented with: +X = shoulder_plane.normal (from mortise centerline toward tenon) +Y = caller-provided up_direction (orthogonalized) +Z = derived via right-hand rule |
ButtJointCSGParts
¶
Representation of the geometry components that make up a butt joint. They are combined by unioning the positive parts, then differencing the negative parts.
DovetailTenonGeometeryResult
¶
Bases: NamedTuple
Result of computing the geometry for a dovetail tenon.
Attributes:
| Name | Type | Description |
|---|---|---|
tenon_csg |
CSG representing the tenon shape to be cut from the butt timber. |
|
mortise_csg |
CSG representing the mortise shape to be cut from the receiving timber. |
DovetailTenonWedgeAccessoryParameters
¶
Bases: NamedTuple
Parameters for an optional wedge accessory for a dovetail tenon.
__
| \ wedge_tip_stickout
| ____________________ <- wedge_small_height measured at this line | | | | ___ | \ wedge_back_extra_length |_
Attributes:
| Name | Type | Description |
|---|---|---|
wedge_from_receiving_timber_side |
bool
|
If true, the wedge is designed to be cut from the receiving timber and inserted from that side. If false, the wedge is designed to be cut from the tenon timber and inserted from the tenon side. We must have tenon_depth + receiving_timber_extra_depth > the matching width on the receivingtimber for this to work |
wedge_angle |
Numeric
|
The angle of the wedge taper. 0 means a rectangular wedge, X means a wedge with an X angle taper. |
wedge_extra_height |
Numeric
|
Extra height added to the wedge. The small height of the wedge is calculated as dovetail_depth + wedge_extra_height. The reason for this is that a minimum size of dovetail_depth is required for the joint to physically assemble. |
wedge_from_receiving_timber_side
class-attribute
instance-attribute
¶
TuskTenonGeometryResult
¶
Bases: NamedTuple
Result of computing the geometry for a tusk tenon's crosswise locking key.
Attributes:
| Name | Type | Description |
|---|---|---|
tenon_hole_negative_csg |
CutCSG
|
The crosswise hole cut through the (through-)tenon that the tusk key slides into. |
mortise_clearance_negative_csg |
Optional[CutCSG]
|
Extra clearance cut into the receiving (mortise) timber, only present when its rough stock still surrounds the tenon at the tusk hole's position (None otherwise). |
tusk_accessory_csg |
CSGAccessory
|
The tusk key itself. |
PegPositionSpace
¶
Bases: Enum
Which timber's coordinate space to use when interpreting peg positions and orientations.
SimplePegParameters
¶
Parameters for simple pegs in mortise and tenon joints.
Attributes:
| Name | Type | Description |
|---|---|---|
shape |
PegShape
|
Shape specification for the peg (from PegShape enum) |
peg_positions |
List[Tuple[Numeric, Numeric]]
|
List of (distance_from_shoulder, distance_from_centerline) tuples - First value: distance along length axis measured from shoulder of tenon - Second value: distance in perpendicular axis measured from center |
peg_position_space |
Tuple[PegPositionSpace, PegPositionSpace]
|
Controls which timber's coordinate system is used to interpret each component of peg_positions. A tuple of (shoulder_axis_space, lateral_axis_space). - shoulder_axis_space (first element): controls distance_from_shoulder direction. TENON = along tenon length axis. MORTISE = along mortise length axis. - lateral_axis_space (second element): controls distance_from_centerline direction. TENON = perpendicular to peg face normal and tenon length axis. MORTISE = along mortise length axis. |
size |
Numeric
|
Peg diameter (for round pegs) or side length (for square pegs) |
depth |
Optional[Numeric]
|
Depth measured from mortise face where peg goes in (None means all the way through the mortise timber) |
tenon_hole_offset |
Numeric
|
Offset distance of the hole in the tenon towards the shoulder so that the peg tightens the joint up. You should usually set this to 1-2mm |
peg_orientation |
Tuple[PegPositionSpace, Numeric]
|
Controls which timber's face axes the peg cross-section is aligned to, plus an optional CCW rotation around the drill axis. A tuple of (space, ccw_rotation_angle). - space: TENON = align peg Y axis with the tenon length axis. MORTISE = align peg Y axis with the mortise length axis. - ccw_rotation_angle: counter-clockwise rotation (in radians) around the drill axis applied on top of the face-aligned basis. 0 = no rotation. |
stickout_length |
Optional[Numeric]
|
Length the peg protrudes beyond the mortise entry face. If None, the peg sticks out by half its depth. |
peg_position_space
class-attribute
instance-attribute
¶
peg_position_space: Tuple[PegPositionSpace, PegPositionSpace] = (PegPositionSpace.TENON, PegPositionSpace.TENON)
peg_orientation
class-attribute
instance-attribute
¶
peg_orientation: Tuple[PegPositionSpace, Numeric] = (PegPositionSpace.TENON, scalar(0))
PegPositionResult
¶
Computed geometry for a single peg, all positions and orientations in global space.
Attributes:
| Name | Type | Description |
|---|---|---|
tenon_face_position_global |
V3
|
Center of the peg hole on the tenon face (no draw-bore offset). |
tenon_face_position_with_offset_global |
V3
|
Center of the peg hole on the tenon face, shifted toward the shoulder by tenon_hole_offset for draw-bore tightening. |
mortise_entry_position_global |
V3
|
Center of the peg hole on the mortise entry face. |
orientation_global |
Orientation
|
Orientation of the peg (Z-axis = drill direction into the timber). |
peg_depth |
Numeric
|
Depth of the peg hole (full chord through the mortise, or explicit depth). |
stickout_length |
Numeric
|
Length the peg protrudes beyond the mortise entry face. |
TimberFace
¶
Bases: Enum
get_direction
¶
get_direction() -> Direction3D
Get the direction vector for this face in world coordinates.
Source code in kumiki/timber.py
is_perpendicular
¶
is_perpendicular(other: TimberFace) -> bool
Check if two faces are perpendicular to each other.
Perpendicular face pairs (orthogonal axes): - X-axis faces (RIGHT, LEFT) <-> Y-axis faces (FRONT, BACK) - X-axis faces (RIGHT, LEFT) <-> Z-axis faces (TOP, BOTTOM) - Y-axis faces (FRONT, BACK) <-> Z-axis faces (TOP, BOTTOM)
Source code in kumiki/timber.py
get_opposite_face
¶
get_opposite_face() -> TimberFace
Get the opposite face (the face on the opposite side of the timber).
Opposite pairs: - TOP <-> BOTTOM - RIGHT <-> LEFT - FRONT <-> BACK
Source code in kumiki/timber.py
rotate_about
¶
rotate_about(face: TimberFace) -> TimberFace
Rotate this face by 90 degrees about face's outward-normal axis
(a quarter turn using the right-hand rule around that normal).
If this face IS the rotation axis (self == face or self == face.get_opposite_face()), it lies on the axis and is unaffected by the rotation, so it is returned unchanged.
Source code in kumiki/timber.py
TimberEnd
¶
Bases: Enum
TimberLongFace
¶
Bases: Enum
is_perpendicular
¶
is_perpendicular(other: TimberLongFace) -> bool
Check if two long faces are perpendicular to each other.
Perpendicular face pairs: - RIGHT <-> FRONT, RIGHT <-> BACK - LEFT <-> FRONT, LEFT <-> BACK
Source code in kumiki/timber.py
rotate_left
¶
rotate_left() -> TimberLongFace
Rotate the long face left (90 degrees counter-clockwise).
TimberCenterline
¶
Bases: Enum
Cutting
dataclass
¶
Cutting(timber: PerfectTimberWithin, maybe_top_end_cut_distance_from_bottom: Optional[Numeric] = None, maybe_bottom_end_cut_distance_from_bottom: Optional[Numeric] = None, negative_csg: Optional[CutCSG] = None, label: CutCSGLabel = NoLabel(), assembly_freedom: Optional[AssemblyFreedom] = None, assembly_ordering: Ordering = Ordering())
A set of cuts on a timber (to create a joint, for example), defined by a CSG object representing the volume to be removed.
The CSG object represents the volume to be REMOVED from the timber (negative CSG), in LOCAL coordinates (relative to timber.bottom_position).
maybe_top_end_cut_distance_from_bottom
class-attribute
instance-attribute
¶
maybe_top_end_cut_distance_from_bottom: Optional[Numeric] = None
maybe_bottom_end_cut_distance_from_bottom
class-attribute
instance-attribute
¶
maybe_bottom_end_cut_distance_from_bottom: Optional[Numeric] = None
label
class-attribute
instance-attribute
¶
label: CutCSGLabel = field(default_factory=CutCSGLabel.NoLabel)
assembly_freedom
class-attribute
instance-attribute
¶
assembly_freedom: Optional[AssemblyFreedom] = None
get_maybe_top_end_cut
¶
get_maybe_top_end_cut() -> Optional[HalfSpace]
Return the top end cut HalfSpace derived from distance metadata.
Source code in kumiki/timber.py
get_maybe_bottom_end_cut
¶
get_maybe_bottom_end_cut() -> Optional[HalfSpace]
Return the bottom end cut HalfSpace derived from distance metadata.
Source code in kumiki/timber.py
get_negative_csg_local
¶
get_negative_csg_local() -> Optional[CutCSG]
Get the complete negative CSG including end cuts.
Returns the union of negative_csg with any end cuts that are defined, or None when this cutting removes nothing at all.
Source code in kumiki/timber.py
make_end_cut_distance_from_bottom
staticmethod
¶
make_end_cut_distance_from_bottom(timber: PerfectTimberWithin, end: TimberEnd, distance_from_end_to_cut: Numeric) -> Numeric
Convert distance-from-end to cut-plane distance from timber bottom.
Source code in kumiki/timber.py
Matrix
¶
Immutable: _data is set once at construction and never written to
again (enforced both by omitting __setitem__ and by marking the
underlying numpy buffer read-only), matching the frozen dataclasses
(Transform/Orientation/Axis) that hold Matrix-typed fields elsewhere in
this module -- without this, some_frozen_transform.position[0] = 5
would silently succeed despite the dataclass being frozen.
Source code in kumiki/rule.py
det
¶
cross
¶
equals
¶
equals(other: Matrix, tolerance: Optional[float] = None) -> bool
Elementwise approximate equality (tolerates float noise from trig/sqrt).
Source code in kumiki/rule.py
norm
¶
tolist
¶
__getitem__
¶
Source code in kumiki/rule.py
__iter__
¶
__len__
¶
__mul__
¶
__rmul__
¶
__truediv__
¶
__add__
¶
__radd__
¶
__sub__
¶
__rsub__
¶
__neg__
¶
__eq__
¶
Transform
dataclass
¶
Transform(position: V3, orientation: Orientation)
Represents a 3D transformation with position and orientation. Encapsulates both translation and rotation for objects in 3D space.
identity
classmethod
¶
identity() -> Transform
Create an identity transform at origin with identity orientation.
local_to_global
¶
Convert a point from local coordinates to global world coordinates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
local_point
|
V3
|
A point in local coordinates |
required |
Returns:
| Type | Description |
|---|---|
V3
|
The same point in global world coordinates |
Source code in kumiki/rule.py
global_to_local
¶
Convert a point from global world coordinates to local coordinates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
global_point
|
V3
|
A point in global world coordinates |
required |
Returns:
| Type | Description |
|---|---|
V3
|
The same point in local coordinates |
Source code in kumiki/rule.py
numeric_local_to_global
¶
Convert local to global using numeric (Float) math. For hot paths like CSG.
numeric_global_to_local
¶
Convert global to local using numeric (Float) math. For hot paths like CSG.
to_global_transform
¶
Convert this transform to global coordinates relative to a parent transform.
invert
¶
invert() -> Transform
Return the inverse of this transform.
For a transform T that converts local to global (global = T * local), the inverse converts global to local (local = T^-1 * global).
Source code in kumiki/rule.py
__mul__
¶
Compose two transforms: result = self * other.
This applies other first, then self. Equivalent to: global = self.local_to_global(other.local_to_global(local))
Source code in kumiki/rule.py
to_local_transform
¶
Convert this transform to local coordinates relative to a parent transform.
rotate_around_axis
¶
Rotate this transform counterclockwise around an axis and return the new transform.
The axis can be positioned anywhere in space (not just through the origin). Uses Rodrigues' rotation formula after translating to make the axis pass through origin.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
axis
|
Axis
|
Axis with position and direction to rotate around |
required |
radians
|
Numeric
|
Angle to rotate in radians (counterclockwise when looking along axis direction) |
required |
Returns:
| Type | Description |
|---|---|
Transform
|
New Transform with rotated position and orientation |
Source code in kumiki/rule.py
Comparison
¶
Bases: Enum
Enum for safe comparison operations
Orientation
dataclass
¶
Represents a 3D rotation using a 3x3 rotation matrix. I guess we never slerp and don't care about memory usage so apparently we're using matrices to implement this class.
matrix
class-attribute
instance-attribute
¶
__post_init__
¶
Convert to Matrix and validate that the matrix is 3x3.
Source code in kumiki/rule.py
multiply
¶
multiply(other: Orientation) -> Orientation
Multiply this orientation with another orientation. Returns a new Orientation representing the combined rotation.
Source code in kumiki/rule.py
invert
¶
invert() -> Orientation
Return the inverse of this orientation. For rotation matrices, the inverse is the transpose.
flip
¶
flip(flip_x: bool = False, flip_y: bool = False, flip_z: bool = False) -> Orientation
Return the orientation with the given axes flipped.
Source code in kumiki/rule.py
__mul__
¶
__mul__(other: Orientation) -> Orientation
__repr__
¶
rotate_right
classmethod
¶
rotate_right() -> Orientation
Rotate right: +X axis rotates to -Y axis (clockwise around Z)
rotate_left
classmethod
¶
rotate_left() -> Orientation
Rotate left: +X axis rotates to +Y axis (counterclockwise around Z)
from_angle_axis
classmethod
¶
from_angle_axis(radians: Numeric, axis: Direction3D) -> Orientation
Create an orientation from an angle-axis rotation (Rodrigues' formula).
Source code in kumiki/rule.py
identity
staticmethod
¶
identity() -> Orientation
from_z_and_y
staticmethod
¶
from_z_and_y(z_direction: Direction3D, y_direction: Direction3D) -> Orientation
Create an Orientation from z and y direction vectors. Computes x = y × z to complete the right-handed coordinate system.
Source code in kumiki/rule.py
from_z_and_x
staticmethod
¶
from_z_and_x(z_direction: Direction3D, x_direction: Direction3D) -> Orientation
Create an Orientation from z and x direction vectors. Computes y = z × x to complete the right-handed coordinate system.
Source code in kumiki/rule.py
from_x_and_y
staticmethod
¶
from_x_and_y(x_direction: Direction3D, y_direction: Direction3D) -> Orientation
Create an Orientation from x and y direction vectors. Computes z = x × y to complete the right-handed coordinate system.
Source code in kumiki/rule.py
from_axis_angle
staticmethod
¶
from_axis_angle(axis: Direction3D, radians: Numeric) -> Orientation
Create an Orientation representing a rotation around an axis by an angle. Uses Rodrigues' rotation formula.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
axis
|
Direction3D
|
Direction vector to rotate around (will be normalized) |
required |
radians
|
Numeric
|
Angle to rotate in radians |
required |
Returns:
| Type | Description |
|---|---|
Orientation
|
Orientation object representing the rotation |
Source code in kumiki/rule.py
from_euleryZYX
staticmethod
¶
from_euleryZYX(yaw: Numeric, pitch: Numeric, roll: Numeric) -> Orientation
Create an Orientation from Euler angles using ZYX rotation sequence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
yaw
|
Numeric
|
Rotation around Z-axis (radians) |
required |
pitch
|
Numeric
|
Rotation around Y-axis (radians) |
required |
roll
|
Numeric
|
Rotation around X-axis (radians) |
required |
Returns:
| Type | Description |
|---|---|
Orientation
|
Orientation object with combined rotation matrix |
The rotation sequence is: 1. Yaw (Z-axis rotation) 2. Pitch (Y-axis rotation) 3. Roll (X-axis rotation)
Source code in kumiki/rule.py
facing_west
staticmethod
¶
facing_west() -> Orientation
Horizontal timber with top face up. This is the IDENTITY orientation.
- Length: +X (local) = -X (west) in global
- Width: +Y (local) = -Y (south) in global
- Facing: +Z (up)
Source code in kumiki/rule.py
facing_east
staticmethod
¶
facing_east() -> Orientation
Horizontal timber with top face up. 180° rotation around Z axis from facing_west.
- Length: +X (local) = +X (east) in global
- Width: +Y (local) = +Y (north) in global
- Facing: +Z (up)
Source code in kumiki/rule.py
facing_north
staticmethod
¶
facing_north() -> Orientation
Horizontal timber with top face up. 90° counterclockwise rotation around Z axis from facing_west.
- Length: +X (local) = +Y (north) in global
- Width: +Y (local) = -X (west) in global
- Facing: +Z (up)
Source code in kumiki/rule.py
facing_south
staticmethod
¶
facing_south() -> Orientation
Horizontal timber with top face up. 90° clockwise rotation around Z axis from facing_west.
- Length: +X (local) = -Y (south) in global
- Width: +Y (local) = +X (east) in global
- Facing: +Z (up)
Source code in kumiki/rule.py
pointing_up
staticmethod
¶
pointing_up() -> Orientation
Vertical timber with LENGTH pointing upward (+Z). This is the same as pointing_forward.
- Length (local +X) → +Z (up) in global
- Width (local +Y) → +Y (north) in global
- Facing (local +Z) → -X (west) in global
Source code in kumiki/rule.py
pointing_down
staticmethod
¶
pointing_down() -> Orientation
Vertical timber with LENGTH pointing downward (-Z).
- Length (local +X) → -Z (down) in global
- Width (local +Y) → +Y (north) in global
- Facing (local +Z) → +X (east) in global
Source code in kumiki/rule.py
pointing_forward
staticmethod
¶
pointing_forward() -> Orientation
Vertical timber with LENGTH pointing upward (+Z). Identical to pointing_up.
- Length (local +X) → +Z (up) in global
- Width (local +Y) → +Y (north) in global
- Facing (local +Z) → -X (west) in global
Source code in kumiki/rule.py
pointing_backward
staticmethod
¶
pointing_backward() -> Orientation
Vertical timber with LENGTH pointing upward (+Z), rotated 180° from pointing_forward.
- Length (local +X) → +Z (up) in global
- Width (local +Y) → -Y (south) in global
- Facing (local +Z) → +X (east) in global
Source code in kumiki/rule.py
pointing_left
staticmethod
¶
pointing_left() -> Orientation
Vertical timber with LENGTH pointing upward (+Z), rotated 90° CCW from pointing_forward.
- Length (local +X) → +Z (up) in global
- Width (local +Y) → -X (west) in global
- Facing (local +Z) → -Y (south) in global
Source code in kumiki/rule.py
pointing_right
staticmethod
¶
pointing_right() -> Orientation
Vertical timber with LENGTH pointing upward (+Z), rotated 90° CW from pointing_forward.
- Length (local +X) → +Z (up) in global
- Width (local +Y) → +X (east) in global
- Facing (local +Z) → +Y (north) in global
Source code in kumiki/rule.py
Plane
dataclass
¶
Plane(normal: Direction3D, point: V3)
Represents an oriented, infinite plane with origin in 3D space.
__repr__
¶
from_transform_and_direction
staticmethod
¶
from_transform_and_direction(transform: Transform, direction: Direction3D) -> Plane
Create a plane from a transform and a direction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transform
|
Transform
|
Transform defining the position and orientation |
required |
direction
|
Direction3D
|
Direction in the transform's local coordinate system |
required |
Returns:
| Type | Description |
|---|---|
Plane
|
Plane with normal in global coordinates and point at transform position |
Source code in kumiki/geometry.py
CutCSGLabel
dataclass
¶
The name a CSG node carries, if anyone gave it one.
A wrapper rather than a bare Optional[str] so that what a label carries can grow -- provenance, namespacing, whatever naming turns out to need -- without revisiting every node that constructs one.
An unnamed node gets NoLabel() rather than None, so csg.label is always a
CutCSGLabel and reading it never needs a None check first. Test for a name
with the label's truthiness or is_labeled(); read it with .name.
CutCSG
dataclass
¶
Bases: ABC
Base class for all CSG operations.
label
class-attribute
instance-attribute
¶
label: CutCSGLabel = field(default_factory=CutCSGLabel.NoLabel, kw_only=True)
__repr__
abstractmethod
¶
display_name
classmethod
¶
What this kind of CSG is called where a person reads it.
Derived from the class name -- "path extrusion" -- so a new CSG type names itself; subclasses override where a shorter word is the one people actually use ("union", not "solid union").
Distinct from the class name, which stays the machine-readable kind: the viewer keys structural decisions off that and must not follow wording changes.
Source code in kumiki/cutcsg.py
get_declared_features
¶
get_declared_features(source: FeatureSource = BOTH) -> List[CSGFeature]
Features this node names on its own boundary, whether or not any point lies on them.
Empty by default, and it stays empty for the compound nodes: a SolidUnion, Difference or Intersection has no surface of its own to name, only the surfaces its children contribute. The primitives that do have a boundary get this from HasFeatures instead.
Source code in kumiki/cutcsg.py
collect_feature_hits
¶
collect_feature_hits(point: V3, tolerances: FeatureTestTolerances) -> List[OwnedFeatureHit]
Every declared feature in this subtree that point lies on.
Each feature is tested at the tolerance its own type calls for, right here -- a face at the face tolerance, a declared edge at the edge one. Compound nodes extend this over their children; they declare nothing themselves.
Real and non-real features are gated differently, which is the whole
reason real exists:
- A real feature names actual surface, so the point has to be on the boundary of the primitive declaring it. That gate is a surface question, hence the face tolerance whatever the feature's own type.
- A non-real feature (a bore's centre axis, a reference plane) names nothing the CSG tree ever cut, so boolean operations cannot have removed it and the gate does not apply.
Takes a concrete FeatureTestTolerances, not an optional one: the defaulting happens once, at the public entry point, so nothing on the recursive path can quietly re-default.
Source code in kumiki/cutcsg.py
find_all_features
¶
find_all_features(point: V3, test_tolerances: Optional[FeatureTestTolerances] = None) -> List[OwnedFeatureHit]
Every feature at point: those declared in this subtree, plus the edges they form with each other.
Two gathers, because "near enough to count" means a different distance depending on what is being asked. The first collects features at the tolerance each one's type calls for. The second collects faces at the EDGE tolerance and pairs them, which is what makes an edge selectable from further away than either of its faces -- a face 1.5mm off cannot claim the point itself, but it can still form an edge that is selectable there, because you cannot click exactly on a line.
Derivation happens here rather than inside collect_feature_hits, and so runs once, at whichever node the caller asked about. Putting it in the recursive gather would either recurse into itself or have every nested compound re-derive what its parent derives.
Source code in kumiki/cutcsg.py
find_first_feature
¶
find_first_feature(point: V3, test_tolerances: Optional[FeatureTestTolerances] = None) -> Optional[OwnedFeatureHit]
The best feature at point, or None. Uses default sorting rules.
Non-real features win outright over real ones. They are lines and points inside or alongside the solid, so anything selecting one has deliberately snapped to it, and a surface it happens to sit on should not steal the click. Priority breaks ties within each of the two.
Source code in kumiki/cutcsg.py
contains_point
abstractmethod
¶
Check if a point is contained within the CSG object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is inside or on the boundary of the CSG object, False otherwise |
Source code in kumiki/cutcsg.py
is_point_on_boundary
abstractmethod
¶
Check if a point is on the boundary of the CSG object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is on the boundary of the CSG object, False otherwise |
Source code in kumiki/cutcsg.py
get_outward_normal
abstractmethod
¶
get_outward_normal(point: V3, eps: Optional[Numeric] = None) -> Optional[Direction3D]
Get the outward normal vector at a boundary point.
This method should only be called if is_point_on_boundary(point) is True. For points not on the boundary, behavior is undefined.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
A point on the boundary (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
Optional[Direction3D]
|
The outward normal vector at the point, or None if cannot be determined |
Source code in kumiki/cutcsg.py
get_aabb
abstractmethod
¶
get_aabb() -> BoundingBox
Return the axis-aligned bounding box (AABB) of this CSG object.
Each bound is Optional[Numeric] — None means unbounded in that direction.
Primitives with infinite extent (HalfSpace, or prisms/cylinders with start_distance or end_distance set to None) cannot produce a finite AABB. They emit a UserWarning and return a BoundingBox with all fields set to None.
Source code in kumiki/cutcsg.py
EmptyCSG
dataclass
¶
HalfSpace
dataclass
¶
Bases: HasFeatures, CutCSG
An infinite half-plane defined by a normal vector and offset from origin.
The half-plane includes all points P such that: P · normal >= offset
The offset represents the signed distance from the origin along the normal direction
where the plane is located. Positive offset moves the plane in the direction of the normal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
normal
|
Normal vector pointing into the half-space (3x1 Matrix) |
required | |
offset
|
Distance from origin along normal direction where plane is located (default: 0) |
required |
default_features
¶
default_features() -> Dict[FeatureKey, CSGFeature]
Its one surface. See RectangularPrism.default_features for the group.
Source code in kumiki/cutcsg.py
display_name
classmethod
¶
__repr__
¶
contains_point
¶
Check if a point is contained within the half-plane.
A point P is in the half-plane if (P · normal) >= offset
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is in the half-plane (including boundary), False otherwise |
Source code in kumiki/cutcsg.py
is_point_on_boundary
¶
Check if a point is on the boundary of the half-plane.
A point P is on the boundary if (P · normal) == offset
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is on the boundary plane, False otherwise |
Source code in kumiki/cutcsg.py
get_outward_normal
¶
get_outward_normal(point: V3, eps: Optional[Numeric] = None) -> Optional[Direction3D]
Get the outward normal vector at a boundary point.
For a HalfSpace, the outward normal is always the opposite of thenormal vector itself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
A point on the boundary |
required |
Returns:
| Type | Description |
|---|---|
Optional[Direction3D]
|
The outward normal vector (the HalfSpace's normal) |
Source code in kumiki/cutcsg.py
get_aabb
¶
get_aabb() -> BoundingBox
RectangularPrism
dataclass
¶
Bases: HasFeatures, CutCSG
A prism with rectangular cross-section, optionally infinite in one or both ends. Note,they are parameterized similar to the Timber class which is atypical for such a primitive.
The prism is defined by: - A transform (position and orientation in global coordinates) - A cross-section size (width (x-axis)) x height (y-axis)) in the local XY plane - Start and end distances along the local Z-axis from the position
So the center point of the size cross section is at position and the timber extends out in -z by start_distance and +z by end_distance.
Use None for start_distance or end_distance to make the prism infinite in that direction.
The orientation matrix defines the local coordinate system where: - X-axis (first column) is the width direction (size[0]) - Y-axis (second column) is the height direction (size[1]) - Z-axis (third column) is the length/axis direction
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
size
|
Cross-section dimensions [width, height] (2x1 Matrix) |
required | |
transform
|
Transform (position and orientation) in global coordinates (default: identity) |
required | |
start_distance
|
Distance from position along Z-axis to start of prism (None = |
required | |
end_distance
|
Distance from position along Z-axis to end of prism (None = infinite) |
required |
transform
class-attribute
instance-attribute
¶
default_features
¶
default_features() -> Dict[FeatureKey, CSGFeature]
Every face and arris a prism has, named without anyone asking.
ALL DEFAULTS ARE IN FeatureGroup.NONE, AND THIS IS LOAD BEARING. The group is what decides which features may pair to form a DERIVED edge, and derived edges are found by pairing every face near a query point with every other -- O(k^2) in k. Before defaults, a primitive nobody had named contributed k = 0. Putting these in a pairing group instead would set k to a dozen per primitive across the whole tree, and produce a mass of derived edges that are geometrically real and mean nothing.
A default is a thing you can SELECT and MEASURE TO, not a thing that combines. Anything wanting to combine is authored, with a group chosen on purpose -- which is what _ptw_face_tags and the joint code do.
Source code in kumiki/cutcsg.py
display_name
classmethod
¶
get_bottom_position
¶
get_bottom_position() -> V3
Get the position of the bottom of the prism (at start_distance). Only valid for prisms with finite start_distance.
Returns:
| Type | Description |
|---|---|
V3
|
The 3D position at the bottom of the prism |
Raises:
| Type | Description |
|---|---|
ValueError
|
If start_distance is None (infinite prism) |
Source code in kumiki/cutcsg.py
get_top_position
¶
get_top_position() -> V3
Get the position of the top of the prism (at end_distance). Only valid for prisms with finite end_distance.
Returns:
| Type | Description |
|---|---|
V3
|
The 3D position at the top of the prism |
Raises:
| Type | Description |
|---|---|
ValueError
|
If end_distance is None (infinite prism) |
Source code in kumiki/cutcsg.py
__repr__
¶
equals_prism
¶
equals_prism(other: RectangularPrism) -> bool
Check if this prism equals another prism.
Uses SymPy's equals() method for numeric comparisons to handle symbolic values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
RectangularPrism
|
Another RectangularPrism to compare with |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if all components are equal, False otherwise |
Source code in kumiki/cutcsg.py
contains_point
¶
Check if a point is contained within the prism.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is inside or on the boundary of the prism, False otherwise |
Source code in kumiki/cutcsg.py
is_point_on_boundary
¶
Check if a point is on the boundary of the prism.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is on the boundary of the prism, False otherwise |
Source code in kumiki/cutcsg.py
get_outward_normal
¶
get_outward_normal(point: V3, eps: Optional[Numeric] = None) -> Optional[Direction3D]
Get the outward normal vector at a boundary point.
Returns the normalized outward normal for the face that contains this point. If the point is on multiple faces (edge or corner), returns one of the normals.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
A point on the boundary |
required |
Returns:
| Type | Description |
|---|---|
Optional[Direction3D]
|
The outward normal vector at the point, or None if cannot be determined |
Source code in kumiki/cutcsg.py
get_aabb
¶
get_aabb() -> BoundingBox
Source code in kumiki/cutcsg.py
SolidUnion
dataclass
¶
Bases: CutCSG
CSG union operation - combines multiple CSG objects.
The union represents the set of all points that are in ANY of the child CSG objects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
children
|
List of CSG objects to union together |
required |
display_name
classmethod
¶
__repr__
¶
contains_point
¶
Check if a point is contained within the union.
A point is in the union if it's in ANY of the children.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is in any of the children, False otherwise |
Source code in kumiki/cutcsg.py
is_point_on_boundary
¶
Check if a point is on the boundary of the union.
A point is on the boundary if it's on the boundary of at least one child and not in the interior of any other child.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is on the boundary of the union, False otherwise |
Source code in kumiki/cutcsg.py
get_outward_normal
¶
get_outward_normal(point: V3, eps: Optional[Numeric] = None) -> Optional[Direction3D]
Get the outward normal vector at a boundary point.
For a union, we check all children that have the point on their boundary and return the average of their outward normals. The reason we do this is because this method is used to check if a point is on the boundary through Differences and using an average normal here tends to behave better on weird non-convex geometry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
A point on the boundary |
required |
Returns:
| Type | Description |
|---|---|
Optional[Direction3D]
|
The average outward normal vector, or None if cannot be determined |
Source code in kumiki/cutcsg.py
collect_feature_hits
¶
collect_feature_hits(point: V3, tolerances: FeatureTestTolerances) -> List[OwnedFeatureHit]
Source code in kumiki/cutcsg.py
get_aabb
¶
get_aabb() -> BoundingBox
Source code in kumiki/cutcsg.py
Intersection
dataclass
¶
Bases: CutCSG
CSG intersection operation - keeps only points common to both child CSG objects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
left
|
First CSG object |
required | |
right
|
Second CSG object |
required |
__repr__
¶
contains_point
¶
is_point_on_boundary
¶
Source code in kumiki/cutcsg.py
get_outward_normal
¶
get_outward_normal(point: V3, eps: Optional[Numeric] = None) -> Optional[Direction3D]
Source code in kumiki/cutcsg.py
collect_feature_hits
¶
collect_feature_hits(point: V3, tolerances: FeatureTestTolerances) -> List[OwnedFeatureHit]
Source code in kumiki/cutcsg.py
get_aabb
¶
get_aabb() -> BoundingBox
Source code in kumiki/cutcsg.py
Difference
dataclass
¶
Bases: CutCSG
CSG difference operation - subtracts multiple CSG objects from a base object.
The difference represents: base - subtract[0] - subtract[1] - ... All points in base that are NOT in any of the subtract objects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base
|
The base CSG object to subtract from |
required | |
subtract
|
List of CSG objects to subtract from the base |
required |
__repr__
¶
contains_point
¶
Check if a point is contained within the difference.
A point is in the difference if it's in the base and NOT strictly inside any subtract object. Special case: if a point is on the boundary of both base and subtract, it's excluded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is in base but not in any subtract objects, False otherwise |
Source code in kumiki/cutcsg.py
is_point_on_boundary
¶
Check if a point is on the boundary of the difference.
A point is on the boundary if: 1. It's contained in the difference (base - subtract), AND 2. Either: a. It's on the boundary of the base, OR b. It's strictly inside the base but on the boundary of at least one subtract object
Note: For case 2b, the point creates a new boundary surface (the "hole" surface). The point must be on the subtract boundary but NOT inside the subtract (i.e., on the surface of the hole facing the remaining material).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is on the boundary of the difference, False otherwise |
Source code in kumiki/cutcsg.py
get_outward_normal
¶
get_outward_normal(point: V3, eps: Optional[Numeric] = None) -> Optional[Direction3D]
Get the outward normal vector at a boundary point.
For a difference, if the point is on the boundary of the base CSG, return that normal. Otherwise, go through the subtract CSGs and return the average of their normals (negated).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
A point on the boundary |
required |
Returns:
| Type | Description |
|---|---|
Optional[Direction3D]
|
The outward normal vector, or None if cannot be determined |
Source code in kumiki/cutcsg.py
collect_feature_hits
¶
collect_feature_hits(point: V3, tolerances: FeatureTestTolerances) -> List[OwnedFeatureHit]
Source code in kumiki/cutcsg.py
get_aabb
¶
get_aabb() -> BoundingBox
ConvexPolygonSimpleLoft
dataclass
¶
Bases: HasFeatures, CutCSG
A solid formed by straight-line lofting between two convex polygons in parallel planes, connected index-to-index (vertex i of bottom_points connects by a straight line to vertex i of top_points). Generalizes ConvexPolygonExtrusion to the case where the cross-section changes shape/size/offset along the length instead of staying constant -- ConvexPolygonExtrusion is the degenerate case where bottom_points == top_points.
bottom_points and top_points must each independently be a valid convex polygon (same rules as ConvexPolygonExtrusion.is_valid()) with the SAME number of points wound in the SAME direction. Intermediate (lofted) cross-sections are NOT checked for convexity or simplicity -- if the correspondence between the two profiles is "twisted" enough (e.g. a profile rotated relative to the other), an intermediate cross-section can become non-convex or self-intersecting, which is undefined behavior for this primitive. This is safe for tapers/relief pockets where each vertex moves along a roughly-monotonic path (the common case for joinery), but this is NOT a general-purpose polygon morph.
Side faces are ruled surfaces and are only planar in the special case where the taper is a pure independent per-axis scale from one profile to the other (e.g. a rectangle-to-rectangle taper on the same axes); get_outward_normal accounts for this and is not necessarily constant across a side face.
The polygons live in the local XY plane, with bottom_points at start_distance and top_points at end_distance along the local Z-axis, matching the position/orientation conventions of RectangularPrism and ConvexPolygonExtrusion. Unlike those two, start_distance/end_distance must both be finite -- an infinite loft has no meaningful cross-section to loft towards.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bottom_points
|
convex polygon at start_distance (local XY plane) |
required | |
top_points
|
convex polygon at end_distance (local XY plane), same point count and winding direction as bottom_points |
required | |
start_distance
|
distance from position along Z-axis to bottom_points |
required | |
end_distance
|
distance from position along Z-axis to top_points |
required | |
transform
|
Transform (position and orientation) in global coordinates (default: identity) |
required |
transform
class-attribute
instance-attribute
¶
default_features
¶
default_features() -> Dict[FeatureKey, CSGFeature]
Two caps and a side per edge of the profile, as an extrusion has.
Source code in kumiki/cutcsg.py
display_name
classmethod
¶
get_bottom_position
¶
get_bottom_position() -> V3
Get the position of the bottom of the loft (at start_distance).
get_top_position
¶
get_top_position() -> V3
Get the position of the top of the loft (at end_distance).
__repr__
¶
is_valid
¶
Check if the ConvexPolygonSimpleLoft is valid.
Checks: 1. bottom_points and top_points each have at least 3 points 2. bottom_points and top_points have the same number of points 3. end_distance > start_distance 4. bottom_points and top_points are each individually convex
Does NOT check that intermediate (lofted) cross-sections stay convex or simple -- see class docstring.
Source code in kumiki/cutcsg.py
contains_point
¶
Check if a point is contained within the loft.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is inside or on the boundary, False otherwise |
Source code in kumiki/cutcsg.py
is_point_on_boundary
¶
Check if a point is on the boundary of the loft.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
Point to test (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the point is on the boundary, False otherwise |
Source code in kumiki/cutcsg.py
get_outward_normal
¶
get_outward_normal(point: V3, eps: Optional[Numeric] = None) -> Optional[Direction3D]
Get the outward normal vector at a boundary point.
For the top/bottom caps this is the (constant) local ±Z axis. For a side face, the face is in general a ruled (non-planar) surface, so the normal is computed from the face's parametric partial derivatives at this point rather than being constant across the face.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
V3
|
A point on the boundary |
required |
Returns:
| Type | Description |
|---|---|
Optional[Direction3D]
|
The outward normal vector at the point, or None if cannot be determined |
Source code in kumiki/cutcsg.py
3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 | |
get_aabb
¶
get_aabb() -> BoundingBox
Source code in kumiki/cutcsg.py
ArrangementNames
¶
Bases: Enum
identifies each of the timbers in the various arrangements, we just use one enum for convenience but we really only want to refer to timbers specific to a certain arrangement when using this class
ButtJointTimberArrangement
¶
front_face_on_butt_timber
class-attribute
instance-attribute
¶
front_face_on_butt_timber: Optional[TimberLongFace] = None
top_face_on_butt_timber
class-attribute
instance-attribute
¶
top_face_on_butt_timber: Optional[TimberLongFace] = None
compute_normalized_timber_cross_product
¶
compute_normalized_timber_cross_product() -> Direction3D
Compute the normalized cross product of the butt timber and receiving timber length directions.
Source code in kumiki/construction.py
compute_arrangement_acute_angle
¶
compute_arrangement_acute_angle() -> Numeric
Compute the angle between the 2 timbers
Source code in kumiki/construction.py
check_plane_aligned
¶
Return None if timbers are plane-aligned and front/top face are in plane, else an error message.
Source code in kumiki/construction.py
check_face_aligned_and_orthogonal
¶
Return None if timbers are face-aligned, else an error message.
Unlike check_plane_aligned, this does not validate front_face_on_butt_timber: callers that use this (e.g. drop-in dovetail/housed butt joints) already apply their own, looser front-face validation suited to non-coplanar orthogonal arrangements.
Source code in kumiki/construction.py
check_perfection
¶
Return None if both timbers are perfect, else an error message.
Source code in kumiki/construction.py
CrossJointScribeReliefConfig
dataclass
¶
Configuration for cross joint relief "Scribe" here means one timber is scribed onto the other and completely cut away
cross_timber_1
staticmethod
¶
ButtJointScribeReliefConfig
dataclass
¶
Configuration for butt joint relief "Scribe" here means one timber is scribed onto the other and completely cut away
butt_timber
staticmethod
¶
NotchFrom
¶
ButtJointNotchReliefConfig
dataclass
¶
Configuration for butt joint relief via chop_butt_joint_shoulder_notch_relief_4sided.
Unlike ButtJointScribeReliefConfig (which scribes one timber's whole imperfect body onto the other), this relieves only the material near the inset shoulder using the 4-sided frustum notch -- see chop_butt_joint_shoulder_notch_relief_4sided.
Attributes:
| Name | Type | Description |
|---|---|---|
notch_from |
NotchFrom
|
Which plane the notch is measured from. - Shoulder (default): the notch is anchored to the actual (possibly inset) shoulder plane. The only value cut_mortise_and_tenon_joint itself supports. - Face: only supported by cut_mortise_and_tenon_joint_on_plane_aligned_timbers / _on_face_aligned_timbers. The joint is still fit at the real (inset) shoulder (via the default scribe-based housing cut), but the notch relief itself is anchored to the mortise entry face -- as if mortise_shoulder_inset were 0 -- so it reads as starting at the timber's outer face regardless of how deep the shoulder is actually inset. |
SpliceJointScribeReliefConfig
dataclass
¶
Configuration for splice joint relief "Scribe" here means one timber is scribed onto the other and completely cut away
timber1
staticmethod
¶
CornerJointScribeReliefConfig
dataclass
¶
Configuration for corner joint relief "Scribe" here means one timber is scribed onto the other and completely cut away
timber1
staticmethod
¶
DoubleButtJointScribeReliefConfig
dataclass
¶
Configuration for double butt joint relief.
first_timber_to_be_scribed is scribed first, then
second_timber_to_be_scribed is scribed onto the remaining timber.
with_order
staticmethod
¶
with_order(first_timber_to_be_scribed: ArrangementNames, second_timber_to_be_scribed: ArrangementNames)
Create a DoubleButtJointScribeReliefConfig from the order of which timbers to be scribed
Source code in kumiki/joints/workshop/shavings/relief.py
TripleButtJointScribeReliefConfig
dataclass
¶
Configuration for triple butt joint relief.
first_timber_to_be_scribed is scribed first, then
second_timber_to_be_scribed, then third_timber_to_be_scribed.
with_order
staticmethod
¶
with_order(first_timber_to_be_scribed: ArrangementNames, second_timber_to_be_scribed: ArrangementNames, third_timber_to_be_scribed: ArrangementNames)
Create a TripleButtJointScribeReliefConfig from the order of which timbers to be scribed
Source code in kumiki/joints/workshop/shavings/relief.py
QuadrupleButtJointScribeReliefConfig
dataclass
¶
Configuration for quadruple butt joint relief.
first_timber_to_be_scribed is scribed first, then
second_timber_to_be_scribed, third_timber_to_be_scribed, and
fourth_timber_to_be_scribed.
with_order
staticmethod
¶
with_order(first_timber_to_be_scribed: ArrangementNames, second_timber_to_be_scribed: ArrangementNames, third_timber_to_be_scribed: ArrangementNames, fourth_timber_to_be_scribed: ArrangementNames)
Create a QuadrupleButtJointScribeReliefConfig from the order of which timbers to be scribed
Source code in kumiki/joints/workshop/shavings/relief.py
CrossCapJointScribeReliefConfig
dataclass
¶
Configuration for cross-cap joint relief.
first_timber_to_be_scribed is scribed first, then
second_timber_to_be_scribed is scribed onto the remaining timber.
with_order
staticmethod
¶
with_order(first_timber_to_be_scribed: ArrangementNames, second_timber_to_be_scribed: ArrangementNames)
Create a CrossCapJointScribeReliefConfig from the order of which timbers to be scribed
Source code in kumiki/joints/workshop/shavings/relief.py
BraceJointScribeReliefConfig
dataclass
¶
Configuration for brace joint relief.
The 2 braced timbers are always scribed onto the brace timber.
with_order
staticmethod
¶
with_order(first_timber_to_be_scribed: ArrangementNames, second_timber_to_be_scribed: ArrangementNames)
Create a BraceJointScribeReliefConfig from the order of which timbers to be scribed
Source code in kumiki/joints/workshop/shavings/relief.py
DropinButtJointSweepScribeReliefConfig
dataclass
¶
Configuration for drop in butt joint relief
The butting (drop in) timber is always scribed onto the receiving timber
Actually the butting timber and its entire swept volume in the drop-in path are scribed onto the receiving timber
ShoulderReliefCSGGeometry
dataclass
¶
CSG geometry produced by the shoulder notch relief functions.
receiving_timber_notch_negative_CSG: cut applied to the receiving (mortise) timber, expressed in that timber's local frame.butting_timber_relief_negative_CSG: cut applied to the butting (tenon) timber, expressed in that timber's local frame.Noneonly when no relief geometry is necessary (currently always populated).
orientation_pointing_towards_face_sitting_on_face
¶
orientation_pointing_towards_face_sitting_on_face(towards_face: TimberFace, sitting_face: TimberFace) -> Orientation
Returns a marking orientation with +z toward towards_face and +y pointing into the timber from sitting_face.
Marking transforms use a convention where, for transforms sitting on a timber face, +y points into the timber. This helper builds that orientation from two perpendicular faces.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
towards_face
|
TimberFace
|
The face the orientation's +z axis should point toward. |
required |
sitting_face
|
TimberFace
|
The face the orientation is sitting on; its outward normal becomes -y. |
required |
Returns:
| Type | Description |
|---|---|
Orientation
|
Orientation with +z pointing toward towards_face and +y pointing into the timber. |
Raises:
| Type | Description |
|---|---|
AssertionError
|
If towards_face and sitting_face are not perpendicular. |
Source code in kumiki/joints/workshop/shavings/shavings.py
scribe_face_plane_onto_centerline
¶
scribe_face_plane_onto_centerline(face: TimberFace, face_timber: TimberLike) -> UnsignedPlane
Mark the face plane on a timber.
Returns the plane defined by the face on face_timber. This plane can then be measured onto another timber's centerline to find shoulder plane positions in various butt joints.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
face
|
TimberFace
|
The face on face_timber to mark |
required |
face_timber
|
TimberLike
|
The timber whose face defines the plane |
required |
Returns:
| Type | Description |
|---|---|
UnsignedPlane
|
UnsignedPlane representing the face plane. This can be measured onto a centerline using |
UnsignedPlane
|
mark_distance_from_end_along_centerline() to find intersection points. |
Example
Mark the plane for timber_b's FRONT face¶
face_plane = scribe_face_plane_onto_centerline( ... face=TimberFace.FRONT, ... face_timber=timber_b ... )
Then measure onto timber_a's centerline¶
marking = mark_distance_from_end_along_centerline(face_plane, timber_a) shoulder_distance = measurement.distance
Source code in kumiki/joints/workshop/shavings/shavings.py
locate_pat_shoulder_plane_from_centerline_to_reference_face
¶
locate_pat_shoulder_plane_from_centerline_to_reference_face(shoulder_timber: TimberLike, reference_timber: TimberLike, reference_face: TimberFace) -> Plane
Compute a shoulder plane on shoulder_timber using a face plane on reference_timber.
This helper assumes a plane-aligned arrangement. It scribes the reference face plane onto the shoulder timber centerline, then returns the timber cross-section plane at that mark (normal = shoulder timber length direction).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shoulder_timber
|
TimberLike
|
Timber receiving the shoulder plane. |
required |
reference_timber
|
TimberLike
|
Timber that owns the reference face. |
required |
reference_face
|
TimberFace
|
Face on |
required |
Returns:
| Type | Description |
|---|---|
Plane
|
Plane perpendicular to |
Source code in kumiki/joints/workshop/shavings/shavings.py
scribe_centerline_onto_centerline
¶
scribe_centerline_onto_centerline(timber: TimberLike) -> Line
Mark the centerline of a timber.
Returns the Line representing the timber's centerline. This line can then be measured onto another timber's centerline to find closest points between skew centerlines.
This is useful for positioning timbers relative to each other, especially in complex 3D joints where centerlines may be skew (non-intersecting, non-parallel).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
TimberLike
|
The timber whose centerline to mark |
required |
Returns:
| Type | Description |
|---|---|
Line
|
Line representing the timber's centerline. This can be measured onto another |
Line
|
timber's centerline using mark_distance_from_end_along_centerline() to find closest points. |
Example
Mark the centerline of timber_b¶
centerline_b = scribe_centerline_onto_centerline(timber_b)
Then measure onto timber_a's centerline¶
measurement_a = mark_distance_from_end_along_centerline(centerline_b, timber_a) dist_a = measurement_a.distance
Source code in kumiki/joints/workshop/shavings/shavings.py
check_timber_overlap_for_splice_joint_is_sensible
¶
check_timber_overlap_for_splice_joint_is_sensible(timberA: TimberLike, timberB: TimberLike, timberA_end: TimberEnd, timberB_end: TimberEnd) -> Optional[str]
Check if two timbers overlap in a sensible way for a splice joint.
A sensible splice joint configuration requires: 1. The joint ends are pointing in opposite directions (anti-parallel) 2. The joint end planes either touch each other or go past each other 3. The joint end planes have not gone so far past each other that they reach the opposite end of the other timber
ASCII diagram of a sensible splice joint: A |==================| <- timberA_end timberB_end -> |==================| B
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timberA
|
TimberLike
|
First timber in the splice joint |
required |
timberB
|
TimberLike
|
Second timber in the splice joint |
required |
timberA_end
|
TimberEnd
|
Which end of timberA is being joined (TOP or BOTTOM) |
required |
timberB_end
|
TimberEnd
|
Which end of timberB is being joined (TOP or BOTTOM) |
required |
Returns:
| Type | Description |
|---|---|
Optional[str]
|
Optional[str]: None if the configuration is sensible, otherwise a string explaining why the configuration fails the sensibility check |
Example
error = check_timber_overlap_for_splice_joint_is_sensible( ... gooseneck, receiving, TimberEnd.BOTTOM, TimberEnd.TOP ... ) if error: ... print(f"Joint configuration error: {error}")
Source code in kumiki/joints/workshop/shavings/shavings.py
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
chop_timber_end_with_prism
¶
chop_timber_end_with_prism(timber: TimberLike, end: TimberEnd, distance_from_end_to_cut: Numeric, label: CutCSGLabel = CutCSGLabel('timber_end_prism_cut')) -> RectangularPrism
Create a RectangularPrism CSG for chopping off material from a timber end (in local coordinates).
Creates a CSG prism in the timber's local coordinate system that starts at distance_from_end_to_cut from the timber end and extends to infinity in the timber length direction. The prism has the same cross-section size as the timber.
This is useful when you need a volumetric cut that exactly matches the timber's cross-section (e.g., for CSGCut objects in compound cuts).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
TimberLike
|
The timber to create a chop prism for |
required |
end
|
TimberEnd
|
Which end to chop from (TOP or BOTTOM) |
required |
distance_from_end_to_cut
|
Numeric
|
Distance from the end where the cut begins |
required |
Returns:
| Name | Type | Description |
|---|---|---|
RectangularPrism |
RectangularPrism
|
A CSG prism in local coordinates representing the material beyond distance_from_end_to_cut from the end, extending to infinity |
Example
Chop everything beyond 2 inches from the top of a timber¶
chop_prism = chop_timber_end_with_prism(my_timber, TimberEnd.TOP, scalar(2))
This creates a semi-infinite prism starting 2 inches from the top¶
Source code in kumiki/joints/workshop/shavings/shavings.py
chop_timber_end_with_half_plane
¶
chop_timber_end_with_half_plane(timber: TimberLike, end: TimberEnd, distance_from_end_to_cut: Numeric, label: CutCSGLabel = NoLabel()) -> HalfSpace
Create a HalfSpace CSG for chopping off material from a timber end (in local coordinates).
Creates a half-plane cut in the timber's local coordinate system, perpendicular to the timber's length direction, positioned at distance_from_end_to_cut from the specified end. The half-plane removes everything beyond that distance.
This is simpler and more efficient than a prism-based cut when you just need a planar cut perpendicular to the timber's length (e.g., for simple butt joints or splice joints).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
TimberLike
|
The timber to create a chop half-plane for |
required |
end
|
TimberEnd
|
Which end to chop from (TOP or BOTTOM) |
required |
distance_from_end_to_cut
|
Numeric
|
Distance from the end where the cut plane is positioned |
required |
label
|
CutCSGLabel
|
What the resulting plane is called in the CSG tree. Left to the caller because an end-chop means something different in every joint that makes one; the top_end_cut / bottom_end_cut names belong to a Cutting's own maybe-end-cuts, not to every plane of this shape. |
NoLabel()
|
Returns:
| Name | Type | Description |
|---|---|---|
HalfSpace |
HalfSpace
|
A half-plane in local coordinates that removes material beyond distance_from_end_to_cut from the end |
Example
Chop everything beyond 2 inches from the top of a timber¶
chop_plane = chop_timber_end_with_half_plane(my_timber, TimberEnd.TOP, scalar(2))
This creates a half-plane 2 inches from the top, removing everything beyond¶
Source code in kumiki/joints/workshop/shavings/shavings.py
chop_lap_on_timber_end
¶
chop_lap_on_timber_end(lap_timber: TimberLike, lap_timber_end: TimberEnd, lap_timber_face: TimberFace, lap_length: Numeric, lap_shoulder_position_from_lap_timber_end: Numeric, lap_depth: Numeric, label: CutCSGLabel = CutCSGLabel('lap_cut')) -> Tuple[CutCSG, HalfSpace]
Create CSG cuts for a lap joint between two timber ends.
Creates material removal volumes for both timbers in a lap joint configuration where one timber (top lap) has material removed from one face, and the other timber (bottom lap) has material removed from the opposite face so they interlock.
lap_timber_face
v |--------| lap_length
╔════════════════════════╗ - ║face_lap_timber ║ | lap_depth ║ ╔════════╝ - ║ ║ ║ ║ ╚═══════════════╝ ^ lap_shoulder_position_from_lap_timber_end
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lap_timber
|
TimberLike
|
The timber that will have material removed from the specified face |
required |
lap_timber_end
|
TimberEnd
|
Which end of the top lap timber is being joined |
required |
lap_timber_face
|
TimberFace
|
Which face of the top lap timber to remove material from |
required |
lap_length
|
Numeric
|
Length of the lap region along the timber length |
required |
lap_shoulder_position_from_lap_timber_end
|
Numeric
|
Distance from the timber end to the shoulder (inward) |
required |
lap_depth
|
Numeric
|
Depth of material to remove (measured from lap_timber_face) |
required |
Returns:
| Type | Description |
|---|---|
CutCSG
|
Tuple of (lap_prism, end_cut_half_plane) representing material to remove from the timber |
HalfSpace
|
Both CSGs are in local coordinates of the timber |
Example
Create a half-lap joint¶
top_lap, top_end_cut = chop_lap_on_timber_end( ... timber_a, TimberEnd.TOP, ... TimberFace.BOTTOM, lap_length=4, lap_depth=2, shoulder_pos=1 ... )
Source code in kumiki/joints/workshop/shavings/shavings.py
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 | |
chop_lap_on_timber_ends
¶
chop_lap_on_timber_ends(top_lap_timber: TimberLike, top_lap_timber_end: TimberEnd, bottom_lap_timber: TimberLike, bottom_lap_timber_end: TimberEnd, top_lap_timber_face: TimberLongFace, lap_length: Numeric, top_lap_shoulder_position_from_top_lap_shoulder_timber_end: Numeric, lap_depth: Numeric, label: CutCSGLabel = CutCSGLabel('lap_cut')) -> Tuple[Tuple[CutCSG, HalfSpace], Tuple[CutCSG, HalfSpace]]
Create CSG cuts for a lap joint between two timber ends.
Creates material removal volumes for both timbers in a lap joint configuration where one timber (top lap) has material removed from one face, and the other timber (bottom lap) has material removed from the opposite face so they interlock.
top_lap_timber_face
v |--------| lap_length
╔════════════════════════╗╔══════╗ - ║face_lap_timber ║║ ║ | lap_depth ║ ╔════════╝║ ║ - ║ ║╔════════╝ ║ ║ ║║ timberB ║ ╚═══════════════╝╚═══════════════╝ ^ top_lap_shoulder_position_from_top_lap_shoulder_timber_end
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
top_lap_timber
|
TimberLike
|
The timber that will have material removed from the specified face |
required |
top_lap_timber_end
|
TimberEnd
|
Which end of the top lap timber is being joined |
required |
bottom_lap_timber
|
TimberLike
|
The timber that will have material removed from the opposite face |
required |
bottom_lap_timber_end
|
TimberEnd
|
Which end of the bottom lap timber is being joined |
required |
top_lap_timber_face
|
TimberLongFace
|
Which face of the top lap timber to remove material from |
required |
lap_length
|
Numeric
|
Length of the lap region along the timber length |
required |
top_lap_shoulder_position_from_top_lap_shoulder_timber_end
|
Numeric
|
Distance from the timber end to the shoulder (inward) |
required |
lap_depth
|
Numeric
|
Depth of material to remove (measured from top_lap_timber_face) |
required |
Returns:
| Type | Description |
|---|---|
Tuple[CutCSG, HalfSpace]
|
Tuple of ((top_lap_prism, top_end_cut), (bottom_lap_prism, bottom_end_cut)) |
Tuple[CutCSG, HalfSpace]
|
Each tuple contains the lap CSG and end cut HalfSpace for that timber |
Tuple[Tuple[CutCSG, HalfSpace], Tuple[CutCSG, HalfSpace]]
|
All CSGs are in local coordinates of their respective timbers |
Example
Create a half-lap joint¶
(top_lap, top_end), (bottom_lap, bottom_end) = chop_lap_on_timber_ends( ... timber_a, TimberEnd.TOP, ... timber_b, TimberEnd.BOTTOM, ... TimberFace.BOTTOM, lap_length=4, lap_depth=2, shoulder_pos=1 ... )
Source code in kumiki/joints/workshop/shavings/shavings.py
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 | |
chop_profile_on_timber_face
¶
chop_profile_on_timber_face(timber: TimberLike, end: TimberEnd, face: TimberFace, profile: Union[List[V2], List[List[V2]]], depth: Numeric, profile_y_offset_from_end: Numeric = scalar(0), label: CutCSGLabel = CutCSGLabel('profile_cut')) -> Union[SolidUnion, ConvexPolygonExtrusion]
Create a CSG extrusion of a profile (or multiple profiles) on a timber face. See the diagram below for understanding how to interpret the profile in the timber's local space based on the end and face arguments.
end
timber v ^ ╔════════════════════════╗ -x ║face ║< (0,profile_y_offset_from_end) of the profile +y -> ╚════════════════════════╝ +x v
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
TimberLike
|
The timber to create a profile for |
required |
end
|
TimberEnd
|
Which end to create the profile on (determines the origin and rotation of the profile) |
required |
face
|
TimberFace
|
Which face to create the profile on (determines the origin, rotation, and extrusion direction of the profile) |
required |
profile
|
Union[List[V2], List[List[V2]]]
|
Either a single profile (List[V2]) or multiple profiles (List[List[V2]]). Multiple profiles are provided as a convenience for creating non-convex shapes by unioning multiple convex polygon extrusions. |
required |
depth
|
Numeric
|
Depth to extrude the profile through the timber's face |
required |
profile_y_offset_from_end
|
Numeric
|
Offset in the Y direction (along timber length from end). The profile will be translated by -profile_y_offset_from_end, so the origin (0,0) in profile coordinates corresponds to (0, profile_y_offset_from_end) in the timber's end-face coordinate system. |
scalar(0)
|
Returns:
| Type | Description |
|---|---|
Union[SolidUnion, ConvexPolygonExtrusion]
|
CutCSG representing the extruded profile(s) in the timber's local coordinates. |
Union[SolidUnion, ConvexPolygonExtrusion]
|
If multiple profiles are provided, returns a SolidUnion of all extruded profiles. |
Notes
- The profile is positioned at the intersection of the specified end and face
- Profile coordinates: X-axis points into timber from end, Y-axis across face, origin at (0,0) on face
- The extrusion extends inward from the face by the specified depth
- For non-convex shapes, provide multiple profiles (List[List[V2]]) which will be individually extruded and unioned together
- Each individual profile uses ConvexPolygonExtrusion, so complex non-convex shapes should be decomposed into multiple convex profiles
Source code in kumiki/joints/workshop/shavings/shavings.py
684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 | |
draw_gooseneck_polygon_NONCONVEX
¶
draw_gooseneck_polygon_NONCONVEX(length: Numeric, small_width: Numeric, large_width: Numeric, head_length: Numeric) -> List[V2]
Returns the non-convex gooseneck profile as a single polygon (for reference/visualization).
The gooseneck shape has a narrow neck that widens into a trapezoidal head. This polygon is non-convex and cannot be used directly with chop_profile_on_timber_face. Use draw_gooseneck_polygon_CONVEX (aliased as draw_gooseneck_polygon) for actual cutting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
length
|
Numeric
|
Total length of the gooseneck shape along the profile Y-axis. |
required |
small_width
|
Numeric
|
Width of the neck (the narrow portion). |
required |
large_width
|
Numeric
|
Width of the head (the wide trapezoid base; must be > small_width). |
required |
head_length
|
Numeric
|
Length of the trapezoidal head portion. |
required |
Returns:
| Type | Description |
|---|---|
List[V2]
|
List of 2D points forming the non-convex gooseneck polygon (counter-clockwise). |
Source code in kumiki/joints/workshop/shavings/shavings.py
draw_gooseneck_polygon_CONVEX
¶
draw_gooseneck_polygon_CONVEX(length: Numeric, small_width: Numeric, large_width: Numeric, head_length: Numeric) -> List[List[V2]]
Returns the gooseneck profile decomposed into convex polygons for use with chop_profile_on_timber_face.
The non-convex gooseneck shape is split into two convex polygons — a neck rectangle and a head trapezoid — whose union gives the full gooseneck. This is the format required by chop_profile_on_timber_face (List[List[V2]]).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
length
|
Numeric
|
Total length of the gooseneck shape along the profile Y-axis. |
required |
small_width
|
Numeric
|
Width of the neck (the narrow portion). |
required |
large_width
|
Numeric
|
Width of the head (the wide trapezoid base; must be > small_width). |
required |
head_length
|
Numeric
|
Length of the trapezoidal head portion. |
required |
Returns:
| Type | Description |
|---|---|
List[List[V2]]
|
List of two convex polygon point lists: [neck_rectangle, head_trapezoid]. |
Source code in kumiki/joints/workshop/shavings/shavings.py
solve_assembly
¶
solve_assembly(members: Sequence[AssemblyMember], joints: Sequence[AssemblyJoint], clearout_clearance: float = _DEFAULT_CLEAROUT_CLEARANCE, should_cancel: Optional[Callable[[], bool]] = None) -> Optional[AssemblySolution]
Solve the disassembly sequence for an abstract assembly graph.
Returns None when no member has any translational freedom. On an unsolvable ordering the already-solved steps (including the failing ordering's earlier substeps) are returned with an AssemblyFailure — it never raises for unsolvability.
Raises NotImplementedError for rotational freedoms and ValueError for joints referencing unknown member keys.
Source code in kumiki/assembly.py
1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 | |
compute_timber_orientation
¶
compute_timber_orientation(length_direction: Direction3D, width_direction: Direction3D) -> Orientation
Compute the orientation matrix from length and width directions
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
length_direction
|
Direction3D
|
Direction vector for the length axis as 3D vector, the +length direction is the +Z direction |
required |
width_direction
|
Direction3D
|
Direction vector for the width axis as 3D vector, the +width direction is the +X direction |
required |
Returns:
| Type | Description |
|---|---|
Orientation
|
Orientation object representing the timber's orientation in 3D space |
Source code in kumiki/timber.py
create_timber
¶
create_timber(length: Numeric, size: V2, bottom_position: V3, length_direction: Direction3D, width_direction: Direction3D, ticket: Optional[Union[TimberTicket, str]] = None) -> Timber
Factory function to create a Timber with computed orientation from direction vectors
This is the main way to construct Timber instances. It takes direction vectors and computes the proper orientation matrix automatically.
AGENT NOTE: AVOID this function if possible, prefer methods like join_timber, attach_timber, create_*_timber_on_footprint, or even create_axis_aligned_timber, which are more robust and easier to use.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
length
|
Numeric
|
Length of the timber |
required |
size
|
V2
|
Cross-sectional size (width, height) as 2D vector, width is the X dimension (left to right), height is the Y dimension (front to back) |
required |
bottom_position
|
V3
|
Position of the bottom point (center of cross-section) as 3D vector |
required |
length_direction
|
Direction3D
|
Direction vector for the length axis as 3D vector, the +length direction is the +Z direction |
required |
width_direction
|
Direction3D
|
Direction vector for the width axis as 3D vector, the +width direction is the +X direction |
required |
ticket
|
Optional[Union[TimberTicket, str]]
|
Optional ticket for this timber (can be TimberTicket object or string name, used for rendering/debugging) |
None
|
Returns:
| Type | Description |
|---|---|
Timber
|
Timber instance with computed orientation |
Source code in kumiki/timber.py
did_end_cuts_extend_timber
¶
did_end_cuts_extend_timber(timber: PerfectTimberWithin, cuts: List[Cutting]) -> bool
Check if any end cuts extend beyond the timber's original bounds.
An end cut extends beyond if: - Top cut: The cutting plane is at z > timber.length (cuts beyond the top) - Bottom cut: The cutting plane is at z < 0 (cuts beyond the bottom)
In local coordinates, HalfSpace end cuts are defined with: - Top cuts: normal pointing up (+Z), offset at the cut location - Bottom cuts: normal pointing down (-Z), offset at the cut location (negative value)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
PerfectTimberWithin
|
The timber being cut |
required |
cuts
|
List[Cutting]
|
List of cuts on the timber |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if any end cut extends beyond the timber's original length |
Source code in kumiki/timber.py
make_compound_joint
¶
make_compound_joint(joints: List[Joint], ticket: JointTicket) -> Joint
Create a compound joint that combines multiple joints together.
The cuttings and accessories from all joints are merged into a single Joint object. Numeric suffixes are added to accessory and cutting keys if there are conflicts. The tickets of the input joints are ignored.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
joints
|
List[Joint]
|
List of Joint objects to combine |
required |
ticket
|
JointTicket
|
JointTicket for the compound joint |
required |
Source code in kumiki/timber.py
require_check
¶
add_milestone
¶
Emit a milestone marker for the viewer loading screen.
Writes a JSON protocol message to the real stdout pipe so the viewer extension can display progress during script execution. No-ops when not running inside the Kigumi extension (checks KIGUMI_VIEWER_MILESTONES environment variable).
Source code in kumiki/timber.py
solve_frame_assembly
¶
solve_frame_assembly(frame: Frame, should_cancel: Optional[Callable[[], bool]] = None) -> Optional[AssemblySolution]
Solve the disassembly sequence for a frame's source joints.
Adapts the frame into the abstract assembly graph of kumiki/assembly.py — one AssemblyMember per distinct timber/accessory (keyed by ticket kumiki_id, positioned at the timber centroid) and one AssemblyJoint per source joint — then delegates to solve_assembly.
Returns None when no member of any source joint has an assembly freedom.
Source code in kumiki/timber.py
3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 | |
intersect_planes
¶
The infinite line where two planes meet, or None if they never do.
None covers three cases that all mean "no line here": either plane missing (a caller passing through a locate() that declined), the planes parallel, and the planes coincident. Coincident planes are geometrically a whole shared plane rather than a line, so they are not an intersection this can describe -- that relation is worth capturing separately, since two coincident faces is exactly the rough-matches-perfect test, but it is not an edge.
The returned direction is normalised; the returned point is the point on the line closest to the origin.
Source code in kumiki/geometry.py
planes_are_parallel
¶
Whether two planes never meet in a line (parallel, or the same plane).
arris_against_cap
¶
arris_against_cap(side: int, sides: int, end: bool) -> FeatureKey
The arris where a side meets one of the caps.
One of the two places that know how the ARRIS run is laid out; see FeatureCategory. Reading "arris.5" back needs the side count, which nothing in the code has to do -- a key is matched and named, never decoded -- so that cost falls on a person rather than on a caller.
Source code in kumiki/cutcsg.py
corner_on_cap
¶
corner_on_cap(vertex: int, vertices: int, end: bool) -> FeatureKey
Vertex n of the start or end profile, in the one CORNER run.
default_feature_name
¶
default_feature_name(key: FeatureKey) -> str
What a default feature is called when nobody has named it.
Deterministic, so a default is referenceable -- from a drawing, a measurement, an override -- without anyone having authored a name for it. Lower case and dotted to sit alongside the authored names already in use, which look like "ptw.front" and "rough.back_right".
Source code in kumiki/cutcsg.py
feature_groups_intersect
¶
feature_groups_intersect(a: FeatureGroup, b: FeatureGroup) -> bool
derive_edge_hits
¶
derive_edge_hits(owner: CutCSG, face_hits: List[OwnedFeatureHit]) -> List[OwnedFeatureHit]
Every edge formed by a pair of face_hits, owned by owner.
The pairs come from a scan run at the edge tolerance, so if two faces both turned up there, the conjunction that defines their edge holds at that tolerance by construction -- no further point testing needed. That is what makes this O(k^2) over the few faces near the point rather than over everything the subtree declares.
Not deduplicated: derivation runs once, at whichever node the caller queried, so nothing arrives here twice. Names are not unique enough to dedupe by anyway -- two tenons on one timber legitimately declare the same face names, which makes their edges share a name while being genuinely different edges.
Source code in kumiki/cutcsg.py
csg_children
¶
The nodes directly beneath csg; empty for a primitive.
Source code in kumiki/cutcsg.py
csg_children_with_parity
¶
csg_children_with_parity(csg: CutCSG, parity: CSGParity = ADDITIVE) -> List[Tuple[CutCSG, CSGParity]]
The nodes directly beneath csg, each with its own parity.
The one statement of the rule: a Difference's subtract children invert, and nothing else does. A union's children are each monotone-increasing in the union, an intersection's operands in the intersection, and a Difference's base in the difference -- so those all inherit.
Children come back in csg_children order.
Source code in kumiki/cutcsg.py
walk_csg_with_parity
¶
walk_csg_with_parity(root: CutCSG, parity: CSGParity = ADDITIVE) -> Iterator[Tuple[CutCSG, CSGParity]]
Every node beneath root, including root, with its parity.
Parity belongs to a node's POSITION, not to the node: a node has no parent pointer and cannot answer on its own, and the same subtree placed twice in one tree can have a different answer each time. So this yields one entry per occurrence and always starts from a root -- there is no way to ask a node about itself.
Two subtract edges cancel: in A - (B - C) the C is ADDITIVE, and
indeed C restores material that B removed.
Source code in kumiki/cutcsg.py
translate_profile
¶
translate_profiles
¶
Translate a list of profiles by a given translation vector.
decompose_simple_polygon_into_convex_pieces
¶
Decompose a simple (non-self-intersecting) polygon, given as an ordered list of (u, v) points, into convex quads/triangles whose union equals the polygon — via horizontal (constant-v) trapezoidal decomposition.
See pathcsg.decompose_path_into_convex_pieces for the same algorithm generalized to a Path (lines + arcs): it sweeps directly over a Path's segments instead of a pre-tessellated point list, so the expensive exact-arithmetic part runs over the (small) segment count rather than however many points arc tessellation would otherwise produce. Not wired together with this function (would need pathcsg -> cutcsg -> pathcsg, which is circular) — kept as two independent implementations of the same sweep for now.
Splits the polygon at every vertex's v-coordinate, and within each resulting v-band, finds every edge active there, sorts their u-crossings left to right, and pairs them up with the standard even-odd polygon-fill rule (1st-2nd pair is interior, 3rd-4th pair is interior, and so on). This handles overlapping v-ranges between edges correctly (unlike naively treating each edge as its own independent band), and degenerate edges that double back along another edge (contributing paired, zero-width crossings) simply cancel out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
Profile
|
Ordered polygon vertices (u, v), last connects back to first. v need not be monotonic along the boundary. |
required |
Returns:
| Type | Description |
|---|---|
List[Profile]
|
List of convex pieces, each a Profile (quad or triangle) suitable for |
List[Profile]
|
ConvexPolygonExtrusion. |
Source code in kumiki/cutcsg.py
3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 | |
translate_csg
¶
Return a copy of the CSG object translated by the given vector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
csg
|
CutCSG
|
The CSG object to translate |
required |
translation
|
V3
|
3D translation vector (3x1 Matrix) |
required |
Returns:
| Type | Description |
|---|---|
CutCSG
|
A new CSG object with the same structure but translated by translation |
Source code in kumiki/cutcsg.py
create_axis_aligned_timber
¶
create_axis_aligned_timber(bottom_position: V3, length: Numeric, size: V2, length_direction: TimberFace, width_direction: Optional[TimberFace] = None, ticket: Optional[Union[TimberTicket, str]] = None) -> Timber
Creates an axis-aligned timber using TimberFace to reference directions in the world coordinate system.
AGENT NOTE: Prefer methods like join_timber, attach_face/plane_aligned_timber, create_*_timber_on_footprint, which are more robust and easier to use.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bottom_position
|
V3
|
Position of the bottom point of the timber |
required |
length
|
Numeric
|
Length of the timber |
required |
size
|
V2
|
Cross-sectional size (width, height) |
required |
length_direction
|
TimberFace
|
Direction for the timber's length axis |
required |
width_direction
|
Optional[TimberFace]
|
Optional direction for the timber's width axis. If not provided, defaults to RIGHT (+X) unless length_direction is RIGHT, in which case TOP (+Z) is used. |
None
|
ticket
|
Optional[Union[TimberTicket, str]]
|
Optional ticket for this timber (can be Ticket object or string name, used for rendering/debugging) |
None
|
Returns:
| Type | Description |
|---|---|
Timber
|
New timber with the specified axis-aligned orientation |
Source code in kumiki/construction.py
create_vertical_timber_on_footprint_corner
¶
create_vertical_timber_on_footprint_corner(footprint: Footprint, corner_index: int, length: Numeric, location_type: FootprintLocation, size: V2, ticket: Optional[Union[TimberTicket, str]] = None) -> Timber
Creates a vertical timber (post) on a footprint boundary corner.
The post is positioned on an orthogonal boundary corner (where two boundary sides are perpendicular) according to the location type:
Location types: - INSIDE: Post has one vertex of bottom face on the boundary corner, with 2 edges aligned with the 2 boundary sides, post extends inside the boundary - OUTSIDE: Post positioned with opposite vertex on the boundary corner, extends outside - CENTER: Post center is on the boundary corner, with 2 edges parallel to boundary sides
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
footprint
|
Footprint
|
The footprint to place the timber on |
required |
corner_index
|
int
|
Index of the boundary corner |
required |
length
|
Numeric
|
Length of the vertical timber (height) |
required |
location_type
|
FootprintLocation
|
Where to position the timber relative to the boundary corner |
required |
size
|
V2
|
Timber size (width, depth) as a 2D vector |
required |
ticket
|
Optional[Union[TimberTicket, str]]
|
Optional ticket for this timber (can be Ticket object or string name, used for rendering/debugging) |
None
|
Returns:
| Type | Description |
|---|---|
Timber
|
Timber positioned vertically on the footprint boundary corner |
Source code in kumiki/construction.py
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | |
create_vertical_timber_on_footprint_side
¶
create_vertical_timber_on_footprint_side(footprint: Footprint, side_index: int, distance_along_side: Numeric, length: Numeric, location_type: FootprintLocation, size: V2, ticket: Optional[Union[TimberTicket, str]] = None) -> Timber
Creates a vertical timber (post) positioned at a point along a footprint boundary side.
The post is placed at a specified distance along the boundary side from the starting corner.
Location types: - INSIDE: One edge of bottom face lies on boundary side, center of edge at the point, post extends inside - OUTSIDE: One edge of bottom face lies on boundary side, center of edge at the point, post extends outside - CENTER: Center of bottom face is on the point, 2 edges of bottom face parallel to boundary side
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
footprint
|
Footprint
|
The footprint to place the timber on |
required |
side_index
|
int
|
Index of the boundary side (from corner[side_index] to corner[side_index+1]) |
required |
distance_along_side
|
Numeric
|
Distance from the starting corner along the side (0 = at start corner) |
required |
length
|
Numeric
|
Length of the vertical timber (height) |
required |
location_type
|
FootprintLocation
|
Where to position the timber relative to the boundary side |
required |
size
|
V2
|
Timber size (width, depth) as a 2D vector |
required |
ticket
|
Optional[Union[TimberTicket, str]]
|
Optional ticket for this timber (can be Ticket object or string name, used for rendering/debugging) |
None
|
Returns:
| Type | Description |
|---|---|
Timber
|
Timber positioned vertically at the specified point on the footprint boundary side |
Source code in kumiki/construction.py
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | |
create_horizontal_timber_on_footprint
¶
create_horizontal_timber_on_footprint(footprint: Footprint, corner_index: int, location_type: FootprintLocation, size: V2, length: Optional[Numeric] = None, ticket: Optional[Union[TimberTicket, str]] = None) -> Timber
Creates a horizontal timber (mudsill) on the footprint boundary side.
The mudsill runs from corner_index to corner_index + 1 along the boundary side. With the face ends of the mudsill timber starting/ending on the footprint corners.
Location types: - INSIDE: One edge of the timber lies on the boundary side, timber is on the inside - OUTSIDE: One edge of the timber lies on the boundary side, timber is on the outside - CENTER: The centerline of the timber lies on the boundary side
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
footprint
|
Footprint
|
The footprint to place the timber on |
required |
corner_index
|
int
|
Index of the starting boundary corner |
required |
location_type
|
FootprintLocation
|
Where to position the timber relative to the boundary side |
required |
size
|
V2
|
Timber size (width, height) as a 2D vector |
required |
length
|
Optional[Numeric]
|
Length of the timber (optional; if not provided, uses boundary side length) |
None
|
ticket
|
Optional[Union[TimberTicket, str]]
|
Optional ticket for this timber (can be Ticket object or string name, used for rendering/debugging) |
None
|
Returns:
| Type | Description |
|---|---|
Timber
|
Timber positioned on the footprint boundary side |
Source code in kumiki/construction.py
stretch_timber
¶
stretch_timber(timber: Timber, end: TimberEnd, overlap_length: Numeric, extend_length: Numeric) -> Timber
Creates a new timber extending the original timber by a given length.
The original timber is conceptually discarded and replaced with a new timber that is the original timber plus the extension.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
end
|
TimberEnd
|
The end of the timber to extend |
required |
overlap_length
|
Numeric
|
Length of timber to overlap with existing timber |
required |
extend_length
|
Numeric
|
Length of timber to extend beyond the end of the original timber (does not include the overlap length) |
required |
Source code in kumiki/construction.py
split_timber
¶
split_timber(timber: Timber, distance_from_bottom: Numeric, ticket1: Optional[Union[TimberTicket, str]] = None, ticket2: Optional[Union[TimberTicket, str]] = None) -> Tuple[Timber, Timber]
Split a timber into two timbers at the specified distance from the bottom.
The original timber is conceptually discarded and replaced with two new timbers: - The first timber extends from the original bottom to the split point - The second timber extends from the split point to the original top
Both timbers maintain the same cross-sectional size and orientation as the original. You will often follow this with a splice joint to join the two timbers together.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
Timber
|
The timber to split |
required |
distance_from_bottom
|
Numeric
|
Distance along the timber's length where to split (0 < distance < timber.length) |
required |
ticket1
|
Optional[Union[TimberTicket, str]]
|
Optional ticket for the bottom timber (defaults to "{original_name}_bottom") |
None
|
ticket2
|
Optional[Union[TimberTicket, str]]
|
Optional ticket for the top timber (defaults to "{original_name}_top") |
None
|
Returns:
| Type | Description |
|---|---|
Timber
|
Tuple of (bottom_timber, top_timber) where: |
Timber
|
|
Tuple[Timber, Timber]
|
|
Example
If a timber has length 10 and is split at distance 3: - bottom_timber has length 3, same origin as original - top_timber has length 7, origin at distance 3 from original origin
Source code in kumiki/construction.py
attach_timber
¶
attach_timber(original_timber: TimberLike, size: V2, attached_timber_direction: Direction3D, attached_timber_length: Numeric, attached_timber_opposite_length: Numeric = scalar(0), attached_timber_width_direction: Optional[Direction3D] = None, attached_timber_end_that_points_towards_original_timber: TimberEnd = BOTTOM, original_timber_end_to_measure_from_for_length_position: TimberEnd = BOTTOM, length_position_measurement: Numeric = scalar(0), lateral_offset: Numeric = scalar(0), ticket: Optional[Union[TimberTicket, str]] = None)
NOTE this function is perhaps not so useful in practice, it's mainly here for completeness. Perhaps there are some cases where it's a better alternative to create_timber
Creates a timber that is attached to original_timber.
The original timber is referred to as "original_timber" and the new timber as "attached_timber".
Positioning¶
The attached timber's attached_timber_end_that_points_towards_original_timber end position is length_position_measurement away from
original_timber_end_to_measure_from_for_length_position and then lateral_offset away from the centerline of the original timber,
measured in the direction
length axis of the original timber CROSS length axis of the created attached timber
Orientation¶
The attached timber's orientation is such that its length axis is in the attached_timber_direction direction
and its right face best aligns with attached_timber_right_direction.
if attached_timber_right_direction is None then the direction of the TOP face of the original timber is used instead.
Returns:
| Type | Description |
|---|---|
|
The new attached timber, face-aligned with and positioned relative to the original timber. |
Source code in kumiki/construction.py
537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 | |
attach_plane_aligned_timber
¶
attach_plane_aligned_timber(original_timber: TimberLike, size: V2, original_timber_long_face_that_attached_timber_points_to: TimberLongFace, attached_timber_angle: Numeric, attached_timber_length_or_target: Union[Numeric, TimberLike], attached_timber_stickout: Stickout = nostickout(), attached_timber_end_that_points_towards_original_timber: TimberEnd = BOTTOM, original_timber_end_to_measure_from_for_length_position: TimberEnd = BOTTOM, attached_timber_long_face_to_measure_to_for_length_position: Union[TimberLongFace, TimberCenterline] = CENTERLINE, length_position_measurement: Numeric = scalar(0), original_timber_face_to_measure_from_for_lateral_position: Union[TimberFace, TimberCenterline] = CENTERLINE, attached_timber_long_face_to_measure_to_for_lateral_position: Union[TimberLongFace, TimberCenterline] = CENTERLINE, lateral_position_measurement: Numeric = scalar(0), ticket: Optional[Union[TimberTicket, str]] = None) -> Timber
Creates a timber that is plane-aligned with and attached to original_timber at an angle.
Generalizes :func:attach_face_aligned_timber: the attached timber's length axis lies in the
plane spanned by the original timber's length axis and the normal of
original_timber_long_face_that_attached_timber_points_to (the face it points out of), making
an angle of attached_timber_angle with the original timber's length axis. The attached
timber stays plane-aligned with the original (two of its long faces remain parallel to the
original's lateral faces). attach_face_aligned_timber is the attached_timber_angle == pi/2
(perpendicular) case.
attached_timber_end_that_points_towards_original_timber chooses which end of the attached
timber sits on the original-timber side; note that this flips the realized angle to
pi - attached_timber_angle.
Extents¶
attached_timber_length_or_target places the target end (the end pointing away from the
original timber):
- a numeric length extends the timber along its (tilted) length axis, measured from the
original timber's centerline.
- a timber extends the attached timber until its centerline just touches the target timber's
reference feature selected by attached_timber_stickout.stickoutReference2, taken on the
target's silhouette projected onto the plane spanned by the original timber's length axis
and the attach direction: its CENTER_LINE, or the near (INSIDE) / far (OUTSIDE) boundary of
the silhouette (for a target plane-aligned with that plane these are its long faces; for a
rotated target, its projected corner edges). If the target's centerline is parallel to the
lateral axis it projects to a single point, which is dropped perpendicularly onto the
attached timber's length axis. stickout2 then extends the target end beyond that
feature. stickout2 is ignored (with a warning if set) when a numeric length is given
instead.
attached_timber_stickout places the start end (the end that attaches to the original
timber): the start end is where the attached timber's centerline just touches the original
timber's feature selected by stickoutReference1 — its CENTER_LINE (default), the INSIDE
face (the face the attached timber points out of), or the OUTSIDE face (the opposite face) —
extended by stickout1 beyond it.
Everything else follows attach_face_aligned_timber:
- the length-position is measured along the original timber's length axis from
original_timber_end_to_measure_from_for_length_position to
attached_timber_long_face_to_measure_to_for_length_position (or orthogonally to its centerline).
- the lateral-position is measured along the lateral axis from
original_timber_face_to_measure_from_for_lateral_position to
attached_timber_long_face_to_measure_to_for_lateral_position (or orthogonally to its centerline).
All measurements are taken from the perfect timber within of the original and attached timber.
original_timber_long_face_that_attached_timber_points_to chosen on the wrong side (e.g. the
target actually lies opposite the face's outward normal) would otherwise solve for a
non-positive attached timber length. Rather than failing in that case, this automatically
retries with the opposite long face (RIGHT<->LEFT or FRONT<->BACK) and, if that succeeds,
emits a warning and uses it instead. Only fails if BOTH the requested face and its opposite
produce a non-positive length.
Returns:
| Type | Description |
|---|---|
Timber
|
The new attached timber, plane-aligned with and positioned relative to the original timber. |
Source code in kumiki/construction.py
625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 | |
attach_face_aligned_timber
¶
attach_face_aligned_timber(original_timber: TimberLike, size: V2, original_timber_long_face_that_attached_timber_points_to: TimberLongFace, attached_timber_length_or_target: Union[Numeric, TimberLike], attached_timber_stickout: Stickout = nostickout(), attached_timber_end_that_points_towards_original_timber: TimberEnd = BOTTOM, original_timber_end_to_measure_from_for_length_position: TimberEnd = BOTTOM, attached_timber_long_face_to_measure_to_for_length_position: Union[TimberLongFace, TimberCenterline] = CENTERLINE, length_position_measurement: Numeric = scalar(0), original_timber_face_to_measure_from_for_lateral_position: Union[TimberFace, TimberCenterline] = CENTERLINE, attached_timber_long_face_to_measure_to_for_lateral_position: Union[TimberLongFace, TimberCenterline] = CENTERLINE, lateral_position_measurement: Numeric = scalar(0), ticket: Optional[Union[TimberTicket, str]] = None) -> Timber
Creates a timber that is face-aligned with and attached to original_timber.
The original timber is referred to as "original_timber" and the new timber as "attached_timber". The attached timber runs perpendicular to the chosen long face of the original timber and is fully face-aligned with it.
All measurements are taken from the perfect timber within of the original and attached timber
Orientation¶
The attached timber's length axis runs along the normal of
original_timber_long_face_that_attached_timber_points_to (the face it "points to" / sticks
out of). attached_timber_end_that_points_towards_original_timber chooses which end
(TOP/BOTTOM) of the attached timber sits on the original-timber side. If the chosen face turns
out to be on the wrong side of the target, this automatically retries with its opposite long
face instead of failing -- see :func:attach_plane_aligned_timber.
The attached timber's height and width axis orientation are determined by: - attached_timber_long_face_to_measure_to_for_lateral_position - attached_timber_long_face_to_measure_to_for_length_position so that these faces are parallel to the features on original_timber that they are measured from.
Extents¶
attached_timber_length_or_target places the far end of the attached timber: either a
numeric length measured from the original timber's centerline, or a timber to extend to (up
to the feature selected by attached_timber_stickout.stickoutReference2, plus
stickout2 beyond it). attached_timber_stickout places the near end relative to the
original timber's feature selected by stickoutReference1 (CENTER_LINE by default),
extended by stickout1 beyond it. See :func:attach_plane_aligned_timber for details.
Positioning¶
The attached timber's position is such that the distance between
original_timber_end_to_measure_from_for_length_position and attached_timber_long_face_to_measure_to_for_length_position
is length_position_measurement,
and the distance between
original_timber_face_to_measure_from_for_lateral_position and attached_timber_long_face_to_measure_to_for_lateral_position
is lateral_position_measurement.
Returns:
| Type | Description |
|---|---|
Timber
|
The new attached timber, face-aligned with and positioned relative to the original timber. |
Source code in kumiki/construction.py
951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 | |
join_timbers
¶
join_timbers(timber1: PerfectTimberWithin, timber2: PerfectTimberWithin, location_on_timber1: Numeric, location_on_timber2: Optional[Numeric] = None, lateral_offset: Numeric = scalar(0), stickout: Stickout = nostickout(), size: Optional[V2] = None, orientation_width_vector: Optional[Direction3D] = None, ticket: Optional[Union[TimberTicket, str]] = None) -> Timber
Joins two timbers by creating a connecting timber from centerline to centerline.
This function creates a timber that connects the centerline of timber1 to the centerline of timber2. The joining timber's length direction goes from timber1 to timber2, and its position can be laterally offset from this centerline-to-centerline path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber1
|
PerfectTimberWithin
|
First timber to join (start point) |
required |
timber2
|
PerfectTimberWithin
|
Second timber to join (end point) |
required |
location_on_timber1
|
Numeric
|
Position along timber1's length where the joining timber starts |
required |
location_on_timber2
|
Optional[Numeric]
|
Optional position along timber2's length where the joining timber ends. If not provided, uses the same Z-height as location_on_timber1. |
None
|
lateral_offset
|
Numeric
|
Lateral offset of the joining timber perpendicular to the direct centerline-to-centerline path. The offset direction is determined by the cross product of timber1's length direction and the joining direction. Defaults to scalar(0) (no offset). |
scalar(0)
|
stickout
|
Stickout
|
How much the joining timber extends beyond each connection point (both sides). Always measured from centerlines in this function. Defaults to Stickout.nostickout() if not provided. |
nostickout()
|
size
|
Optional[V2]
|
Optional size (width, height) of the joining timber. If not provided, determined from timber1's size based on orientation. |
None
|
orientation_width_vector
|
Optional[Direction3D]
|
Optional width direction hint for the created timber in global space. Will be automatically projected onto the normal plane of the length axis of the created timber. This is useful for specifying orientation like "face up" for rafters. If not provided, uses timber1's length direction projected onto the perpendicular plane. If the provided vector is parallel to the joining direction, falls back to timber1's width direction. |
None
|
ticket
|
Optional[Union[TimberTicket, str]]
|
Optional ticket for this timber (can be Ticket object or string name, used for rendering/debugging) |
None
|
Returns:
| Type | Description |
|---|---|
Timber
|
New timber connecting timber1 and timber2 along their centerlines |
Source code in kumiki/construction.py
1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 | |
join_plane_aligned_on_plane_aligned_timbers
¶
join_plane_aligned_on_plane_aligned_timbers(timber1: PerfectTimberWithin, timber2: PerfectTimberWithin, location_on_timber1: Numeric, location_on_timber2: Numeric, stickout: Stickout, size: V2, lateral_offset_from_timber1: Numeric = scalar(0), feature_to_mark_on_joining_timber: Optional[TimberFeature] = None, orientation_long_face_on_timber1: Optional[TimberLongFace] = None, orientation_long_face_on_timber2: Optional[TimberLongFace] = RIGHT, ticket: Optional[Union[TimberTicket, str]] = None) -> Timber
Joins two plane-aligned timbers with a connecting timber that lies in their shared plane.
Deprecated: This function's own signature is the main reason for the deprecation --
the lateral-offset/feature-marking/orientation-long-face parameters are hard to reason
about together. attach_plane_aligned_timber covers the same cases with a signature
that, while still not simple, is a bit easier to follow. Prefer it for new code.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber1
|
PerfectTimberWithin
|
First timber to join |
required |
timber2
|
PerfectTimberWithin
|
Second timber to join (plane-aligned with timber1) |
required |
location_on_timber1
|
Numeric
|
Position along timber1's length where the joining timber attaches |
required |
location_on_timber2
|
Numeric
|
Position along timber2's length where the joining timber attaches |
required |
stickout
|
Stickout
|
How much the joining timber extends beyond each connection point |
required |
size
|
V2
|
Cross-sectional size (width, height) of the joining timber |
required |
lateral_offset_from_timber1
|
Numeric
|
Lateral offset, in the axis perpendicular to the shared plane, from feature_to_mark_on_joining_timber. Defaults to scalar(0). |
scalar(0)
|
feature_to_mark_on_joining_timber
|
Optional[TimberFeature]
|
Optional feature on the created timber to use as the reference for the lateral offset. It is intended for you to use the locate_face or locate_long_edge functions to create a plane or line on a timber. If not provided, uses the centerline. If a plane is provided, the "origin" of the plane is used for longitudinal positioning (i.e. location_on_timber1). In the case of locate_face, the origin aligns with the center of the created timber. |
None
|
orientation_long_face_on_timber1
|
Optional[TimberLongFace]
|
Optional long face of timber1, on the shared plane, to orient against. If None, an arbitrary face of timber1 on the shared plane is used. |
None
|
orientation_long_face_on_timber2
|
Optional[TimberLongFace]
|
The long face on the created timber that will align with orientation_long_face_on_timber1. Defaults to TimberLongFace.RIGHT. |
RIGHT
|
ticket
|
Optional[Union[TimberTicket, str]]
|
Optional ticket for this timber (can be Ticket object or string name, used for rendering/debugging) |
None
|
Returns:
| Type | Description |
|---|---|
Timber
|
New timber that joins timber1 and timber2, lying in their shared plane |
Source code in kumiki/construction.py
1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 | |
join_face_aligned_on_face_aligned_timbers
¶
join_face_aligned_on_face_aligned_timbers(timber1: PerfectTimberWithin, timber2: PerfectTimberWithin, location_on_timber1: Numeric, stickout: Stickout, size: V2, lateral_offset_from_timber1: Numeric = scalar(0), feature_to_mark_on_joining_timber: Optional[TimberFeature] = None, orientation_face_on_timber1: Optional[TimberFace] = None, ticket: Optional[Union[TimberTicket, str]] = None) -> Timber
Joins two face-aligned timbers with a perpendicular timber.
Deprecated: This function's own signature is the main reason for the deprecation --
the lateral-offset/feature-marking/orientation-face parameters are hard to reason
about together. attach_face_aligned_timber covers the same cases with a signature
that, while still not simple, is a bit easier to follow. Prefer it for new code.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber1
|
PerfectTimberWithin
|
First timber to join |
required |
timber2
|
PerfectTimberWithin
|
Second timber to join (face-aligned with timber1) |
required |
location_on_timber1
|
Numeric
|
Position along timber1's length where the joining timber attaches |
required |
stickout
|
Stickout
|
How much the joining timber extends beyond each connection point |
required |
size
|
V2
|
Cross-sectional size (width, height) of the joining timber |
required |
lateral_offset_from_timber1
|
Numeric
|
Lateral offset from timber1's centerline reference. Defaults to scalar(0). |
scalar(0)
|
feature_to_mark_on_joining_timber
|
Optional[TimberFeature]
|
Optional feature on the create timber to use as the reference for the lateral offset. It is intended for you to use the locate_face or locate_long_edge functions to create a plane or line on a timber. If not provided, uses the centerline. If a plane is provided, the "origin" of the plane is used for longitudinal positioning (i.e. location_on_timber1). In the case of locate_face, the origin aligns with the center of the created timber. |
None
|
orientation_face_on_timber1
|
Optional[TimberFace]
|
Optional face of timber1 to orient against. If provided, the width direction of the created timber will align with this face on timber1. If not provided, uses timber1's length direction projected onto the perpendicular plane. |
None
|
ticket
|
Optional[Union[TimberTicket, str]]
|
Optional ticket for this timber (can be Ticket object or string name, used for rendering/debugging) |
None
|
Returns:
| Type | Description |
|---|---|
Timber
|
New timber that joins timber1 and timber2 |
Source code in kumiki/construction.py
1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 | |
get_point_on_feature
¶
get_point_on_feature(feature: Union[UnsignedPlane, Plane, Line, Point, LineOnPlane], timber: PerfectTimberWithin) -> V3
Get a point on a feature.
Source code in kumiki/measuring.py
locate_face
¶
locate_face(timber: PerfectTimberWithin, face: SomeTimberFace) -> Plane
Measure a face on a timber, returning a Plane centered on the face pointing outward.
The plane's normal points OUT of the timber (away from the timber's interior), and the plane's point is positioned at the center of the face surface.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
PerfectTimberWithin
|
The timber to measure |
required |
face
|
SomeTimberFace
|
The face to measure |
required |
Returns:
| Type | Description |
|---|---|
Plane
|
Plane with normal pointing outward from the face and point at the face center |
Example
plane = locate_face(timber, TimberFace.RIGHT)
plane.normal points in +X direction (outward from RIGHT face)¶
plane.point is at the center of the RIGHT face surface¶
Source code in kumiki/measuring.py
locate_edge
¶
locate_edge(timber: PerfectTimberWithin, edge: EdgeOrCenterline) -> Line
Measure any edge or centerline on a timber, returning a Line along it.
For TimberCenterline.CENTERLINE: direction = timber length direction, point at mid-length center. For TimberEdge values: uses canonical_line_from_corner to get the starting corner and direction face, then computes the global position and direction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
PerfectTimberWithin
|
The timber to measure |
required |
edge
|
EdgeOrCenterline
|
Which edge or centerline to measure |
required |
Returns:
| Type | Description |
|---|---|
Line
|
Line representing the edge in global coordinates |
Source code in kumiki/measuring.py
locate_long_edge
¶
locate_long_edge(timber: PerfectTimberWithin, edge: TimberLongEdge) -> Line
Measure a long edge on a timber. Thin wrapper around locate_edge.
locate_short_edge
¶
locate_short_edge(timber: PerfectTimberWithin, edge: TimberShortEdge) -> Line
Measure a short edge on a timber. Thin wrapper around locate_edge.
locate_edge_on_face
¶
locate_edge_on_face(timber: PerfectTimberWithin, edge: TimberLongEdge, face: TimberFace) -> LineOnPlane
locate_position_on_centerline_from_bottom
¶
locate_position_on_centerline_from_bottom(timber: PerfectTimberWithin, distance: Numeric) -> Point
Measure a position at a specific point along the timber's centerline, measured from the bottom.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
PerfectTimberWithin
|
The timber to measure on |
required |
distance
|
Numeric
|
Distance along the timber's length direction from the bottom position |
required |
Returns:
| Type | Description |
|---|---|
Point
|
Point on the timber's centerline at the specified distance from bottom |
Source code in kumiki/measuring.py
locate_position_on_centerline_from_top
¶
locate_position_on_centerline_from_top(timber: PerfectTimberWithin, distance: Numeric) -> Point
Measure a position at a specific point along the timber's centerline, measured from the top.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
PerfectTimberWithin
|
The timber to measure on |
required |
distance
|
Numeric
|
Distance along the timber's length direction from the top position |
required |
Returns:
| Type | Description |
|---|---|
Point
|
Point on the timber's centerline at the specified distance from top |
Source code in kumiki/measuring.py
locate_bottom_center_position
¶
locate_bottom_center_position(timber: PerfectTimberWithin) -> Point
Measure the position of the center of the bottom cross-section of the timber.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
PerfectTimberWithin
|
The timber to measure on |
required |
Returns:
| Type | Description |
|---|---|
Point
|
Point at the center of the bottom cross-section |
Source code in kumiki/measuring.py
locate_top_center_position
¶
locate_top_center_position(timber: PerfectTimberWithin) -> Point
Measure the position of the center of the top cross-section of the timber.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
PerfectTimberWithin
|
The timber to measure on |
required |
Returns:
| Type | Description |
|---|---|
Point
|
Point at the center of the top cross-section |
Source code in kumiki/measuring.py
locate_into_face
¶
locate_into_face(distance: Numeric, face: SomeTimberFace, timber: PerfectTimberWithin) -> UnsignedPlane
Measure a distance from a face on a timber.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
distance
|
Numeric
|
How far into the timber, along the face's inward normal, to place the plane |
required |
face
|
SomeTimberFace
|
The face to measure from (can be TimberFace, TimberEnd, or TimberLongFace) |
required |
timber
|
PerfectTimberWithin
|
The timber to measure on |
required |
Returns:
| Type | Description |
|---|---|
UnsignedPlane
|
UnsignedPlane parallel to the face, offset |
UnsignedPlane
|
normal. This is the inverse of mark_distance_from_face_in_normal_direction: if |
UnsignedPlane
|
feature = locate_into_face(d, face, timber), then |
UnsignedPlane
|
mark_distance_from_face_in_normal_direction(feature, timber, face).distance == d |
Source code in kumiki/measuring.py
locate_plane_from_centerline_in_direction
¶
locate_plane_from_centerline_in_direction(timber: PerfectTimberWithin, direction: Direction3D) -> Plane
mark_distance_from_face_in_normal_direction
¶
mark_distance_from_face_in_normal_direction(feature: Union[UnsignedPlane, Plane, Line, Point, LineOnPlane], timber: PerfectTimberWithin, face: SomeTimberFace) -> DistanceFromFace
Mark a feature onto a face on a timber.
Returns a DistanceFromFace measurement representing the distance from the face to the feature, measured INTO the timber. Positive means the feature is inside the timber (deeper than the face surface). Negative means the feature is outside the timber (shallower than the face surface).
This is the inverse of locate_into_face: If feature = locate_into_face(d, face, timber), then mark_distance_from_face_in_normal_direction(feature, timber, face).distance = d
Source code in kumiki/measuring.py
mark_distance_from_corner_along_edge_by_intersecting_plane
¶
mark_distance_from_corner_along_edge_by_intersecting_plane(plane: Union[UnsignedPlane, Plane], timber: PerfectTimberWithin, edge: Union[TimberLongEdge, TimberShortEdge, EdgeOrCenterline], end: TimberEnd) -> DistanceFromCornerAlongEdge
Mark onto an edge by intersecting a plane, returning a DistanceFromCornerAlongEdge.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plane
|
Union[UnsignedPlane, Plane]
|
the plane to intersect with |
required |
timber
|
PerfectTimberWithin
|
the timber whose edge we're intersecting |
required |
edge
|
Union[TimberLongEdge, TimberShortEdge, EdgeOrCenterline]
|
the edge to intersect with (TimberLongEdge, TimberShortEdge, TimberEdge, or TimberCenterline) |
required |
end
|
TimberEnd
|
the end of the timber to mark from |
required |
Returns:
| Type | Description |
|---|---|
DistanceFromCornerAlongEdge
|
DistanceFromCornerAlongEdge with the signed distance from the end to the |
DistanceFromCornerAlongEdge
|
intersection. Positive means into the timber from the end. |
Source code in kumiki/measuring.py
mark_distance_from_corner_along_edge_by_finding_closest_point_on_line
¶
mark_distance_from_corner_along_edge_by_finding_closest_point_on_line(line: Line, timber: PerfectTimberWithin, edge: Union[TimberLongEdge, TimberShortEdge, EdgeOrCenterline], end: TimberEnd) -> DistanceFromCornerAlongEdge
Mark onto an edge by finding the closest point to a line, returning a DistanceFromCornerAlongEdge.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
line
|
Line
|
The line feature to mark from |
required |
timber
|
PerfectTimberWithin
|
The timber whose edge we're marking to |
required |
edge
|
Union[TimberLongEdge, TimberShortEdge, EdgeOrCenterline]
|
The edge to mark to (TimberLongEdge, TimberShortEdge, TimberEdge, or TimberCenterline) |
required |
end
|
TimberEnd
|
Which end of the timber to mark from |
required |
Returns:
| Type | Description |
|---|---|
DistanceFromCornerAlongEdge
|
DistanceFromCornerAlongEdge with the signed distance from the end to the closest point. |
Source code in kumiki/measuring.py
mark_distance_from_end_along_centerline
¶
mark_distance_from_end_along_centerline(feature: Union[UnsignedPlane, Plane, Line, Point, LineOnPlane], timber: PerfectTimberWithin, end: TimberEnd = BOTTOM) -> DistanceFromPointIntoFace
Mark a feature onto the centerline of a timber.
Returns a DistanceFromPointIntoFace measurement representing the distance from the specified end of the timber to the intersection/closest point on the centerline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
feature
|
Union[UnsignedPlane, Plane, Line, Point, LineOnPlane]
|
The feature to mark (Plane, Line, Point, etc.) |
required |
timber
|
PerfectTimberWithin
|
The timber whose centerline we're marking to |
required |
end
|
TimberEnd
|
Which end of the timber to mark from (defaults to BOTTOM) |
BOTTOM
|
Returns:
| Type | Description |
|---|---|
DistanceFromPointIntoFace
|
DistanceFromPointIntoFace with distance from the specified end to where the feature intersects/is closest |
DistanceFromPointIntoFace
|
to the centerline. Positive means into the timber from the end. The point is set to the end's |
DistanceFromPointIntoFace
|
centerline position. |
Source code in kumiki/measuring.py
mark_plane_from_edge_in_direction
¶
mark_plane_from_edge_in_direction(plane: Union[UnsignedPlane, Plane, LineOnPlane], timber: PerfectTimberWithin, edge: EdgeOrCenterline) -> PlaneFromEdgeInDirection
Mark a plane onto a timber edge, returning the direction and signed distance from the edge to the plane.
This is the inverse of locate_plane_from_edge_in_direction: if p = locate_plane_from_edge_in_direction(timber, edge, dir, dist), then mark_plane_from_edge_in_direction(p, timber, edge) returns (dir, dist).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plane
|
Union[UnsignedPlane, Plane, LineOnPlane]
|
The plane to mark (its normal becomes the direction) |
required |
timber
|
PerfectTimberWithin
|
The timber whose edge we're measuring from |
required |
edge
|
EdgeOrCenterline
|
Which edge to measure from |
required |
Source code in kumiki/measuring.py
get_rough_support_distance_from_centerline
¶
get_rough_support_distance_from_centerline(timber: PerfectTimberWithin, direction: V2) -> Numeric
distance from cross-section centerline to support plane of the actual timber dimensions in direction
Source code in kumiki/timber_shavings.py
get_rough_support_distance
¶
get_rough_support_distance(timber: PerfectTimberWithin, position_from_bottom: V3, direction: V3) -> Numeric
distance from a 3D local position to support plane of the actual timber dimensions in direction
Source code in kumiki/timber_shavings.py
get_perfect_support_distance
¶
get_perfect_support_distance(timber: PerfectTimberWithin, position_from_bottom: V3, direction: V3) -> Numeric
distance from a 3D local position to support plane of the perfect timber dimensions in direction
Source code in kumiki/timber_shavings.py
find_opposing_face_on_another_timber
¶
find_opposing_face_on_another_timber(reference_timber: PerfectTimberWithin, reference_face: TimberLongFace, target_timber: PerfectTimberWithin) -> TimberFace
Find the opposing face on another timber. Assumes that the target_timber has a face parallel to the reference face on the reference_timber.
Source code in kumiki/timber_shavings.py
create_peg_going_into_face
¶
create_peg_going_into_face(timber: Timber, face: TimberLongFace, distance_from_bottom: Numeric, distance_from_centerline: Numeric, peg_size: Numeric, peg_shape: PegShape, forward_length: Numeric, stickout_length: Numeric) -> Peg
Create a peg that goes into a specified long face of a timber.
The peg is created in the local space of the timber, with the insertion end at the timber's surface and pointing inward perpendicular to the face.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
Timber
|
The timber to insert the peg into |
required |
face
|
TimberLongFace
|
Which long face the peg enters from (RIGHT, LEFT, FRONT, or BACK) |
required |
distance_from_bottom
|
Numeric
|
Distance along the timber's length from the bottom end |
required |
distance_from_centerline
|
Numeric
|
Distance from the timber's centerline along the face |
required |
peg_size
|
Numeric
|
Size/diameter of the peg (for square pegs, this is the side length) |
required |
peg_shape
|
PegShape
|
Shape of the peg (SQUARE or ROUND) |
required |
forward_length
|
Numeric
|
How far the peg reaches in the forward direction |
required |
stickout_length
|
Numeric
|
How far the peg sticks out in the back direction |
required |
Returns:
| Type | Description |
|---|---|
Peg
|
Peg object positioned and oriented appropriately in timber's local space |
Source code in kumiki/timber_shavings.py
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 | |
create_wedge_in_timber_end
¶
create_wedge_in_timber_end(timber: Timber, end: TimberEnd, position: V3, shape: WedgeShape) -> Wedge
Create a wedge at the end of a timber.
The wedge is created in the local space of the timber. In identity orientation, the point of the wedge goes in the length direction (Z-axis in local space).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
Timber
|
The timber to insert the wedge into |
required |
end
|
TimberEnd
|
Which end of the timber (TOP or BOTTOM) |
required |
position
|
V3
|
Position in the timber's cross-section (X, Y in local space, Z ignored) |
required |
shape
|
WedgeShape
|
Specification of wedge dimensions |
required |
Returns:
| Type | Description |
|---|---|
Wedge
|
Wedge object positioned and oriented appropriately in timber's local space |
Source code in kumiki/timber_shavings.py
are_timbers_parallel
¶
are_timbers_parallel(timber1: PerfectTimberWithin, timber2: PerfectTimberWithin, tolerance: Optional[Numeric] = None) -> bool
Check if two timbers have parallel length directions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber1
|
PerfectTimberWithin
|
First timber |
required |
timber2
|
PerfectTimberWithin
|
Second timber |
required |
tolerance
|
Optional[Numeric]
|
Optional tolerance for approximate comparison. If None, attempts exact comparison and uses default epsilon if not possible. |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True if timbers have parallel length directions, False otherwise |
Source code in kumiki/timber_shavings.py
are_timbers_orthogonal
¶
are_timbers_orthogonal(timber1: PerfectTimberWithin, timber2: PerfectTimberWithin, tolerance: Optional[Numeric] = None) -> bool
Check if two timbers have orthogonal (perpendicular) length directions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber1
|
PerfectTimberWithin
|
First timber |
required |
timber2
|
PerfectTimberWithin
|
Second timber |
required |
tolerance
|
Optional[Numeric]
|
Optional tolerance for approximate comparison. If None, automatically uses exact comparison for rational values or fuzzy comparison for floats. |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True if timbers have orthogonal length directions, False otherwise |
Source code in kumiki/timber_shavings.py
are_timbers_face_aligned
¶
are_timbers_face_aligned(timber1: PerfectTimberWithin, timber2: PerfectTimberWithin, tolerance: Optional[Numeric] = None) -> bool
Check if two timbers are face-aligned.
Two timbers are face-aligned if any face of one timber is parallel to any face of the other timber. This occurs when their orientations are related by 90-degree rotations around any axis (i.e., they share the same coordinate grid alignment).
Mathematically, timbers are face-aligned if any of their orthogonal direction vectors (length_direction, width_direction, height_direction) are parallel to each other.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber1
|
PerfectTimberWithin
|
First timber |
required |
timber2
|
PerfectTimberWithin
|
Second timber |
required |
tolerance
|
Optional[Numeric]
|
Optional numerical tolerance for parallel check. If None, uses exact equality. If provided, uses approximate floating-point comparison. |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True if timbers are face-aligned, False otherwise |
Source code in kumiki/timber_shavings.py
do_xy_cross_section_on_parallel_timbers_overlap
¶
do_xy_cross_section_on_parallel_timbers_overlap(timberA: PerfectTimberWithin, timberB: PerfectTimberWithin) -> bool
Check if the cross-section of two parallel timbers overlap.
Converts timberB into timberA's local space and checks if the XY cross-sections (defined by bottom_position and size) overlap.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timberA
|
PerfectTimberWithin
|
First timber |
required |
timberB
|
PerfectTimberWithin
|
Second timber |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the cross-sections overlap, False otherwise |
Source code in kumiki/timber_shavings.py
locate_mortise_timber_shoulder_plane_from_centerline_towards_tenon_timber
¶
locate_mortise_timber_shoulder_plane_from_centerline_towards_tenon_timber(arrangement: ButtJointTimberArrangement, distance_from_centerline: Numeric) -> Plane
Computes the shoulder plane of the mortise timber, offset from its centerline toward the tenon.
The shoulder plane is parallel to the mortise timber's length axis and offset from the mortise centerline in the mortise cross-section toward the tenon. Its reference point is chosen using the tenon centerline relation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
arrangement
|
ButtJointTimberArrangement
|
Butt joint arrangement (receiving_timber = mortise, butt_timber = tenon). |
required |
distance_from_centerline
|
Numeric
|
Signed offset from the mortise centerline toward the tenon. 0 = plane through the mortise centerline. Positive = toward tenon. |
required |
Returns:
| Type | Description |
|---|---|
Plane
|
Plane parallel to the mortise length axis, offset by distance_from_centerline |
Plane
|
from the mortise centerline toward the tenon. |
Source code in kumiki/joints/workshop/shavings/build_a_butt.py
resolve_parallel_shoulder_face
¶
resolve_parallel_shoulder_face(arrangement: ButtJointTimberArrangement, set_mortise_shoulder_parallel_to_face: Union[TimberLongFace, bool]) -> TimberLongFace
Resolves the parallel shoulder face. If set_mortise_shoulder_parallel_to_face is True, it auto-detects the long face most perpendicular to the tenon axis. Otherwise, returns it directly.
Source code in kumiki/joints/workshop/shavings/build_a_butt.py
locate_mortise_timber_shoulder_plane_from_centerplane_towards_long_face
¶
locate_mortise_timber_shoulder_plane_from_centerplane_towards_long_face(arrangement: ButtJointTimberArrangement, distance_from_centerplane: Numeric, face: TimberLongFace) -> Plane
Computes a shoulder plane that is forced to be parallel to a specific face of the mortise timber.
Source code in kumiki/joints/workshop/shavings/build_a_butt.py
compute_butt_joint_shoulder
¶
compute_butt_joint_shoulder(arrangement: ButtJointTimberArrangement, distance_from_centerline_or_centerplane: Numeric, up_direction: Direction3D, set_mortise_shoulder_parallel_to_face: Union[TimberLongFace, bool] = False) -> ButtJointShoulderResult
Compute the shoulder plane and an oriented marking space for a butt joint.
The marking space is positioned where the tenon (butt) timber's centerline intersects the shoulder plane, oriented with: +X = shoulder_plane.normal (from mortise centerline toward tenon) +Y = up_direction (orthogonalized against +X) +Z = right-hand rule cross product
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
arrangement
|
ButtJointTimberArrangement
|
Butt joint arrangement (receiving_timber = mortise, butt_timber = tenon). |
required |
distance_from_centerline_or_centerplane
|
Numeric
|
Signed offset from the mortise centerline toward the tenon. 0 = plane through the mortise centerline. Positive = toward tenon. |
required |
up_direction
|
Direction3D
|
Direction for +Y axis of the marking space. Will be orthogonalized against the shoulder plane normal. |
required |
set_mortise_shoulder_parallel_to_face
|
Union[TimberLongFace, bool]
|
Force shoulder plane parallel to a face. |
False
|
Returns:
| Type | Description |
|---|---|
ButtJointShoulderResult
|
ButtJointShoulderResult with the shoulder plane, intersection point, and marking space. |
Source code in kumiki/joints/workshop/shavings/build_a_butt.py
build_dovetail_shoulder_geometery
¶
build_dovetail_shoulder_geometery(arrangement: ButtJointTimberArrangement, shoulder_result: ButtJointShoulderResult, dovetail_depth: Numeric) -> CutCSG
Creates the shoulder geometry for a dovetail shoulder. The height of the dovetail is determined by the dimensions of the receiving timber. The depth of the dovetail is determined by the dovetail_depth parameter.
|
__| v | \ | \ | _\ | ^ | ^ | | | dovetail_depth | dovetail_pointy_face_on_butt_timber
The resulting CutCSG object is in global space. It includes part of the butt timber itself, not just the dovetail shape. The resulting CutCSG object includes part of the butt timber itself, not just the dovetail shape. This is useful for cutting notches into the receiving timber for non perfect receiving timbers.
Source code in kumiki/joints/workshop/shavings/build_a_butt.py
dovetail_tenon_geometry
¶
dovetail_tenon_geometry(arrangement: ButtJointTimberArrangement, shoulder_result: ButtJointShoulderResult, dovetail_top_side_on_butt_timber: TimberLongFace, tenon_size: V2, tenon_depth: Numeric, dovetail_depth: Numeric, wedge_accessory_parameters: DovetailTenonWedgeAccessoryParameters, tenon_lateral_offset: Numeric = 0, receiving_timber_mortise_extra_depth: Numeric = 0, tenon_waste_label: CutCSGLabel = CutCSGLabel('tenon_waste'), mortise_label: CutCSGLabel = CutCSGLabel('mortise')) -> DovetailTenonGeometeryResult
Build the tenon geometry for a dovetail shoulder. The "top" of dovetail tenon is always flush with dovetail_top_side_on_butt_timber face of the butt timber, however x/y sizing still aligns with the usual width/height axis of the butt timber. tenon_lateral_offset is always in the perpendicular axis of the joint on the tenon timber. When 0, the tenon is laterally centered on the the butt timber.
dovetail_top_side_on_butt_timber
v
|
|
|\ | < dovetail_depth
| \| <
_| ^^^ tenondepth
Source code in kumiki/joints/workshop/shavings/build_a_butt.py
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 | |
tusk_tenon_geometry
¶
tusk_tenon_geometry(arrangement: ButtJointTimberArrangement, opposite_shoulder_position_global: V3, tenon_length_direction: Direction3D, entry_face_designation: TimberLongFace, entry_axis_extent: Numeric, tusk_parameters: Any, rough_half_extent_past_opposite_shoulder: Numeric, tusk_hole_label: CutCSGLabel = CutCSGLabel('tusk_hole'), tusk_clearance_label: CutCSGLabel = CutCSGLabel('tusk_clearance')) -> TuskTenonGeometryResult
Build the crosswise locking key ("tusk") for a through mortise-and-tenon joint, the hole cut through the tenon for it, and (if needed) extra clearance cut into the receiving timber so the key can be slid crosswise into place.
Mirrors dovetail_tenon_geometry's wedge: the key is a tapered prism, driven crosswise (perpendicular to the tenon's own length, through one of the tenon's long faces -- entry_face_designation) through a hole in the through-tenon, positioned at the "opposite shoulder" reference. Like the wedge, it is asymmetric -- one side (Y=0) is flush with the opposite shoulder plane (the receiving timber's exit face), and the other side tapers (per tusk_angle) into the tenon's own body. The key's taper runs along the tenon's own length axis (the narrow, leading edge sits deepest, at the far side of the tenon's own cross-section; the taper continues into the back/tip stickout regions), so driving it in wedges the tenon's shoulder tight against the receiving timber's exit face. It is centered on the tenon's other cross axis (tusk_thickness).
entry_face_designation
v
| _ | _ |
|---|---|
| ___ | |
| / ___ <- tapers down to tusk_small_width at X=entry_axis_extent | |
| /_____ <- flush at Y=0 (opposite shoulder plane / receiving timber exit face) | |
| tusk_back_ tusk_tip_ | |
| stickout stickout | |
| <-- entry_axis_extent --> |
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
arrangement
|
ButtJointTimberArrangement
|
Butt joint arrangement (butt_timber = tenon, receiving_timber = mortise). |
required |
opposite_shoulder_position_global
|
V3
|
Center of the tenon's cross-section at the "opposite shoulder" reference position (where the tusk hole is centered along the tenon's length axis). |
required |
tenon_length_direction
|
Direction3D
|
Direction from the entry shoulder toward the tenon's tip. |
required |
entry_face_designation
|
TimberLongFace
|
Which of the tenon's long faces the tusk is driven in from. |
required |
entry_axis_extent
|
Numeric
|
The tenon's own cross-sectional size along the entry axis. |
required |
tusk_parameters
|
Any
|
Tusk shape parameters. |
required |
rough_half_extent_past_opposite_shoulder
|
Numeric
|
How far the receiving timber's rough stock extends past the "opposite shoulder" reference, measured along tenon_length_direction. <= 0 means no rough excess there (no clearance needed). |
required |
Returns:
| Type | Description |
|---|---|
TuskTenonGeometryResult
|
TuskTenonGeometryResult with the tenon hole, optional mortise clearance, and the tusk |
TuskTenonGeometryResult
|
accessory, all in global space. |
Source code in kumiki/joints/workshop/shavings/build_a_butt.py
743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 | |
compute_peg_positions
¶
compute_peg_positions(arrangement: ButtJointTimberArrangement, shoulder_plane: Plane, peg_parameters: SimplePegParameters, tenon_position: V2) -> List[PegPositionResult]
Compute peg positions in global space for a mortise and tenon joint.
Uses the arrangement's front_face_on_butt_timber as the peg face on the tenon. All computations are done in global space, using the measure/mark pattern where possible.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
arrangement
|
ButtJointTimberArrangement
|
Butt joint arrangement (butt_timber = tenon, receiving_timber = mortise). Must have front_face_on_butt_timber set. |
required |
shoulder_plane
|
Plane
|
The shoulder plane in global space (from _compute_plane_parallel_to_receiving_length_axis_partially_perpendicular_to_butt). |
required |
peg_parameters
|
SimplePegParameters
|
Peg configuration (shape, positions, size, depth, offset). |
required |
tenon_position
|
V2
|
Offset of tenon center from timber centerline in tenon local cross-section (X, Y). |
required |
Returns:
| Type | Description |
|---|---|
List[PegPositionResult]
|
List of PegPositionResult, one per peg_position entry. |
Source code in kumiki/joints/workshop/shavings/build_a_butt.py
982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 | |
scalar
¶
Create a float scalar value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
numerator
|
The numerator (can be int, float, or str) |
required | |
denominator
|
The denominator (default=1) |
1
|
Returns:
| Type | Description |
|---|---|
float
|
float value |
Examples:
scalar(3) # 3.0 scalar(1, 2) # 0.5 scalar(2.5) # 2.5 scalar("1.5") # 1.5 from string scalar("1/32") # Parses fraction string
Source code in kumiki/rule.py
sqrt
¶
Source code in kumiki/rule.py
simplify
¶
No-op: floats need no symbolic simplification. Kept so old call sites
(mostly simplify(a - b) == 0-style exactness checks) still parse; see
safe_equality_test/safe_zero_test for the epsilon-based replacement.
Source code in kumiki/rule.py
prune
¶
giraffe_evalf
¶
giraffe_simplify
¶
giraffe_compare
¶
giraffe_compare(a, b, comparison: Comparison, collapse_mode=None, eps: Optional[float] = None) -> bool
Compare two values: evaluates a - b and applies comparison against zero.
eps overrides the default comparison tolerance for this one call.
Examples:
giraffe_compare(x, y, Comparison.GT) # x > y ? giraffe_compare(x, 0, Comparison.EQ) # x == 0 ?
Source code in kumiki/rule.py
giraffe_dot_product
¶
giraffe_transform_vector
¶
giraffe_normalize_vector
¶
create_v2
¶
create_v3
¶
cross_product
¶
radians
¶
Identity function for angles already in radians. Use this to make it explicit that an angle is in radians.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
angle
|
Numeric
|
Angle value in radians |
required |
Returns:
| Type | Description |
|---|---|
Numeric
|
The same angle value (unchanged) |
Examples:
radians(pi / 2) # 90 degrees in radians radians(pi / 4) # 45 degrees in radians
Source code in kumiki/rule.py
degrees
¶
Convert an angle from degrees to radians.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
angle
|
Numeric
|
Angle value in degrees |
required |
Returns:
| Type | Description |
|---|---|
Numeric
|
Angle value in radians |
Examples:
degrees(90) # 90 degrees = pi/2 radians degrees(45) # 45 degrees = pi/4 radians degrees(180) # 180 degrees = pi radians
Source code in kumiki/rule.py
inches
¶
Create a measurement in meters from inches.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
numerator
|
The numerator (can be int, float, or str) |
required | |
denominator
|
The denominator (default=1) |
1
|
Returns:
| Type | Description |
|---|---|
|
float value in meters |
Examples:
inches(1, 32) # 1/32 inch inches(4) # 4 inches inches(3.5) # 3.5 inches inches("1.5") # 1.5 inches from string inches("1/32") # Parses fraction string
Source code in kumiki/rule.py
feet
¶
Create a measurement in meters from feet.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
numerator
|
The numerator (can be int, float, or str) |
required | |
denominator
|
The denominator (default=1) |
1
|
Returns:
| Type | Description |
|---|---|
|
float value in meters |
Examples:
feet(8) # 8 feet feet(1, 2) # 1/2 foot feet(6.5) # 6.5 feet
Source code in kumiki/rule.py
mm
¶
Create a measurement in meters from millimeters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
numerator
|
The numerator (can be int, float, or str) |
required | |
denominator
|
The denominator (default=1) |
1
|
Returns:
| Type | Description |
|---|---|
|
float value in meters |
Examples:
mm(90) # 90 millimeters mm(1, 2) # 1/2 millimeter mm(25.4) # 25.4 millimeters
Source code in kumiki/rule.py
cm
¶
Create a measurement in meters from centimeters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
numerator
|
The numerator (can be int, float, or str) |
required | |
denominator
|
The denominator (default=1) |
1
|
Returns:
| Type | Description |
|---|---|
|
float value in meters |
Examples:
cm(9) # 9 centimeters cm(1, 2) # 1/2 centimeter cm(2.54) # 2.54 centimeters
Source code in kumiki/rule.py
m
¶
Create a measurement in meters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
numerator
|
The numerator (can be int, float, or str) |
required | |
denominator
|
The denominator (default=1) |
1
|
Returns:
| Type | Description |
|---|---|
|
float value in meters |
Examples:
m(1) # 1 meter m(1, 2) # 1/2 meter m(2.5) # 2.5 meters
Source code in kumiki/rule.py
shaku
¶
Create a measurement in meters from shaku (尺). Traditional Japanese carpentry unit.
1 shaku ≈ 303.03 mm (exactly 10/33 meters)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
numerator
|
The numerator (can be int, float, or str) |
required | |
denominator
|
The denominator (default=1) |
1
|
Returns:
| Type | Description |
|---|---|
|
float value in meters |
Examples:
shaku(1) # 1 shaku shaku(3, 2) # 3/2 shaku (1.5 shaku) shaku(2.5) # 2.5 shaku
Source code in kumiki/rule.py
sun
¶
Create a measurement in meters from sun (寸). Traditional Japanese carpentry unit.
1 sun = 1/10 shaku ≈ 30.303 mm
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
numerator
|
The numerator (can be int, float, or str) |
required | |
denominator
|
The denominator (default=1) |
1
|
Returns:
| Type | Description |
|---|---|
|
float value in meters |
Examples:
sun(1) # 1 sun sun(5) # 5 sun sun(1, 2) # 1/2 sun
Source code in kumiki/rule.py
bu
¶
Create a measurement in meters from bu (分). Traditional Japanese carpentry unit.
1 bu = 1/10 sun = 1/100 shaku ≈ 3.0303 mm
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
numerator
|
The numerator (can be int, float, or str) |
required | |
denominator
|
The denominator (default=1) |
1
|
Returns:
| Type | Description |
|---|---|
|
float value in meters |
Examples:
bu(1) # 1 bu bu(5) # 5 bu bu(1, 2) # 1/2 bu
Source code in kumiki/rule.py
safe_zero_test
¶
Test if a value is approximately zero, within eps (default EPSILON_GENERIC).
safe_equality_test
¶
Test if two values are approximately equal, within eps (default EPSILON_GENERIC).
safe_zero_test_sq
¶
Test whether a SQUARED quantity is approximately zero.
Takes a LINEAR tolerance and squares it internally, so eps means the same thing here as everywhere else in the library: a distance in model units, never a distance squared.
safe_zero_test_sq(dx * dx + dy * dy, eps) # is the distance ~0?
Use this rather than safe_zero_test wherever the value under test is a square. Passing a squared value to safe_zero_test compares it against a linear tolerance, which sounds harmless and is not: at eps=5e-4 it treats any length below 22mm as zero. That has been the shape of two real bugs here already -- polygon edges declared degenerate, and pick tolerances meaning millimetres on one primitive and centimetres on another.
Source code in kumiki/rule.py
are_vectors_parallel
¶
Check if two vectors are parallel.
For normalized vectors: dot product ≈ ±1 means parallel
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vector1
|
Matrix
|
First direction vector |
required |
vector2
|
Matrix
|
Second direction vector |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if |abs(dot_product) - 1| is approximately zero (vectors are parallel) |
Source code in kumiki/rule.py
are_vectors_perpendicular
¶
Check if two vectors are perpendicular.
For any vectors: dot product ≈ 0 means perpendicular
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vector1
|
Matrix
|
First direction vector |
required |
vector2
|
Matrix
|
Second direction vector |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if dot_product is approximately zero (vectors are perpendicular) |
Source code in kumiki/rule.py
make_finite_rectangular_prism_from_half_space
¶
make_finite_rectangular_prism_from_half_space(half_space: HalfSpace, size_of_space: Numeric, depth_of_space: Numeric) -> RectangularPrism
Build a finite RectangularPrism that approximates half_space near its boundary.
The returned prism:
- has its "bottom" face (at start_distance = 0) lying on the half-space boundary plane,
- extends depth_of_space into the half-space (in the +normal direction, i.e. the
direction in which the half-space extends),
- has a square cross-section of size_of_space × size_of_space centered on the
point where the line through the origin along normal meets the boundary plane.
The cross-section orientation perpendicular to the normal is chosen arbitrarily.
Source code in kumiki/cutcsg.py
adopt_csg
¶
adopt_csg(orig_transform: Optional[Transform], adopting_transform: Optional[Transform], csg_in_orig_space: CutCSG) -> CutCSG
Transform a CSG object into another coordinate system.
If orig_transform is provided, the CSG is treated as being in that transform's local coordinates. If orig_transform is None, the CSG is treated as being in global coordinates. If adopting_transform is provided, the result is expressed in that transform's local coordinates. If adopting_transform is None, the result is expressed in global coordinates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
orig_transform
|
Optional[Transform]
|
The transform whose local space the CSG is in, or None for global |
required |
adopting_transform
|
Optional[Transform]
|
The transform whose local space we want the CSG in, or None to return the CSG in global coordinates |
required |
csg_in_orig_space
|
CutCSG
|
The CSG object (in orig_transform local, or global if orig_transform is None) |
required |
Returns:
| Type | Description |
|---|---|
CutCSG
|
A new CSG object in adopting_transform's local coordinates, or in global |
CutCSG
|
coordinates if adopting_transform is None |
Example
cut_on_b = adopt_csg(timber_a.transform, timber_b.transform, cut_csg) csg_in_tenon_local = adopt_csg(None, tenon_timber.transform, csg_global) csg_in_global = adopt_csg(timber_a.transform, None, cut_csg)
Source code in kumiki/cutcsg.py
3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 3719 3720 3721 3722 3723 3724 3725 3726 3727 3728 3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754 3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780 3781 3782 3783 3784 3785 3786 3787 3788 3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 3799 3800 3801 3802 3803 3804 3805 3806 3807 | |
get_center_point_on_face_global
¶
get_center_point_on_face_global(face: SomeTimberFace, timber: PerfectTimberWithin) -> V3
Get the center point of a timber face in global coordinates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
face
|
SomeTimberFace
|
The face to get the center of |
required |
timber
|
PerfectTimberWithin
|
The timber |
required |
Returns:
| Type | Description |
|---|---|
V3
|
Center point of the face surface in global coordinates |
Source code in kumiki/measuring.py
locate_centerline
¶
locate_centerline(timber: PerfectTimberWithin) -> Line
Measure the centerline of a timber. Thin wrapper around locate_edge.
locate_plane_from_edge_in_direction
¶
locate_plane_from_edge_in_direction(timber: PerfectTimberWithin, edge: EdgeOrCenterline, direction: Direction3D, distance: Numeric = scalar(0)) -> Plane
Return a Plane that is parallel to the given edge, has direction as its
normal, and sits distance away from the edge in that direction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber
|
PerfectTimberWithin
|
The timber whose edge to measure from |
required |
edge
|
EdgeOrCenterline
|
Which edge or centerline |
required |
direction
|
Direction3D
|
Normal direction of the resulting plane |
required |
distance
|
Numeric
|
How far from the edge to place the plane (default 0 = through the edge) |
scalar(0)
|
Source code in kumiki/measuring.py
get_perfect_support_distance_from_centerline
¶
get_perfect_support_distance_from_centerline(timber: PerfectTimberWithin, direction: V2) -> Numeric
distance from cross-section centerline to support plane of the perfect timber dimensions in direction
Source code in kumiki/timber_shavings.py
are_timbers_plane_aligned
¶
are_timbers_plane_aligned(timber1: PerfectTimberWithin, timber2: PerfectTimberWithin, tolerance: Optional[Numeric] = None) -> bool
Check if two timbers are plane aligned
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timber1
|
PerfectTimberWithin
|
First timber |
required |
timber2
|
PerfectTimberWithin
|
Second timber |
required |
tolerance
|
Optional[Numeric]
|
Optional numerical tolerance for parallel check. If None, uses exact equality. If provided, uses approximate floating-point comparison. |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True if timbers have at least one pair of parallel long faces, False otherwise |
Source code in kumiki/timber_shavings.py
warn_if_arrangement_timbers_imperfect
¶
Warn when a joint arrangement uses any timber that is not perfect.
Source code in kumiki/joints/workshop/shavings/relief.py
does_shoulder_plane_need_notching
¶
does_shoulder_plane_need_notching(arrangement: ButtJointTimberArrangement, mortise_shoulder_distance_from_centerline_or_centerplane: Numeric, check_against_rough_size: bool = True, set_mortise_shoulder_parallel_to_face: Union[TimberLongFace, bool] = False) -> bool
Determines whether a shoulder notch is needed on the mortise timber.
For plane-aligned timbers, checks whether the shoulder is inset from the mortise face surface. For non-plane-aligned timbers, always returns True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
arrangement
|
ButtJointTimberArrangement
|
Butt joint arrangement (receiving_timber = mortise, butt_timber = tenon). |
required |
mortise_shoulder_distance_from_centerline_or_centerplane
|
Numeric
|
Distance from the mortise centerline to the shoulder plane, measured toward the tenon. |
required |
check_against_rough_size
|
bool
|
If True (default), compare against the mortise timber's
rough half-size on the entry face (using |
True
|
set_mortise_shoulder_parallel_to_face
|
Union[TimberLongFace, bool]
|
If set to a face, then force the mortise shoulder to be parallel to that face. |
False
|
Source code in kumiki/joints/workshop/shavings/relief.py
chop_shoulder_notch_aligned_with_timber
¶
chop_shoulder_notch_aligned_with_timber(notch_timber: TimberLike, butting_timber: TimberLike, butting_timber_end: TimberEnd, distance_from_centerline: Numeric, notch_wall_relief_cut_angle_radians: Numeric = scalar(0), set_mortise_shoulder_parallel_to_face: Union[TimberLongFace, bool] = False, label: CutCSGLabel = CutCSGLabel('shoulder_notch_relief')) -> Union[RectangularPrism, SolidUnion]
Create a shoulder notch on notch_timber at a given distance from its centerline, oriented by the butting_timber's approach direction.
Unlike chop_shoulder_notch_on_timber_face which is aligned to a specific face, this notch is aligned to the shoulder plane derived from the butting timber's approach direction (projected perpendicular to the notch timber's length axis if set_mortise_shoulder_parallel_to_face is not False).
The notch bottom (shoulder plane) is distance_from_centerline away from the notch_timber's centerline. The notch opens outward from the centerline. The notch width is along the notch_timber's length axis and hugs the butting timber's shoulder-plane slice exactly (its perfect cross-section; imperfect material beyond that is scribe relief's job, not the housing's). The span and depth clear the notch timber's entire rough cross-section via a worst-case corner-radius bound -- overshoot is free in both of those directions (the span channel exits the timber's sides, the depth exits its outer face) so neither needs to be exact.
notch_wall_relief_cut_angle_radians is a MINIMUM: the walls are always relieved by at least the butting timber's rake away from the shoulder-plane normal, since anything less would leave housing walls colliding with the raking butting timber above the shoulder plane.
Source code in kumiki/joints/workshop/shavings/relief.py
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 | |
chop_shoulder_notch_on_timber_face
¶
chop_shoulder_notch_on_timber_face(timber: TimberLike, notch_face: TimberFace, distance_along_timber: Numeric, notch_width: Numeric, notch_depth: Numeric, notch_wall_relief_cut_angle: Numeric = scalar(0), label: CutCSGLabel = CutCSGLabel('shoulder_notch_relief')) -> Union[RectangularPrism, SolidUnion]
Create a rectangular shoulder notch on a timber face with optional angled walls.
Source code in kumiki/joints/workshop/shavings/relief.py
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 | |
chop_butt_joint_shoulder_notch_relief_on_plane_aligned_timbers_2sided
¶
chop_butt_joint_shoulder_notch_relief_on_plane_aligned_timbers_2sided(arrangement: ButtJointTimberArrangement, mortise_shoulder_distance_from_centerline_or_centerplane: Numeric, notch_angle: Optional[Numeric] = None) -> ShoulderReliefCSGGeometry | None
Like chop_butt_joint_shoulder_notch_relief_4sided, but restricted to PLANE-ALIGNED
arrangements, where it produces a simpler notch: only 2 of the 4 walls flare via the
dihedral-bisector construction; the other 2 don't flare at all -- they're pushed straight
out, for the notch's ENTIRE depth, to whichever is FURTHER along the "joint normal axis"
-- the shared normal of the two timbers' aligned long faces
(arrangement.compute_normalized_timber_cross_product()) -- between the RECEIVING
timber's own ROUGH edge and the BUTT timber's own ROUGH edge. This guarantees a full
TRANSVERSE relief spanning the receiving timber's entire width on this axis (never a
pocket that stops partway across it), and is safe specifically because plane-alignment
guarantees the butt timber's faces in that axis are exactly PARALLEL to the receiving
timber's own faces there -- there's no dihedral angle to bisect, so a straight-walled
channel is the natural choice, unlike the 4-sided version's fully general per-wall flare.
Geometry, in outline (P = joint normal axis, Q = the butt timber's other cross-sectional
axis, both perpendicular to n_depth = the shoulder plane's normal):
- Along P: both the shoulder-plane cross-section (quad-1) and the far cross-section
(quad-2) span the SAME fixed extent -- per side, the FURTHER of the receiving timber's
own ROUGH half-size and the butt timber's own ROUGH half-size on that axis
(plane-alignment guarantees P is EXACTLY -- not just approximately -- one of EACH
timber's own width/height axes, the same shared axis are_timbers_plane_aligned
identifies, so there's no oblique stretching to account for). Flat, unflared walls in
this axis, for the notch's whole depth.
- Along Q: quad-1 uses the butt timber's PERFECT cross-section as it actually crosses
the shoulder plane (same as the 4-sided version's quad-1 corners) -- NOT simply the
tenon's raw PTW half-size, which understates the footprint whenever the tenon's length
axis isn't perpendicular to the shoulder plane within the (Q, n_depth) plane (e.g. any
raking brace-style joint); quad-2 flares outward via the SAME per-wall dihedral-bisector
construction as the 4-sided version, using the SIGNED dihedral angle between each of
the butt timber's two Q-normal faces and the shoulder plane independently -- negated
normals generally give supplementary (not equal) signed angles, so the two flared
walls generally reach out by different amounts, same as any two non-opposite walls in
the 4-sided version would.
- The two flared (Q-direction) walls reach a common depth exactly as in the 4-sided
case (the deeper of their two natural depths); the two flat (P-direction) walls are,
by construction, already at a fixed extent for that whole depth, so no rescaling is
needed for them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
arrangement
|
ButtJointTimberArrangement
|
butt joint arrangement; must be plane-aligned (raises via
|
required |
mortise_shoulder_distance_from_centerline_or_centerplane
|
Numeric
|
same as the 4-sided version -- signed distance from the receiving timber's centerline to the shoulder plane, toward the butt timber. |
required |
notch_angle
|
Optional[Numeric]
|
optional MINIMUM wall-relief angle (radians) for the 2 flared (Q-axis)
walls, floored independently against each wall's own natural dihedral-bisector
angle -- same "floor, not override" convention as
|
None
|
Returns None when no notch is required -- see does_shoulder_plane_need_notching.
Source code in kumiki/joints/workshop/shavings/relief.py
780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 | |
chop_butt_joint_shoulder_notch_relief_4sided
¶
chop_butt_joint_shoulder_notch_relief_4sided(arrangement: ButtJointTimberArrangement, mortise_shoulder_distance_from_centerline_or_centerplane: Numeric) -> ShoulderReliefCSGGeometry | None
Compute the shoulder notch on the receiving timber AND the matching relief cut on the butting timber, for arrangements where the butt timber may approach the shoulder at a compound angle (not necessarily plane-aligned with the receiving timber).
The notch is a single 4-sided frustum
(a ConvexPolygonSimpleLoft) rather than a union of a straight prism plus 2 tilted
relief prisms, so all 4 walls can relieve independently based on how each of the butt
timber's 4 long faces actually meets the shoulder plane.
Geometry, in outline: - quad-1 is the butt timber's PERFECT cross-section sliced by the shoulder plane (an oblique quadrilateral in general, since the butt timber's length axis need not be perpendicular to the shoulder plane). - Each of quad-1's 4 edges lies exactly on the line where one of the butt timber's long face planes crosses the shoulder plane (by construction: both of that edge's corners sit on that face). For each edge, the relieved wall direction bisects the dihedral angle between that face and the shoulder plane -- this stays on the safe (non-colliding) side of the face for any distance travelled along it, so a wall can safely be extended further than its own bisector's "natural" depth without becoming unsafe. - Each edge's own bisector reaches a different depth for the same in-plane reach (since the 4 dihedral angles generally differ), which would make quad-2 non-planar. Instead we take the deepest of the 4 (the "maximal loft distance") as a common depth, and rescale each edge's in-plane offset to match -- still safely on that edge's own bisector, just further out -- so quad-2 stays flat and parallel to quad-1 and every wall gets at least as much depth-clearance as it individually needs.
Returns None when no notch is required (shoulder sits at or past the receiving
timber's rough entry face) -- see does_shoulder_plane_need_notching.
Source code in kumiki/joints/workshop/shavings/relief.py
1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 | |
chop_scribe_relief
¶
chop_scribe_relief(timber_to_be_scribed_cutting: Cutting, timber_to_be_cut_cutting: Cutting, scribe_relief_label: CutCSGLabel = CutCSGLabel('scribe_relief'), scribe_hollow_label: CutCSGLabel = CutCSGLabel('scribe_hollow_relief')) -> tuple[CutCSG, CutCSG]
scribes timber_to_be_scribed onto timber_to_be_cut such that the entirety of timber_to_be_scribed is cut out of timber_to_be_cut excluding the perfect timber within portion of timber_to_be_cut
Both timbers are given as their (already cut) Cutting, each of which carries
its own timber via Cutting.timber -- callers never need to pass the bare
timbers separately.
timber_to_be_scribed_cutting is the cutting already computed for timber_to_be_scribed elsewhere in the current joint (e.g. the tenon's shoulder cut, plus any end cut) -- material already removed there is excluded from what gets scribed onto timber_to_be_cut, otherwise the relief would be based on timber_to_be_scribed's full, uncut extent (its entire length) rather than just the part of it that actually survives near the joint.
returns a pair of CSG geometries, the first to be removed from timber_to_be_scribed and the second to be removed from timber_to_be_cut, both expressed in their respective local frames
Source code in kumiki/joints/workshop/shavings/relief.py
1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 | |
chop_scribe_relief_and_apply
¶
chop_scribe_relief_and_apply(timber_to_be_scribed_cutting: Cutting, timber_to_be_cut_cutting: Cutting) -> tuple[Cutting, Cutting]
Apply scribe relief cuts from chop_scribe_relief to the given cuttings, unioning the
new relief CSGs into each cutting's existing negative_csg.
Both timbers are given as their (already cut) Cutting, each of which carries
its own timber via Cutting.timber -- callers never need to pass the bare
timbers separately.
Returns (updated_cut_cutting, updated_scribed_cutting) (matching the order of
the early-return path when both timbers are perfect).
Source code in kumiki/joints/workshop/shavings/relief.py
chop_scribe_relief_and_apply_for_butt_joint_arrangement
¶
chop_scribe_relief_and_apply_for_butt_joint_arrangement(relief: ButtJointScribeReliefConfig, butt_cut: Cutting, receiving_cut: Cutting) -> tuple[Cutting, Cutting]
Helper shared by the butt-joint cutting functions: apply scribe relief
between the butt and receiving timbers, honoring
relief.timber_to_be_scribed to decide which timber is scribed onto the
other.
relief is required here -- callers are responsible for skipping this
call entirely when relief isn't configured (e.g. it's None).