Skip to content

Commit fa0abf0

Browse files
Imprinting and matching (#1353)
* Initial implementation * Small tweaks * Disjoint solid handling * Imprinting test * Test latest ocp/gmsh * Dev label * Revert * Assembly iterator implementation * Refactored toFusedCAF * Refactor toVTK * Restructure toVTK for better rendering, refactor toJSON and fix some typos
1 parent ecadff2 commit fa0abf0

File tree

4 files changed

+169
-45
lines changed

4 files changed

+169
-45
lines changed

cadquery/assembly.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,28 @@ def _flatten(self, parents=[]):
543543

544544
return rv
545545

546+
def __iter__(
547+
self,
548+
loc: Optional[Location] = None,
549+
name: Optional[str] = None,
550+
color: Optional[Color] = None,
551+
) -> Iterator[Tuple[Shape, str, Location, Optional[Color]]]:
552+
"""
553+
Assembly iterator yielding shapes, names, locations and colors.
554+
"""
555+
556+
name = f"{name}/{self.name}" if name else self.name
557+
loc = loc * self.loc if loc else self.loc
558+
color = self.color if self.color else color
559+
560+
if self.obj:
561+
yield self.obj if isinstance(self.obj, Shape) else Compound.makeCompound(
562+
s for s in self.obj.vals() if isinstance(s, Shape)
563+
), name, loc, color
564+
565+
for ch in self.children:
566+
yield from ch.__iter__(loc, name, color)
567+
546568
def toCompound(self) -> Compound:
547569
"""
548570
Returns a Compound made from this Assembly (including all children) with the

cadquery/occ_impl/assembly.py

Lines changed: 107 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
from typing import Union, Iterable, Tuple, Dict, overload, Optional, Any, List, cast
1+
from typing import (
2+
Union,
3+
Iterable,
4+
Iterator,
5+
Tuple,
6+
Dict,
7+
overload,
8+
Optional,
9+
Any,
10+
List,
11+
cast,
12+
)
213
from typing_extensions import Protocol
314
from math import degrees
415

@@ -12,7 +23,7 @@
1223
from OCP.Quantity import Quantity_ColorRGBA
1324
from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse
1425
from OCP.TopTools import TopTools_ListOfShape
15-
from OCP.BOPAlgo import BOPAlgo_GlueEnum
26+
from OCP.BOPAlgo import BOPAlgo_GlueEnum, BOPAlgo_MakeConnected
1627
from OCP.TopoDS import TopoDS_Shape
1728

1829
from vtkmodules.vtkRenderingCore import (
@@ -21,8 +32,11 @@
2132
vtkRenderer,
2233
)
2334

35+
from vtkmodules.vtkFiltersExtraction import vtkExtractCellsByType
36+
from vtkmodules.vtkCommonDataModel import VTK_TRIANGLE, VTK_LINE, VTK_VERTEX
37+
2438
from .geom import Location
25-
from .shapes import Shape, Compound
39+
from .shapes import Shape, Solid, Compound
2640
from .exporters.vtk import toString
2741
from ..cq import Workplane
2842

@@ -131,6 +145,14 @@ def children(self) -> Iterable["AssemblyProtocol"]:
131145
def traverse(self) -> Iterable[Tuple[str, "AssemblyProtocol"]]:
132146
...
133147

148+
def __iter__(
149+
self,
150+
loc: Optional[Location] = None,
151+
name: Optional[str] = None,
152+
color: Optional[Color] = None,
153+
) -> Iterator[Tuple[Shape, str, Location, Optional[Color]]]:
154+
...
155+
134156

135157
def setName(l: TDF_Label, name: str, tool):
136158

@@ -227,75 +249,93 @@ def _toCAF(el, ancestor, color) -> TDF_Label:
227249

228250
def toVTK(
229251
assy: AssemblyProtocol,
230-
renderer: vtkRenderer = vtkRenderer(),
231-
loc: Location = Location(),
232252
color: Tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
233253
tolerance: float = 1e-3,
234254
angularTolerance: float = 0.1,
235255
) -> vtkRenderer:
236256

237-
loc = loc * assy.loc
238-
trans, rot = loc.toTuple()
257+
renderer = vtkRenderer()
258+
259+
for shape, _, loc, col_ in assy:
260+
261+
col = col_.toTuple() if col_ else color
262+
trans, rot = loc.toTuple()
263+
264+
data = shape.toVtkPolyData(tolerance, angularTolerance)
265+
266+
# extract faces
267+
extr = vtkExtractCellsByType()
268+
extr.SetInputDataObject(data)
269+
270+
extr.AddCellType(VTK_LINE)
271+
extr.AddCellType(VTK_VERTEX)
272+
extr.Update()
273+
data_edges = extr.GetOutput()
274+
275+
# extract edges
276+
extr = vtkExtractCellsByType()
277+
extr.SetInputDataObject(data)
239278

240-
if assy.color:
241-
color = assy.color.toTuple()
279+
extr.AddCellType(VTK_TRIANGLE)
280+
extr.Update()
281+
data_faces = extr.GetOutput()
242282

243-
if assy.shapes:
244-
data = Compound.makeCompound(assy.shapes).toVtkPolyData(
245-
tolerance, angularTolerance
246-
)
283+
# remove normals from edges
284+
data_edges.GetPointData().RemoveArray("Normals")
247285

286+
# add both to the renderer
248287
mapper = vtkMapper()
249-
mapper.SetInputData(data)
288+
mapper.AddInputDataObject(data_faces)
250289

251290
actor = vtkActor()
252291
actor.SetMapper(mapper)
253292
actor.SetPosition(*trans)
254293
actor.SetOrientation(*map(degrees, rot))
255-
actor.GetProperty().SetColor(*color[:3])
256-
actor.GetProperty().SetOpacity(color[3])
294+
actor.GetProperty().SetColor(*col[:3])
295+
actor.GetProperty().SetOpacity(col[3])
257296

258297
renderer.AddActor(actor)
259298

260-
for child in assy.children:
261-
renderer = toVTK(child, renderer, loc, color, tolerance, angularTolerance)
299+
mapper = vtkMapper()
300+
mapper.AddInputDataObject(data_edges)
301+
302+
actor = vtkActor()
303+
actor.SetMapper(mapper)
304+
actor.SetPosition(*trans)
305+
actor.SetOrientation(*map(degrees, rot))
306+
actor.GetProperty().SetColor(0, 0, 0)
307+
actor.GetProperty().SetLineWidth(2)
308+
309+
renderer.AddActor(actor)
262310

263311
return renderer
264312

265313

266314
def toJSON(
267315
assy: AssemblyProtocol,
268-
loc: Location = Location(),
269316
color: Tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
270317
tolerance: float = 1e-3,
271318
) -> List[Dict[str, Any]]:
272319
"""
273320
Export an object to a structure suitable for converting to VTK.js JSON.
274321
"""
275322

276-
loc = loc * assy.loc
277-
trans, rot = loc.toTuple()
278-
279-
if assy.color:
280-
color = assy.color.toTuple()
281-
282323
rv = []
283324

284-
if assy.shapes:
325+
for shape, _, loc, col_ in assy:
326+
285327
val: Any = {}
286328

287329
data = toString(Compound.makeCompound(assy.shapes), tolerance)
330+
trans, rot = loc.toTuple()
288331

289332
val["shape"] = data
290-
val["color"] = color
333+
val["color"] = col_.toTuple() if col_ else color
291334
val["position"] = trans
292335
val["orientation"] = rot
293336

294337
rv.append(val)
295338

296-
for child in assy.children:
297-
rv.extend(toJSON(child, loc, color, tolerance))
298-
299339
return rv
300340

301341

@@ -331,19 +371,9 @@ def toFusedCAF(
331371
shapes: List[Shape] = []
332372
colors = []
333373

334-
def extract_shapes(assy, parent_loc=None, parent_color=None):
335-
336-
loc = parent_loc * assy.loc if parent_loc else assy.loc
337-
color = assy.color if assy.color else parent_color
338-
339-
for shape in assy.shapes:
340-
shapes.append(shape.moved(loc).copy())
341-
colors.append(color)
342-
343-
for ch in assy.children:
344-
extract_shapes(ch, loc, color)
345-
346-
extract_shapes(assy)
374+
for shape, _, loc, color in assy:
375+
shapes.append(shape.moved(loc).copy())
376+
colors.append(color)
347377

348378
# Initialize with a dummy value for mypy
349379
top_level_shape = cast(TopoDS_Shape, None)
@@ -411,3 +441,37 @@ def extract_shapes(assy, parent_loc=None, parent_color=None):
411441
color_tool.SetColor(cur_lbl, color.wrapped, XCAFDoc_ColorGen)
412442

413443
return top_level_lbl, doc
444+
445+
446+
def imprint(assy: AssemblyProtocol) -> Tuple[Shape, Dict[Shape, Tuple[str, ...]]]:
447+
"""
448+
Imprint all the solids and construct a dictionary mapping imprinted solids to names from the input assy.
449+
"""
450+
451+
# make the id map
452+
id_map = {}
453+
454+
for obj, name, loc, _ in assy:
455+
for s in obj.moved(loc).Solids():
456+
id_map[s] = name
457+
458+
# connect topologically
459+
bldr = BOPAlgo_MakeConnected()
460+
bldr.SetRunParallel(True)
461+
bldr.SetUseOBB(True)
462+
463+
for obj in id_map:
464+
bldr.AddArgument(obj.wrapped)
465+
466+
bldr.Perform()
467+
res = Shape(bldr.Shape())
468+
469+
# make the connected solid -> id map
470+
origins: Dict[Shape, Tuple[str, ...]] = {}
471+
472+
for s in res.Solids():
473+
ids = tuple(id_map[Solid(el)] for el in bldr.GetOrigins(s.wrapped))
474+
# if GetOrigins yields nothing, solid was not modified
475+
origins[s] = ids if ids else (id_map[s],)
476+
477+
return res, origins

cadquery/occ_impl/exporters/assembly.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,9 @@ def _vtkRenderWindow(
140140
Convert an assembly to a vtkRenderWindow. Used by vtk based exporters.
141141
"""
142142

143-
renderer = vtkRenderer()
143+
renderer = toVTK(assy, tolerance=tolerance, angularTolerance=angularTolerance)
144144
renderWindow = vtkRenderWindow()
145145
renderWindow.AddRenderer(renderer)
146-
toVTK(assy, renderer, tolerance=tolerance, angularTolerance=angularTolerance)
147146

148147
renderer.ResetCamera()
149148
renderer.SetBackground(1, 1, 1)

tests/test_assembly.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,3 +1475,42 @@ def test_point_constraint(simple_assy2):
14751475
t2 = assy.children[1].loc.wrapped.Transformation().TranslationPart()
14761476

14771477
assert t2.Modulus() == pytest.approx(1)
1478+
1479+
1480+
@pytest.fixture
1481+
def touching_assy():
1482+
1483+
b1 = cq.Workplane().box(1, 1, 1)
1484+
b2 = cq.Workplane(origin=(1, 0, 0)).box(1, 1, 1)
1485+
1486+
return cq.Assembly().add(b1).add(b2)
1487+
1488+
1489+
@pytest.fixture
1490+
def disjoint_assy():
1491+
1492+
b1 = cq.Workplane().box(1, 1, 1)
1493+
b2 = cq.Workplane(origin=(2, 0, 0)).box(1, 1, 1)
1494+
1495+
return cq.Assembly().add(b1).add(b2)
1496+
1497+
1498+
def test_imprinting(touching_assy, disjoint_assy):
1499+
1500+
# normal usecase
1501+
r, o = cq.occ_impl.assembly.imprint(touching_assy)
1502+
1503+
assert len(r.Solids()) == 2
1504+
assert len(r.Faces()) == 11
1505+
1506+
for s in r.Solids():
1507+
assert s in o
1508+
1509+
# edge usecase
1510+
r, o = cq.occ_impl.assembly.imprint(disjoint_assy)
1511+
1512+
assert len(r.Solids()) == 2
1513+
assert len(r.Faces()) == 12
1514+
1515+
for s in r.Solids():
1516+
assert s in o

0 commit comments

Comments
 (0)