{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "ce1204dd-7916-4b17-a70d-61857cb0a2ca",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 20612 characters]",
      "text/plain": [
       "<Figure size 600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "==== Grid Infill 0.0% ====\n",
      "Total Area            = 31.1879 mm²\n",
      "Shell Area            = 31.1879 mm²\n",
      "Infill Area           = 0.0000 mm²\n",
      "I_x                   = 440.0430\n",
      "I_y                   = 440.0430\n",
      "I_xy                  = -0.0000\n",
      "Polar moment, J       = 880.0860\n",
      "\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 24544 characters]",
      "text/plain": [
       "<Figure size 600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "==== Grid Infill 15.0% ====\n",
      "Total Area            = 41.5764 mm²\n",
      "Shell Area            = 31.1879 mm²\n",
      "Infill Area           = 10.3885 mm²\n",
      "I_x                   = 507.1439\n",
      "I_y                   = 507.1439\n",
      "I_xy                  = -0.0000\n",
      "Polar moment, J       = 1014.2879\n",
      "\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 28980 characters]",
      "text/plain": [
       "<Figure size 600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "==== Grid Infill 30.0% ====\n",
      "Total Area            = 52.1754 mm²\n",
      "Shell Area            = 31.1879 mm²\n",
      "Infill Area           = 20.9874 mm²\n",
      "I_x                   = 567.1050\n",
      "I_y                   = 567.1050\n",
      "I_xy                  = 0.0000\n",
      "Polar moment, J       = 1134.2101\n",
      "\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 34380 characters]",
      "text/plain": [
       "<Figure size 600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "==== Grid Infill 60.0% ====\n",
      "Total Area            = 71.9147 mm²\n",
      "Shell Area            = 31.1879 mm²\n",
      "Infill Area           = 40.7268 mm²\n",
      "I_x                   = 671.7147\n",
      "I_y                   = 671.7147\n",
      "I_xy                  = -0.0000\n",
      "Polar moment, J       = 1343.4294\n",
      "\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 20764 characters]",
      "text/plain": [
       "<Figure size 600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "==== Grid Infill 100.0% ====\n",
      "Total Area            = 100.2505 mm²\n",
      "Shell Area            = 31.1879 mm²\n",
      "Infill Area           = 69.0625 mm²\n",
      "I_x                   = 837.5117\n",
      "I_y                   = 837.5117\n",
      "I_xy                  = -0.0000\n",
      "Polar moment, J       = 1675.0235\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# ------------------------------\n",
    "# Geometry utilities\n",
    "# ------------------------------\n",
    "\n",
    "def _dist_mod(u, spacing):\n",
    "    \"\"\"Distance to nearest line center in a periodic family with pitch=spacing.\"\"\"\n",
    "    r = np.mod(u, spacing)\n",
    "    return np.minimum(r, spacing - r)\n",
    "\n",
    "def _rectilinear_mask(XX, YY, spacing, line_width, angle_deg=0.0, phase=0.0):\n",
    "    \"\"\"\n",
    "    Rectilinear (parallel lines) mask at angle_deg, with true line width.\n",
    "    Lines are centered where (x cos + y sin + phase) is a multiple of spacing.\n",
    "    \"\"\"\n",
    "    th = np.deg2rad(angle_deg)\n",
    "    # Coordinate along the line-normal direction (u-axis)\n",
    "    u =  XX * np.cos(th) + YY * np.sin(th)\n",
    "    dist = _dist_mod(u + phase, spacing)\n",
    "    # Render a line of width 'line_width' around the center => half width threshold\n",
    "    return dist <= (line_width / 2.0)\n",
    "\n",
    "def _spacing_for_grid_density(line_width, f):\n",
    "    \"\"\"\n",
    "    For a two-axis grid (two orthogonal families of rectilinear lines with true width 'w'),\n",
    "    the area fraction is f = 2r - r^2 where r = w/s.\n",
    "    Solve for s: r = 1 - sqrt(1 - f)  =>  s = w / r.\n",
    "    \"\"\"\n",
    "    f = float(np.clip(f, 0.0, 1.0))\n",
    "    if f <= 0.0:\n",
    "        return np.inf\n",
    "    if f >= 1.0:\n",
    "        return line_width  # centers are one line_width apart for a \"solid\" raster\n",
    "    r = 1.0 - np.sqrt(1.0 - f)\n",
    "    return line_width / r\n",
    "\n",
    "# ------------------------------\n",
    "# Area & inertia on a raster mask\n",
    "# ------------------------------\n",
    "\n",
    "def compute_area_moments(XX, YY, mask):\n",
    "    x_vals = XX[mask]\n",
    "    y_vals = YY[mask]\n",
    "\n",
    "    if x_vals.size == 0:\n",
    "        return {\"A\": 0.0, \"x_c\": 0.0, \"y_c\": 0.0, \"I_x\": 0.0, \"I_y\": 0.0, \"I_xy\": 0.0, \"J\": 0.0}\n",
    "\n",
    "    dx = XX[0, 1] - XX[0, 0]\n",
    "    dy = YY[1, 0] - YY[0, 0]\n",
    "    dA = dx * dy\n",
    "\n",
    "    A   = x_vals.size * dA\n",
    "    x_c = float(np.mean(x_vals))\n",
    "    y_c = float(np.mean(y_vals))\n",
    "\n",
    "    x_shift = x_vals - x_c\n",
    "    y_shift = y_vals - y_c\n",
    "\n",
    "    I_x = float(np.sum(y_shift**2) * dA)\n",
    "    I_y = float(np.sum(x_shift**2) * dA)\n",
    "    I_xy = float(np.sum(x_shift * y_shift) * dA)\n",
    "    J   = I_x + I_y\n",
    "\n",
    "    return {\"A\": A, \"x_c\": x_c, \"y_c\": y_c, \"I_x\": I_x, \"I_y\": I_y, \"I_xy\": I_xy, \"J\": J}\n",
    "\n",
    "# ------------------------------\n",
    "# Main: Prusa-style rectilinear/grid with shells\n",
    "# ------------------------------\n",
    "\n",
    "def prusa_style_grid_or_rectilinear(\n",
    "    width, height,\n",
    "    infill_fraction,\n",
    "    line_width=0.42,\n",
    "    shell_layers=2,\n",
    "    base_infill_angle_deg=45.0,\n",
    "    grid=True,                 # True = \"Grid\" (two passes 90° apart in same layer). False = Rectilinear (one pass).\n",
    "    z_height=0.0,              # used for optional phase shift if desired\n",
    "    phase_per_mm=0.0,          # set >0 to phase-shift infill vs Z (like alternating)\n",
    "    N=800,\n",
    "    plot=True,\n",
    "    verbose=True\n",
    "):\n",
    "    \"\"\"\n",
    "    width, height: mm, outer dimensions of a rectangle (centered at origin)\n",
    "    infill_fraction: 0..1\n",
    "    line_width: true extrusion width (mm)\n",
    "    shell_layers: number of perimeters (each line_width wide)\n",
    "    base_infill_angle_deg: angle of first rectilinear pass\n",
    "    grid: True => add a second pass at +90° (Prusa \"Grid\"); False => single rectilinear pass\n",
    "    z_height: current Z (mm)\n",
    "    phase_per_mm: optional phase (mm) added along 'u' per mm Z (0 disables shifting)\n",
    "    \"\"\"\n",
    "\n",
    "    # Grid for rasterization\n",
    "    xs = np.linspace(-width/2,  width/2,  N)\n",
    "    ys = np.linspace(-height/2, height/2, N)\n",
    "    XX, YY = np.meshgrid(xs, ys)\n",
    "\n",
    "    # ----- Shells as true lines (perimeter loops), centered inside by 0.5*w\n",
    "    shell_mask = np.zeros_like(XX, dtype=bool)\n",
    "    for i in range(shell_layers):\n",
    "        off = (i + 0.5) * line_width  # center each perimeter inside the outline\n",
    "\n",
    "        # vertical walls: x = -W/2+off, x = +W/2-off\n",
    "        shell_mask |= np.abs(XX - (-width/2  + off)) <= (line_width / 2)\n",
    "        shell_mask |= np.abs(XX - ( +width/2  - off)) <= (line_width / 2)\n",
    "\n",
    "        # horizontal walls: y = -H/2+off, y = +H/2-off\n",
    "        shell_mask |= np.abs(YY - (-height/2 + off)) <= (line_width / 2)\n",
    "        shell_mask |= np.abs(YY - ( +height/2 - off)) <= (line_width / 2)\n",
    "\n",
    "    # Inner rectangle where infill is allowed\n",
    "    inner_offset = shell_layers * line_width\n",
    "    inner_rect = (\n",
    "        (np.abs(XX) <= (width/2  - inner_offset)) &\n",
    "        (np.abs(YY) <= (height/2 - inner_offset))\n",
    "    )\n",
    "\n",
    "    # ----- Infill\n",
    "    if infill_fraction <= 0.0:\n",
    "        infill_mask = np.zeros_like(XX, dtype=bool)\n",
    "        spacing = np.inf\n",
    "    elif infill_fraction >= 1.0:\n",
    "        # \"solid\" raster in both directions\n",
    "        spacing = line_width\n",
    "        masks = []\n",
    "        angles = [base_infill_angle_deg] + ([base_infill_angle_deg + 90.0] if grid else [])\n",
    "        phase = phase_per_mm * z_height\n",
    "        for a in angles:\n",
    "            masks.append(_rectilinear_mask(XX, YY, spacing, line_width, angle_deg=a, phase=phase))\n",
    "        infill_mask = np.logical_or.reduce(masks) & inner_rect\n",
    "    else:\n",
    "        spacing = _spacing_for_grid_density(line_width, infill_fraction) if grid \\\n",
    "                  else line_width / infill_fraction  # 1-axis rectilinear density\n",
    "        masks = []\n",
    "        angles = [base_infill_angle_deg] + ([base_infill_angle_deg + 90.0] if grid else [])\n",
    "        phase = phase_per_mm * z_height\n",
    "        for a in angles:\n",
    "            masks.append(_rectilinear_mask(XX, YY, spacing, line_width, angle_deg=a, phase=phase))\n",
    "        infill_mask = np.logical_or.reduce(masks) & inner_rect\n",
    "\n",
    "    final_mask = shell_mask | infill_mask\n",
    "\n",
    "    # ----- Plot\n",
    "    if plot:\n",
    "        plt.figure(figsize=(6, 6))\n",
    "        img = np.where(final_mask, 1.0, np.nan)\n",
    "        plt.imshow(img, origin='lower',\n",
    "                   extent=[-width/2, width/2, -height/2, height/2],\n",
    "                   interpolation='nearest')\n",
    "        title = \"Grid\" if grid else \"Rectilinear\"\n",
    "        plt.title(f\"{title} @ {infill_fraction*100:.1f}%  |  shells={shell_layers}×{line_width:.2f}  angle={base_infill_angle_deg:.0f}°\")\n",
    "        plt.xlabel(\"X (mm)\")\n",
    "        plt.ylabel(\"Y (mm)\")\n",
    "        plt.gca().set_aspect('equal', 'box')\n",
    "        plt.grid(True)\n",
    "        plt.show()\n",
    "\n",
    "    # ----- Numbers\n",
    "    if verbose:\n",
    "        total = compute_area_moments(XX, YY, final_mask)\n",
    "        shells = compute_area_moments(XX, YY, shell_mask)\n",
    "        A_infill = total[\"A\"] - shells[\"A\"]\n",
    "        print(f\"==== {('Grid' if grid else 'Rectilinear')} Infill {infill_fraction*100:.1f}% ====\")\n",
    "        print(f\"Total Area            = {total['A']:.4f} mm²\")\n",
    "        print(f\"Shell Area            = {shells['A']:.4f} mm²\")\n",
    "        print(f\"Infill Area           = {A_infill:.4f} mm²\")\n",
    "        print(f\"I_x                   = {total['I_x']:.4f}\")\n",
    "        print(f\"I_y                   = {total['I_y']:.4f}\")\n",
    "        print(f\"I_xy                  = {total['I_xy']:.4f}\")\n",
    "        print(f\"Polar moment, J       = {total['J']:.4f}\")\n",
    "        print()\n",
    "\n",
    "    return final_mask  # boolean raster of the slice\n",
    "\n",
    "# ------------------------------\n",
    "# Demo\n",
    "# ------------------------------\n",
    "if __name__ == \"__main__\":\n",
    "    W = 10.0\n",
    "    H = 10.0\n",
    "    shells = 2\n",
    "    wline  = 0.42\n",
    "\n",
    "    # Try a few densities like a slicer preview\n",
    "    for f in [0.00, 0.15, 0.30, 0.60, 1.00]:\n",
    "        prusa_style_grid_or_rectilinear(\n",
    "            width=W, height=H,\n",
    "            infill_fraction=f,\n",
    "            line_width=wline,\n",
    "            shell_layers=shells,\n",
    "            base_infill_angle_deg=45.0,  # typical default\n",
    "            grid=True,                   # True => \"Grid\", False => single rectilinear\n",
    "            z_height=0.0,                # set >0 and phase_per_mm>0 to shift with Z\n",
    "            phase_per_mm=0.0,\n",
    "            N=800,\n",
    "            plot=True,\n",
    "            verbose=True\n",
    "        )\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "9514c26c-f86b-475a-98cb-68aa20f27ac5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Infill max area: 30.99 mm² at z = 0.00 mm\n",
      "Infill min area: 30.99 mm² at z = 0.00 mm\n",
      "Shell area (at min): 30.84 mm²\n",
      "Infill area ratio: 0.00 %\n",
      "Total area ratio: 0.00 %\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 61968 characters]",
      "text/plain": [
       "<Figure size 1000x800 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# --- Analysis block: Area & Moments vs Height for Prusa-style Grid ---\n",
    "\n",
    "from scipy.signal import argrelextrema\n",
    "\n",
    "# Parameters\n",
    "width = 10.0\n",
    "height = 10.0\n",
    "infill_fraction = 0.44\n",
    "shell_layers = 2\n",
    "line_thickness = 0.42\n",
    "base_infill_angle_deg = 45.0\n",
    "use_grid = True          # True => Grid (two passes 90° apart in same layer)\n",
    "phase_per_mm = 0.0       # set >0 to shift infill vs Z; 0 keeps pattern fixed\n",
    "N = 1000                 # raster resolution (match function call)\n",
    "\n",
    "z_heights = np.linspace(0, 20, 100)\n",
    "\n",
    "# Utility: rebuild the *same* shell mask used by the generator\n",
    "def build_shell_line_mask(XX, YY, width, height, line_width, shell_layers):\n",
    "    shell_mask = np.zeros_like(XX, dtype=bool)\n",
    "    for i in range(shell_layers):\n",
    "        off = (i + 0.5) * line_width  # center each perimeter inside the outline\n",
    "\n",
    "        # vertical walls: x = -W/2+off, x = +W/2-off\n",
    "        shell_mask |= np.abs(XX - (-width/2  + off)) <= (line_width / 2)\n",
    "        shell_mask |= np.abs(XX - ( +width/2  - off)) <= (line_width / 2)\n",
    "\n",
    "        # horizontal walls: y = -H/2+off, y = +H/2-off\n",
    "        shell_mask |= np.abs(YY - (-height/2 + off)) <= (line_width / 2)\n",
    "        shell_mask |= np.abs(YY - ( +height/2 - off)) <= (line_width / 2)\n",
    "    return shell_mask\n",
    "\n",
    "areas = []\n",
    "shell_areas = []\n",
    "infill_areas = []\n",
    "I_xs = []\n",
    "I_ys = []\n",
    "Js = []\n",
    "\n",
    "for z in z_heights:\n",
    "    # Generate the infill/shell mask for this Z (plot/verbose disabled)\n",
    "    mask = prusa_style_grid_or_rectilinear(\n",
    "        width=width, height=height,\n",
    "        infill_fraction=infill_fraction,\n",
    "        line_width=line_thickness,\n",
    "        shell_layers=shell_layers,\n",
    "        base_infill_angle_deg=base_infill_angle_deg,\n",
    "        grid=use_grid,\n",
    "        z_height=z,\n",
    "        phase_per_mm=phase_per_mm,\n",
    "        N=N,\n",
    "        plot=False,\n",
    "        verbose=False\n",
    "    )\n",
    "\n",
    "    # Rebuild the *same* raster grid used inside the function (match mask shape)\n",
    "    Ny, Nx = mask.shape\n",
    "    xs = np.linspace(-width / 2,  width / 2,  Nx)\n",
    "    ys = np.linspace(-height / 2, height / 2, Ny)\n",
    "    XX, YY = np.meshgrid(xs, ys)\n",
    "\n",
    "    # Build the *same* shell-line mask for correct shell area\n",
    "    shell_mask = build_shell_line_mask(XX, YY, width, height, line_thickness, shell_layers)\n",
    "\n",
    "    # Area/moments\n",
    "    mtot = compute_area_moments(XX, YY, mask)\n",
    "    mshell = compute_area_moments(XX, YY, shell_mask)\n",
    "\n",
    "    A_total = mtot[\"A\"]\n",
    "    A_shell = mshell[\"A\"]\n",
    "    A_infill = A_total - A_shell\n",
    "\n",
    "    areas.append(A_total)\n",
    "    shell_areas.append(A_shell)\n",
    "    infill_areas.append(A_infill)\n",
    "    I_xs.append(mtot[\"I_x\"])\n",
    "    I_ys.append(mtot[\"I_y\"])\n",
    "    Js.append(mtot[\"J\"])\n",
    "\n",
    "# Convert to NumPy for analysis\n",
    "z_np = np.asarray(z_heights)\n",
    "areas_np = np.asarray(areas)\n",
    "\n",
    "# Local maxima/minima (may be empty if phase_per_mm=0 gives flat lines)\n",
    "max_indices = argrelextrema(areas_np, np.greater)[0]\n",
    "min_indices = argrelextrema(areas_np, np.less)[0]\n",
    "\n",
    "# Global max/min\n",
    "i_max = int(np.argmax(areas_np))\n",
    "i_min = int(np.argmin(areas_np))\n",
    "print(f\"Infill max area: {infill_areas[i_max]:.2f} mm² at z = {z_np[i_max]:.2f} mm\")\n",
    "print(f\"Infill min area: {infill_areas[i_min]:.2f} mm² at z = {z_np[i_min]:.2f} mm\")\n",
    "print(f\"Shell area (at min): {shell_areas[i_min]:.2f} mm²\")\n",
    "\n",
    "infill_peak_area_ratio = (infill_areas[i_max] / max(infill_areas[i_min], 1e-12) - 1) * 100\n",
    "print(f\"Infill area ratio: {infill_peak_area_ratio:.2f} %\")\n",
    "\n",
    "total_peak_area_ratio = (areas_np[i_max] / max(areas_np[i_min], 1e-12) - 1) * 100\n",
    "print(f\"Total area ratio: {total_peak_area_ratio:.2f} %\")\n",
    "\n",
    "# -------------------\n",
    "# Plotting\n",
    "# -------------------\n",
    "fig, axs = plt.subplots(2, 1, figsize=(10, 8), sharex=True)\n",
    "\n",
    "# Area plot\n",
    "axs[0].plot(z_np, areas_np, label=\"Total Area\", linewidth=2)\n",
    "axs[0].plot(z_np, shell_areas, label=\"Shell Area\", linestyle='--')\n",
    "axs[0].plot(z_np, infill_areas, label=\"Infill Area\", linestyle='-.')\n",
    "\n",
    "# Scatter markers (if any)\n",
    "if max_indices.size:\n",
    "    axs[0].scatter(z_np[max_indices], areas_np[max_indices], color='red', label=\"Local Max\", zorder=5)\n",
    "if min_indices.size:\n",
    "    axs[0].scatter(z_np[min_indices], areas_np[min_indices], color='blue', label=\"Local Min\", zorder=5)\n",
    "\n",
    "axs[0].set_ylabel(\"Area (mm²)\")\n",
    "axs[0].legend()\n",
    "title_name = \"Grid\" if use_grid else \"Rectilinear\"\n",
    "axs[0].set_title(f\"Area vs Height for {infill_fraction*100:.1f}% {title_name} Infill\")\n",
    "\n",
    "# Moments plot\n",
    "axs[1].plot(z_np, I_xs, label=\"I_x\", linewidth=2)\n",
    "axs[1].plot(z_np, I_ys, label=\"I_y\", linestyle='--')\n",
    "axs[1].plot(z_np, Js, label=\"Polar moment J\", linestyle='-.')\n",
    "axs[1].set_xlabel(\"Z Height (mm)\")\n",
    "axs[1].set_ylabel(\"Moment of Inertia\")\n",
    "axs[1].legend()\n",
    "axs[1].set_title(\"Moments of Inertia vs Height\")\n",
    "\n",
    "plt.tight_layout()\n",
    "axs[1].tick_params(axis='x', labelrotation=0, labelsize=10)\n",
    "axs[1].set_xticks(np.linspace(z_np.min(), z_np.max(), 11))\n",
    "plt.grid(True)\n",
    "plt.show()\n"
   ]
  }
 ],
 "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
}
