Shape Editing Architecture#
Internal architecture of the defeaturing module for developers.
Note
This document is intended for developers who want to extend the shape_editing module or understand its internal algorithms. For user-facing documentation, see the Shape Editing.
Shape Editing Architecture#
This document provides internal documentation for developers working on or extending the shape_editing module. It covers the architecture, key algorithms, and extension patterns.
Module Structure#
volmdlr_tools/shape_editing/
├── __init__.py # Public API exports
├── history_graph.py # DAG-based history tracking
└── defeaturing/
├── __init__.py # Submodule exports
├── defeaturer.py # High-level fluent API
├── face.py # FaceRemover (base utility)
├── feature.py # FeatureRemover (unified)
├── isolated_feature.py # Soft feature removal
├── complex_feature.py # Hard feature removal
├── blend_chain.py # Blend chain removal
└── incremental_blend.py # Iterative blend removal
Layer Responsibilities#
High Level (defeaturer.py):
User-facing API
Operation orchestration
History management
Fluent interface
Mid Level (feature.py, blend_chain.py, incremental_blend.py):
Feature-type-specific logic
Strategy selection (soft vs hard)
Chain detection for blends
Low Level (face.py, isolated_feature.py, complex_feature.py):
Direct OpenCascade operations
BRepTools_ReShape for soft removal
BRepAlgoAPI_Defeaturing for hard removal
Design Principles#
1. Layered Abstraction#
Each layer has a single responsibility:
User Code
│
▼
Defeaturer (orchestration + history)
│
▼
FeatureRemover (unified interface)
│
├──► IsolatedFeatureRemover (soft)
│ │
│ ▼
│ FaceRemover (BRepTools_ReShape)
│
└──► ComplexFeatureRemover (hard)
│
▼
BRepAlgoAPI_Defeaturing (OCC)
2. Automatic Fallback#
FeatureRemover implements a fallback strategy:
def remove_features(self, face_indices, try_complex=True):
# Step 1: Try soft removal
soft_remover = IsolatedFeatureRemover(...)
soft_remover.remove_features(face_indices)
# Step 2: Check for unremoved faces
if soft_remover.unremoved_faces and try_complex:
# Step 3: Fall back to hard removal
complex_remover = ComplexFeatureRemover(...)
complex_remover.remove_features(list(soft_remover.unremoved_faces))
3. History Preservation#
Every remover preserves OpenCascade’s BRepTools_History:
class SomeRemover:
@property
def history(self) -> BRepTools_History:
return self._history
The Defeaturer concatenates these into a unified HistoryGraph.
4. Lazy AAG Initialization#
The AttributedAdjacencyGraph is expensive to compute. All removers follow the lazy pattern:
def __init__(self, shape, aag=None):
self._shape = shape
self._aag = aag # May be None
@property
def aag(self):
if self._aag is None:
self._aag = AttributedAdjacencyGraph(shape=self._shape)
return self._aag
5. Fluent Interface#
Defeaturer returns self from all operations:
def remove_cavities(self) -> "Defeaturer":
# ... implementation ...
return self
Enabling: Defeaturer(solid).remove_cavities().remove_blends().result
Key Algorithms#
Soft vs Hard Feature Detection#
The classify_insertion_type() function in isolated_feature.py determines how a feature face is inserted into a master face:
def classify_insertion_type(feature_face, master_face) -> InsertionType:
# 1. Find common edges
common_edges = find_common_edges(feature_face, master_face)
if not common_edges:
return InsertionType.UNDEFINED
# 2. Get master's outer wire
outer_wire = ShapeAnalysis.OuterWire(master_face)
# 3. For each common edge, check if it's in an inner wire
for edge in common_edges:
if not is_edge_in_inner_wire(edge, master_face, outer_wire):
return InsertionType.HARD
return InsertionType.SOFT
Soft insertion: All common edges lie on inner wires (holes/openings). Hard insertion: At least one common edge touches the outer boundary.
History Concatenation#
The concatenate() method in HistoryGraph merges two histories:
Before concatenation:
History 1: History 2:
A ─► A' A' ─► A''
B ─► B' B' ─► B''
C ─► C' (new root)
After concatenation:
A ─► A' ─► A''
B ─► B' ─► B''
C ─► C'
Algorithm:
Get leaves of history 1 (nodes with no outgoing edges)
Get roots of history 2
Match using
shape.IsPartner()(topological identity)For matches: copy history 2’s subtree as children of history 1’s leaf
For unmatched roots: add as new roots to history 1
def concatenate(self, other):
this_leaves = self.get_leaves()
other_roots = other.roots
for leaf_node_id, leaf_shape in this_leaves:
if is_deleted(leaf_node_id):
continue
for root_id in other_roots:
root_shape = other.get_shape(root_id)
if leaf_shape.IsPartner(root_shape):
# Merge: copy root's children to leaf
self._deep_copy_subtree(other, root_id, leaf_node_id)
merged_roots.add(root_id)
break
# Add unmerged roots as new roots
for root_id in other_roots:
if root_id not in merged_roots:
new_root = self._deep_copy_subtree(other, root_id, None)
self._roots.append(new_root)
Blend Chain Suppressibility#
BlendChainRemover validates that a blend chain can be suppressed:
Condition 1: Angular span For cylindrical blends, the angular span must be ≤ 180°:
def is_blend_suppressible(face):
if is_cylindrical(face):
angular_span = compute_angular_span(face)
if angular_span > math.pi:
return False
return True
Condition 2: Cross-edge reference count Edges between blend faces must be referenced exactly twice (by two blend faces):
def check_cross_edges(blend_chain, aag):
for edge in get_cross_edges(blend_chain):
ref_count = count_blend_references(edge, blend_chain, aag)
if ref_count != 2:
return False
return True
OpenCascade Integration#
BRepTools_ReShape#
Used by FaceRemover for soft feature removal:
from OCP.BRepTools import BRepTools_ReShape
reshaper = BRepTools_ReShape()
for face in faces_to_remove:
reshaper.Remove(face)
result = reshaper.Apply(original_shape)
BRepAlgoAPI_Defeaturing#
Used by ComplexFeatureRemover for hard features:
from OCP.BRepAlgoAPI import BRepAlgoAPI_Defeaturing
defeaturer = BRepAlgoAPI_Defeaturing()
defeaturer.SetShape(shape)
for face in faces_to_remove:
defeaturer.AddFaceToRemove(face)
defeaturer.Build()
result = defeaturer.Shape()
history = defeaturer.History()
This algorithm:
Extends surrounding surfaces
Intersects them to create new boundary edges
Rebuilds the solid with the new topology
BRepTools_History#
Tracks topological modifications:
history = defeaturer.History()
# Check if a shape was removed
if history.IsRemoved(face):
print("Face was deleted")
# Get modified images
modified_list = history.Modified(face)
for new_face in modified_list:
print(f"Face became: {new_face}")
# Get generated shapes
generated_list = history.Generated(face)
Extending the Module#
Adding a New Feature Remover#
Follow this template:
class MyFeatureRemover:
"""Remove my-type features from a shape."""
def __init__(
self,
shape: Union[Solid, Shell],
aag: Optional[AttributedAdjacencyGraph] = None,
):
self._shape = shape
self._aag = aag
self._result: Optional[Union[Solid, Shell]] = None
self._history: Optional[BRepTools_History] = None
@property
def aag(self) -> AttributedAdjacencyGraph:
"""Lazy AAG initialization."""
if self._aag is None:
self._aag = AttributedAdjacencyGraph(shape=self._shape)
return self._aag
@property
def result(self) -> Optional[Union[Solid, Shell]]:
"""Get the resulting shape after removal."""
return self._result
@property
def history(self) -> Optional[BRepTools_History]:
"""Get the OCC history for this operation."""
return self._history
def remove_features(self, face_indices: list[int]) -> bool:
"""
Remove features specified by face indices.
:param face_indices: List of 0-based face indices.
:return: True if successful.
"""
# Your implementation here
# 1. Validate inputs
# 2. Perform removal using OCC
# 3. Set self._result and self._history
# 4. Return success status
pass
Integrating with Defeaturer#
To add a new operation to Defeaturer:
def remove_my_features(self, **params) -> "Defeaturer":
"""
Remove my-type features from the current shape.
:return: Self for method chaining.
"""
# 1. Optionally extract features using FeatureProcessor
# 2. Use self._perform_removal() or implement custom logic
# 3. Return self
# Example using existing infrastructure:
processor = FeatureProcessor(shape=self._current_shape)
processor.extract_my_features() # Your extraction method
self._perform_removal("remove_my_features", processor.my_features, **params)
return self
# Or custom implementation:
faces_before = self._ensure_aag().map_faces.Extent()
shape_before = self._current_shape.wrapped
remover = MyFeatureRemover(shape=self._current_shape, aag=self._aag)
if remover.remove_features(face_indices):
self._current_shape = remover.result
self._invalidate_aag()
self._record_step(
"remove_my_features",
features_removed=len(face_indices),
faces_before=faces_before,
faces_after=self._ensure_aag().map_faces.Extent(),
shape_before=shape_before,
history=remover.history,
)
return self
Adding History Support#
To track history in your remover:
Store the
BRepTools_Historyfrom OCC operationsExpose it via a
historypropertyThe
Defeaturerwill automatically concatenate it
def remove_features(self, face_indices):
# ... perform operation ...
# Capture history from OCC
self._history = occ_operation.History()
return success
Performance Considerations#
AAG caching: Always pass
aagparameter when available to avoid recomputation.Batch operations: Remove multiple features in one call rather than one at a time.
Soft-first strategy: The fallback to hard removal is automatic but slower. If you know features are soft, use
IsolatedFeatureRemoverdirectly.Incremental vs single-pass: For
remove_blends(), single-pass (incremental=False) is faster but may fail on complex geometries.
See Also#
Shape Editing - User guide
Defeaturing Concepts - Core concepts
Feature Classification - Feature classification system (related)