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

past-exam-of-the-computer-science-course-of-the-university-of-oxford/2015/geometric-modelling-2015-fillet.py
"""Oxford 2015 geometric modelling Q1: circular fillet and local CSG.

Run from any directory; writes geometric-modelling-2015-fillet.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-fillet-mpl-'))
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon, Circle
import numpy as np

q = np.sqrt(3.)
A, B, C, D, EE, O = np.array([[2-q, 0], [2, 1], [1, 1], [1, 2], [0, 2], [0, 0]])
M = np.array([2-q/4, .75])
N = np.array([1.5, 1.])
K = np.array([1.5, q/2])
r = 1-q/2
theta = np.linspace(-np.pi/3, np.pi/2, 160)
arc = K+r*np.column_stack((np.cos(theta), np.sin(theta)))
fig, (ax, tree) = plt.subplots(1, 2, figsize=(9, 4.6), layout='constrained', gridspec_kw={'width_ratios':[1.35, 1]})
ax.add_patch(Polygon([A, B, C, D, EE, O], facecolor='#f9dcd6', edgecolor='.5', lw=1))
ax.add_patch(Polygon(np.vstack([A, M, arc, N, C, D, EE, O]), facecolor='#dbeaf6', edgecolor='#123e67', lw=1.5))
ax.add_patch(Circle(K, r, fill=False, edgecolor='#29906a', ls=':', lw=1.5))
ax.plot(*arc.T, color='#167b50', lw=3, zorder=5, label='retained circular arc')
x = np.linspace(1.35, 2.1, 100)
ax.plot(x, 1-(x-1.5)/(2-q), '--', color='#81519b', lw=1.5, label='chord clipping line g = 0')
for p in [M, N]:
    ax.plot([K[0], p[0]], [K[1], p[1]], ':', color='#167b50', lw=1.5)
for label, p, offset in [('M',M,(6,-15)),('N',N,(-17,7)),('K',K,(-19,-2)),('B',B,(7,3))]:
    ax.plot(*p, 'o', color='#123e67', ms=4, zorder=6)
    ax.annotate(label, p, xytext=offset, textcoords='offset points', fontsize=11, zorder=6)
ax.text(1.89, .9, 'removed\ntip', color='#9c3631', ha='center', fontsize=10)
ax.set(xlim=(1.35, 2.08), ylim=(.48, 1.12), xlabel='x', ylabel='y')
ax.set_aspect('equal')
ax.grid(alpha=.15)
ax.legend(loc='lower right', fontsize=8)
ax.set_title('Tangent circle and localized cut')

tree.set(xlim=(0,1), ylim=(0,1))
tree.axis('off')
nodes = {'root':(.5,.85),'original':(.2,.56),'union':(.75,.56),'halfplane':(.56,.25),'disk':(.94,.25)}
for a,b in [('root','original'),('root','union'),('union','halfplane'),('union','disk')]:
    pa,pb=nodes[a],nodes[b]
    tree.plot([pa[0],pb[0]],[pa[1],pb[1]], color='.4', zorder=1)
for name,text in [('root','intersection'),('original','original P'),('union','union'),('halfplane','g ≤ 0'),('disk','disk')]:
    tree.text(*nodes[name],text,ha='center',va='center',fontsize=11,
              bbox=dict(boxstyle='round,pad=.4',fc='white',ec='#123e67'),zorder=2)
tree.text(.5,.04,'Keep the old solid AND\n(the far side of MN OR the disk)',ha='center',va='center',fontsize=10)
tree.set_title('Blend embedded in the CSG tree')
fig.savefig('geometric-modelling-2015-fillet.png',dpi=125,facecolor='white',transparent=False)
plt.close(fig)