Contents in this wiki are for entertainment purposes only
This is not fiction ∞ this is psience of mind

Talk:Claude Code on Torus Knot Winding Form (FreeCAD)

From Catcliffe Development
Jump to navigation Jump to search


In Development

(Python runs inside FreeCAD to provide coding against the FreeCAD object model exposed to the Python environment.)
 
Local: C:\Users\XenoEngineer\AppData\Roaming\FreeCAD\Macro

XenoEngineer —research and development 18:42, 16 January 2026 (UTC)

FreeCAD 
# Torus Knot Groove Generator for FreeCAD - v3.4
# Centralized defaults + FreeCAD object model notes

import FreeCAD as App
import FreeCADGui as Gui
import Part
import math
from FreeCAD import Vector
from PySide import QtCore, QtGui

# =============================================================================
# GOLDEN RATIO CONSTANTS
# =============================================================================
PHI = (1 + math.sqrt(5)) / 2      # ~1.618033988749895
PHI_4 = PHI ** 4                   # ~6.854101966249685
PHI_4_MINUS_1 = PHI_4 - 1          # ~5.854101966249685

# =============================================================================
# DEFAULT PARAMETERS - Edit these for quick testing
# =============================================================================
DEFAULTS = {
  # Knot topology
  'P': 3,                     # Toroidal windings (through hole)
  'Q': 2,                     # Poloidal windings (around tube)
  'phases': 1,                # Number of parallel windings

  # Scale (minor radius drives everything via golden ratio)
  'minor_radius': 200.0,      # r in mm - tube radius

  # Groove
  'groove_diameter': 9 		  # 3.175,   # 1/8 inch copper tubing OD
  'steps': 400,               # Path resolution (more = smoother)

  # Output options
  'do_boolean': False,        # Skip boolean for fast preview
  'hide_base': True,          # Hide base torus after boolean
}

# =============================================================================
# FREECAD OBJECT MODEL NOTES
# =============================================================================
"""
FreeCAD Architecture:

1. App (FreeCAD module)
 - App.ActiveDocument: Current document (like a file)
 - App.newDocument("Name"): Create new document
 - Documents contain Objects

2. Document Objects (doc.addObject)
 - "Part::Feature": A geometric shape container
 - obj.Shape: The actual geometry (Part.Shape)
 - obj.Name: Internal identifier
 - obj.Label: Display name (can have spaces)

3. Part Module (geometry kernel - wraps OpenCASCADE)
 - Part.makeTorus(R, r): Solid torus
 - Part.makeCircle(radius, center, normal): Circle edge
 - Part.Wire([edges]): Connect edges into a wire
 - Part.BSplineCurve(): Parametric curve
 - shape.cut(tool): Boolean subtraction
 - shape.fuse(other): Boolean union

4. Shape Types (hierarchy)
 - Solid: 3D volume (what we print)
 - Shell: Surface boundary of a solid
 - Face: Single surface
 - Wire: Connected edges (our knot paths)
 - Edge: Single curve segment
 - Vertex: Point

5. Gui (FreeCADGui module)
 - Gui.ActiveDocument: GUI side of document
 - obj.ViewObject: Visual properties (color, visibility)
 - Gui.updateGui(): Force display refresh
 - Gui.SendMsgToActiveView("ViewFit"): Zoom to fit

6. Sweep Operations
 - wire.makePipe(profile): Simple sweep
 - wire.makePipeShell([profiles], solid, frenet): Advanced sweep
 - Frenet frame: Profile orientation follows curve curvature
"""

