Skip to content

offseting -- Offset or thicken surfaces

The functions here allow to move or extrude meshes along their normals

Note

Any offseting provided here only rely on the vertex and edge normals of the input meshes, so don't expect a nice result if your mesh is too chaotic to have meaningful normals

inflate_offsets(surface, offset, method='face')

Displacements vectors for points of a surface we want to inflate.

Parameters:

Name Type Description Default
offset float

the distance from the surface to the offset surface. Its meaning depends on method

required
method

determines if the distance is from the old to the new faces, edges or points possible values: 'face', 'edge', 'point'

'face'
Source code in madcad/offseting.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def inflate_offsets(surface: Mesh, offset: float, method='face') -> '[vec3]':
	''' Displacements vectors for points of a surface we want to inflate.

	Parameters:
		offset:
			the distance from the surface to the offset surface. Its meaning depends on `method`
		method:
			determines if the distance is from the old to the new faces, edges or points
			possible values: `'face', 'edge', 'point'`
	'''
	pnormals = surface.vertexnormals()

	# smooth normal offsets laterally when they are closer than `offset`
	outlines = surface.outlines_oriented()
	l = len(pnormals)
	normals = deepcopy(pnormals)
	for i in range(5):	# the number of steps is the diffusion distance through the mesh
		for a,b in outlines:
			d = surface.points[a] - surface.points[b]		# edge direction
			t = cross(pnormals[a]+pnormals[b], d)	# surface tangent normal to the edge
			# contribution stars when the offset points are closer than `offset`
			contrib = 1 - smoothstep(0, offset, length(offset*(pnormals[a]-pnormals[b])+d))
			normals[a] += contrib * 0.5*project(pnormals[b]-pnormals[a], t)
			normals[b] += contrib * 0.5*project(pnormals[a]-pnormals[b], t)
		# renormalize
		for i in range(l):
			pnormals[i] = normals[i] = normalize(normals[i])

	# compute offset length depending on the method
	if method == 'face':
		lengths = [inf]*len(pnormals)
		for face in surface.faces:
			fnormal = surface.facenormal(face)
			for p in face:
				lengths[p] = min(lengths[p], 1/dot(pnormals[p], fnormal))
		return typedlist((pnormals[p]*lengths[p]*offset   for p in range(len(pnormals))), dtype=vec3)

	elif method == 'edge':
		lengths = [inf]*len(pnormals)
		for edge,enormal in surface.edgenormals().items():
			for p in edge:
				lengths[p] = min(lengths[p], 1/dot(pnormals[p], enormal))
		return typedlist((pnormals[p]*lengths[p]*offset	for p in range(len(pnormals))), dtype=vec3)

	elif method == 'point':
		return typedlist((pnormals[p]*offset	for p in range(len(pnormals))), dtype=vec3)

inflate(surface, offset, method='face')

Move all points of the surface to make a new one at a certain distance of the last one

inflate result

Parameters:

Name Type Description Default
offset float

the distance from the surface to the offseted surface. its meaning depends on method

required
method

determines if the distance is from the old to the new faces, edges or points

'face'

Examples:

>>> sphere = pierce(
...         icosphere(O, 1),
...         brick(min=vec3(0), max=vec3(2)) .flip(),
...         )
>>> inflate(sphere, 0.1)
Source code in madcad/offseting.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def inflate(surface:Mesh, offset:float, method='face') -> 'Mesh':
	''' Move all points of the surface to make a new one at a certain distance of the last one

	![inflate result](../screenshots/offseting-inflate.png)

	Parameters:
		offset:       the distance from the surface to the offseted surface. its meaning depends on `method`
		method:       determines if the distance is from the old to the new faces, edges or points

	Examples:
		>>> sphere = pierce(
		...         icosphere(O, 1),
		...         brick(min=vec3(0), max=vec3(2)) .flip(),
		...         )
		>>> inflate(sphere, 0.1)
	'''
	return Mesh(
				typedlist((p+d  if isfinite(d) else p   for p,d in zip(surface.points, inflate_offsets(surface, offset, method))), dtype=vec3),
				surface.faces,
				surface.tracks,
				surface.groups)

thicken(surface, thickness, alignment=0, method='face')

Thicken a surface by extruding it, points displacements are made along normal.

thicken result

Parameters:

Name Type Description Default
thickness float

determines the distance between the two surfaces (can be negative to go the opposite direction to the normal).

required
alignment float

specifies which side is the given surface: 0 is for the first, 1 for the second side, 0.5 thicken all apart the given surface.

0
method

determines if the thickness is from the old to the new faces, edges or points

'face'

Examples:

