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
Revision as of 18:42, 16 January 2026 by XenoEngineer (talk | contribs) (Created page with "Category:FreeCAD Category:Python Category:Torus Knot Winding Form ==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 ~~~~ <pre style="margin-left:3em; font:normal 14px terminal;">FreeCAD # Torus Knot Groove Generator for FreeCAD - v3.4 # Centralized defaults + FreeCAD object model notes import FreeC...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
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()