# =============================================================================
# DIALOG CLASS
# =============================================================================
class TorusKnotDialog(QtGui.QDialog):
  def __init__(self, parent=None):
      super(TorusKnotDialog, self).__init__(parent)
      self.setWindowTitle("3-Phase Golden Quartic Torus Knot")
      self.setMinimumWidth(400)
      self.init_ui()
      self.setup_defaults()  # Apply defaults after UI is built

  def init_ui(self):
      layout = QtGui.QVBoxLayout(self)

      # Info header
      info = QtGui.QLabel(
          "Golden Quartic Ratio:\n"
          "  R = phi^4 units,  r = phi^4-1 units\n"
          "  Hole:Major = 1 : 6.8541"
      )
      info.setStyleSheet("color: #555; font-style: italic; margin-bottom: 10px;")
      layout.addWidget(info)

      # --- Knot Parameters ---
      knot_group = QtGui.QGroupBox("Knot Topology")
      knot_layout = QtGui.QFormLayout(knot_group)

      self.spin_p = QtGui.QSpinBox()
      self.spin_p.setRange(1, 50)
      self.spin_p.setToolTip("Toroidal windings - times through the hole")
      knot_layout.addRow("P (toroidal):", self.spin_p)

      self.spin_q = QtGui.QSpinBox()
      self.spin_q.setRange(1, 50)
      self.spin_q.setToolTip("Poloidal windings - times around the tube")
      knot_layout.addRow("Q (poloidal):", self.spin_q)

      self.spin_phases = QtGui.QSpinBox()
      self.spin_phases.setRange(1, 6)
      self.spin_phases.setToolTip("Parallel windings for multi-phase drive")
      knot_layout.addRow("Phases:", self.spin_phases)

      layout.addWidget(knot_group)

      # --- Scale (Golden Ratio Locked) ---
      scale_group = QtGui.QGroupBox("Scale (Golden Quartic Locked)")
      scale_layout = QtGui.QFormLayout(scale_group)

      self.spin_minor = QtGui.QDoubleSpinBox()
      self.spin_minor.setRange(10.0, 500.0)
      self.spin_minor.setDecimals(3)
      self.spin_minor.setSuffix(" mm")
      self.spin_minor.setToolTip("Minor radius (r) - sets overall scale")
      self.spin_minor.valueChanged.connect(self.update_calculated_values)
      scale_layout.addRow("Minor Radius (r):", self.spin_minor)

      self.label_major = QtGui.QLabel()
      scale_layout.addRow("Major Radius (R):", self.label_major)

      self.label_hole = QtGui.QLabel()
      scale_layout.addRow("Hole Radius:", self.label_hole)

      self.label_outer = QtGui.QLabel()
      scale_layout.addRow("Outer Diameter:", self.label_outer)

      layout.addWidget(scale_group)

      # --- Groove Parameters ---
      groove_group = QtGui.QGroupBox("Groove Parameters")
      groove_layout = QtGui.QFormLayout(groove_group)

      self.spin_groove = QtGui.QDoubleSpinBox()
      self.spin_groove.setRange(0.5, 20.0)
      self.spin_groove.setDecimals(3)
      self.spin_groove.setSuffix(" mm")
      self.spin_groove.setToolTip("Diameter of groove (match tubing OD)")
      groove_layout.addRow("Groove Diameter:", self.spin_groove)

      self.spin_steps = QtGui.QSpinBox()
      self.spin_steps.setRange(100, 3000)
      self.spin_steps.setSingleStep(50)
      self.spin_steps.setToolTip("Path points - higher = smoother but slower")
      groove_layout.addRow("Path Steps:", self.spin_steps)

      layout.addWidget(groove_group)

      # --- Output Options ---
      output_group = QtGui.QGroupBox("Output Options")
      output_layout = QtGui.QVBoxLayout(output_group)

      self.check_boolean = QtGui.QCheckBox("Perform Boolean Cut (slow)")
      self.check_boolean.setToolTip(
          "OFF: Fast preview - shows torus + groove solids separately\n"
          "ON: Cuts grooves into torus (can take minutes at full scale)"
      )
      output_layout.addWidget(self.check_boolean)

      self.check_hide_base = QtGui.QCheckBox("Hide base torus after cut")
      output_layout.addWidget(self.check_hide_base)

      layout.addWidget(output_group)

      # --- Buttons ---
      button_box = QtGui.QDialogButtonBox(
          QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
      )
      button_box.accepted.connect(self.accept)
      button_box.rejected.connect(self.reject)
      layout.addWidget(button_box)

  def setup_defaults(self):
      """Apply DEFAULTS dict to all dialog widgets."""
      self.spin_p.setValue(DEFAULTS['P'])
      self.spin_q.setValue(DEFAULTS['Q'])
      self.spin_phases.setValue(DEFAULTS['phases'])
      self.spin_minor.setValue(DEFAULTS['minor_radius'])
      self.spin_groove.setValue(DEFAULTS['groove_diameter'])
      self.spin_steps.setValue(DEFAULTS['steps'])
      self.check_boolean.setChecked(DEFAULTS['do_boolean'])
      self.check_hide_base.setChecked(DEFAULTS['hide_base'])
      self.update_calculated_values()

  def update_calculated_values(self):
      """Recalculate derived dimensions from minor radius."""
      r = self.spin_minor.value()
      unit = r / PHI_4_MINUS_1          # 1 unit in mm
      R = PHI_4 * unit                   # Major radius
      hole_radius = unit                 # Hole = 1 unit
      outer_diameter = 2 * (R + r)       # Total OD

      self.label_major.setText(f"{R:.3f} mm")
      self.label_hole.setText(f"{hole_radius:.3f} mm")
      self.label_outer.setText(f"{outer_diameter:.3f} mm")

  def get_parameters(self):
      """Collect all parameters into a dict for the generator."""
      r = self.spin_minor.value()
      unit = r / PHI_4_MINUS_1
      R = PHI_4 * unit

      return {
          'P': self.spin_p.value(),
          'Q': self.spin_q.value(),
          'phases': self.spin_phases.value(),
          'major_radius': R,
          'minor_radius': r,
          'groove_diameter': self.spin_groove.value(),
          'steps': self.spin_steps.value(),
          'do_boolean': self.check_boolean.isChecked(),
          'hide_base': self.check_hide_base.isChecked()
      }


