Skip to content

scheme -- Annotation functionalities

This module provides annotation functions to quickly measure and show things on meshes

This is more to be considered as a rendering feature, rather than a proper measuring system. Everything is built on the class Scheme that provide a very versatile way to structure and render simple but animated schematics

Those functions are designed to be very simple to use, (sometimes even minimalistic)

    >>> mesh = brick(width=vec3(3,2,1))
    >>> show([
    ...     mesh,
    ...     note_distance_planes(mesh.group(4), mesh.group(2)),
    ...     note_leading(mesh.group(3), text='truc'),
    ...     ])

Schematics system

Scheme(vertices=None, spaces=None, primitives=None, **kwargs)

An object containing schematics.

This is a buffer object, it isnot intended to be useful to modify a scheme.

Attributes:

Name Type Description
spaces list

a space is any function giving a mat4 to position a point on the screen (openGL convensions as used)

vertices list

a vertex is a tuple

(space id, position, normal, color, layer, track, flags)

primitives list

list of buffers (of point indices, edges, triangles, depending on the exact primitive type), associaded to each supported shader in the scheem

currently supported shaders are:

  • 'line' uniform opaque/transparent lines
  • 'fill' uniform opaque/transparent triangles
  • 'ghost' triangles of surface that fade when its normal is close to the view
components list

objects to display setting their local space to one of the spaces

current dict

last vertex definition, implicitely reused for convenience

Source code in madcad/scheme.py
90
91
92
93
94
95
96
97
98
def __init__(self, vertices=None, spaces=None, primitives=None, **kwargs):
	self.vertices = vertices or [] # list of vertices
	self.spaces = spaces or []	# definition of each space
	self.primitives = primitives or {} # list of indices for each shader
	self.components = []	# displayables associated to spaces
	# for creation: last vertex inserted
	self.current = {'color':fvec4(settings.colors['annotation'],1), 'flags':0, 'layer':0, 'space':world, 'shader':'line', 'track':0, 'normal':fvec3(0)}
	self.continuous = False
	self.set(**kwargs)

set(*args, **kwargs)

Change the specified attributes in the current default vertex definition

Source code in madcad/scheme.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def set(self, *args, **kwargs):
	''' Change the specified attributes in the current default vertex definition '''
	if args:
		if len(args) == 1 and isinstance(args[0], dict):
			kwargs = args[0]
		else:
			raise TypeError('Scheme.set expects keywords argument or one unique dictionnary argument')
	self.current.update(kwargs)
	# register the space if not already known
	if not isinstance(self.current['space'], int):
		try:	i = self.spaces.index(self.current['space'])
		except ValueError:	
			i = len(self.spaces)
			self.spaces.append(self.current['space'])
		self.current['space'] = i
	if not isinstance(self.current['color'], fvec4):
		self.current['color'] = fvec4(self.current['color'])

	return self

add(obj, **kwargs)

Add an object to the scheme. If it is a mesh, it's merged in the current buffers. Else it is added as a component to the current space.

Source code in madcad/scheme.py
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
215
216
217
218
219
220
221
222
223
def add(self, obj, **kwargs):
	''' Add an object to the scheme.
		If it is a mesh, it's merged in the current buffers. 
		Else it is added as a component to the current space.
	'''
	self.set(kwargs)
	if self.current['shader'] not in self.primitives:
		self.primitives[self.current['shader']] = indices = []
	else:
		indices = self.primitives[self.current['shader']]
	l = len(self.vertices)
	continuing = False

	if isinstance(obj, (Mesh,Web)):
		self.vertices.extend([
							self.current['space'], 
							fvec3(p), 
							self.current['normal'], 
							self.current['color'], 
							self.current['layer'], 
							self.current['track'], 
							self.current['flags'],
						]  for p in obj.points)
	if isinstance(obj, Mesh):
		indices.extend(((a+l, b+l, c+l)  for a,b,c in obj.faces))
		for i,n in enumerate(obj.vertexnormals()):
			# prefer +inf to nan or -inf because the normal is supposed to be positive and nan is not comparable
			if not isfinite(n):
				n = vec3(inf)
			self.vertices[i+l][2] = n
	elif isinstance(obj, Web):
		indices.extend(((a+l, b+l)  for a,b in obj.edges))
	elif isinstance(obj, vec3):
		self.vertices.append([
							self.current['space'], 
							fvec3(obj), 
							self.current['normal'], 
							self.current['color'], 
							self.current['layer'], 
							self.current['track'], 
							self.current['flags'],
							])
		continuing = True
		if self.continuous:
			indices.append((l-1, l))

	elif hasattr(obj, '__iter__'):
		n = len(self.vertices)
		for obj in obj:
			if isinstance(obj, (fvec3, vec3)):
				self.vertices.append([
							self.current['space'], 
							fvec3(obj), 
							self.current['normal'], 
							self.current['color'], 
							self.current['layer'], 
							self.current['track'], 
							self.current['flags'],
							])
				n += 1
			else:
				self.add(obj)
		indices.extend((i,i+1)	for i in range(l, n-1))
	else:
		self.component(obj)

	self.continuous = continuing
	return self