>>> sphere = pierce(
...         icosphere(O, 1),
...         brick(min=vec3(0), max=vec3(2)) .flip(),
...         )
>>> thicken(sphere, 0.1)
Source code in madcad/offseting.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def thicken(surface: Mesh, thickness: float, alignment:float=0, method='face') -> 'Mesh':
	''' Thicken a surface by extruding it, points displacements are made along normal.

	![thicken result](../screenshots/offseting-thicken.png)

	Parameters:
		thickness:    determines the distance between the two surfaces (can be negative to go the opposite direction to the normal).
		alignment:    specifies which side is the given surface: 0 is for the first, 1 for the second side, 0.5 thicken all apart the given surface.
		method:       determines if the thickness is from the old to the new faces, edges or points

	Examples:
		>>> sphere = pierce(
		...         icosphere(O, 1),
		...         brick(min=vec3(0), max=vec3(2)) .flip(),
		...         )
		>>> thicken(sphere, 0.1)
	'''
	displts = inflate_offsets(surface, thickness, method)

	a = alignment
	b = alignment-1
	m = (	Mesh(
				typedlist((p+d*a  for p,d in zip(surface.points,displts)), dtype=vec3),
				surface.faces[:],
				surface.tracks[:],
				surface.groups)
		+	Mesh(
				typedlist((p+d*b  for p,d in zip(surface.points,displts)), dtype=vec3),
				surface.faces,
				surface.tracks,
				surface.groups[:]
				) .flip()
		)
	t = len(m.groups)
	l = len(surface.points)
	m.groups.append(None)
	for e in surface.outlines_oriented():
		mkquad(m, (e[0], e[1], e[1]+l, e[0]+l), t)
	return m

expand(surface, offset, collapse=True)

Generate a surface expanding the input mesh on the tangent of the ouline neighboring faces

expand result

Parameters:

Name Type Description Default
offset float

distance from the outline point to the expanded outline points

required
collapse

if True, expanded points leading to crossing edges will collapse into one

True

Examples:

>>> expand(
...     revolution(wire([vec3(1,1,0), vec3(0,1,0)]), Axis(O,X), pi),
...     0.5)
Source code in madcad/offseting.py
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
163
164
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def expand(surface: Mesh, offset: float, collapse=True) -> Mesh:
	''' Generate a surface expanding the input mesh on the tangent of the ouline neighboring faces

	![expand result](../screenshots/offseting-expand.png)

	Parameters:
		offset:		distance from the outline point to the expanded outline points
		collapse:	if True, expanded points leading to crossing edges will collapse into one

	Examples:
		>>> expand(
		...     revolution(wire([vec3(1,1,0), vec3(0,1,0)]), Axis(O,X), pi),
		...     0.5)
	'''
	# outline with associated face normals
	pts = surface.points
	edges = {}
	for face in surface.faces:
		for e in ((face[0], face[1]), (face[1], face[2]), (face[2],face[0])):
			if e in edges:	del edges[e]
			else:			edges[(e[1], e[0])] = surface.facenormal(face)

	# return the point on tangent for a couple of edges from the frontier
	def tangent(e0, e1):
		mid = axis_midpoint(
				(pts[e0[1]], pts[e0[0]] - pts[e0[1]]),
				(pts[e1[0]], pts[e1[1]] - pts[e1[0]]),
				)
		d0 = pts[e0[1]] - pts[e0[0]]
		d1 = pts[e1[1]] - pts[e1[0]]
		n0, n1 = edges[e0], edges[e1]
		t = normalize(cross(n0, n1) + NUMPREC*(d0*length(d1)-d1*length(d0)) + NUMPREC**2 * (cross(n0, d0) + cross(n1, d1)))
		if dot(t, cross(n0, d0)) < 0:
			t = -t
		return mid + t * offset

	# cross neighbooring normals
	for loop in suites(edges, cut=False):
		assert loop[-1] == loop[0],  "non-manifold input mesh"
		loop.pop()
		# compute the offsets, and remove anticipated overlapping geometries
		extended = [None]*len(loop)
		for i in range(len(loop)):
			# consecutive edges around i-1
			ei0 = (loop[i-2], loop[i-1])
			ei1 = (loop[i-1], loop[i])
			ti = tangent(ei0, ei1)
			if collapse:
				tk = deepcopy(ti)
				weight = 1
				# j is moving to find how much points to gather
				ej0 = ei0
				for j in reversed(range(i-len(loop)+1, i-1)):
					# consecutive edges aroung j
					ej0, ej1 = (loop[j-1], loop[j]), ej0
					tj = tangent(ej0, ej1)

					if dot(ti - tj, pts[ei1[0]] - pts[ej0[1]]) <= NUMPREC * length2(pts[ei1[0]] - pts[ej0[1]]):
						tk += tj
						weight += 1
					else:
						break
				# store tangents
				for k in range(j+1, i):
					extended[k] = tk/weight
			else:
				extended[i-1] = ti

		# insert the new points
		j = l = len(pts)
		g = len(surface.groups)
		surface.groups.append(None)
		for i in range(len(extended)):
			if extended[i] != extended[i-1]:
				pts.append(extended[i])

		# create the faces
		for i in range(len(extended)):
			if extended[i] != extended[i-1]:
				mkquad(surface, (loop[i-1], loop[i], j, (j if j > l else len(pts)) -1), g)
				j += 1
			else:
				mktri(surface, (loop[i-1], loop[i], (j if j > l else len(pts)) -1), g)

	return surface