# =============================================================================
# SWEEP HELPER - Tries multiple methods for robustness
# =============================================================================
def sweep_with_fallbacks(profile_wire, path_wire):
  """
  Attempt multiple sweep strategies.
  Returns a Solid shape, or None if all fail.

  FreeCAD sweep methods:
  - makePipeShell: Most control, can fail on complex paths
  - makePipe: Simpler, sometimes more robust
  - Frenet vs non-Frenet: How profile orientation is computed
  """
  methods = [
      ("makePipeShell Frenet+Solid",
       lambda: path_wire.makePipeShell([profile_wire], True, True)),
      ("makePipeShell Frenet+Shell->Solid",
       lambda: Part.makeSolid(path_wire.makePipeShell([profile_wire], False, True))),
      ("makePipe",
       lambda: path_wire.makePipe(profile_wire)),
      ("makePipeShell NonFrenet",
       lambda: path_wire.makePipeShell([profile_wire], True, False)),
  ]

  for name, method in methods:
      try:
          App.Console.PrintMessage(f"    Trying {name}...\n")
          shape = method()
          if shape is None:
              continue
          if shape.ShapeType == "Solid":
              App.Console.PrintMessage(f"    Success: {name}\n")
              return shape
          if shape.ShapeType in ("Shell", "Compound"):
              solid = Part.makeSolid(shape)
              if solid.ShapeType == "Solid":
                  App.Console.PrintMessage(f"    Success: {name} + makeSolid\n")
                  return solid
      except Exception as e:
          App.Console.PrintMessage(f"    {name} failed: {e}\n")

  return None


