Sign in

vitor-internCopy the usernamekit-logo-remix-remix

solid_library.py

In source/At 17e3b44Copy the commit ID

17e3b44History

Preview

Code7.7 KiB
"""A geometric library built from projected volumes and a shared palette."""
from pathlib import Path
import math
import subprocess
import xml.etree.ElementTree as E

NS='{http://www.w3.org/2000/svg}'
E.register_namespace('',NS[1:-1])
BLUE='#0067ed'
PALETTE=['#0067ed','#21c864','#ff5548','#f4ce2c']
LABELS=['Cube','Cuboid','Tower','Slab','Triangular prism','Hexagonal prism','Pyramid','Pyramid frustum','Tetrahedron','Octahedron','Diamond','Wedge','Cylinder','Disc','Cone','Cone frustum','Sphere','Hemisphere','Torus','Tube','Arch','Portal','Open ring','Hollow box','Stairs','L block','T block','Cross block','U block','Zigzag block','Pentagonal prism','Octagonal prism','Rounded block','Capsule','Ellipsoid','Stack']

def add(p,tag,**a):return E.SubElement(p,NS+tag,{k.replace('_','-'):str(v) for k,v in a.items()})
def ring(n,r=1,z=0,phase=0):return [(r*math.cos(phase+2*math.pi*i/n),r*math.sin(phase+2*math.pi*i/n),z) for i in range(n)]
def loft(rings,caps=True):
 f=[];n=len(rings[0])
 if caps:f+=[list(reversed(rings[0])),rings[-1]]
 for a,b in zip(rings,rings[1:]):
  f += [[a[i],a[(i+1)%n],b[(i+1)%n],b[i]] for i in range(n)]
 return f

def prism(n=4,h=1.8,r=1,top=None):return loft([ring(n,r,0,math.pi/4),ring(n,r if top is None else top,h,math.pi/4)])
def box(x=1.5,y=1.5,z=1.5,ox=0,oy=0,oz=0):
 a=[(-x/2+ox,-y/2+oy,oz),(x/2+ox,-y/2+oy,oz),(x/2+ox,y/2+oy,oz),(-x/2+ox,y/2+oy,oz)]
 return loft([a,[(u,v,z+oz) for u,v,_ in a]])
def profile(points,depth=.65):return loft([[(x,-depth/2,z) for x,z in points],[(x,depth/2,z) for x,z in points]])
def rounded_ring(z,radius=.22):
 p=[]
 for cx,cy,start in [(.75-radius,.75-radius,0),(-.75+radius,.75-radius,90),(-.75+radius,-.75+radius,180),(.75-radius,-.75+radius,270)]:
  for j in range(7):
   t=math.radians(start+j*15);p.append((cx+radius*math.cos(t),cy+radius*math.sin(t),z))
 return p

def globe(rx=1,ry=1,rz=1,half=False,capsule=False):
 rows=[]
 for j in range(17):
  t=(math.pi/2 if half else math.pi)*j/16
  zz=rz*math.cos(t)+(0 if half else rz)
  if capsule:zz+=.75 if math.cos(t)>=0 else 0
  rows.append([(rx*math.sin(t)*math.cos(i*2*math.pi/40),ry*math.sin(t)*math.sin(i*2*math.pi/40),zz) for i in range(40)])
 return [list(reversed(face)) for face in loft(rows)]

def torus(start=0,end=2*math.pi):
 rows=[]
 for j in range(49):
  a=start+(end-start)*j/48
  rows.append([((.88+.27*math.cos(b))*math.cos(a),(.88+.27*math.cos(b))*math.sin(a),.3+.27*math.sin(b)) for b in [i*2*math.pi/16 for i in range(16)]])
 return [list(reversed(face)) for face in loft(rows,caps=end-start<6.2)]

def tube(n=64):
 a,b,c,d=ring(n,1,0),ring(n,1,1.5),ring(n,.61,1.5),ring(n,.61,0)
 return loft([a,b,c,d,a],False)