component(obj, **kwargs)

Add an object as component associated to the current space

Source code in madcad/scheme.py
225
226
227
228
229
230
def component(self, obj, **kwargs):
	''' Add an object as component associated to the current space '''
	self.set(**kwargs)
	self.components.append((self.current['space'], obj))

	return self

__add__(other)

Return the union of the two schemes.

The current settings are those from first scheme

Source code in madcad/scheme.py
124
125
126
127
128
129
130
131
def __add__(self, other):
	''' Return the union of the two schemes.

		The current settings are those from first scheme
	'''
	r = deepcopy(self)
	r += other
	return r

halo_world(position) dataclass

position instance-attribute

__call__(view)

Source code in madcad/scheme.py
472
473
474
475
476
def __call__(self, view):
	center = view.uniforms['view'] * (view.uniforms['world'] * fvec4(self.position,1))
	m = fmat4(1)
	m[3] = center
	return m

halo_view(position) dataclass

position instance-attribute

__call__(view)

Source code in madcad/scheme.py
481
482
483
484
485
486
487
488
def __call__(self, view):
	center = view.uniforms['view'] * (view.uniforms['world'] * fvec4(self.position,1))
	proj = view.uniforms['proj']
	e = proj * fvec4(1,1,center.z,1)
	e /= e[3]
	m = fmat4(1/e[1])
	m[3] = center
	return m

halo_screen(position) dataclass

position instance-attribute

__call__(view)

Source code in madcad/scheme.py
493
494
495
496
497
498
499
def __call__(self, view):
	center = view.uniforms['view'] * (view.uniforms['world'] * fvec4(self.position,1))
	e = view.uniforms['proj'] * fvec4(1,1,center.z,1)
	e /= e[3]
	m = fmat4(2/(e[1]*view.target.height))
	m[3] = center
	return m

scale_screen(center) dataclass

center instance-attribute

__call__(view)

Source code in madcad/scheme.py
504
505
506
507
508
def __call__(self, view):
	m = view.uniforms['view'] * view.uniforms['world']
	e = view.uniforms['proj'] * fvec4(1,1,(m*self.center).z,1)
	e /= e[3]
	return m * translate(self.center) * scale(fvec3(2 / (e[1]*view.target.height)))

scale_view(center) dataclass

center instance-attribute

__call__(view)

Source code in madcad/scheme.py
513
514
515
516
517
def __call__(self, view):
	m = view.uniforms['view'] * view.uniforms['world']
	e = view.uniforms['proj'] * fvec4(1,1,(m*self.center).z,1)
	e /= e[3]
	return m * translate(self.center) * scale(fvec3(1 / e[1]))

Annotation functions

note_leading(placement, offset=None, text='here')

Place a leading note at given position

note_leading

placement can be any of Mesh, Web, Wire, axis, vec3