# =============================================================================
# MAIN GENERATOR FUNCTION
# =============================================================================
def create_torus_knot_grooves(params):
  """
  Generate a torus with parallel knot grooves.

  FreeCAD workflow:
  1. Create/clear document
  2. Build base torus (Part.makeTorus)
  3. For each phase:
     a. Generate knot path points
     b. Create BSpline curve -> Wire
     c. Create circular profile at path start
     d. Sweep profile along path -> Solid
  4. Optional: Boolean cut grooves from torus
  """
  P = params['P']
  Q = params['Q']
  PHASES = params['phases']
  MAJOR_RADIUS = params['major_radius']
  MINOR_RADIUS = params['minor_radius']
  GROOVE_DIAMETER = params['groove_diameter']
  STEPS = params['steps']
  DO_BOOLEAN = params['do_boolean']
  HIDE_BASE = params['hide_base']

  App.Console.PrintMessage("=" * 50 + "\n")
  App.Console.PrintMessage("Torus Knot Generator v3.4\n")
  App.Console.PrintMessage("=" * 50 + "\n")
  App.Console.PrintMessage(f"Knot: ({P},{Q}) x {PHASES} phases\n")
  App.Console.PrintMessage(f"Torus: R={MAJOR_RADIUS:.2f}mm, r={MINOR_RADIUS:.2f}mm\n")
  App.Console.PrintMessage(f"Groove: {GROOVE_DIAMETER}mm, Steps: {STEPS}\n")
  App.Console.PrintMessage(f"Boolean: {'YES' if DO_BOOLEAN else 'NO (preview)'}\n")
  App.Console.PrintMessage("-" * 50 + "\n")

  # --- Document setup ---
  # FreeCAD keeps geometry in Documents. Each document has Objects.
  if App.ActiveDocument is None:
      doc = App.newDocument("GoldenTorusKnot")
  else:
      doc = App.ActiveDocument
      # Clear existing objects for fresh run
      for obj in doc.Objects:
          doc.removeObject(obj.Name)
  doc.recompute()
  Gui.updateGui()

  # --- Create base torus ---
  # Part.makeTorus(majorRadius, minorRadius) returns a Solid
  App.Console.PrintMessage("Creating base torus...\n")
  base_torus_shape = Part.makeTorus(MAJOR_RADIUS, MINOR_RADIUS)

  # Wrap shape in a document object so it appears in the tree
  base_torus = doc.addObject("Part::Feature", "BaseTorus")
  base_torus.Shape = base_torus_shape

  # ViewObject controls visual properties
  if not DO_BOOLEAN:
      base_torus.ViewObject.Transparency = 70

  # --- Generate each phase ---
  groove_solids = []
  colors = [(1,0,0), (0,1,0), (0,0,1), (1,1,0), (1,0,1), (0,1,1)]

  for phase in range(PHASES):
      phase_offset = phase * 2 * math.pi / PHASES
      phase_name = chr(ord('A') + phase)

      App.Console.PrintMessage(f"\nPhase {phase_name}:\n")
      App.Console.PrintMessage(f"  Generating knot path ({STEPS} points)...\n")

      # Build knot path points
      # Parametric torus knot: point winds P times toroidally, Q times poloidally
      knot_points = []
      t_offset = 0.0001  # Avoid degenerate tangent at t=0

      for i in range(STEPS):
          t = t_offset + (2 * math.pi * i) / STEPS
          poloidal_angle = Q * t + phase_offset

          # Distance from Z-axis varies with poloidal position
          radius = MAJOR_RADIUS + MINOR_RADIUS * math.cos(poloidal_angle)
          x = radius * math.cos(P * t)
          y = radius * math.sin(P * t)
          z = MINOR_RADIUS * math.sin(poloidal_angle)

          knot_points.append(Vector(x, y, z))

      # Create smooth curve through points
      # BSplineCurve.interpolate() fits a curve exactly through the points
      # PeriodicFlag=True makes it close smoothly
      knot_curve = Part.BSplineCurve()
      knot_curve.interpolate(knot_points, PeriodicFlag=True)

      # Convert curve to edge, then to wire
      knot_edge = knot_curve.toShape()  # -> Edge
      knot_wire = Part.Wire([knot_edge])  # -> Wire

      # Store wire in document for visualization
      knot_obj = doc.addObject("Part::Feature", f"KnotPath_{phase_name}")
      knot_obj.Shape = knot_wire
      knot_obj.ViewObject.LineColor = colors[phase % 6]
      knot_obj.ViewObject.LineWidth = 2.0

      # --- Create groove profile ---
      # Circle at path start, perpendicular to tangent
      start_point = knot_wire.Vertexes[0].Point
      first_edge = knot_wire.Edges[0]
      tangent = first_edge.tangentAt(first_edge.FirstParameter)

      # makeCircle(radius, center, normal) - normal = tangent direction
      profile_circle = Part.makeCircle(GROOVE_DIAMETER / 2, start_point, tangent)
      profile_wire = Part.Wire([profile_circle])

      # --- Sweep profile along path ---
      App.Console.PrintMessage(f"  Sweeping groove...\n")
      Gui.updateGui()

      solid = sweep_with_fallbacks(profile_wire, knot_wire)

      if solid is None:
          App.Console.PrintError(f"  Phase {phase_name}: SWEEP FAILED\n")
          continue

      groove_solids.append(solid)

      # Store groove solid in document
      groove_obj = doc.addObject("Part::Feature", f"GrooveSolid_{phase_name}")
      groove_obj.Shape = solid

      if not DO_BOOLEAN:
          groove_obj.ViewObject.Visibility = True
          groove_obj.ViewObject.ShapeColor = colors[phase % 6]
          groove_obj.ViewObject.Transparency = 30
      else:
          groove_obj.ViewObject.Visibility = False

  App.Console.PrintMessage(f"\n{len(groove_solids)}/{PHASES} grooves created\n")

  # --- Boolean cut (optional) ---
  if DO_BOOLEAN and groove_solids:
      App.Console.PrintMessage("-" * 50 + "\n")
      App.Console.PrintMessage("Boolean cuts...\n")

      try:
          result_shape = base_torus_shape
          for i, groove in enumerate(groove_solids):
              name = chr(ord('A') + i)
              App.Console.PrintMessage(f"  Cutting {name}...\n")
              Gui.updateGui()
              result_shape = result_shape.cut(groove)

          final = doc.addObject("Part::Feature", "TorusWithGrooves")
          final.Shape = result_shape

          if HIDE_BASE:
              base_torus.ViewObject.Visibility = False

      except Exception as e:
          App.Console.PrintError(f"Boolean cut failed: {e}\n")
  else:
      App.Console.PrintMessage("Preview mode - boolean skipped\n")

  # --- Finalize ---
  doc.recompute()
  Gui.updateGui()

  if Gui.activeDocument() and Gui.activeDocument().activeView():
      Gui.activeDocument().activeView().viewAxometric()
      Gui.SendMsgToActiveView("ViewFit")

  App.Console.PrintMessage("=" * 50 + "\n")
  App.Console.PrintMessage("Done!\n")
  App.Console.PrintMessage("=" * 50 + "\n")
  return True


# =============================================================================
# ENTRY POINT
# =============================================================================
def run():
  """Show dialog and run generator."""
  dialog = TorusKnotDialog(Gui.getMainWindow())
  if dialog.exec_() == QtGui.QDialog.Accepted:
      create_torus_knot_grooves(dialog.get_parameters())
  else:
      App.Console.PrintMessage("Cancelled.\n")


run()