Skip to content

assembly -- Functions to group and move together 3D objects

convenient data structures and functions for putting parts together and explode them to other functions

Solid(*args, **kwargs)

Bases: dict

Movable group of objects

A Solid is just like a dictionary with a pose.

Examples:

>>> mypart = icosphere(vec3(0), 1)
>>> s = Solid(part=mypart, anything=vec3(0))   # create a solid with whatever inside
>>> st = s.transform(vec3(1,2,3))   # make a new translated solid, keeping the same content without copy
>>> # use content as attributes
>>> s.part
<Mesh ...>
>>> # put any content in as a dict
>>> s['part']
<Mesh ...>
>>> s['whatever'] = vec3(5,2,1)
Source code in madcad/assembly/__init__.py
43
44
45
46
47
def __init__(self, *args, **kwargs):
	super().__init__(*args, **kwargs)
	self.__dict__ = self
	if 'pose' not in self:
		self.pose = mat4()

pose instance-attribute

placement matrix, it defines a base in which other attributes are displayed

transform(value)

Displace the solid by the transformation

Source code in madcad/assembly/__init__.py
49
50
51
def transform(self, value) -> 'Solid':
	''' Displace the solid by the transformation '''
	return Solid(self).update(pose = transform(value) * self.pose)

place(*args, **kwargs)

Strictly equivalent to .transform(placement(...)), see placement for parameters specifications.

Source code in madcad/assembly/__init__.py
53
54
55
def place(self, *args, **kwargs) -> 'Solid':
	''' Strictly equivalent to `.transform(placement(...))`, see `placement` for parameters specifications. '''
	return self.transform(placement(*args, **kwargs))

loc(*args)

chain transforms in the given key path

Source code in madcad/assembly/__init__.py
57
58
59
60
def loc(self, *args) -> object:
	''' chain transforms in the given key path '''
	obj, transform = self._unroll(args)
	return transform

deloc(*args)

return the object at the given key path with the chain of transforms applied to it

Source code in madcad/assembly/__init__.py
62
63
64
65
def deloc(self, *args) -> object:
	''' return the object at the given key path with the chain of transforms applied to it '''
	obj, transform = self._unroll(args)
	return obj.transform(transform)

append(value)

Add an item in self.content, a key is automatically created for it and is returned

Source code in madcad/assembly/__init__.py
82
83
84
85
86
87
def append(self, value) -> int:
	''' Add an item in self.content, a key is automatically created for it and is returned '''
	key = next(i 	for i in range(len(self.content)+1)
					if i not in self.content	)
	self[key] = value
	return key

display(scene)

Source code in madcad/assembly/__init__.py
95
96
97
def display(self, scene):
	from .displays import SolidDisplay
	return SolidDisplay(scene, self)

placement(*pairs, precision=0.001)

Return a transformation matrix that solved the placement constraints given by the surface pairs

Parameters:

Name Type Description Default
pairs

a list of pairs to convert to kinematic joints

  • items can be couples of surfaces to convert to joints using guessjoint
  • tuples (joint_type, a, b) to build joints joint_type(solida, solidb, a, b)
()
precision

surface guessing and kinematic solving precision (distance)

0.001

Each pair define a joint between the two assumed solids (a solid for the left members of the pairs, and a solid for the right members of the pairs). Placement will return the pose of the first relatively to the second, satisfying the constraints.

suppose we have those parts to assemble and it's hard to guess the precise pose transform between them

before placement

placement gives the pose for the screw to make the selected surfaces coincide

after placement

Examples:

