{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "d5e79bb2-9fc5-4a71-82e2-14ce475e7a5d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "density=0.10  shell=2x0.42mm\n",
      "  A_shell  = 30.78 mm^2\n",
      "  L_infill = 15.10 mm\n",
      "  A_infill = 6.34 mm^2\n",
      "  A_total  = 37.12 mm^2\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 29552 characters]",
      "text/plain": [
       "<Figure size 500x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "density=0.15  shell=2x0.42mm\n",
      "  A_shell  = 30.78 mm^2\n",
      "  L_infill = 28.05 mm\n",
      "  A_infill = 11.78 mm^2\n",
      "  A_total  = 42.56 mm^2\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 38796 characters]",
      "text/plain": [
       "<Figure size 500x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "density=0.60  shell=2x0.42mm\n",
      "  A_shell  = 30.78 mm^2\n",
      "  L_infill = 119.42 mm\n",
      "  A_infill = 50.16 mm^2\n",
      "  A_total  = 80.93 mm^2\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 101484 characters]",
      "text/plain": [
       "<Figure size 500x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import numpy as np\n",
    "from math import ceil, pi\n",
    "\n",
    "EPS = 1e-9\n",
    "\n",
    "# ----------------- Prusa gyroid core -----------------\n",
    "\n",
    "def prusa_f(x, z_sin, z_cos, vertical, flip):\n",
    "    if vertical:\n",
    "        phase_offset = (np.pi if z_cos < 0 else 0.0) + np.pi\n",
    "        a   = np.sin(x + phase_offset)\n",
    "        b   = -z_cos\n",
    "        res = z_sin * np.cos(x + phase_offset + (np.pi if flip else 0.0))\n",
    "        r   = np.sqrt(a*a + b*b)\n",
    "        return np.arcsin(a/r) + np.arcsin(res/r) + np.pi\n",
    "    else:\n",
    "        phase_offset = np.pi if z_sin < 0.0 else 0.0\n",
    "        a   = np.cos(x + phase_offset)\n",
    "        b   = -z_sin\n",
    "        res = z_cos * np.sin(x + phase_offset + (0.0 if flip else np.pi))\n",
    "        r   = np.sqrt(a*a + b*b)\n",
    "        return np.arcsin(a/r) + np.arcsin(res/r) + 0.5*np.pi\n",
    "\n",
    "def _cross2(ax, ay, bx, by): return ax*by - ay*bx\n",
    "\n",
    "def make_one_period(width_units, z_cos, z_sin, vertical, flip, tol_units):\n",
    "    dx = np.pi/2.0\n",
    "    limit = min(2.0*np.pi, width_units)\n",
    "    xs = list(np.arange(0.0, limit-EPS, dx)) + [limit]\n",
    "    pts = [[x, prusa_f(x, z_sin, z_cos, vertical, flip)] for x in xs]\n",
    "    # refinement loop\n",
    "    while True:\n",
    "        n0 = len(pts)\n",
    "        new = [pts[0]]\n",
    "        for i in range(1, len(pts)):\n",
    "            lx, ly = new[-1]\n",
    "            rx, ry = pts[i]\n",
    "            xm = 0.5*(lx + rx)\n",
    "            ym = prusa_f(xm, z_sin, z_cos, vertical, flip)\n",
    "            if abs(_cross2(xm-lx, ym-ly, xm-rx, ym-ry)) > tol_units*tol_units:\n",
    "                new.append([xm, ym])\n",
    "            new.append([rx, ry])\n",
    "        pts = new\n",
    "        if len(pts) == n0: break\n",
    "        pts.sort(key=lambda p: p[0])\n",
    "    return np.asarray(pts)\n",
    "\n",
    "def make_wave(one_period, width_units, height_units, y0, scale_mm_per_unit,\n",
    "              z_cos, z_sin, vertical, flip):\n",
    "    pts = one_period.copy()\n",
    "    period = pts[-1,0]\n",
    "    if abs(width_units - period) > EPS:\n",
    "        n = len(pts)\n",
    "        pts = pts[:-1].tolist()\n",
    "        while pts[-1][0] < width_units - EPS:\n",
    "            src = pts[-n+1]\n",
    "            pts.append([src[0] + period, src[1]])\n",
    "        y_end = prusa_f(width_units, z_sin, z_cos, vertical, flip)\n",
    "        pts.append([width_units, y_end])\n",
    "    else:\n",
    "        pts = pts.tolist()\n",
    "\n",
    "    out = []\n",
    "    for x,y in pts:\n",
    "        y = y + y0\n",
    "        y = min(max(y, 0.0), height_units)   # clamp like Prusa\n",
    "        if vertical: x, y = y, x             # swap *after* clamp\n",
    "        out.append([x*scale_mm_per_unit, y*scale_mm_per_unit])\n",
    "    return np.asarray(out)\n",
    "\n",
    "def make_gyroid_waves_prusa(z_mm, density, spacing_mm, width_mm, height_mm,\n",
    "                            density_adjust=1.0, pattern_tolerance_mm=0.5,\n",
    "                            pad_modules=int(ceil(2.0*np.pi))):\n",
    "    \"\"\"\n",
    "    Returns list of polylines (Nx2 arrays) in mm (NOT clipped).\n",
    "    \"\"\"\n",
    "    dens = max(0.0, density * density_adjust)\n",
    "    if dens <= 0.0: return []\n",
    "\n",
    "    scale = spacing_mm / dens      # mm per “unit”\n",
    "    tol_units = min(spacing_mm/2.0, pattern_tolerance_mm) / scale\n",
    "\n",
    "    # z in “units”\n",
    "    z = z_mm / scale\n",
    "    z_sin, z_cos = np.sin(z), np.cos(z)\n",
    "\n",
    "    # Prusa-like module counts (ceil + 1)\n",
    "    modules_x = int(ceil(width_mm  / scale)) + 1\n",
    "    modules_y = int(ceil(height_mm / scale)) + 1\n",
    "\n",
    "    # emulate align_to_grid padding (2π “modules”)\n",
    "    Wm = modules_x + pad_modules\n",
    "    Hm = modules_y + pad_modules\n",
    "\n",
    "    vertical = (abs(z_sin) <= abs(z_cos))\n",
    "    lower, upper = 0.0, Hm\n",
    "    flip = True\n",
    "    if vertical:\n",
    "        flip = False\n",
    "        lower = -np.pi\n",
    "        upper = Wm - np.pi/2.0\n",
    "        Wm, Hm = Hm, Wm            # swap like the C++ after computing bounds\n",
    "\n",
    "    odd  = make_one_period(Wm, z_cos, z_sin, vertical, flip, tol_units)\n",
    "    flip = not flip\n",
    "    even = make_one_period(Wm, z_cos, z_sin, vertical, flip, tol_units)\n",
    "\n",
    "    lines = []\n",
    "    y0 = lower\n",
    "    while y0 < upper + EPS:\n",
    "        lines.append(make_wave(odd,  Wm, Hm, y0, scale, z_cos, z_sin, vertical, flip))\n",
    "        y0 += np.pi\n",
    "        if y0 < upper + EPS:\n",
    "            lines.append(make_wave(even, Wm, Hm, y0, scale, z_cos, z_sin, vertical, flip))\n",
    "        y0 += np.pi\n",
    "\n",
    "    # translate back by the padding so the original box sits inside\n",
    "    shift_mm = pad_modules * scale\n",
    "    for i in range(len(lines)):\n",
    "        lines[i][:,0] -= shift_mm\n",
    "        lines[i][:,1] -= shift_mm\n",
    "    return lines\n",
    "\n",
    "# ----------------- rectangle clipping & length -----------------\n",
    "\n",
    "INSIDE, LEFT, RIGHT, BOTTOM, TOP = 0, 1, 2, 4, 8\n",
    "\n",
    "def _outcode(x, y, xmin, ymin, xmax, ymax):\n",
    "    code = INSIDE\n",
    "    if x < xmin: code |= LEFT\n",
    "    elif x > xmax: code |= RIGHT\n",
    "    if y < ymin: code |= BOTTOM\n",
    "    elif y > ymax: code |= TOP\n",
    "    return code\n",
    "\n",
    "def _clip_segment(x0, y0, x1, y1, xmin, ymin, xmax, ymax):\n",
    "    c0 = _outcode(x0, y0, xmin, ymin, xmax, ymax)\n",
    "    c1 = _outcode(x1, y1, xmin, ymin, xmax, ymax)\n",
    "    while True:\n",
    "        if not (c0 | c1):\n",
    "            return x0, y0, x1, y1, True\n",
    "        if c0 & c1:\n",
    "            return 0, 0, 0, 0, False\n",
    "        c = c0 or c1\n",
    "        if c & TOP:\n",
    "            x = x0 + (x1 - x0) * (ymax - y0) / (y1 - y0); y = ymax\n",
    "        elif c & BOTTOM:\n",
    "            x = x0 + (x1 - x0) * (ymin - y0) / (y1 - y0); y = ymin\n",
    "        elif c & RIGHT:\n",
    "            y = y0 + (y1 - y0) * (xmax - x0) / (x1 - x0); x = xmax\n",
    "        else:\n",
    "            y = y0 + (y1 - y0) * (xmin - x0) / (x1 - x0); x = xmin\n",
    "        if c == c0:\n",
    "            x0, y0 = x, y; c0 = _outcode(x0, y0, xmin, ymin, xmax, ymax)\n",
    "        else:\n",
    "            x1, y1 = x, y; c1 = _outcode(x1, y1, xmin, ymin, xmax, ymax)\n",
    "\n",
    "def _clip_polylines_to_rect(lines, xmin, ymin, xmax, ymax):\n",
    "    out = []\n",
    "    for P in lines:\n",
    "        if len(P) < 2: continue\n",
    "        cur = []\n",
    "        for i in range(len(P)-1):\n",
    "            x0,y0 = P[i]\n",
    "            x1,y1 = P[i+1]\n",
    "            x0c,y0c,x1c,y1c,ok = _clip_segment(x0,y0,x1,y1,xmin,ymin,xmax,ymax)\n",
    "            if not ok:\n",
    "                if cur: out.append(np.array(cur)); cur=[]\n",
    "                continue\n",
    "            if not cur:\n",
    "                cur = [[x0c,y0c],[x1c,y1c]]\n",
    "            else:\n",
    "                if abs(cur[-1][0]-x0c) > 1e-9 or abs(cur[-1][1]-y0c) > 1e-9:\n",
    "                    out.append(np.array(cur)); cur=[[x0c,y0c],[x1c,y1c]]\n",
    "                else:\n",
    "                    cur.append([x1c,y1c])\n",
    "        if cur: out.append(np.array(cur))\n",
    "    return out\n",
    "\n",
    "def _total_length_mm(lines):\n",
    "    L = 0.0\n",
    "    for P in lines:\n",
    "        if len(P) < 2: continue\n",
    "        d = np.diff(P, axis=0)\n",
    "        L += float(np.sum(np.hypot(d[:,0], d[:,1])))\n",
    "    return L\n",
    "\n",
    "# ----------------- area calculator with shell -----------------\n",
    "\n",
    "def compute_areas_with_shell(width_mm, height_mm,\n",
    "                             z_mm, spacing_mm, density,\n",
    "                             line_width_mm,\n",
    "                             shell_perimeters=2,\n",
    "                             density_adjust=1.0,\n",
    "                             pattern_tolerance_mm=0.5,\n",
    "                             phase_shift_mm=(0.0, 0.0)):\n",
    "    \"\"\"\n",
    "    Returns a dict with:\n",
    "      A_shell, A_infill, A_total, L_infill, lines_infill (clipped), inner_box (x0,y0,W_in,H_in)\n",
    "    \"\"\"\n",
    "    # --- exact rectangular shell area ---\n",
    "    t = shell_perimeters * line_width_mm\n",
    "    W_in = max(0.0, width_mm  - 2.0*t)\n",
    "    H_in = max(0.0, height_mm - 2.0*t)\n",
    "    A_shell = width_mm*height_mm - (W_in * H_in)\n",
    "\n",
    "    # --- generate waves and clip to the inner rectangle ---\n",
    "    if W_in <= 0.0 or H_in <= 0.0:\n",
    "        # no infill area remains\n",
    "        return {\n",
    "            \"A_shell\": A_shell,\n",
    "            \"A_infill\": 0.0,\n",
    "            \"A_total\": A_shell,\n",
    "            \"L_infill\": 0.0,\n",
    "            \"lines_infill\": [],\n",
    "            \"inner_box\": (0.0, 0.0, 0.0, 0.0)\n",
    "        }\n",
    "\n",
    "    # generate full pattern (unclipped)\n",
    "    raw = make_gyroid_waves_prusa(\n",
    "        z_mm, density, spacing_mm, width_mm, height_mm,\n",
    "        density_adjust=density_adjust, pattern_tolerance_mm=pattern_tolerance_mm\n",
    "    )\n",
    "\n",
    "    # Optional small phase shift to avoid degenerate lines exactly on borders\n",
    "    if phase_shift_mm != (0.0, 0.0):\n",
    "        dx, dy = phase_shift_mm\n",
    "        raw = [P + np.array([dx, dy]) for P in raw]\n",
    "\n",
    "    # clip to inner rect [t, t] – [t+W_in, t+H_in]\n",
    "    lines_infill = _clip_polylines_to_rect(raw, t, t, t + W_in, t + H_in)\n",
    "\n",
    "    # area ≈ total extruded length × line width\n",
    "    L_infill = _total_length_mm(lines_infill)\n",
    "    A_infill = L_infill * line_width_mm\n",
    "    A_total  = A_shell + A_infill\n",
    "\n",
    "    return {\n",
    "        \"A_shell\": A_shell,\n",
    "        \"A_infill\": A_infill,\n",
    "        \"A_total\": A_total,\n",
    "        \"L_infill\": L_infill,\n",
    "        \"lines_infill\": lines_infill,\n",
    "        \"inner_box\": (t, t, W_in, H_in)\n",
    "    }\n",
    "\n",
    "# ----------------- example -----------------\n",
    "if __name__ == \"__main__\":\n",
    "    # Your test values:\n",
    "    W = H = 10.0\n",
    "    z = 12\n",
    "    spacing = 0.15\n",
    "    line_w = 0.42\n",
    "    perims = 2\n",
    "\n",
    "    for d in (0.10, 0.15, 0.60):\n",
    "        res = compute_areas_with_shell(\n",
    "            width_mm=W, height_mm=H,\n",
    "            z_mm=z, spacing_mm=spacing, density=d,\n",
    "            line_width_mm=line_w, shell_perimeters=perims,\n",
    "            density_adjust=1.0, pattern_tolerance_mm=0.5,\n",
    "            phase_shift_mm=(0.05, 0.0)  # tiny shift so lines don't land on the frame\n",
    "        )\n",
    "        print(f\"density={d:.2f}  shell={perims}x{line_w:.2f}mm\")\n",
    "        print(f\"  A_shell  = {res['A_shell']:.2f} mm^2\")\n",
    "        print(f\"  L_infill = {res['L_infill']:.2f} mm\")\n",
    "        print(f\"  A_infill = {res['A_infill']:.2f} mm^2\")\n",
    "        print(f\"  A_total  = {res['A_total']:.2f} mm^2\")\n",
    "\n",
    "        # Optional: plot the infill inside the shell\n",
    "        import matplotlib.pyplot as plt\n",
    "        plt.figure(figsize=(5,5))\n",
    "        # draw shell rectangle\n",
    "        t = perims*line_w\n",
    "        plt.plot([t, t+res[\"inner_box\"][2], t+res[\"inner_box\"][2], t, t],\n",
    "                 [t, t, t+res[\"inner_box\"][3], t+res[\"inner_box\"][3], t])\n",
    "        for P in res[\"lines_infill\"]:\n",
    "            if len(P) >= 2:\n",
    "                plt.plot(P[:,0], P[:,1])\n",
    "        plt.xlim(0, W); plt.ylim(0, H); plt.gca().set_aspect('equal'); plt.grid(True)\n",
    "        plt.title(f\"Gyroid infill inside {perims} perimeters, d={d:.2f}\")\n",
    "        plt.xlabel(\"X (mm)\"); plt.ylabel(\"Y (mm)\")\n",
    "        plt.show()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "8e7aaee9-297a-4965-aa76-aaddb440b105",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Infill max area: 14.30 mm² at z = 7.07 mm\n",
      "Infill min area: 9.58 mm² at z = 1.62 mm\n",
      "Shell area: 30.78 mm²\n",
      "Infill area ratio: 49.23 %\n",
      "Infill area ratio: 49.23 %\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 138280 characters]",
      "text/plain": [
       "<Figure size 1000x800 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from scipy.signal import argrelextrema\n",
    "\n",
    "# Parameters (same as before)\n",
    "width = 10.0\n",
    "height = 10.0\n",
    "infill_fraction = 0.15\n",
    "shell_layers = 2\n",
    "line_thickness = 0.42\n",
    "spacing_mm = 0.15  # Prusa “module size”\n",
    "z_heights = np.linspace(0, 20, 100)\n",
    "\n",
    "# Data storage\n",
    "areas = []\n",
    "shell_areas = []\n",
    "infill_areas = []\n",
    "lengths = []\n",
    "\n",
    "# Loop using Prusa model\n",
    "for z in z_heights:\n",
    "    result = compute_areas_with_shell(\n",
    "        width_mm=width,\n",
    "        height_mm=height,\n",
    "        z_mm=z,\n",
    "        spacing_mm=spacing_mm,\n",
    "        density=infill_fraction,\n",
    "        line_width_mm=line_thickness,\n",
    "        shell_perimeters=shell_layers,\n",
    "        density_adjust=1.0,\n",
    "        pattern_tolerance_mm=0.5,\n",
    "        phase_shift_mm=(0.05, 0.0)\n",
    "    )\n",
    "\n",
    "    areas.append(result[\"A_total\"])\n",
    "    shell_areas.append(result[\"A_shell\"])\n",
    "    infill_areas.append(result[\"A_infill\"])\n",
    "    lengths.append(result[\"L_infill\"])\n",
    "\n",
    "# Convert to NumPy for analysis\n",
    "z_np = np.array(z_heights)\n",
    "areas_np = np.array(areas)\n",
    "shell_areas_np = np.array(shell_areas)\n",
    "infill_areas_np = np.array(infill_areas)\n",
    "lengths_np = np.array(lengths)\n",
    "\n",
    "# Analyze\n",
    "i_max = np.argmax(infill_areas_np)\n",
    "i_min = np.argmin(infill_areas_np)\n",
    "print(f\"Infill max area: {infill_areas_np[i_max]:.2f} mm² at z = {z_np[i_max]:.2f} mm\")\n",
    "print(f\"Infill min area: {infill_areas_np[i_min]:.2f} mm² at z = {z_np[i_min]:.2f} mm\")\n",
    "print(f\"Shell area: {shell_areas_np[i_min]:.2f} mm²\")\n",
    "#infill_peak_area_ratio = (infill_areas_np[i_max] / infill_areas_np[i_min] - 1) * 100\n",
    "if infill_areas_np[i_min] > 1e-6:\n",
    "    infill_peak_area_ratio = (infill_areas_np[i_max] / infill_areas_np[i_min] - 1) * 100\n",
    "    print(f\"Infill area ratio: {infill_peak_area_ratio:.2f} %\")\n",
    "else:\n",
    "    print(\"Infill min area is zero — cannot compute ratio.\")\n",
    "\n",
    "print(f\"Infill area ratio: {infill_peak_area_ratio:.2f} %\")\n",
    "\n",
    "# Plot\n",
    "fig, axs = plt.subplots(2, 1, figsize=(10, 8), sharex=True)\n",
    "\n",
    "axs[0].plot(z_np, areas_np, label=\"Total Area\", linewidth=2)\n",
    "axs[0].plot(z_np, shell_areas_np, label=\"Shell Area\", linestyle='--')\n",
    "axs[0].plot(z_np, infill_areas_np, label=\"Infill Area\", linestyle='-.')\n",
    "axs[0].set_ylabel(\"Area (mm²)\")\n",
    "axs[0].legend()\n",
    "axs[0].set_title(f\"Prusa Gyroid Area vs Z Height (density={infill_fraction:.2f})\")\n",
    "\n",
    "axs[1].plot(z_np, lengths_np, label=\"Infill Line Length\", linewidth=2)\n",
    "axs[1].set_ylabel(\"Length (mm)\")\n",
    "axs[1].set_xlabel(\"Z Height (mm)\")\n",
    "axs[1].set_title(\"Infill Line Length vs Z Height\")\n",
    "axs[1].legend()\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.grid(True)\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "917f72f0-a1b9-43dd-8294-bc40df50411a",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
