Two intermeshed hot/cold toroid networks of tubing sit at the apex or drain of the hourglass funnels, directing the hot and cold ejection ports perpendicular to the funneled action where their corresponding temperatures are recycled, re-temperatured, and brought back to their corresponding funnel of same temperature. The choice between one mixed exit port, or two being hot/cold pends further simulation results. Obviously, choosing to spin the vortex as to further increase its potency is considered, and probably correct.

#!/usr/bin/env python3
"""
===============================================================================
HDGL HOURGLASS — FULL INTEGRATED ASSEMBLY
===============================================================================
Three concentric shells, each a complete Tesla-valve hourglass:
- hot branch climbs +z, cold branch descends -z
- both branches spiral inward toward the shared toroid plane at z=0
- the toroid void IS the apex — flow enters from the funnel coils,
circulates through the intermeshed toroid tubes, exits 90° radially
Per shell (3 shells):
- 3 threads (multi-start) x 2 branches (hot/cold) = 6 funnel spirals
- each spiral is num_cells Tesla-valve segments (forward + mirrored alternating)
- coil amplitude tapered by the drain factor so it never goes negative
Per shell, the toroid:
- R_major = drained spine radius at the funnel rim (emerges from the geometry)
- r_minor = A_eff at the rim (emerges from the geometry)
- n_turns = 6 (3 hot entries + 3 cold entries, alternating every 60 deg)
- TUBE geometry — swept square cross-section, not centerlines
- hot coil and cold coil phase-offset so their outer points interleave exactly
and every funnel thread lands on an outer point of its own coil
Exit pipes (3 per branch per shell = 9 total per branch):
- anchored at the 3 outer points NOT used for funnel entry
- run radially outward (perpendicular to z axis)
- tube radius = channel_width / 2
Coil amplitude taper — derived, not invented:
A_eff = min(A_base + shell_idx*4, drained_spine)
This is just "the coil cannot exceed the space available to it."
It emerges from the geometry rather than a separate constant.
Run inside FreeCAD's Python console to build. Run standalone for preview.
===============================================================================
"""
from pathlib import Path
import math
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
try:
import FreeCAD as App
import Part
HAVE_FREECAD = True
except ImportError:
HAVE_FREECAD = False
# ============================================================================
# HDGL SUBSTRATE
# ============================================================================
def T(x):
return 1.0 + 1.0 / x
def emergent_omega(seed=1.5, tol=1e-15, max_iter=1000):
x = seed
for _ in range(max_iter):
y = T(x)
if abs(y - x) < tol:
return y
x = y
return x
OMEGA = emergent_omega()
# ============================================================================
# PHYSICAL / MANUFACTURING CONSTANTS
# ============================================================================
L = 32.0 # local Tesla-valve segment length
A_base = 12.0 # coil amplitude at shell 0
k = 2.6 # Tesla-valve re-injection sharpness
num_cells = 6 # segments from toroid to outer rim, per branch
channel_width = 3.8
pitch_base = 40.0 # vertical climb per unit of turns
num_shells = 3
threads_per_shell = 3
TOTAL_L = num_cells * L
APEX_EPS = 1e-9
tube_radius = channel_width / 2.0 # coil tube wall
pipe_length_scale = 1.5 # exit pipe length = R_major * scale
# ============================================================================
# FUNNEL GEOMETRY — shared by both branches
# ============================================================================
def funnel_drained_spine(progress, shell_idx):
shell_rate = OMEGA ** (shell_idx + 1)
turns = progress * OMEGA
spine_r = max(shell_rate * turns, APEX_EPS)
return spine_r / T(spine_r)
def funnel_A_eff(shell_idx, drained):
A = A_base + shell_idx * 4
return min(A, drained)
def map_funnel(vec, theta_offset, shell_idx, branch):
"""
vec.x = distance from the toroid plane (0 = toroid, TOTAL_L = outer rim)
branch = +1 (hot, +z) or -1 (cold, -z)
Coil amplitude tapered so it never exceeds the available spine radius.
"""
progress = (vec.x / TOTAL_L) * num_cells
turns = progress * OMEGA
binary_mod = math.sin(math.pi * progress)
trinary_mod = math.sin(2.0 * math.pi * progress / 3.0)
phase_mod = 0.075 * binary_mod + 0.050 * trinary_mod
angle = 2.0 * math.pi * turns * branch + theta_offset + phase_mod * branch
drained = funnel_drained_spine(progress, shell_idx)
A_eff = funnel_A_eff(shell_idx, drained)
r = max(drained + vec.y * (A_eff / max(A_base + shell_idx * 4, APEX_EPS)), APEX_EPS)
z = pitch_base * turns * branch + vec.z
return (r * math.cos(angle), r * math.sin(angle), z)
# ============================================================================
# TOROID GEOMETRY — sized from the funnel rim
# ============================================================================
def toroid_params(shell_idx):
"""
R_major and r_minor emerge from the funnel rim geometry.
n_turns=6 so 3 hot entry + 3 cold entry outer-points fit with 30-deg gaps.
phi_offset_hot/cold rotate the toroid so funnel threads land on outer points.
"""
drained = funnel_drained_spine(num_cells, shell_idx)
A_eff = funnel_A_eff(shell_idx, drained)
R_major = drained
r_minor = A_eff
n_turns = 6
turns_rim = num_cells * OMEGA
phi_h = math.radians((math.degrees(2 * math.pi * turns_rim) % 60))
phi_c = math.radians((math.degrees(-2 * math.pi * turns_rim) % 60 + (60 / n_turns / 2)) % 60)
return R_major, r_minor, n_turns, phi_h, phi_c
def toroid_centerline(phi, R_major, r_minor, n_turns, phi_offset):
theta = n_turns * (phi - phi_offset)
r = R_major + r_minor * math.cos(theta)
return (r * math.cos(phi), r * math.sin(phi), r_minor * math.sin(theta))
def toroid_outer_points(R_major, r_minor, n_turns, phi_offset):
"""phi angles where the coil sits on its outer surface (theta=0 mod 2pi)."""
return [(phi_offset + 2 * math.pi * m / n_turns) % (2 * math.pi)
for m in range(n_turns)]
# ============================================================================
# FREECAD GEOMETRY PRIMITIVES
# ============================================================================
if HAVE_FREECAD:
def make_tri_face(p1, p2, p3):
return Part.Face(Part.makePolygon([
App.Vector(*p1), App.Vector(*p2), App.Vector(*p3), App.Vector(*p1)]))
def sweep_tube(centerline_fn, n_pts, half):
"""Sweep a square tube (half=half-width) along a centerline function."""
paths = [[], [], [], []]
for i in range(n_pts):
t = i / (n_pts - 1)
cx, cy, cz = centerline_fn(t)
rl = math.hypot(cx, cy)
rdx, rdy = (cx / rl, cy / rl) if rl > 1e-9 else (1.0, 0.0)
for j, (ro, zo) in enumerate([(-half,-half),(half,-half),(half,half),(-half,half)]):
paths[j].append((cx + rdx*ro, cy + rdy*ro, cz + zo))
faces = []
for j in range(4):
pc, pn = paths[j], paths[(j+1)%4]
for i in range(len(pc)-1):
a,b,c,d = pc[i],pc[i+1],pn[i],pn[i+1]
faces += [make_tri_face(a,b,d), make_tri_face(a,d,c)]
sc = [p[0] for p in paths]; ec = [p[-1] for p in paths]
faces += [make_tri_face(sc[0],sc[1],sc[2]), make_tri_face(sc[0],sc[2],sc[3])]
faces += [make_tri_face(ec[0],ec[1],ec[2]), make_tri_face(ec[0],ec[2],ec[3])]
return Part.Solid(Part.makeShell(faces))
def create_funnel_segment(x_offset, local_mirror, theta_off, shell_idx, branch):
m = -1 if local_mirror else 1
A = A_base + shell_idx * 4
c_height = channel_width
half = channel_width / 2.0
num_pts = 30
def centerline(t):
lx = (L + 1.5) * t + x_offset - 0.75
drained = funnel_drained_spine((lx / TOTAL_L) * num_cells, shell_idx)
A_eff = funnel_A_eff(shell_idx, drained)
shape = m * math.sin(math.pi * t) * math.exp(-k * t) * (1 - t) ** 1.2
ly = shape * A_eff
return map_funnel(
type('V', (), {'x': lx, 'y': ly, 'z': 0})(),
theta_off, shell_idx, branch)
return sweep_tube(centerline, num_pts, half)
def create_toroid_tube_freecad(shell_idx, branch):
R_major, r_minor, n_turns, phi_h, phi_c = toroid_params(shell_idx)
phi_offset = phi_h if branch > 0 else phi_c
num_pts = n_turns * 24 + 1
def centerline(t):
phi = 2 * math.pi * t
return toroid_centerline(phi, R_major, r_minor, n_turns, phi_offset)
return sweep_tube(centerline, num_pts, tube_radius)
def create_exit_pipe_freecad(shell_idx, branch, phi_exit):
R_major, r_minor, n_turns, phi_h, phi_c = toroid_params(shell_idx)
phi_offset = phi_h if branch > 0 else phi_c
anchor = toroid_centerline(phi_exit, R_major, r_minor, n_turns, phi_offset)
direction = App.Vector(math.cos(phi_exit), math.sin(phi_exit), 0.0)
base = App.Vector(*anchor)
return Part.makeCylinder(tube_radius, R_major * pipe_length_scale, base, direction)
# ============================================================================
# BUILD
# ============================================================================
def build_full_assembly():
doc = App.activeDocument() or App.newDocument("HDGL_Hourglass_Full")
for shell_idx in range(num_shells):
R_major, r_minor, n_turns, phi_h, phi_c = toroid_params(shell_idx)
print(f"\nShell {shell_idx}: R_major={R_major:.2f} r_minor={r_minor:.2f} void={R_major-r_minor:.2f}")
for branch, label, color, phi_off in [
(+1.0, "Hot", (0.85,0.25,0.10), phi_h),
(-1.0, "Cold", (0.15,0.45,0.90), phi_c),
]:
voids = []
# Funnel segments — full length, all cells
for t_idx in range(threads_per_shell):
theta_off = (2 * math.pi / threads_per_shell) * t_idx
for n in range(num_cells):
voids.append(create_funnel_segment(n*L, False, theta_off, shell_idx, branch))
voids.append(create_funnel_segment(n*L + L/2, True, theta_off, shell_idx, branch))
print(f" {label}: fusing {len(voids)} funnel segments...")
fluid = voids[0]
for v in voids[1:]:
fluid = fluid.fuse(v)
# Toroid tube
print(f" {label}: building toroid tube...")
trd = create_toroid_tube_freecad(shell_idx, branch)
fluid = fluid.fuse(trd)
# Exit pipes — outer points NOT used for funnel entry
all_outer = toroid_outer_points(R_major, r_minor, n_turns, phi_off)
turns_rim = num_cells * OMEGA
arrival_angles = set()
for t_idx in range(threads_per_shell):
theta_off = (2 * math.pi / threads_per_shell) * t_idx
arr = (2 * math.pi * turns_rim * branch + theta_off) % (2 * math.pi)
arrival_angles.add(round(arr, 2))
exit_phis = [p for p in all_outer
if not any(abs((p - a + math.pi) % (2*math.pi) - math.pi) < 0.05
for a in arrival_angles)]
print(f" {label}: adding {len(exit_phis)} exit pipes...")
for phi_e in exit_phis:
pipe = create_exit_pipe_freecad(shell_idx, branch, phi_e)
fluid = fluid.fuse(pipe)
obj = doc.addObject("Part::Feature", f"Shell{shell_idx}_{label}")
obj.Shape = fluid
if App.GuiUp:
obj.ViewObject.ShapeColor = color
obj.ViewObject.Transparency = 30
doc.recompute()
print("\nFull hourglass assembly complete.")
# ============================================================================
# PREVIEW — centerlines + toroid tubes, full hourglass
# ============================================================================
def render_full_preview():
OUT_DIR = (Path(__file__).resolve().parent if "__file__" in globals() else Path.cwd()) / "hdgl_graphs"
OUT_DIR.mkdir(parents=True, exist_ok=True)
paths_out = []
N_FUNNEL = 800
N_TOROID = 600
# ---- 3D hourglass view ----
fig = plt.figure(figsize=(14, 16))
ax = fig.add_subplot(111, projection="3d")
shell_hot_cmaps = [plt.cm.autumn, plt.cm.Reds, plt.cm.YlOrRd]
shell_cold_cmaps = [plt.cm.winter, plt.cm.Blues, plt.cm.GnBu ]
for shell_idx in range(num_shells):
R_major, r_minor, n_turns, phi_h, phi_c = toroid_params(shell_idx)
A = A_base + shell_idx * 4
for branch, label, cmaps, phi_off in [
(+1.0, "hot", shell_hot_cmaps, phi_h),
(-1.0, "cold", shell_cold_cmaps, phi_c),
]:
cmap = cmaps[shell_idx]
color = cmap(0.65)
# Funnel centerlines with coil wiggle
for t_idx in range(threads_per_shell):
theta_off = (2 * math.pi / threads_per_shell) * t_idx
for n in range(num_cells):
for local_mirror, x_off in [(False, n*L), (True, n*L+L/2)]:
m = -1 if local_mirror else 1
xs, ys, zs = [], [], []
for i in range(N_FUNNEL // (num_cells * 2)):
t = i / max(N_FUNNEL // (num_cells * 2) - 1, 1)
lx = (L + 1.5) * t + x_off - 0.75
drained = funnel_drained_spine((lx/TOTAL_L)*num_cells, shell_idx)
A_eff = funnel_A_eff(shell_idx, drained)
shape = m * math.sin(math.pi*t) * math.exp(-k*t) * (1-t)**1.2
ly = shape * A_eff
p = map_funnel(
type('V',(),{'x':lx,'y':ly,'z':0})(),
theta_off, shell_idx, branch)
xs.append(p[0]); ys.append(p[1]); zs.append(p[2])
ax.plot(xs, ys, zs, color=color, linewidth=0.9, alpha=0.75)
# Toroid centerline tube (represented as a thick line here)
xs, ys, zs = [], [], []
for i in range(N_TOROID):
phi = 2 * math.pi * i / (N_TOROID - 1)
cx, cy, cz = toroid_centerline(phi, R_major, r_minor, n_turns, phi_off)
xs.append(cx); ys.append(cy); zs.append(cz)
ax.plot(xs, ys, zs,
color='tab:red' if branch > 0 else 'tab:blue',
linewidth=2.5, alpha=0.9,
label=f"shell{shell_idx} {label} toroid" if t_idx == 0 else None)
# Exit pipes
all_outer = toroid_outer_points(R_major, r_minor, n_turns, phi_off)
turns_rim = num_cells * OMEGA
arrivals = set()
for ti in range(threads_per_shell):
toff = (2*math.pi/threads_per_shell)*ti
arr = (2*math.pi*turns_rim*branch + toff) % (2*math.pi)
arrivals.add(round(arr, 2))
exit_phis = [p for p in all_outer
if not any(abs((p-a+math.pi)%(2*math.pi)-math.pi) < 0.05
for a in arrivals)]
for phi_e in exit_phis:
anchor = toroid_centerline(phi_e, R_major, r_minor, n_turns, phi_off)
dx, dy = math.cos(phi_e), math.sin(phi_e)
ex = anchor[0] + dx * R_major * pipe_length_scale
ey = anchor[1] + dy * R_major * pipe_length_scale
ax.plot([anchor[0], ex], [anchor[1], ey], [anchor[2], anchor[2]],
color='tab:red' if branch > 0 else 'tab:blue',
linewidth=2.5, linestyle='--', alpha=0.9)
ax.set_title("HDGL Full Hourglass Assembly\nhot = warm tones cold = cool tones dashed = exit pipes",
fontsize=12)
ax.set_xlabel("x"); ax.set_ylabel("y"); ax.set_zlabel("z")
fig.tight_layout()
p1 = OUT_DIR / "hdgl_full_hourglass_3d.png"
fig.savefig(p1, dpi=160, bbox_inches="tight")
plt.close(fig)
paths_out.append(p1)
# ---- Side profile: shows the hourglass silhouette ----
fig2, ax2 = plt.subplots(figsize=(8, 14))
for shell_idx in range(num_shells):
R_major, r_minor, n_turns, phi_h, phi_c = toroid_params(shell_idx)
for branch, cname in [(+1.0,'tab:red'), (-1.0,'tab:blue')]:
phi_off = phi_h if branch > 0 else phi_c
for t_idx in range(threads_per_shell):
theta_off = (2*math.pi/threads_per_shell)*t_idx
for n in range(num_cells):
for local_mirror, x_off in [(False,n*L),(True,n*L+L/2)]:
m = -1 if local_mirror else 1
rs, zs = [], []
for i in range(40):
t = i/39
lx = (L+1.5)*t+x_off-0.75
drained = funnel_drained_spine((lx/TOTAL_L)*num_cells, shell_idx)
A_eff = funnel_A_eff(shell_idx, drained)
shape = m*math.sin(math.pi*t)*math.exp(-k*t)*(1-t)**1.2
ly = shape*A_eff
px,py,pz = map_funnel(type('V',(),{'x':lx,'y':ly,'z':0})(),
theta_off, shell_idx, branch)
rs.append(math.hypot(px,py)); zs.append(pz)
ax2.plot(rs, zs, color=cname, linewidth=0.7, alpha=0.5)
ax2.plot([-r for r in rs], zs, color=cname, linewidth=0.7, alpha=0.5)
# toroid silhouette
for i in range(N_TOROID):
pass # already drawn in the 3d view; side view gets funnel only for clarity
ax2.axhline(0, color='gold', linewidth=1.2, linestyle='--', label='toroid plane (z=0)')
ax2.set_aspect('equal', adjustable='box')
ax2.set_title("Hourglass side profile\nred=hot (+z) blue=cold (−z) toroid plane at z=0")
ax2.set_xlabel("radius (mirrored for silhouette)")
ax2.set_ylabel("z")
ax2.legend()
fig2.tight_layout()
p2 = OUT_DIR / "hdgl_full_hourglass_side.png"
fig2.savefig(p2, dpi=160, bbox_inches="tight")
plt.close(fig2)
paths_out.append(p2)
# ---- Top-down: shows toroid intermeshing ----
fig3, ax3 = plt.subplots(figsize=(12, 12))
for shell_idx in range(num_shells):
R_major, r_minor, n_turns, phi_h, phi_c = toroid_params(shell_idx)
for branch, cname, phi_off in [
(+1.0,'tab:red',phi_h), (-1.0,'tab:blue',phi_c)]:
xs, ys = [], []
for i in range(N_TOROID):
phi = 2*math.pi*i/(N_TOROID-1)
cx,cy,cz = toroid_centerline(phi, R_major, r_minor, n_turns, phi_off)
xs.append(cx); ys.append(cy)
ax3.plot(xs, ys, color=cname, linewidth=1.2, alpha=0.85)
all_outer = toroid_outer_points(R_major, r_minor, n_turns, phi_off)
turns_rim = num_cells*OMEGA
arrivals = set()
for ti in range(threads_per_shell):
toff=(2*math.pi/threads_per_shell)*ti
arr=(2*math.pi*turns_rim*branch+toff)%(2*math.pi)
arrivals.add(round(arr,2))
exit_phis = [p for p in all_outer
if not any(abs((p-a+math.pi)%(2*math.pi)-math.pi)<0.05 for a in arrivals)]
for phi_e in exit_phis:
anchor = toroid_centerline(phi_e, R_major, r_minor, n_turns, phi_off)
dx,dy = math.cos(phi_e), math.sin(phi_e)
ex = anchor[0]+dx*R_major*pipe_length_scale
ey = anchor[1]+dy*R_major*pipe_length_scale
ax3.plot([anchor[0],ex],[anchor[1],ey],
color=cname, linewidth=2.5, linestyle='--', alpha=0.9)
ax3.set_aspect('equal', adjustable='box')
ax3.set_title("Top-down: intermeshed toroids (3 shells)\nred=hot coils blue=cold coils dashed=exit pipes")
ax3.set_xlabel("x"); ax3.set_ylabel("y")
fig3.tight_layout()
p3 = OUT_DIR / "hdgl_full_hourglass_topdown.png"
fig3.savefig(p3, dpi=160, bbox_inches="tight")
plt.close(fig3)
paths_out.append(p3)
return paths_out
# ============================================================================
# ANIMATION — particles riding real geometry, toroid included
# ============================================================================
def render_animation_frames():
OUT_DIR = (Path(__file__).resolve().parent if "__file__" in globals() else Path.cwd()) / "hdgl_graphs"
OUT_DIR.mkdir(parents=True, exist_ok=True)
N_FRAMES = 90
N_PARTICLES_PER_THREAD = 4
# Build particle paths: funnel then toroid then exit pipe
particle_paths = []
for shell_idx in range(num_shells):
R_major, r_minor, n_turns, phi_h, phi_c = toroid_params(shell_idx)
for branch, cname, phi_off in [(+1.0,'tomato',''), (-1.0,'royalblue','')]:
phi_off = phi_h if branch > 0 else phi_c
for t_idx in range(threads_per_shell):
theta_off = (2*math.pi/threads_per_shell)*t_idx
# funnel path (outer rim -> toroid plane)
funnel_pts = []
for n in range(num_cells-1, -1, -1): # outer to inner
for local_mirror, x_off in [(True, n*L+L/2), (False, n*L)]:
m = -1 if local_mirror else 1
for i in range(20):
t = (19-i)/19
lx = (L+1.5)*t+x_off-0.75
drained = funnel_drained_spine((lx/TOTAL_L)*num_cells, shell_idx)
A_eff = funnel_A_eff(shell_idx, drained)
shape = m*math.sin(math.pi*t)*math.exp(-k*t)*(1-t)**1.2
ly = shape*A_eff
px,py,pz = map_funnel(type('V',(),{'x':lx,'y':ly,'z':0})(),
theta_off, shell_idx, branch)
funnel_pts.append((px,py,pz))
# toroid path (full loop starting from funnel arrival angle)
turns_rim = num_cells*OMEGA
arr_phi = (2*math.pi*turns_rim*branch + theta_off) % (2*math.pi)
toroid_pts = []
for i in range(60):
phi = (arr_phi + 2*math.pi*i/59) % (2*math.pi)
toroid_pts.append(toroid_centerline(phi, R_major, r_minor, n_turns, phi_off))
# pick the exit pipe closest to 180 deg from entry
all_outer = toroid_outer_points(R_major, r_minor, n_turns, phi_off)
arrivals = set()
for ti in range(threads_per_shell):
toff=(2*math.pi/threads_per_shell)*ti
arr=(2*math.pi*turns_rim*branch+toff)%(2*math.pi)
arrivals.add(round(arr,2))
exit_phis = [p for p in all_outer
if not any(abs((p-a+math.pi)%(2*math.pi)-math.pi)<0.05 for a in arrivals)]
target_exit = (arr_phi + math.pi) % (2*math.pi)
phi_e = min(exit_phis, key=lambda p: abs((p-target_exit+math.pi)%(2*math.pi)-math.pi))
exit_anchor = toroid_centerline(phi_e, R_major, r_minor, n_turns, phi_off)
# exit pipe path
dx,dy = math.cos(phi_e), math.sin(phi_e)
pipe_pts = [(exit_anchor[0]+dx*R_major*pipe_length_scale*s/19,
exit_anchor[1]+dy*R_major*pipe_length_scale*s/19,
exit_anchor[2]) for s in range(20)]
full_path = funnel_pts + toroid_pts + pipe_pts
particle_paths.append((full_path, cname, shell_idx))
# Render frames
fig = plt.figure(figsize=(10, 12), facecolor='#0d0d12')
ax = fig.add_subplot(111, projection='3d', facecolor='#0d0d12')
all_pts = [p for path,c,s in particle_paths for p in path]
xs_all = [p[0] for p in all_pts]
ys_all = [p[1] for p in all_pts]
zs_all = [p[2] for p in all_pts]
frame_paths = []
for frame_i in range(N_FRAMES):
ax.cla()
ax.set_facecolor('#0d0d12')
ax.set_xlim(min(xs_all), max(xs_all))
ax.set_ylim(min(ys_all), max(ys_all))
ax.set_zlim(min(zs_all), max(zs_all))
ax.set_axis_off()
ax.set_title("HDGL Hourglass — hot/cold riding real geometry",
color='white', fontsize=11, pad=4)
phase = frame_i / N_FRAMES
for path, cname, shell_idx in particle_paths:
n = len(path)
for pp in range(N_PARTICLES_PER_THREAD):
frac = (phase + pp/N_PARTICLES_PER_THREAD) % 1.0
idx = int(frac * n)
x,y,z = path[idx]
# tail
tail_len = max(n//12, 3)
tail_idxs = [(idx - j) % n for j in range(tail_len)]
tx = [path[ti][0] for ti in tail_idxs]
ty = [path[ti][1] for ti in tail_idxs]
tz = [path[ti][2] for ti in tail_idxs]
alphas = np.linspace(0.7, 0.0, tail_len)
for seg in range(len(tx)-1):
ax.plot(tx[seg:seg+2], ty[seg:seg+2], tz[seg:seg+2],
color=cname, linewidth=1.2, alpha=float(alphas[seg]))
ax.scatter([x],[y],[z], color=cname, s=18, alpha=0.95, depthshade=False)
frame_path = OUT_DIR / f"anim_{frame_i:03d}.png"
fig.savefig(frame_path, dpi=110, bbox_inches='tight',
facecolor='#0d0d12', edgecolor='none')
frame_paths.append(frame_path)
if frame_i % 10 == 0:
print(f" frame {frame_i}/{N_FRAMES}")
plt.close(fig)
return frame_paths
# ============================================================================
# MAIN
# ============================================================================
if __name__ == "__main__":
print(f"OMEGA = {OMEGA:.15f}")
print()
print("Rendering static previews...")
preview_paths = render_full_preview()
for p in preview_paths:
print(f" [OK] {p}")
print()
print("Rendering animation frames...")
frame_paths = render_animation_frames()
print(f" {len(frame_paths)} frames saved")
# stitch to GIF if imageio available
try:
import imageio
gif_path = Path(frame_paths[0]).parent / "hdgl_hourglass_animation.gif"
with imageio.get_writer(gif_path, mode='I', fps=24, loop=0) as writer:
for fp in frame_paths:
writer.append_data(imageio.imread(fp))
print(f" [GIF] {gif_path}")
except ImportError:
print(" (imageio not available — frames saved as PNGs, stitch manually)")
if HAVE_FREECAD:
print()
print("Building FreeCAD solid...")
build_full_assembly()
else:
print()
print("FreeCAD not available — previews and animation only.")
print("Run inside FreeCAD's Python console to build the solid.")
