codex/past-exam-of-the-computer-science-course-of-the-university-of-oxford/2015/geometric-modelling-2015-hull.py

past-exam-of-the-computer-science-course-of-the-university-of-oxford/2015/geometric-modelling-2015-hull.py
"""Oxford 2015 geometric modelling Q3: incremental hull stages.

Run from any directory; writes geometric-modelling-2015-hull.png there.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
"""
import os
import tempfile
os.environ.setdefault('MPLCONFIGDIR', tempfile.mkdtemp(prefix='gmod-hull-mpl-'))
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from mpl_toolkits.mplot3d import proj3d
import numpy as np

P = {k:np.array(v,float) for k,v in dict(A=(0,-1,0),B=(0,1,0),C=(-1,0,-1),D=(0,0,1),E=(1,0,0)).items()}
initial = ['ACB','ABD','ADC','BCD']
final = ['ADC','BCD','ACE','CBE','BDE','DAE']
fig = plt.figure(figsize=(11,4),layout='constrained')
axes = [fig.add_subplot(1,3,i+1,projection='3d') for i in range(3)]
for i,ax in enumerate(axes):
    faces = final if i==2 else initial
    colors = ['#9dc9e7' if f in ['ADC','BCD'] else ('#f09c8d' if i==1 else '#b4d7ed') for f in faces]
    poly = Poly3DCollection([[P[k] for k in f] for f in faces],facecolors=colors,edgecolors='#26485c',alpha=.55,lw=1.2)
    ax.add_collection3d(poly)
    for label,p in P.items():
        if i==0 and label=='E':continue
        ax.scatter(*p,color='#123e67',s=16,depthshade=False)
    ax.set(xlim=(-1.25,1.25),ylim=(-1.25,1.25),zlim=(-1.25,1.25),xlabel='x',ylabel='y',zlabel='z')
    ax.set_box_aspect((1,1,1))
    ax.view_init(elev=22,azim=42)
    ax.set_xticks([-1,0,1]);ax.set_yticks([-1,0,1]);ax.set_zticks([-1,0,1])
    ax.tick_params(labelsize=8,pad=0)
    for label,p in P.items():
        if i==0 and label=='E':continue
        px,py,_=proj3d.proj_transform(*p,ax.get_proj())
        ax.annotate(label,(px,py),xytext=(5,5),textcoords='offset points',fontsize=11,
                    color='#142a39',zorder=100)
axes[0].set_title('Initial tetrahedron ABCD',fontsize=11)
axes[1].set_title('Visible cap: ACB and ABD',fontsize=11)
axes[2].set_title('Final hull: retain 2, add 4 faces',fontsize=11)
horizon=[P[k] for k in 'ACBDA']
axes[1].plot(*np.array(horizon).T,color='#963899',lw=3)
for k in 'ACBD':
    axes[1].plot(*np.array([P[k],P['E']]).T,'--',color='#963899',lw=1)
fig.savefig('geometric-modelling-2015-hull.png',dpi=125,facecolor='white',transparent=False)
plt.close(fig)