def arch():
 pts=[(1.1,0),(1.1,1)]
 pts += [(1.1*math.cos(a),1+1.1*math.sin(a)) for a in [i*math.pi/32 for i in range(33)]]
 pts += [(-1.1,0),(-.64,0),(-.64,1)]
 pts += [(.64*math.cos(a),1+.64*math.sin(a)) for a in [math.pi-i*math.pi/32 for i in range(33)]]
 pts += [(.64,0)]
 return profile(pts)

def voxels(cells):
 f=[];cells=set(cells)
 for x,y,z in cells:
  for dx,dy,dz in [(1,0,0),(-1,0,0),(0,1,0),(0,-1,0),(0,0,1),(0,0,-1)]:
   if (x+dx,y+dy,z+dz) in cells:continue
   if dx:vs=[(x+(dx>0),y,z),(x+(dx>0),y+1,z),(x+(dx>0),y+1,z+1),(x+(dx>0),y,z+1)]
   elif dy:vs=[(x,y+(dy>0),z),(x+1,y+(dy>0),z),(x+1,y+(dy>0),z+1),(x,y+(dy>0),z+1)]
   else:vs=[(x,y,z+(dz>0)),(x+1,y,z+(dz>0)),(x+1,y+1,z+(dz>0)),(x,y+1,z+(dz>0))]
   # Orient the face normal away from the occupied cell.
   u=[vs[1][i]-vs[0][i] for i in range(3)];v=[vs[2][i]-vs[0][i] for i in range(3)]
   n=(u[1]*v[2]-u[2]*v[1],u[2]*v[0]-u[0]*v[2],u[0]*v[1]-u[1]*v[0])
   if n[0]*dx+n[1]*dy+n[2]*dz<0:vs.reverse()
   f.append(vs)
 return f

def meshes():
 fs=[box(),box(2.2,1.2,1.2),box(1,1,2.8),box(2.2,1.7,.42),prism(3),prism(6),prism(4,2.2,1.25,0),prism(4,1.65,1.2,.63)]
 fs += [loft([ring(3,1.2,0,math.pi/4),[(0,0,2.1)]*3]),loft([[(0,0,0)]*4,ring(4,1.15,1.2),[(0,0,2.4)]*4]),loft([[(0,0,0)]*6,ring(6,1.1,1.6),ring(6,.55,2.4)]),profile([(-1,0),(1,0),(-1,1.6)],1.3)]
 fs += [prism(64,1.9),prism(64,.4,1.15),prism(64,2.2,1,0),prism(64,1.6,1.1,.55),globe(),globe(half=True),torus(),tube(),arch()]
 fs += [voxels([(x,0,z) for x in range(3) for z in range(3) if x in [0,2] or z==2]),torus(.35,5.3),voxels([(x,y,z) for x in range(3) for y in range(3) for z in range(2) if x in [0,2] or y in [0,2]])]
 fs += [voxels([(x,0,z) for x in range(3) for z in range(x+1)]),voxels([(0,0,0),(1,0,0),(2,0,0),(0,1,0),(0,2,0)]),voxels([(x,0,0) for x in range(3)]+[(1,1,0),(1,2,0)]),voxels([(1,1,0),(0,1,0),(2,1,0),(1,0,0),(1,2,0)]),voxels([(0,y,0) for y in range(3)]+[(2,y,0) for y in range(3)]+[(1,0,0)]),voxels([(0,0,0),(1,0,0),(1,1,0),(2,1,0),(2,2,0)])]
 fs += [prism(5),prism(8),loft([rounded_ring(0),rounded_ring(1.5)]),globe(.7,.7,.7,capsule=True),globe(1.4,.85,.75),box(1.9,1.9,.55)+box(1.4,1.4,.55,oz=.55)+box(.9,.9,.55,oz=1.1)]
 assert len(fs)==36
 return fs

def project(v):
 x,y,z=v;return (x-y,.5*(x+y)-math.sqrt(1.5)*z)
def depth(face):return sum(x+y+math.sqrt(2/3)*z for x,y,z in face)/len(face)
def normal(face):
 for i in range(1,len(face)-1):
  a=[face[i][j]-face[0][j] for j in range(3)];b=[face[i+1][j]-face[0][j] for j in range(3)]
  n=(a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]);l=math.sqrt(sum(v*v for v in n))
  if l>1e-8:return tuple(v/l for v in n)
 return (0,0,1)