>>> # get the transformation for the pose
>>> pose = placement(
...             (screw['part'].group(0), other['part'].group(44)),  # two cylinder surfaces: Cylindrical joint
...             (screw['part'].group(4), other['part'].group(25)),    # two planar surfaces: Planar joint
...             )  # solve everything to get solid's pose
>>> # apply the transformation to the solid
>>> screw.pose = pose
>>> # or
>>> screw.place(
...             (screw['part'].group(0), other['part'].group(44)),
...             (screw['part'].group(4), other['part'].group(25)),
...             )
>>> screw.place(
...             (Revolute, screw['axis'], other['screw_place']),
...             )
Source code in madcad/assembly/__init__.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def placement(*pairs, precision=1e-3):
	''' Return a transformation matrix that solved the placement constraints given by the surface pairs

	Parameters:
		pairs:	a list of pairs to convert to kinematic joints

			- items can be couples of surfaces to convert to joints using `guessjoint`
			- tuples (joint_type, a, b)  to build joints `joint_type(solida, solidb, a, b)`

		precision: surface guessing and kinematic solving precision (distance)

	Each pair define a joint between the two assumed solids (a solid for the left members of the pairs, and a solid for the right members of the pairs). Placement will return the pose of the first relatively to the second, satisfying the constraints.

	suppose we have those parts to assemble and it's hard to guess the precise pose transform between them

	![before placement](../screenshots/placement-before.png)

	placement gives the pose for the screw to make the selected surfaces coincide

	![after placement](../screenshots/placement-after.png)

	Examples:
		>>> # get the transformation for the pose
		>>> pose = placement(
		...		(screw['part'].group(0), other['part'].group(44)),  # two cylinder surfaces: Cylindrical joint
		...		(screw['part'].group(4), other['part'].group(25)),    # two planar surfaces: Planar joint
		...		)  # solve everything to get solid's pose
		>>> # apply the transformation to the solid
		>>> screw.pose = pose

		>>> # or
		>>> screw.place(
		...		(screw['part'].group(0), other['part'].group(44)),
		...		(screw['part'].group(4), other['part'].group(25)),
		...		)

		>>> screw.place(
		...		(Revolute, screw['axis'], other['screw_place']),
		...		)
	'''
	from ..reverse import guessjoint
	from ..kinematic import Kinematic # circular import

	joints = []
	for pair in pairs:
		if len(pair) == 2:		joints.append(guessjoint((0, 1), *pair, precision*0.25))
		elif len(pair) == 3:	joints.append(pair[0]((0, 1), *pair[1:]))
		else:
			raise TypeError('incorrect pair definition', pair)

	if len(joints) > 1:
		kin = Kinematic(joints)
		parts = kin.parts(kin.solve())
		return affineInverse(parts[1]) * parts[0]
	else:
		return affineInverse(joints[0].direct(joints[0].default))

explode(solids, spacing=1, offsets=None)

Move the given solids away from each other in the way of an exploded view. It makes easier to seen the details of an assembly . See explode_offsets for the algorithm.

before explode after explode

Parameters:

Name Type Description Default
solids

a list of solids (copies of each will be made before displacing)

required
spacing

spacing factor, 0 for no displacement, 1 for normal displacement

1
offsets

if given, must be the result of explode_offsets(solids)

None

Examples:

>>> # pick some raw model and separate parts
>>> imported = read(folder+'/some_assembly.stl')
>>> imported.mergeclose()
>>> parts = []
>>> for part in imported.islands():
...     part.strippoints()
...     parts.append(Solid(part=segmentation(part)))
...
>>> # explode the assembly to look into it
>>> exploded = explode(parts)
Source code in madcad/assembly/__init__.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def explode(solids, spacing=1, offsets=None) -> list[Solid]:
	'''
	Move the given solids away from each other in the way of an exploded view.
	It makes easier to seen the details of an assembly . See `explode_offsets` for the algorithm.

	![before explode](../screenshots/explode-before.png)
	![after explode](../screenshots/explode-after.png)

	Parameters:
		solids:		a list of solids (copies of each will be made before displacing)
		spacing:	spacing factor, 0 for no displacement, 1 for normal displacement
		offsets:	if given, must be the result of `explode_offsets(solids)`

	Examples:
		>>> # pick some raw model and separate parts
		>>> imported = read(folder+'/some_assembly.stl')
		>>> imported.mergeclose()
		>>> parts = []
		>>> for part in imported.islands():
		...     part.strippoints()
		...     parts.append(Solid(part=segmentation(part)))
		...
		>>> # explode the assembly to look into it
		>>> exploded = explode(parts)

	'''
	if not offsets:
		offsets = explode_offsets([
			SolidBox(
				box = boundingbox(list(solid.values()), ignore=True),
				place = solid.pose,
				)
			for solid in solids], spacing)
	return [solid.transform(offset)  for solid, offset in zip(solids, offsets)]

