Sign in

bold-guanaco-0e2sCopy the usernamekit-logo-remix-remix-remix

solid_library.py

In source/

mainHistory

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
  • 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
Size
7.7 KiB (7,935 bytes)
Object ID
b992a01…faeCopy the object ID
Storage
Stored as one object
Verification