kumiki.cutcsg¶
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.BoundingBox -- you do not need to import from the submodule path shown in the heading above.
kumiki.cutcsg
¶
CutCSG - Constructive Solid Geometry operations for Kumiki
This module provides CSG primitives and operations for representing timber cuts and geometry operations. All operations use plain Python floats (see rule.py); comparisons go through the safe_* helpers so they carry a tolerance rather than testing for bit-exact equality.
The low-level point tests -- contains_point, is_point_on_boundary,
get_outward_normal -- take an optional eps that widens that tolerance for
the duration of the call.
The feature queries -- find_all_features, find_first_feature, and CSGFeature. test_point_unbounded -- take a test tolerance instead, which is a different wearing similar clothes. An epsilon absorbs float error; a test tolerance absorbs the gap between a raycast hit on the triangulated mesh and the analytic surface it stands for, and how far a click lands from an edge or a point it cannot hit exactly. See FeatureTestTolerances.
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()}
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]
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
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.
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
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
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
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
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
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
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
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
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 | |