# This file is part of pymadcad, distributed under license LGPL v3
''' 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'),
... ])
'''
import moderngl as mgl
import numpy.core as np
import glm
from operator import itemgetter
from collections import deque
from .mathutils import *
from .rendering import Display
from .common import resourcedir
from .mesh import Mesh, Web, Wire, web, wire, mesh_distance, connef, connpe, edgekey, arrangeface, arrangeedge
from .rendering import Displayable, writeproperty, overrides
from .text import TextDisplay, textsize
from .primitives import *
from . import mathutils
from . import generation as gt
from . import settings
__all__ = ['Scheme',
'note_leading',
'note_floating',
'note_label',
'note_distance',
'note_angle',
'note_distance_planes',
'note_distance_set',
'note_angle_planes',
'note_angle_edge',
'note_radius',
'note_bounds',
]
[docs]class Scheme:
''' An object containing schematics.
This is a buffer object, it isnot intended to be useful to modify a scheme.
Attributes:
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
'''
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.display['annotation_color'],1), 'flags':0, 'layer':0, 'space':world, 'shader':'wire', 'track':0, 'normal':fvec3(0)}
self.set(**kwargs)
def __iadd__(self, other):
''' Concatenante the content of an other scheme in the current one
the current settings stay as before concatenation
'''
ls = len(self.spaces)
lv = len(self.vertices)
lt = max(self.tracks)+1
self.spaces.extend(other.spaces)
self.components.extend(other.components)
self.vertices.extend([
v[0] + ls,
*v[1:5],
v[5] + lt,
v[6],
] for v in other.vertices)
for shader, prims in other.primitives:
if shader not in self.primitives:
self.primitives[shader] = []
self.primitives[shader].extend(tuple(i+lv for i in p) for p in prims)
return self
[docs] 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
[docs] 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
[docs] 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)
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 f, track in zip(obj.faces, obj.tracks):
for p in f:
self.vertices[p+l][5] = track
for i,n in enumerate(obj.vertexnormals()):
self.vertices[i+l][2] = n
elif isinstance(obj, Web):
indices.extend(((a+l, b+l) for a,b in obj.edges))
for e, track in zip(obj.edges, obj.tracks):
for p in e:
self.vertices[p+l][5] = track
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)
return self
[docs] 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
class display(Display):
''' Display for schemes
Attributes:
:spaces: numpy array of matrices for each space, sent as uniform to the shader
:vb_vertices: vertex buffer for vertices
:vas: vertex array associated to each shader
'''
max_spaces = 32 # this maximum size of the spaces array must match the size in the shader
def __init__(self, scene, sch):
ctx = scene.ctx
# load the resources
self.shaders, self.shader_ident = scene.resource('scheme', self.load)
# switch to array indexed spaces
self.spaces = typedlist.full(fmat4(0), self.max_spaces)
self.spacegens = list(sch.spaces)
if len(self.spacegens) > self.max_spaces:
print('warning: the number of local spaces exceeds the arbitrary build-in limit of {}'.format(self.max_spaces))
self.components = [(space,scene.display(obj)) for space,obj in sch.components]
self.nidents = max(v[5] for v in sch.vertices)+1
self.box = boundingbox(
(fvec3(v[1]) for v in sch.vertices if self.spacegens[v[0]] is world),
default=Box(center=fvec3(0), width=fvec3(-inf)),
)
# prepare the buffer of vertices
vertices = np.empty(len(sch.vertices), 'u1, 3f4, 3f4, 4u1, f4, u2, u1')
for i,v in enumerate(sch.vertices):
vertices[i] = (
*v[:3],
u8vec4(v[3]*255),
*v[4:]
)
self.vb_vertices = ctx.buffer(vertices)
verticesdef = [(self.vb_vertices, 'u1 3f4 3f4 4f1 f4 u2 u1',
'space',
'v_position',
'v_normal',
'v_color',
'v_layer',
'v_ident',
'v_flags')]
# prepare the rending commands
ident_triangles = []
ident_lines = []
self.vas = {}
self.vai_triangles = None
self.vai_lines = None
for shname,batch in sch.primitives.items():
if not batch: continue
if shname not in self.shaders: raise KeyError('no shader for name {}'.format(repr(shname)))
prim, shader = self.shaders[shname]
vb_indices = ctx.buffer(np.array(batch, 'u4'))
self.vas[shname] = (prim, ctx.vertex_array(shader, verticesdef, vb_indices, skip_errors=True))
if prim == mgl.LINES: ident_lines.extend(batch)
elif prim == mgl.TRIANGLES: ident_triangles.extend(batch)
if ident_triangles: self.vai_triangles = ctx.vertex_array(self.shader_ident, verticesdef, ctx.buffer(np.array(ident_triangles, 'u4')), skip_errors=True)
if ident_lines: self.vai_lines = ctx.vertex_array(self.shader_ident, verticesdef, ctx.buffer(np.array(ident_lines, 'u4')), skip_errors=True)
def __del__(self):
for prim, va in self.vas.values():
va.release()
self.vas.clear()
if self.vai_triangles:
self.vai_triangles.release()
if self.vai_lines:
self.vai_lines.release()
if self.vb_vertices:
self.vb_vertices.release()
self.vb_vertices = None
def load(self, scene):
''' Load shaders and all static data for the current OpenGL context '''
vert = open(resourcedir+'/shaders/scheme.vert').read()
shader_ident = scene.ctx.program(
vertex_shader=vert,
fragment_shader=open(resourcedir+'/shaders/scheme-ident.frag').read(),
)
shaders = {
'line': (mgl.LINES, scene.ctx.program(
vertex_shader=vert,
fragment_shader=open(resourcedir+'/shaders/scheme-uniform.frag').read(),
)),
'fill': (mgl.TRIANGLES, scene.ctx.program(
vertex_shader=vert,
fragment_shader=open(resourcedir+'/shaders/scheme-uniform.frag').read(),
)),
'ghost': (mgl.TRIANGLES, scene.ctx.program(
vertex_shader=vert,
fragment_shader=open(resourcedir+'/shaders/scheme-ghost.frag').read(),
)),
}
return shaders, shader_ident
def compute_spaces(self, view):
''' Compute the new spaces for this frame.
This is meant to be overriden when new spaces are required
'''
view.uniforms['world'] = self.world
for i,gen in enumerate(self.spacegens):
self.spaces[i] = gen(view)
if self.components:
invview = affineInverse(view.uniforms['view'])
for space,disp in self.components:
disp.world = invview * self.spaces[space]
def render(self, view):
''' Render each va in self.vas '''
#self.compute_spaces(view)
for name in self.vas:
shader = self.shaders[name][1]
prim, va = self.vas[name]
shader['spaces'].write(self.spaces)
shader['proj'].write(view.uniforms['proj'])
shader['highlight'].write( fvec4(fvec3(settings.display['select_color_line']), 0.5) if self.selected else fvec4(0) )
va.render(prim)
def identify(self, view):
''' Render all the triangles and lines for identification '''
self.shader_ident['startident'] = view.identstep(self.nidents)
self.shader_ident['spaces'].write(self.spaces)
self.shader_ident['proj'].write(view.uniforms['proj'])
if self.vai_lines: self.vai_lines.render(mgl.LINES)
if self.vai_triangles: self.vai_triangles.render(mgl.TRIANGLES)
def stack(self, scene):
yield ((), 'screen', -1, self.compute_spaces)
yield ((), 'screen', 2, self.render)
yield ((), 'ident', 2, self.identify)
for space,disp in self.components:
yield from disp.stack(scene)
class SchemeInstance:
def __init__(self, name, scheme, *kwargs):
self.name = name
self.scheme = scheme
vars(self).update(kwargs)
class display(Scheme.display):
def __init__(self, scene, instance):
self.instance = instance
disp = scene.resource(id(instance.scheme), lambda: inst.scheme.display(scene, instance.scheme))
vars(self).update(vars(disp))
self.spaces = deepcopy(disp.spaces)
def compute_spaces(self, view):
''' Computes the new spaces for this frame.
This is meant to be overriden when new spaces are required
'''
view.uniforms['world'] = self.world
for i,gen in enumerate(self.spacegens):
self.spaces[i] = gen(view, self.instance)
if self.components:
invview = affineInverse(view.uniforms['view'])
for space,disp in self.components:
disp.world = invview * self.spaces[space]
# create standard spaces
[docs]def view(view):
proj = view.uniforms['proj']
return fmat4(1/proj[0][0], 0,0,0,
0, 1/proj[1][1], 0,0,
0,0,1,0,
0,0,-1,1)
[docs]def screen(view):
return fmat4(view.target.width/2,0,0,0,
0,view.target.height/2,0,0,
0,0,1,0,
0,0,-1,1)
def ubiquity(view):
#return translate(fvec3(0,0,-1)) * fmat4(fmat3(view.uniforms['view']) * fmat3(view.uniforms['world']))
proj = view.uniforms['proj']
f = - (proj[3][2]-proj[3][3]) / (proj[2][2] - proj[2][3])
n = - (proj[3][2]+proj[3][3]) / (proj[2][2] + proj[2][3])
d = 3*n
e = proj * fvec4(1,1,d,1)
e /= e[3]
return translate(fvec3(0,0,d)) * fmat4(
fmat3(1/e[1])
* fmat3(view.uniforms['view'])
* fmat3(view.uniforms['world']))
[docs]def world(view):
return view.uniforms['view'] * view.uniforms['world']
[docs]def halo_world(position):
position = fvec4(position,1)
def mat(view):
center = view.uniforms['view'] * (view.uniforms['world'] * position)
m = fmat4(1)
m[3] = center
return m
return mat
[docs]def halo_view(position):
position = fvec4(position,1)
def mat(view):
center = view.uniforms['view'] * (view.uniforms['world'] * position)
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
return mat
[docs]def halo_screen(position):
position = fvec4(position,1)
def mat(view):
center = view.uniforms['view'] * (view.uniforms['world'] * position)
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
return mat
[docs]def scale_screen(center):
def mat(view):
m = view.uniforms['view'] * view.uniforms['world']
e = view.uniforms['proj'] * fvec4(1,1,(m*center).z,1)
e /= e[3]
return m * translate(center) * scale(fvec3(2 / (e[1]*view.target.height)))
return mat
[docs]def scale_view(center):
def mat(view):
m = view.uniforms['view'] * view.uniforms['world']
e = view.uniforms['proj'] * fvec4(1,1,(m*center).z,1)
e /= e[3]
return m * translate(center) * scale(fvec3(1 / e[1]))
return mat
def halo_screen_flipping(position, offset):
position = fvec4(position,1)
def mat(view):
numprec = 1e-7
center = view.uniforms['view'] * (view.uniforms['world'] * position)
e = view.uniforms['proj'] * fvec4(1,1,center.z,1)
e /= e[3]
screen_center = view.uniforms['proj'] * center
screen_center /= screen_center[3]
screen_tip = view.uniforms['proj'] * (view.uniforms['view'] * (view.uniforms['world'] * fvec4(offset, 0)))
screen_tip /= -abs(screen_tip[3]) - numprec
if screen_tip[0] <= screen_center[0] + numprec:
flip = fvec4(1,1,1,1)
else:
flip = fvec4(-1,1,1,1)
m = fmat4(*(flip * 2/(e[1]*view.target.height)))
m[3] = center
return m
return mat
def mesh_placement(mesh) -> '(pos,normal)':
''' Return an axis for placement of a note on the given object '''
if isinstance(mesh, Mesh):
# group center normal
center = mesh.barycenter()
f = min(mesh.faces,
key=lambda f: distance2(center, sum(mesh.facepoints(f))/3)
)
normal = mesh.facenormal(f)
pos = sum(mesh.facepoints(f)) / 3
elif isinstance(mesh, Web):
center = mesh.barycenter()
e = min(mesh.edges,
key=lambda e: distance2(center, (mesh.points[e[0]]+mesh.points[e[1]])/2)
)
normal = dirbase(normalize(mesh.points[e[0]]-mesh.points[e[1]]))[0]
pos = mix(mesh.points[e[0]], mesh.points[e[1]], 0.5)
elif isinstance(mesh, Wire):
return mesh_placement(web(mesh))
elif isaxis(mesh):
pos, normal = mesh
elif isinstance(mesh, vec3):
normal = Z
pos = mesh
elif hasattr(mesh, 'mesh'):
return mesh_placement(mesh.mesh())
else:
raise TypeError('unable to place note on a {}'.format(type(mesh)))
return pos, normal
[docs]def note_floating(position, text, align=(0,0)):
''' place a floating note at given position '''
return Displayable(TextDisplay, position, text, align=align, color=settings.display['annotation_color'], size=settings.display['view_font_size'])
[docs]def note_leading(placement, offset=None, text='here'):
''' Place a leading note at given position
`placement` can be any of `Mesh, Web, Wire, axis, vec3`
'''
origin, normal = mesh_placement(placement)
if not offset:
offset = 0.2 * length(boundingbox(placement).width) * normal
elif isinstance(offset, (float,int)):
offset = offset*normal
elif not isinstance(offset, vec3):
raise TypeError('offset must be scalar or vector')
color = settings.display['annotation_color']
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(2*pi, (vec3(0), z),
web([vec3(0), 16*z+4*x]),
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
[docs]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
'''
# 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.display['annotation_color']
# 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(
2*pi,
(vec3(0),project),
web([vec3(0), 3*x-12*z]),
resolution=('div',8)),
space=scale_screen(fvec3(ao)))
sch.add(gt.revolution(
2*pi,
(vec3(0),project),
web([vec3(0), 3*x+12*z]),
resolution=('div',8)),
space=scale_screen(fvec3(bo)))
return sch
[docs]def note_distance_planes(s0, s1, offset=None, d=None, tol=None, text=None):
''' Place a distance quotation between 2 meshes
`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.width) * normalize(noproject(p0+p1 - 2*box.center, n0))
return note_distance(p0, p1, offset, n0, d, tol, text)
[docs]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
`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)
[docs]def note_angle(a0, a1, offset=0, d=None, tol=None, text=None, unit='deg', side=False):
''' Place an angle quotation between 2 axis '''
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.display['annotation_color']
# 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(
2*pi,
(vec3(0),x0),
web([vec3(0), 3*d0+12*x0]),
resolution=('div',8)),
space=scale_screen(fvec3(p0)))
sch.add(gt.revolution(
2*pi,
(vec3(0),x1),
web([vec3(0), 3*d1-12*x1]),
resolution=('div',8)),
space=scale_screen(fvec3(p1)))
return sch
[docs]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
'''
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).width)
if not isinstance(offset, vec3):
offset = normal * offset
if abs(dot(offset, normal))**2 < length2(place)*1e-4:
offset += 0.2 * length(boundingbox(mesh).width) * 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.display['annotation_color']
note.set(shader='line', layer=1e-4, color=fvec4(color,0.3), space=world)
note.add([place, arrowplace])
return note
import numpy.linalg
import numpy as np
from .mesh import connpp
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
'''
def propagate_pp(conn, start, maxrank):
front = [(0,s) for s in start]
seen = set()
while front:
rank, p = front.pop()
if p in seen: continue
seen.add(p)
yield p
if rank < maxrank:
for n in conn[p]:
if n not in seen:
front.append((rank+1, n))
if isinstance(mesh, Mesh):
if not conn: conn = connpp(mesh.faces)
if not normals: normals = mesh.vertexnormals()
it = ( (p, list(propagate_pp(conn, [p], propagate))) for p in connpp )
elif isinstance(mesh, Web):
if not conn: conn = connpp(mesh.edges)
if not normals:
normals = [vec3(0) for p in mesh.points]
pts = 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, list(propagate_pp(conn, [p], propagate))) for p in connpp )
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 type')
def analyse():
pts = mesh.points
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,
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)
# diagonalize the curve tensor to get the principal curvatures
diag, transfer = np.linalg.eigh(mat2(2*au, auv,
auv, 2*av))
yield 1/np.max(np.abs(diag)), p
return min(analyse(), key=itemgetter(0), default=None)
[docs]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
[docs]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
[docs]def note_bounds(obj):
''' Create dimension annotations on the boundingbox of an object '''
box = boundingbox(obj)
size = 0.05 * length(box.width)
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,
]
def _mesh_direction(mesh):
if isinstance(mesh, Mesh): return mesh.facenormal(0)
elif isinstance(mesh, Web): return mesh.edgedirection(0)
elif isinstance(mesh, Wire): return mesh[1]-mesh[0]
elif isaxis(mesh): return mesh[1]
else:
raise TypeError('only Mesh and Web are supported')
[docs]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)
[docs]def note_angle_edge(part, edge, offset=0, d=None, tol=None, text=None, unit='deg'):
''' Place an angle quotation around a mesh edge '''
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).width)
return note_angle(
(o, cross(z,d0)),
(o, cross(d1,z)),
offset, d, tol, text, unit)
def note_absciss(axis, pts):
indev
def note_diameter(mesh, direction=None, offset=None, d=None, tol=None, text=None):
assert direction is not None
for f in mesh.faces:
for p in f:
if dot(mesh.points[p], direction):
indev
def note_surface(placement, offset=None, roughness=None, method=None):
indev
[docs]def note_label(placement, offset=None, text='!', style='rect'):
''' Place a text label upon an object
`placement` can be any of `Mesh, Web, Wire, axis, vec3`
'''
p, normal = mesh_placement(placement)
if not offset:
offset = 0.2 * length(boundingbox(placement).width) * normal
elif isinstance(offset, (float,int)):
offset = offset*normal
elif not isinstance(offset, vec3):
raise TypeError('offset must be scalar or vector')
color = settings.display['annotation_color']
x,_,z = dirbase(normalize(offset))
sch = Scheme()
sch.set(color=fvec4(color,0.7))
sch.add(gt.revolution(
2*pi,
(vec3(0),z),
web([6*x, 10*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.flatsurface(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
def note_iso(p, offset, type, text, refs=(), label=None):
indev
#from .scheme import SchemeInstance
#class BaseDisplay(Display):
#scheme = None
#def __init__(self, scene, matrix, color=None):
#if not color: color = settings.display['annotation_color']
#if not self.scheme:
#type(self).scheme = sch = Scheme(layer=1e-4)
#space = lambda view, instance: world(view) * instance.matrix
#directions = [vec3(1,0,0), vec3(0,1,0), vec3(0,0,1)]
#for i in range(len(directions)):
#o = vec3(0)
#x,z = directions[i-1], directions[i]
#def tip(view, instance, z=z):
#s = space(view, instance)
#return scale_screen(s*z) * fmat4(fmat3(s))
#sch.add([o, z],
#space=tip,
#color=fvec4(color,1),
#shader='line',
#)
#sch.add(revolution(2*pi, (o,z), web([o, -20*z+9*x])),
#space=tip,
#color=fvec4(color,0.8),
#shader='fill',
#)
#if isinstance(matrix, (dmat3,fmat3,dquat,fquat)): matrix = fmat4(fmat3(matrix))
#elif isinstance(matrix, fmat4): matrix = fmat4(matrix)
#self.disp = scene.display(SchemeInstance(self.scheme, matrix=matrix))
#def stack(self, scene):
#yield from self.disp.stack(scene)
def note_base(matrix, color=None, labels=('X', 'Y', 'Z'), name=None, size=0.4):
if not color: color = settings.display['annotation_color']
if not name: name = ''
if not labels: labels = [None]*3
if isinstance(matrix, (dmat3,fmat3)):
center = None
directions = mat3(matrix)
elif isinstance(matrix, (dmat4,fmat4)):
if transpose(matrix)[3] != vec4(0,0,0,1):
raise TypeError('mat4 must be affine in order to be displayed')
center = vec3(matrix[3])
directions = mat3(mat4(matrix))
else:
raise TypeError('only matrix with dimension 3 or 4 can be displayed')
sch = Scheme(layer=1e-4)
for axislabel, direction in zip(labels, directions):
o = vec3(0)
x,y,z = dirbase(normalize(direction))
if center:
sch.add([center], space=world)
sch.add([o, size*direction],
space=scale_view(fvec3(center)) if center else ubiquity,
color=fvec4(color,1),
shader='line',
)
sch.add(gt.revolution(2*pi, (o,z), web([o, -14*z+3.6*x])),
space=scale_screen_scale_view(fvec3(center), fvec3(size*direction)) if center else scale_screen_ubiquity(fvec3(size*direction)),
color=fvec4(color,0.8),
shader='fill',
)
if axislabel or name:
sch.add(Displayable(TextDisplay, 5*z, axislabel+' '+name, align=(0.5,0.5), color=color))
return sch
def note_rotation(quaternion, color=None, size=0.4):
if not color: color = settings.display['annotation_color']
direction = vec3(glm.axis(quaternion))
angle = glm.angle(quaternion)
o = vec3(0)
x,y,z = dirbase(direction)
tend = -sin(angle)*x + cos(angle)*y
rend = cos(angle)*x + sin(angle)*y
sch = Scheme(layer=1e-4)
sch.set(
space=ubiquity,
color=fvec4(color,0.6),
shader='line',
)
sch.add([-size*direction, -0.2*size*direction])
sch.add([-0.1*size*direction, 0.1*size*direction])
sch.add([0.2*size*direction, size*direction])
sch.add(gt.revolution(2*pi, (o,z), web([o, -14*z+3.6*x])),
space=scale_screen_ubiquity(fvec3(size*direction)),
color=fvec4(color,0.6),
shader='fill',
)
sch.set(
space=ubiquity,
color=fvec4(color,1),
shader='line',
)
sch.add([size*cos(t)*x + size*sin(t)*y for t in linrange(0, angle, step=0.1)])
sch.add([size*cos(t)*x + size*sin(t)*y for t in linrange(0, 2*pi, step=0.1)], color=fvec4(color,0.2))
sch.add(gt.revolution(2*pi, (size*rend, tend), web([size*tend, size*tend-14*tend+3.6*rend])),
space=scale_screen_ubiquity(fvec3(size*rend)),
color=fvec4(color,0.8),
shader='fill',
)
return sch
'''
transforms = [
mat3(),
angleAxis(pi/2, normalize(Z+X)),
translate(2*Y) * scale(vec3(0.2, 0.3, 1)),
translate(2*X),
rotate(1, vec3(1,2,3)),
translate(-2*X) * mat4(scaledir(vec3(1,2,-3)*0.1)),
]
'''
def scale_screen_scale_view(center, offset):
def mat(view):
m = view.uniforms['view'] * view.uniforms['world']
e = view.uniforms['proj'] * fvec4(1,1,(m*center).z,1)
e /= e[3]
return m * translate(center) * scale(fvec3(1/e[1])) * translate(offset) * scale(fvec3(2/view.target.height))
return mat
def scale_screen_ubiquity(offset):
def mat(view):
proj = view.uniforms['proj']
f = - (proj[3][2]-proj[3][3]) / (proj[2][2] - proj[2][3])
n = - (proj[3][2]+proj[3][3]) / (proj[2][2] + proj[2][3])
d = 3*n
e = proj * fvec4(1,1,d,1)
e /= e[3]
d2 = 2/view.target.height
return translate(fvec3(0,0,d)) * fmat4(
fmat3(1/e[1])
* fmat3(view.uniforms['view'])
* fmat3(view.uniforms['world'])) * translate(offset) * scale(fvec3(d2))
return mat
def base_display(scene, mat):
return scene.display(note_base(mat))
#try: return scene.display(note_base(mat))
#except TypeError as err: return Display()
def quat_display(scene, quat):
try: return scene.display(note_rotation(quat))
except TypeError: return Display()
overrides.update({
dmat4: base_display,
fmat4: base_display,
dmat3: base_display,
fmat3: base_display,
dquat: quat_display,
fquat: quat_display,
})