Source code in madcad/scheme.py
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
def note_leading(placement, offset=None, text='here'):
	''' Place a leading note at given position

		![note_leading](../screenshots/note_leading.png)

		`placement` can be any of `Mesh, Web, Wire, axis, vec3`
	'''
	origin, normal = mesh_placement(placement)
	if not offset:
		offset = 0.2 * length(boundingbox(placement).size) * normal
	elif isinstance(offset, (float,int)):
		offset = offset*normal
	elif not isinstance(offset, vec3):
		raise TypeError('offset must be scalar or vector')

	color = settings.colors['annotation']
	sch = Scheme(color=fvec4(color,0.7))
	sch.add([origin, origin+offset], shader='line', space=world)
	x,y,z = dirbase(normalize(vec3(offset)))
	sch.add(
		gt.revolution( 
			web([vec3(0), 16*z+4*x]), 
			Axis(vec3(0), z),
			resolution=('div',8),
			), 
		shader='fill',
		space=scale_screen(fvec3(origin)),
		)
	font = settings.display['view_font_size']
	sch.set(space=halo_screen_flipping(fvec3(origin+offset), fvec3(offset)))
	sch.add([vec3(0), vec3(font*2, 0, 0)], shader='line')
	sch.add(Displayable(TextDisplay, vec3(textsize(text)[0]/2*0.78*font + 2*font, 0, 0), text, align=('center', 0.5), color=fvec4(color,1), size=font))

	return sch

note_floating(position, text, align=(0, 0))

place a floating note at given position

note_floating

Source code in madcad/scheme.py
578
579
580
581
582
583
def note_floating(position, text, align=(0,0)):
	''' place a floating note at given position

		![note_floating](../screenshots/note_floating.png)
	'''
	return Displayable(TextDisplay, position, text, align=align, color=settings.colors['annotation'], size=settings.display['view_font_size'])

note_distance(a, b, offset=0, project=None, d=None, tol=None, text=None, side=False)

Place a distance quotation between 2 points, the distance can be evaluated along vector project if specified

note_distance

Source code in madcad/scheme.py
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
def note_distance(a, b, offset=0, project=None, d=None, tol=None, text=None, side=False):
	''' Place a distance quotation between 2 points,
		the distance can be evaluated along vector `project` if specified

		![note_distance](../screenshots/note_distance.png)
	'''
	# get text to display
	if not project:					project = normalize(b-a)
	elif dot(project, b-a) < 0:		project = -project
	if not d:	d = abs(dot(b-a, project))
	if not text:
		if isinstance(tol,str): text = '{d:.4g}  {tol}'
		elif tol:               text = '{d:.4g}  ± {tol}'
		else:                   text = '{d:.4g}'
	text = text.format(d=d, tol=tol)
	color = settings.colors['annotation']
	# convert input vectors
	x = dirbase(project, b-a)[0]
	z = project if side else -project
	if not isinstance(offset, vec3):
		offset = offset * x
	shift = 0.5 * mathutils.project(b-a, x)	if length2(x) else 0
	ao = a + offset + shift
	bo = b + offset - shift
	# create scheme
	sch = Scheme()
	sch.set(shader='line', layer=1e-4, color=fvec4(color,0.3))
	sch.add([a, ao])
	sch.add([b, bo])
	sch.set(layer=-1e-4, color=fvec4(color,0.7))
	sch.add([ao, bo])
	sch.add(Displayable(TextDisplay,
				mix(ao,bo,0.5), 
				text, 
				align=('center', 0.5), 
				size=settings.display['view_font_size'], 
				color=fvec4(color,1)))
	sch.set(shader='fill')
	sch.add(gt.revolution(
				web([vec3(0), 3*x-12*z]), 
				Axis(vec3(0),project), 
				resolution=('div',8)), 
			space=scale_screen(fvec3(ao)))
	sch.add(gt.revolution(
				web([vec3(0), 3*x+12*z]), 
				Axis(vec3(0),project), 
				resolution=('div',8)), 
			space=scale_screen(fvec3(bo)))
	return sch

note_distance_planes(s0, s1, offset=None, d=None, tol=None, text=None)

Place a distance quotation between 2 meshes

note_distance_planes

s0 and s1 can be any of Mesh, Web, Wire

Source code in madcad/scheme.py
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
def note_distance_planes(s0, s1, offset=None, d=None, tol=None, text=None):
	''' Place a distance quotation between 2 meshes

		![note_distance_planes](../screenshots/note_distance_planes.png)

		`s0` and `s1` can be any of `Mesh, Web, Wire`
	'''
	p0, n0 = mesh_placement(s0)
	p1, n1 = mesh_placement(s1)
	if length2(cross(n0,n1)) > 1e-10:
		raise ValueError('surfaces are not parallel')
	if not offset:
		box = boundingbox(s0,s1)
		offset = 0.2 * length(box.size) * normalize(noproject(p0+p1 - 2*box.center, n0))
	return note_distance(p0, p1, offset, n0, d, tol, text)

