History Tracking#

Track shape evolution through defeaturing operations using the HistoryGraph.

Note

Prerequisites: Defeaturing Concepts and Defeaturer

This section covers advanced topics for applications that need to trace face correspondence through multiple operations. Most users can skip this and use the convenience methods on Defeaturer.

Tracking Shape Evolution with HistoryGraph#

When defeaturing a CAD model, you often need to know what happened to specific faces. Did a face get deleted? Was it modified into a different face? The HistoryGraph class provides answers through a graph-based tracking system.

Why Track History?#

Consider a workflow where you:

  1. Extract a feature (e.g., a hole) from a model

  2. Defeature the model to remove blends

  3. Need to find where the hole’s faces ended up in the simplified model

Without history tracking, you’d have to re-identify the feature in the new geometry. With HistoryGraph, you can directly query: “What happened to face X?”

Common use cases:

  • Correspondence mapping: Map faces from original to defeatured model

  • Deleted face detection: Check if a specific feature was removed

  • Pipeline tracing: Track changes through multiple operations

  • Visualization: Show which faces changed and how

Key Concepts#

Evolution Types#

The HistoryGraph tracks two types of shape evolution:

MODIFIED: A shape was replaced by another shape.

  • The original shape is marked as inactive

  • The new shape takes its place

  • Example: A face is trimmed and becomes a smaller face

GENERATED: A new shape was created from an existing shape.

  • The source shape remains active

  • The new shape is a child of the source

  • Example: An edge split creates two new edges from one

The DAG Structure#

HistoryGraph maintains a directed acyclic graph (DAG) where:

  • Nodes represent shapes (faces, edges) with metadata

  • Edges represent evolution relationships

Original Operation 1          Operation 2
────────────────────────────────────────────────────────

Face A ─── MODIFIED ───> Face A' ─── MODIFIED ───> Face A''
                              │
                              └── GENERATED ───> Face B

Face C ─── [deleted] ───> (marked as deleted)

Face D ─── [unchanged] ───> Face D (remains active)

Each node stores:

  • shape: The actual TopoDS_Shape object

  • is_deleted: Whether the shape was deleted

  • is_active: Whether the shape is in the current model

  • operation_id: Which operation created/modified it

Using HistoryGraph with Defeaturer#

The simplest way to use history tracking is through the Defeaturer class, which maintains a unified HistoryGraph automatically:

from volmdlr.shapes import Solid
from volmdlr_tools.shape_editing import Defeaturer

# Load model and perform defeaturing
solid = Solid.from_brep("model.brep")
defeaturer = Defeaturer(solid)
defeaturer.remove_cavities()
defeaturer.remove_blends(max_radius=5.0)

# Access the unified history graph
history = defeaturer.history_graph

# Query a specific face from the original model
original_faces = list(solid.faces)
original_face = original_faces[10].wrapped  # Get the TopoDS_Face

# Was it deleted?
if history.is_deleted(original_face):
    print("Face 10 was deleted")
elif history.is_modified(original_face):
    # Get what it became
    final_faces = history.get_last_modified(original_face)
    print(f"Face 10 evolved into {len(final_faces)} face(s)")
else:
    print("Face 10 is unchanged")

Convenience Methods on Defeaturer#

The Defeaturer class provides shortcuts for common queries:

# Check if a face was deleted
is_gone = defeaturer.is_face_deleted(original_face)

# Get the final state of a face
final_shapes = defeaturer.get_last_modified(original_face)

# Get complete evolution info as a dictionary
evolution = defeaturer.get_face_evolution(original_face)
print(f"Deleted: {evolution['deleted']}")
print(f"Modified: {evolution['modified']}")
print(f"Final shapes: {len(evolution['modified_to'])}")
print(f"Operations: {evolution['operations']}")

# Get indices of all deleted faces
deleted_indices = defeaturer.get_deleted_face_indices()
print(f"Deleted face indices: {deleted_indices}")

Query Methods Reference#

Method

Description

Returns

is_deleted(shape)

Check if shape was deleted

bool

is_modified(shape)

Check if shape has modifications

bool

is_active(shape)

Check if shape is in current model

bool

has_generated(shape)

Check if shape generated new shapes

bool

get_modified(shape)

Get immediate modifications

list[TopoDS_Shape]

get_generated(shape)

Get generated shapes

list[TopoDS_Shape]

get_last_modified(shape)

Get final state (follows chain)

list[TopoDS_Shape]

get_last_modified_or_original(shape)

