How to create a concave fillet between two joined parts and subsequently remove the lower (helper) part
Opened this issue · 1 comments
Problem Description:
The goal is to add a filleted transition at the bottom of Part 1.
To do this, BRepAlgoAPI_Fuse is used to fuse Part 1 with a helper Part 2, and then BRepFilletAPI_MakeFillet is applied to the intersection edges between the two parts to create concave fillets, resulting in Part 3.
The objective is to remove the original helper Part 2, keeping only the transition fillet.
Observed Behavior:
Using BRepAlgoAPI_Cut to subtract Part 2 from the fused Part 3 does not fully remove Part 2.
A small thickness must be left behind, otherwise an error occurs:
File "d:/xx.py", line 469, in
display.DisplayShape(cut1, update=True)
However, if a new helper Part 4 is created and BRepAlgoAPI_Cut is used to retain the lower portion (the fillet) of the original Part 2, the operation succeeds and displays correctly.
Using BOPAlgo_Splitter to split Part 3 at the interface with Part 2’s surface also results in unwanted extra faces being retained.
Additional Notes:
Part 1 is geometrically complex, with a convex fillet on the back side that gradually transitions to a radius of 0 (actually set to 0.001 in the code, since using 0 directly causes the fillet to fail). This transition ends exactly at the bottom face.
If Part 1 is a simple cylinder and Part 2 is a box, then BRepAlgoAPI_Cut successfully removes Part 2 after the fillet is added.
It is also observed that when applying a convex fillet to two colinear edges, the maximum fillet radius cannot reach half of the edge length. For example, for a cube with edge length 10, the fillet radius cannot be 5 when applied to two parallel edges.
Expected Behavior:
1.Be able to completely remove the original helper Part 2, preserving only the added fillet.
2.Be able to define fillet radii from d/2 down to 0, enabling a smooth transition with radius approaching zero.
Some of the code is as follows:
##filleted ##
filletD32 = BRepFilletAPI_MakeFillet(rectangle_L2_H_shapeD32 )
filletD32.SetFilletShape(FSH)
parAndRad2 = TColgp_Array1OfPnt2d(1, 3)
parAndRad2.SetValue(1, gp_Pnt2d(0, 0.001))
parAndRad2.SetValue(2, gp_Pnt2d(0.5, D/2*(1/2*(L_R2/(L_R2+L2)))*2))
parAndRad2.SetValue(3, gp_Pnt2d(1, D/2(L_R2/(L_R2+L2))**2))
edgesD32 = []
edge_explorerD32 = TopExp_Explorer(rectangle_L2_H_shapeD32 , TopAbs_EDGE)
while edge_explorerD32.More():
edgeD32 = topods_Edge(edge_explorerD32.Current())
edgesD32.append(edgeD32)
edge_explorerD32.Next()
edges_to_filletD32 = [3,2]
for edge_indexD32 in edges_to_filletD32:
if edge_indexD32 < len(edgesD32):
filletD32.Add(parAndRad2, edgesD32[edge_indexD32])
#print(f"Applied fillet to edge {edge_indexD32}")
#else:
#print(f"Edge index {edge_indexD32} is out of range")
resultD32 = filletD32.Shape()
##Remove the bottom part (Part 2)##
box2 = BRepPrimAPI_MakeBox(gp_Pnt(-D2,-H,-3H),4D, L+L1+2H, 2*H).Shape()
cut1 =BRepAlgoAPI_Cut(filleted_shape4,box2).Shape()
How about this.
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder
from OCC.Core.gp import gp_Pnt, gp_Ax2, gp_Dir
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Fuse, BRepAlgoAPI_Cut
from OCC.Core.BRepFilletAPI import BRepFilletAPI_MakeFillet
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_EDGE, TopAbs_SOLID, TopAbs_VERTEX
from OCC.Core.BRep import BRep_Tool
from OCC.Core.TopoDS import topods, TopoDS_Compound
from OCC.Core.BRep import BRep_Builder
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.Bnd import Bnd_Box
from OCC.Display.SimpleGui import init_display
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Common
def solids_from_shape(shape):
solids = []
exp = TopExp_Explorer(shape, TopAbs_SOLID)
while exp.More():
solids.append(exp.Current())
exp.Next()
return solids
def bbox_of_shape(shape):
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
return (xmin, ymin, zmin, xmax, ymax, zmax)
def build_compound_from_solids(solids):
# Build a TopoDS_Compound using BRep_Builder to be compatible with
# pythonocc-core versions that don't expose BRepBuilderAPI_MakeCompound.
builder = BRep_Builder()
compound = TopoDS_Compound()
builder.MakeCompound(compound)
for s in solids:
builder.Add(compound, s)
return compound
def edge_midpoint(edge):
expv = TopExp_Explorer(edge, TopAbs_VERTEX)
pts = []
while expv.More():
v = expv.Current()
pts.append(BRep_Tool.Pnt(v))
expv.Next()
if not pts:
return None
x = sum(p.X() for p in pts) / len(pts)
y = sum(p.Y() for p in pts) / len(pts)
z = sum(p.Z() for p in pts) / len(pts)
return gp_Pnt(x, y, z)
def edge_center_z(edge):
expv = TopExp_Explorer(edge, TopAbs_VERTEX)
zs = []
while expv.More():
v = expv.Current()
p = BRep_Tool.Pnt(v)
zs.append(p.Z())
expv.Next()
if not zs:
return None
return sum(zs) / len(zs)
if __name__ == "__main__":
display, start_display, add_menu, add_function_to_menu = init_display()
# --- Create Part1 and helper Part2 ---
# Example 1: Part1 = cylinder, Part2 = box under it
part1 = BRepPrimAPI_MakeCylinder(10.0, 20.0).Shape() # radius 10, height 20
part1 = BRepPrimAPI_MakeBox(gp_Pnt(-10, -10, 0), 20, 20, 20).Shape()
# create a helper cylinder that intrudes into part1 (so an internal/interface edge exists)
# Cylinder centered at origin, starting below z=0 so it intersects the bottom of part1
helper_axis = gp_Ax2(gp_Pnt(0, 0, -6), gp_Dir(0, 0, 1))
part2 = BRepPrimAPI_MakeCylinder(helper_axis, 8.0, 12.0).Shape()
# --- Method A: Cut with slightly enlarged helper cylinder (use tolerance) ---
eps = 0e-3
cutter = BRepPrimAPI_MakeCylinder(helper_axis, 8.0 + eps, 12.0 + 2 * eps).Shape()
# Fuse them
fused = BRepAlgoAPI_Fuse(part1, part2).Shape()
# --- Find candidate edges to fillet ---
# Strategy: compute the geometric intersection edges between the
# original Part1 and Part2 (before fuse) using BRepAlgoAPI_Common,
# then match those intersection edges to the edges in the fused shape
# by comparing midpoints. This ensures we pick the internal/interface
# edges that will produce a concave fillet.
# edges in the fused shape
fused_edges = []
exp = TopExp_Explorer(fused, TopAbs_EDGE)
while exp.More():
fused_edges.append(topods.Edge(exp.Current()))
exp.Next()
# compute intersection (common) edges between original parts
common_shape = BRepAlgoAPI_Common(part1, part2).Shape()
common_edges = []
expc = TopExp_Explorer(common_shape, TopAbs_EDGE)
while expc.More():
common_edges.append(topods.Edge(expc.Current()))
expc.Next()
# map common edges to fused edges by proximity of midpoints
tol = 1e-3
edges_to_fillet = []
common_mids = [edge_midpoint(e) for e in common_edges]
fused_mids = [edge_midpoint(e) for e in fused_edges]
for i, fm in enumerate(fused_mids):
if fm is None:
continue
for cm in common_mids:
if cm is None:
continue
if fm.Distance(cm) < tol:
edges_to_fillet.append(i)
break
print(
f"common edges found: {len(common_edges)}, fused edges: {len(fused_edges)}, matched: {len(edges_to_fillet)}"
)
# if none found, fallback to a small neighborhood selection (visual debug)
if not edges_to_fillet:
print("No interface edges found by common(); falling back to z-based selection")
for i, e in enumerate(fused_edges):
cz = edge_center_z(e)
if cz is None:
continue
if abs(cz - 0.0) < 1.5:
edges_to_fillet.append(i)
# --- Apply fillet ---
# For each matched fused edge, perform a min/max exploration instead of a
# fixed list. Strategy (inline, no functionization):
# - find a small starting radius and ensure it succeeds (exponential search up)
# - find the maximum successful radius by exponential growth until failure,
# then binary-search the success/failure boundary to tighten max
# - similarly tighten the minimum successful radius (by halving + binary search)
# - choose the midpoint between min and max, try apply; if apply fails,
# binary-search between min and midpoint on the current_shape to find
# a feasible radius
current_shape = fused
fused_mids = [edge_midpoint(e) for e in fused_edges]
succeeded = []
skipped = []
# numeric parameters
global_min = 1e-6
global_max_cap = 100.0
binary_tol = 1e-4
for idx in edges_to_fillet:
target_edge = fused_edges[idx]
target_mid = fused_mids[idx]
if target_mid is None:
skipped.append((idx, "no midpoint"))
continue
# --- find a starting small radius that succeeds ---
r_small = global_min
found_small = False
for _ in range(60): # avoid infinite loop; exponential growth up to cap
try:
t = BRepFilletAPI_MakeFillet(fused)
t.Add(float(r_small), target_edge)
_ = t.Shape()
found_small = True
break
except Exception:
r_small *= 2.0
if r_small > global_max_cap:
break
if not found_small:
skipped.append((idx, "no small radius succeeded"))
print(f"Edge {idx}: no small radius succeeded up to cap {global_max_cap}")
continue
# tighten minimal successful radius by halving below r_small
lo = r_small / 2.0
hi = r_small
min_success = r_small
if lo >= global_min:
# ensure lo is a failure, else reduce further
for _ in range(60):
try:
t = BRepFilletAPI_MakeFillet(fused)
t.Add(float(lo), target_edge)
_ = t.Shape()
# lo also succeeds -> move down
min_success = lo
hi = lo
lo = lo / 2.0
if lo < global_min:
break
except Exception:
# lo failed -> binary search between lo and hi
break
# binary search to refine min_success
fail = lo
succ = hi
for _ in range(40):
if succ - fail <= binary_tol:
break
mid = (succ + fail) / 2.0
try:
t = BRepFilletAPI_MakeFillet(fused)
t.Add(float(mid), target_edge)
_ = t.Shape()
succ = mid
min_success = mid
except Exception:
fail = mid
# --- find maximal successful radius ---
r_hi = max(r_small, 1.0)
last_success = None
first_fail = None
# exponential grow until failure or cap
while True:
try:
t = BRepFilletAPI_MakeFillet(fused)
t.Add(float(r_hi), target_edge)
_ = t.Shape()
last_success = r_hi
r_hi *= 2.0
if r_hi > global_max_cap:
# test cap and stop
try:
t = BRepFilletAPI_MakeFillet(fused)
t.Add(float(global_max_cap), target_edge)
_ = t.Shape()
last_success = global_max_cap
except Exception:
first_fail = global_max_cap
break
except Exception:
first_fail = r_hi
break
if last_success is None:
skipped.append((idx, "no upper radius success"))
print(
f"Edge {idx}: failed to find any upper success starting from {r_small}"
)
continue
# binary search between last_success and first_fail to tighten max
max_success = last_success
if first_fail is None:
first_fail = last_success * 2.0
lo = last_success
hi = first_fail
for _ in range(50):
if hi - lo <= binary_tol:
break
mid = (lo + hi) / 2.0
try:
t = BRepFilletAPI_MakeFillet(fused)
t.Add(float(mid), target_edge)
_ = t.Shape()
lo = mid
max_success = mid
except Exception:
hi = mid
# finalize min/max
rmin = float(min_success)
rmax = float(max_success)
apply_r = (rmin + rmax) / 2.0
apply_r = 0.1e-6
apply_r = 1.0
print(
f"Edge {idx} explored range: min={rmin}, max={rmax}, chosen(mid)={apply_r}"
)
# find matching edge on current_shape by midpoint proximity
cur_edges = []
exp_cur = TopExp_Explorer(current_shape, TopAbs_EDGE)
while exp_cur.More():
cur_edges.append(topods.Edge(exp_cur.Current()))
exp_cur.Next()
best_idx = None
best_dist = 1e9
for i, ce in enumerate(cur_edges):
m = edge_midpoint(ce)
if m is None:
continue
d = m.Distance(target_mid)
if d < best_dist:
best_dist = d
best_idx = i
if best_idx is None or best_dist > 5.0:
skipped.append((idx, "no matching edge on current shape", best_dist))
continue
# try to apply midpoint; if fails, binary-search between rmin and apply_r on current_shape
applied_ok = False
try:
fillet_op = BRepFilletAPI_MakeFillet(current_shape)
fillet_op.Add(float(apply_r), cur_edges[best_idx])
new_shape = fillet_op.Shape()
current_shape = new_shape
succeeded.append((idx, apply_r))
applied_ok = True
print(f"Applied fillet on edge {idx} with radius {apply_r}")
except Exception as e:
# fallback: binary search between rmin and apply_r to find feasible radius on current_shape
lo = rmin
hi = apply_r
for _ in range(40):
if hi - lo <= binary_tol:
break
mid = (lo + hi) / 2.0
try:
f2 = BRepFilletAPI_MakeFillet(current_shape)
f2.Add(float(mid), cur_edges[best_idx])
_ = f2.Shape()
lo = mid
applied_radius = mid
except Exception:
hi = mid
# final attempt with lo
try:
f3 = BRepFilletAPI_MakeFillet(current_shape)
f3.Add(float(lo), cur_edges[best_idx])
new_shape3 = f3.Shape()
current_shape = new_shape3
succeeded.append((idx, lo))
applied_ok = True
print(f"Fallback applied on edge {idx} with radius {lo}")
except Exception as e2:
skipped.append((idx, f"apply failed after fallback {e2}"))
filleted_shape = current_shape
print("fillet summary: succeeded=", succeeded, "skipped=", skipped)
# Do not display the intermediate filleted_shape here to avoid
# keeping the helper visible alongside the final result.
cutA = BRepAlgoAPI_Cut(filleted_shape, cutter).Shape()
# Decompose cutA into solids and remove any solid that still has
# significant overlap with the cutter (ensures helper is removed).
all_solids_after_cut = solids_from_shape(cutA)
keep_solids = []
removed_info = []
for s in all_solids_after_cut:
common = BRepAlgoAPI_Common(s, cutter).Shape()
props = GProp_GProps()
vol = 0.0
try:
brepgprop.VolumeProperties(common, props)
vol = props.Mass()
except Exception:
vol = 0.0
# If overlap volume is tiny, keep the solid; otherwise, it's helper residue -> remove
if vol < 1e-6:
keep_solids.append(s)
else:
removed_info.append(vol)
if removed_info:
print(
f"Removed {len(removed_info)} solids due to overlap volumes: {removed_info}"
)
# Fallback: if nothing kept (unlikely), use bbox-based filter on filleted_shape
if not keep_solids:
print(
"No solids kept after overlap filtering; falling back to bbox thresholding"
)
all_solids = solids_from_shape(filleted_shape)
z_threshold = 0.0
for s in all_solids:
xmin, ymin, zmin, xmax, ymax, zmax = bbox_of_shape(s)
if zmax >= z_threshold + 1e-6:
keep_solids.append(s)
final_result = (
build_compound_from_solids(keep_solids) if keep_solids else TopoDS_Compound()
)
display.DisplayShape(final_result)
display.FitAll()
start_display()