note_distance_set(s0, s1, offset=0, d=None, tol=None, text=None)

Place a distance quotation between 2 objects. This is the distance between the closest elements of both sets

note_distance_set

s0 and s1 can be any of Mesh, Web, Wire, vec3

Source code in madcad/scheme.py
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
def note_distance_set(s0, s1, offset=0, d=None, tol=None, text=None):
	''' Place a distance quotation between 2 objects. This is the distance between the closest elements of both sets

		![note_distance_set](../screenshots/note_distance_set.png)

		`s0` and `s1` can be any of `Mesh, Web, Wire, vec3`
	'''
	dist, p0, p1 = mesh_distance(s0, s1)
	if not d:	d = dist
	# take the point and the higher dimension primitive
	if not isinstance(p0,int):	s0,p0, s1,p1 = s1,p1, s0,p0
	p0 = s0.points[p0]
	pts = s1.points
	if isinstance(p1, tuple):
		if len(p1) == 2:	p1 = pts[p1[0]] + project(p0-pts[p1[0]], pts[p1[1]]-pts[p1[0]])
		elif len(p1) == 3:	p1 = p0 + project(pts[p1[0]]-p0, cross(pts[p1[1]]-pts[p1[0]], pts[p1[2]]-pts[p1[0]]))
	else:
		p1 = s1.points[p1]

	return note_distance(p0, p1, offset, None, d, tol, text)

note_bounds(obj)

Create dimension annotations on the boundingbox of an object

note_bounds

Source code in madcad/scheme.py
909
910
911
912
913
914
915
916
917
918
919
920
921
def note_bounds(obj):
	''' Create dimension annotations on the boundingbox of an object

		![note_bounds](../screenshots/note_bounds.png)
	'''
	box = boundingbox(obj)
	size = 0.05 * length(box.size)
	return [
		note_distance(box.min, vec3(box.max.x, box.min.y, box.min.z), offset=size*vec3(0,-1,-1)),
		note_distance(box.min, vec3(box.min.x, box.max.y, box.min.z), offset=size*vec3(-1,0,-1)),
		note_distance(box.min, vec3(box.min.x, box.min.y, box.max.z), offset=size*vec3(-1,-1,0)),
		box,
		]

note_radius(mesh, offset=None, d=None, tol=None, text=None, propagate=2)

Place a curvature radius quotation. This will be the minimal curvature radius observed in the mesh As a mesh is generally speaking an approximation of the desired shape, the radius may be approximative as well

note_radius

Source code in madcad/scheme.py
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
def note_radius(mesh, offset=None, d=None, tol=None, text=None, propagate=2):
	''' Place a curvature radius quotation. This will be the minimal curvature radius observed in the mesh
		As a mesh is generally speaking an approximation of the desired shape, the radius may be approximative as well

		![note_radius](../screenshots/note_radius.png)
	'''
	if isinstance(mesh, Mesh):
		normals = mesh.vertexnormals()
		radius, place = mesh_curvature_radius(mesh, normals=normals, propagate=propagate)
		normal = normals[place]
	elif isinstance(mesh, Web):
		conn = connpp(mesh.edges)
		radius, place = mesh_curvature_radius(mesh, conn=conn, propagate=propagate)
		normal = - normalize(sum(normalize(mesh.points[p]-mesh.points[place])  for p in conn[place]))
	elif isinstance(mesh, Wire):
		normals = mesh.vertexnormals()
		radius, place = mesh_curvature_radius(mesh, normals=normals, propagate=propagate)
		normal = normals[place]
	else:
		raise TypeError('input mesh must be Mesh, Web or Wire')
	if not offset:
		offset = 0.2 * length(boundingbox(mesh).size)
	if not isinstance(offset, vec3):
		offset = normal * offset
	if abs(dot(offset, normal))**2 < length2(place)*1e-4:
		offset += 0.2 * length(boundingbox(mesh).size) * normal

	if isinstance(place, tuple):
		place = sum(mesh.points[i]  for i in place) / len(place)
	else:
		place = mesh.points[place]

	arrowplace = place + noproject(offset, normal)
	note = note_leading((arrowplace,normal), offset=project(offset, normal), text=text or 'R {:.4g}'.format(radius))
	color = settings.colors['annotation']
	note.set(shader='line', layer=1e-4, color=fvec4(color,0.3), space=world)
	note.add([place, arrowplace])
	return note

