For the complete index, see /llms.txt. A Markdown version of any documentation page is available by appending .md to its URL or by sending an Accept: text/markdown header.

Browse documentation

Electromagnetism geometry

Winding geometry, a balanced three-phase resultant, and ideal resolver quadrature. Pure TypeScript with no React and no claim to solve a complete electromagnetic field.

0°SERVO / 10
view
variant
horn
drive

Drag around the hub to aim the horn. Step shows the slew rate: the goal jumps, the servo does not.

Theming

Set a role and the same CSS goes in your own app — every robot under it follows.

Install

bunx --bun shadcn@latest add https://robocn.dev/r/electromagnetism-geometry.json

Notes

  • The phase and resolver relationships are ideal calculations. The library does not solve Maxwell's equations, torque, force, heating or a complete electrical circuit.

Usage

import { coilWinding, threePhaseField, resolverSignals } from "@/lib/robocn/electromagnetism"

const winding = coilWinding({ turns: 12, length: 40, radius: 8 })
const field = threePhaseField(0.25, 4)
const channels = resolverSignals(37)

API

exports
PropTypeDefaultDescription
coilWinding(options: CoilWindingOptions) => Vec3[]—Samples a finite helix along x, y or z while keeping its requested length and radius fixed.
threePhaseField(phase: number, poles?: number) => ThreePhaseField—Sums three sinusoidal windings 120 electrical degrees apart and reports the resultant mechanical angle and magnitude.
resolverSignals(angle: number, excitation?: number) => ResolverSignals—Returns ideal excitation-scaled sine and cosine secondary channels for a shaft angle in degrees.

Source

src/lib/robocn/electromagnetism.ts
/**
 * Geometry and ideal phase relationships shared by the electromagnetic
 * machines. This module deliberately contains no React and no field, force,
 * torque, thermal, or circuit simulation.
 */

import type { Vec3 } from "@/lib/robocn/kinematics"

export interface CoilWindingOptions {
  turns: number
  length: number
  radius: number
  samplesPerTurn?: number
  center?: Vec3
  axis?: "x" | "y" | "z"
}

export interface ThreePhaseField {
  phases: [number, number, number]
  /** Mechanical field angle in degrees, wrapped to 0..360. */
  angle: number
  /** Magnitude of the balanced two-dimensional resultant. */
  magnitude: number
}

export interface ResolverSignals {
  sine: number
  cosine: number
}

const finite = (value: number, fallback: number) =>
  Number.isFinite(value) ? value : fallback

const boundedInteger = (value: number, fallback: number, min: number, max: number) =>
  Math.min(max, Math.max(min, Math.round(finite(value, fallback))))

const wrapped = (value: number, period: number) => {
  const safe = finite(value, 0)
  return ((safe % period) + period) % period
}

/** Sample a fixed-envelope helical winding along one world-space axis. */
export function coilWinding(options: CoilWindingOptions): Vec3[] {
  const turns = boundedInteger(options.turns, 4, 1, 64)
  const samplesPerTurn = boundedInteger(options.samplesPerTurn ?? 8, 8, 2, 64)
  const samples = turns * samplesPerTurn
  const length = Math.abs(finite(options.length, 20))
  const radius = Math.abs(finite(options.radius, 5))
  const supplied = options.center ?? { x: 0, y: 0, z: 0 }
  const center = {
    x: finite(supplied.x, 0),
    y: finite(supplied.y, 0),
    z: finite(supplied.z, 0),
  }
  const axis = options.axis ?? "x"

  return Array.from({ length: samples + 1 }, (_, index) => {
    const progress = index / samples
    // Reuse zero at the seam so the two endpoints are byte-stable instead of
    // exposing Math.sin/cos drift at an integer number of turns.
    const turnPhase = index === samples ? 0 : progress * turns * Math.PI * 2
    const axial = (progress - 0.5) * length
    const radialA = Math.cos(turnPhase) * radius
    const radialB = Math.sin(turnPhase) * radius
    if (axis === "y") {
      return { x: center.x + radialA, y: center.y + axial, z: center.z + radialB }
    }
    if (axis === "z") {
      return { x: center.x + radialA, y: center.y + radialB, z: center.z + axial }
    }
    return { x: center.x + axial, y: center.y + radialA, z: center.z + radialB }
  })
}

/**
 * Sum an ideal balanced three-phase stator. `phase` is an electrical cycle;
 * `poles` converts the electrical result to mechanical angle.
 */
export function threePhaseField(phase: number, poles = 2): ThreePhaseField {
  const electrical = wrapped(phase, 1) * Math.PI * 2
  const axes = [0, (Math.PI * 2) / 3, (Math.PI * 4) / 3] as const
  const phases = axes.map((axis) => Math.cos(electrical - axis)) as [number, number, number]
  const x = phases.reduce((sum, amplitude, index) => sum + amplitude * Math.cos(axes[index]), 0)
  const y = phases.reduce((sum, amplitude, index) => sum + amplitude * Math.sin(axes[index]), 0)
  const poleCount = boundedInteger(poles, 2, 2, 64)
  const polePairs = poleCount / 2
  const electricalAngle = wrapped((Math.atan2(y, x) * 180) / Math.PI, 360)

  return {
    phases,
    angle: electricalAngle / polePairs,
    magnitude: Math.hypot(x, y),
  }
}

/** Ideal sine and cosine secondary channels from a rotary transformer. */
export function resolverSignals(angle: number, excitation = 1): ResolverSignals {
  const radians = (wrapped(angle, 360) * Math.PI) / 180
  const drive = Math.min(1, Math.max(-1, finite(excitation, 1)))
  const sine = Math.sin(radians) * drive
  const cosine = Math.cos(radians) * drive
  return {
    sine: Math.abs(sine) < 1e-12 ? 0 : sine,
    cosine: Math.abs(cosine) < 1e-12 ? 0 : cosine,
  }
}