explode_offsets(solids, spacing=0.0)

return offsets for an exploded view

Parameters:

Name Type Description Default
solids list[SolidBox]

objects to explode, described as boundingboxes in their own space

required
spacing

spacing ratio relative to objects sizes (0 means no spacing, 0.5 is a good value)

0.0
Source code in madcad/assembly/__init__.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def explode_offsets(solids:list[SolidBox], spacing=0.) -> list[vec3]:
	''' return offsets for an exploded view

	Args:
		solids: objects to explode, described as boundingboxes in their own space
		spacing:  spacing ratio relative to objects sizes (0 means no spacing, 0.5 is a good value)
	'''
	# create a tree of almost enclosing boxes
	# criterion is intersection over sum
	def ios(a, b):
		return a.intersection(b).volume() / (a.volume() + b.volume())
	def nest(root, new, threshold):
		# try to insert in one of the children
		if root.children:
			parent = max(root.children, key=lambda parent: ios(new.world, parent.world))
			score = ios(new.world, parent.world)
			if score > threshold:
				nest(parent, new, score)
				return
		# otherwise insert here
		root.children.append(new)

	root = _Node(None,
		world = Box(size=-inf),
		local = Box(size=-inf),
		exploded = Box(size=-inf),
		place = mat4(),
		children = [],
		offset = vec3(),
		)
	for index, solid in sorted(enumerate(solids), key=lambda item: item[1].box.volume(), reverse=True):
		nest(root, _Node(index,
			world = solid.box.transform(solid.place),
			local = solid.box,
			exploded = solid.exploded or solid.box,
			place = solid.place,
			children = [],
			offset = None,
			), 0)

	# explode boxes recursively
	def inner_distance(parent, child):
		return min(glm.min(child.min - parent.min, parent.max - child.max) / parent.size)
	def place(node):
		# process lower level first
		for child in node.children:
			place(child)
		# bounding points of already exploded boxes at this level
		exploded = [node.place * p   for p in node.exploded.corners() if isfinite(p)]
		# process box in this level from most inner to most outer
		ordered = sorted(node.children, key=lambda child: inner_distance(node.world, child.world), reverse=True)
		for child in ordered:
			# choose offset direction the closest to exterior of enclosing box
			bounds = _box_planes(node.local, node.place)
			shape = _box_planes(child.local, child.place)
			# move in the direction where the needed offset is the smallest
			direction = - max(shape,
				key=lambda axis: max(
					dot(axis.origin - bound.origin, -axis.direction)
					for bound in bounds
					if dot(axis.direction, bound.direction) < 0)
				).direction

			shape_length = max(dot(axis.origin, direction)  for axis in shape) - min(dot(axis.origin, direction)  for axis in shape)
			shape_bot = min(dot(axis.origin, direction)  for axis in _box_planes(child.exploded, child.place))
			explode_top = max((dot(p, direction)  for p in exploded), default=shape_bot)

			# compute offset along direction to get the box out of the already exploded area
			offset = max(0, explode_top - shape_bot) + spacing*mix(shape_length, max(child.world.size), 0.1)
			# update tree
			child.offset = direction * offset
			exploded.extend(offset + child.place * p   for p in child.exploded.corners())
		# publish new exploded bounds for upper level
		node.exploded |= boundingbox(affineInverse(node.place) * p  for p in exploded)
	place(root)

	# collect offsets from the tree
	offsets = [vec3()  for box in solids]
	def get(node, offset):
		offsets[node.index] = node.offset + offset
		for child in node.children:
			get(child, node.offset + offset)
	for node in root.children:
		get(node, vec3(0))
	return offsets