note_angle(a0, a1, offset=0, d=None, tol=None, text=None, unit='deg', side=False)

Place an angle quotation between 2 axis

note_angle

Source code in madcad/scheme.py
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
def note_angle(a0, a1, offset=0, d=None, tol=None, text=None, unit='deg', side=False):
	''' Place an angle quotation between 2 axis

		![note_angle](../screenshots/note_angle.png)
	'''
	o0, d0 = a0
	o1, d1 = a1
	z = normalize(cross(d0,d1))
	if not isfinite(z):	
		raise ValueError('axis are parallel')
	x0 = cross(d0,z) * (1 if side else -1)
	x1 = cross(d1,z) * (1 if side else -1)
	shift = project(o1-o0, z) * 0.5
	# add it but in a new copy of the vectors
	o0 = o0 + shift
	o1 = o1 + shift
	# get text to display
	if not d:	d = anglebt(d0, d1)
	if unit == 'deg':
		d *= 180/pi
		unit = '°'
	if not text:
		if isinstance(tol,str): text = '{d:.4g}{unit}  {tol}'
		elif tol:               text = '{d:.4g}{unit}  ± {tol}'
		else:                   text = '{d:.4g}{unit}'
	text = text.format(d=d, tol=tol, unit=unit)
	color = settings.colors['annotation']
	# arc center
	if o1 == o0:	center = o0
	else:			center = o0 + unproject(project(o1-o0, x1), d0)
	radius = mix(distance(o0,center), distance(o1,center), 0.5) + offset
	if not radius:	radius = 1
	# arc extremities
	p0 = center+radius*d0
	p1 = center+radius*d1
	sch = Scheme()
	sch.set(shader='line', layer=1e-4, color=fvec4(color,0.3))
	sch.add([p0, o0])
	sch.add([p1, o1])
	arc = ArcCentered((center,z), p0, p1, ('rad',0.05)).mesh()
	sch.add(arc, color=fvec4(color,0.7))
	sch.set(layer=-1e-4)
	sch.add(Displayable(TextDisplay,
				arc[len(arc)//2], 
				text, 
				align=('center', 0.5), 
				size=settings.display['view_font_size'], 
				color=fvec4(color,1)))
	sch.set(shader='fill')
	sch.add(gt.revolution(
				web([vec3(0), 3*d0+12*x0]), 
				Axis(vec3(0),x0), 
				resolution=('div',8)), 
			space=scale_screen(fvec3(p0)))
	sch.add(gt.revolution(
				web([vec3(0), 3*d1-12*x1]), 
				Axis(vec3(0),x1), 
				resolution=('div',8)), 
			space=scale_screen(fvec3(p1)))
	return sch

note_angle_planes(s0, s1, offset=0, d=None, tol=None, text=None, unit='deg')

Place an angle quotation between 2 meshes considered to be plane (surface) or straight (curve)

s0 and s1 can be any of Mesh, Web, Wire, Axis

Source code in madcad/scheme.py
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
def note_angle_planes(s0, s1, offset=0, d=None, tol=None, text=None, unit='deg'):
	''' Place an angle quotation between 2 meshes considered to be plane (surface) or straight (curve) 

		`s0` and `s1` can be any of `Mesh, Web, Wire, Axis`
	'''
	d0, d1 = _mesh_direction(s0), _mesh_direction(s1)
	z = normalize(cross(d0, d1))
	if not isfinite(z):	
		raise ValueError('planes are parallel')
	if isinstance(s0, Mesh) or isaxis(s0):	d0 = cross(d0,z)
	if isinstance(s1, Mesh) or isaxis(s1):	d1 = cross(z,d1)
	return note_angle(
				(mesh_placement(s0)[0], d0), 
				(mesh_placement(s1)[0], d1), 
				offset, d, tol, text, unit)

note_angle_edge(part, edge, offset=0, d=None, tol=None, text=None, unit='deg')

Place an angle quotation around a mesh edge

note_angle_edge

Source code in madcad/scheme.py
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
def note_angle_edge(part, edge, offset=0, d=None, tol=None, text=None, unit='deg'):
	''' Place an angle quotation around a mesh edge

		![note_angle_edge](../screenshots/note_angle_edge.png)
	'''
	f0 = None
	f1 = None
	for face in part.faces:
		for i in range(3):
			if face[i-1] == edge[0] and face[i] == edge[1]:	
				f0 = face
			elif face[i-1] == edge[1] and face[i] == edge[0]:
				f1 = face
	if not f0 or not f1:
		raise ValueError("edge {} doesn't exist or is not between 2 faces".format(f0))
	d0 = part.facenormal(f0)
	d1 = part.facenormal(f1)
	z = normalize(cross(d0,d1))
	o = mix(part.points[edge[0]], part.points[edge[1]], 0.5)
	if not offset:	offset = 0.2 * length(boundingbox(part).size)
	return note_angle(
			(o, cross(z,d0)),
			(o, cross(d1,z)),
			offset, d, tol, text, unit)

note_label(placement, offset=None, text='!', style='rect')

Place a text label upon an object

note_label

placement can be any of Mesh, Web, Wire, axis, vec3

Source code in madcad/scheme.py
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
def note_label(placement, offset=None, text='!', style='rect'):
	''' Place a text label upon an object

		![note_label](../screenshots/note_label.png)

		`placement` can be any of `Mesh, Web, Wire, axis, vec3`
	'''
	p, normal = mesh_placement(placement)
	if not offset:
		offset = 0.2 * length(boundingbox(placement).size) * normal
	elif isinstance(offset, (float,int)):
		offset = offset*normal
	elif not isinstance(offset, vec3):
		raise TypeError('offset must be scalar or vector')

	color = settings.colors['annotation']
	x,_,z = dirbase(normalize(offset))
	sch = Scheme()
	sch.set(color=fvec4(color,0.7))
	sch.add(gt.revolution(
				web([6*x, 10*z]),
				Axis(vec3(0),z),
				resolution=('div',8),
				),
			space=scale_screen(fvec3(p)), shader='fill')
	sch.add([p, p+offset], space=world, shader='line', layer=2e-4)
	font = int(settings.display['view_font_size'] * 1.2)
	r = font*0.8
	print(font, r)
	if style == 'circle':	outline = web(Circle((vec3(0),vec3(0,0,1)), r))
	elif style == 'rect':	outline = [vec3(r,r,0), vec3(-r,r,0), vec3(-r,-r,0), vec3(r,-r,0), vec3(r,r,0)]
	else:
		raise ValueError("style must be 'rect' or 'circle'")
	sch.set(space=halo_screen(fvec3(p+offset)))
	sch.add(outline, shader='line', layer=0)
	sch.add(gt.fill(wire(outline)), color=fvec4(settings.display['background_color'],0), shader='fill', layer=1e-3)
	sch.add(Displayable(TextDisplay,
				p+offset, 
				text, 
				align=('center','center'), 
				size=font, 
				color=color), 
			space=world)
	return sch

Measuring tools

mesh_curvature_radius(mesh, conn=None, normals=None, propagate=2)

Find the minimum curvature radius of a mesh.

Parameters:

Name Type Description Default
mesh

the surface/line to search

required
conn

a point-to-point connectivity (computed if not provided)

None
normals

the vertex normals (computed if not provided)

None
propagate int

the maximum propagation rank for points to pick for the regression

2

Return: (distance: float, point: int) where primitives varies according to the input mesh dimension

Source code in madcad/scheme.py
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
def mesh_curvature_radius(mesh, conn=None, normals=None, propagate=2) -> '(distance, point)':
	''' Find the minimum curvature radius of a mesh.

	Parameters:
		mesh:			the surface/line to search
		conn:			a point-to-point connectivity (computed if not provided)
		normals:		the vertex normals (computed if not provided)
		propagate(int):	the maximum propagation rank for points to pick for the regression

	Return:	`(distance: float, point: int)` where primitives varies according to the input mesh dimension
	'''

	curvatures = mesh_curvatures(mesh, conn, normals, propagate)

	place = min(range(len(mesh.points)),
				key=lambda p: 1/np.max(np.abs(curvatures[p][0]))  if curvatures[p] else inf, 
				default=None)
	return 1/np.max(np.abs(curvatures[place][0])), place

mesh_curvatures(mesh, conn=None, normals=None, propagate=2)

Compute the curvature around a point in a mesh/web/wire

Parameters:

Name Type Description Default
mesh

the surface/line to search

required
conn

a point-to-point connectivity (computed if not provided)

None
normals

the vertex normals (computed if not provided)

None
propagate int

the maximum propagation rank for points to pick for the regression

2
Return

[(tuple, mat3)] where the tuple contains the curvature in each of the column directions in the mat3. The mat3 has the principal directions of curvature

Source code in madcad/scheme.py
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
def mesh_curvatures(mesh, conn=None, normals=None, propagate=2):
	''' Compute the curvature around a point in a mesh/web/wire

	Parameters:
		mesh:			the surface/line to search
		conn:			a point-to-point connectivity (computed if not provided)
		normals:		the vertex normals (computed if not provided)
		propagate(int):	the maximum propagation rank for points to pick for the regression

	Return:
		`[(tuple, mat3)]`
		where the `tuple` contains the curvature in each of the column directions in the `mat3`. The `mat3` has the principal directions of curvature
	'''
	pts = mesh.points

	# propagate though the mesh and return seen points
	def propagate_pp(conn, start, maxrank):
		front = deque((0,s) for s in start)
		seen = set()
		while front:
			rank, p = front.popleft()
			if p in seen:	continue
			seen.add(p)
			if rank < maxrank:
				for n in conn[p]:
					if n not in seen:	
						front.append((rank+1, n))
		return seen

	if isinstance(mesh, Mesh):		
		if not conn:	conn = connpp(mesh.faces)
		if not normals:	normals = mesh.vertexnormals()
		it = ( (p, propagate_pp(conn, [p], propagate))   for p in conn )
	elif isinstance(mesh, Web):	
		if not conn:	conn = connpp(mesh.edges)
		if not normals:	
			normals = [vec3(0)  for p in mesh.points]
			for e in mesh.edges:
				d = pts[e[0]] - pts[e[1]]
				normals[e[0]] += d
				normals[e[1]] -= d
			for i,n in enumerate(normals):
				normals[i] = normalize(n)
		it = ( (p, propagate_pp(conn, [p], propagate))   for p in conn )
	elif isinstance(mesh, Wire):
		if not normals:	normals = mesh.vertexnormals()
		it = ( (mesh.indices[i], mesh.indices[i-propagate:i+propagate])   for i in range(len(mesh.indices)) )
	else:
		raise TypeError('bad input shape type')

	curvatures = [None]*len(pts)

	for p, neigh in it:
		# decide a local coordinate system
		u,v,w = dirbase(normals[p])
		# get neighboors contributions
		b = np.empty(len(neigh))
		a = np.empty((len(neigh), 14))
		for i,n in enumerate(neigh):
			e = pts[n] - pts[p]
			b[i] = dot(e,w)
			eu = dot(e,u)
			ev = dot(e,v)
			# these are the monoms to compose to build a polynom approximating the surface until 4th-order derivatives
			a[i] = (	eu**2, ev**2, eu*ev,  # what we want to get
						# the others terms are only present to catch the surface regularities and not disturb the curvature terms
						eu, ev,
						eu**3, eu**2*ev, eu*ev**2, ev**3,
						eu**4, eu**3*ev, eu**2*ev**2, eu*ev**3, ev**4,
						)

		# least squares resulution, the complexity is roughly the same as inverting a mat3
		(au, av, auv, *_), residuals, *_ = np.linalg.lstsq(a, b, rcond=None)
		# diagonalize the curve tensor to get the principal curvatures
		diag, transfer = np.linalg.eigh(mat2(2*au, auv, 
												auv, 2*av))
		curvatures[p] = diag, mat3(u,v,w) * mat3(mat2(transfer))

	return curvatures