Get final or original if unchanged

Optional[TopoDS_Shape]

get_last_image_or_original(shape)

Get final, or None if deleted

Optional[TopoDS_Shape]

Understanding the Difference#

  • get_modified(): Returns direct children (one step)

  • get_last_modified(): Follows the chain to leaf nodes (all steps)

# If Face A -> Face A' -> Face A''
history.get_modified(face_a)       # Returns [face_a_prime]
history.get_last_modified(face_a)  # Returns [face_a_double_prime]

Visualizing the History#

You can visualize the history graph using the plot_data() method:

# Generate visualization data
plot_data = history.plot_data(layout="spring")

# The result can be used with plot_data library
# Colors indicate state:
# - Red: Deleted shapes
# - Green: Active shapes (in final model)
# - Blue: Intermediate shapes

# Edge colors indicate evolution type:
# - Orange: MODIFIED
# - Purple: GENERATED

Advanced: Working with HistoryGraph Directly#

For custom defeaturing pipelines, you can create and manipulate HistoryGraph instances directly.

Creating from OpenCascade History#

When you use a low-level remover, it provides an OpenCascade BRepTools_History object. Convert it to a HistoryGraph:

from volmdlr_tools.shape_editing import HistoryGraph
from volmdlr_tools.shape_editing.defeaturing import FeatureRemover

# Perform removal
remover = FeatureRemover(shape=my_solid)
remover.remove_features([1, 2, 3])

# Create history graph from OCC history
history = HistoryGraph.from_brep_history(
    my_solid.wrapped,  # Initial shape
    remover.history    # BRepTools_History from OCC
)

Concatenating Histories#

When performing multiple operations, you can merge histories:

# First operation
history1 = HistoryGraph.from_brep_history(shape1, remover1.history)

# Second operation (on the result of first)
history2 = HistoryGraph.from_brep_history(shape2, remover2.history)

# Concatenate: history1 now tracks both operations
history1.concatenate(history2)

# Query against original shape
final_faces = history1.get_last_modified(original_face)

The concatenate() method:

  1. Finds leaves of the first history (shapes with no successors)

  2. Finds roots of the second history

  3. Matches them using IsPartner() (topological identity)

  4. Connects matched pairs to form a unified DAG

Manual Construction#

For complete control, build the graph manually:

history = HistoryGraph()

# Add a root (original shape)
history.add_root(original_face)

# Record a modification
history.add_modified(before_face, after_face)

# Record a deletion
history.set_deleted(removed_face)

# Record a generation
history.add_generated(source_edge, new_edge)

Checking Consistency#

Verify that the history is consistent with a shape:

is_consistent = history.check_consistency(current_shape.wrapped)
if not is_consistent:
    print("Warning: Some active shapes in history not found in current model")

Complete Example#

from volmdlr.shapes import Solid
from volmdlr_tools.shape_editing import Defeaturer

# Load a model
solid = Solid.from_brep("data/brep/ANC101.brep")
original_faces = list(solid.faces)

# Defeature
defeaturer = Defeaturer(solid)
defeaturer.remove_cavities()
defeaturer.remove_blends(max_radius=3.0)

# Analyze what happened to each face
print("Face Evolution Report:")
print("=" * 50)

for i, face in enumerate(original_faces):
    wrapped = face.wrapped

    if defeaturer.is_face_deleted(wrapped):
        status = "DELETED"
    else:
        final = defeaturer.get_last_modified(wrapped)
        if final:
            status = f"MODIFIED -> {len(final)} face(s)"
        else:
            status = "UNCHANGED"

    print(f"Face {i:3d}: {status}")

# Summary
deleted_count = len(defeaturer.get_deleted_face_indices())
print(f"\nTotal faces deleted: {deleted_count}")
print(f"History graph: {defeaturer.history_graph}")

Tips and Best Practices#

  1. Use Defeaturer for most cases: It handles history concatenation automatically.

  2. Query with TopoDS_Shape: The history tracks OpenCascade TopoDS_Shape objects, not volmdlr wrappers. Use .wrapped to get the underlying shape.

  3. Understand shape identity: Two shapes are the same if shape1.IsPartner(shape2) returns True. This is how the history graph matches shapes across operations.

  4. Check for deletion first: Always check is_deleted() before get_last_modified(), as deleted shapes have no final state.

  5. Use get_last_image_or_original(): This method handles the common case where you want the final shape if modified, the original if unchanged, or None if deleted.

See Also#