def tint(color,n):
 # Top surfaces stay bright; the two vertical axes have distinct tones.
 amount=.84+.14*max(0,n[2])+.10*max(0,n[0])-.03*max(0,n[1])
 rgb=[int(color[i:i+2],16) for i in [1,3,5]]
 return '#'+''.join(f'{round(min(255,c*amount+255*max(0,amount-1))):02x}' for c in rgb)

def asset(i,faces):
 root=E.Element(NS+'svg',{'width':'600','height':'600','viewBox':'0 0 300 300','role':'img','aria-label':'KIT '+LABELS[i]})
 pts=[project(v) for f in faces for v in f];xs=[v[0] for v in pts];ys=[v[1] for v in pts]
 scale=min(214/(max(xs)-min(xs)),214/(max(ys)-min(ys)))
 tx=150-(max(xs)+min(xs))*scale/2;ty=150-(max(ys)+min(ys))*scale/2
 group=add(root,'g',transform=f'translate({tx} {ty}) scale({scale})',stroke_width=f'{.25/scale:.5f}',stroke_linejoin='round')
 base=PALETTE[i%4]
 for face in sorted(faces,key=depth):
  p=[project(v) for v in face]
  area=sum(a[0]*b[1]-b[0]*a[1] for a,b in zip(p,p[1:]+p[:1]))
  if abs(area)<1e-9:continue
  color=tint(base,normal(face))
  add(group,'polygon',points=' '.join(f'{x:.4f},{y:.4f}' for x,y in p),fill=color,stroke=color)
 return root

def build(root):
 art=root/'art/solids';pre=root/'previews/solids';art.mkdir(parents=True,exist_ok=True);pre.mkdir(parents=True,exist_ok=True)
 sheet=E.Element(NS+'svg',{'width':'2160','height':'2400','viewBox':'0 0 2160 2400'})
 add(sheet,'rect',width=2160,height=2400,fill='#f3f5f8')
 add(sheet,'text',x=48,y=66,fill=BLUE,font_family='Helvetica, Arial, sans-serif',font_size=34,font_weight=700).text='KIT / Geometric solids'
 add(sheet,'text',x=48,y=104,fill='#536174',font_family='Helvetica, Arial, sans-serif',font_size=18).text='36 volumes · a shared axonometric projection · Uno blue and primary colors'
 for i,faces in enumerate(meshes()):
  svg=asset(i,faces);name=f'{i+1:02}-'+LABELS[i].lower().replace(' ','-')
  path=art/(name+'.svg');path.write_text(E.tostring(svg,encoding='unicode')+'\n')
  subprocess.run(['rsvg-convert','-o',str(pre/(name+'.png')),str(path)],check=True)
  x=48+(i%6)*348;y=140+(i//6)*370
  add(sheet,'rect',x=x,y=y,width=324,height=346,rx=16,fill='white')
  svg.attrib.update(x=str(x+12),y=str(y+6),width='300',height='290');sheet.append(svg)
  add(sheet,'text',x=x+20,y=y+324,fill=BLUE,font_family='Helvetica, Arial, sans-serif',font_size=17,font_weight=700).text=f'{i+1:02} / {LABELS[i]}'
 path=art/'solid-library.svg';path.write_text(E.tostring(sheet,encoding='unicode')+'\n')
 subprocess.run(['rsvg-convert','-o',str(pre/'solid-library.png'),str(path)],check=True)
 print('Built 36 geometric solids and the comparison grid.')

if __name__=='__main__':build(Path(__file__).resolve().parent.parent)

Files

Loading files

Details

Size
7.7 KiB (7,935 bytes)
Object ID
b992a01…faeCopy the object ID
Verification
  • AGENTS.md
  • README.md
  • build.py
  • cover.png
  • manifest.json
Size
7.7 KiB (7,935 bytes)
Object ID
b992a01…faeCopy the object ID
Storage
Stored as one object
Verification