///|
/// PEEC / solver exports (non-geometry utilities).
///
/// This file intentionally keeps analysis/export code separate from the
/// geometry construction in main.mbt and bfem-cage.mbt.
///|
async fn export_helix_centerlines_json(
params : Params,
out_path : String,
) -> Unit {
// This intentionally exports *only* the helix centerlines.
// For PEEC, it’s usually better to export the conductor “intent” rather than
// try to convert STEP/STL into a solver-specific discretization.
//
// Units: millimeters.
let builder = StringBuilder()
builder.write_string("{\"schema\":\"bfem:centerlines:v1\"")
builder.write_string(",\"units\":\"mm\"")
builder.write_string(",\"params\":{")
builder.write_string("\"innerDiam_mm\":\{params.inner_diam}")
builder.write_string(",\"numPairs\":\{params.num_pairs}")
builder.write_string(",\"numSegs\":\{params.num_segs}")
builder.write_string(",\"vertTurns\":\{params.vert_turns}")
builder.write_string(",\"wireWidth_mm\":\{params.wire_width}")
builder.write_string(",\"wireGap_mm\":\{params.wire_gap}")
builder.write_string(
",\"pos\":[\{params.pos.0},\{params.pos.1},\{params.pos.2}]",
)
builder.write_string("}")
builder.write_string(
",\"cross_section\":{\"shape\":\"square\",\"width_mm\":\{params.wire_width}}",
)
let delta_z = (params.wire_gap + params.wire_width) /
(params.num_pairs.to_double() - 1)
builder.write_string(",\"paths\":[")
let mut first_path = true
for pair_num in 1..<=params.num_pairs {
let z_off = (pair_num - 1).to_double() * delta_z
let local_pos = (params.pos.0, params.pos.1, params.pos.2 + z_off)
let local_params = { ..params, pos: local_pos, }
let start_angle = 180.0 *
(pair_num.to_double() - 1) /
params.num_pairs.to_double()
let (radius, height_per_turn) = coil_turn_dimensions(pair_num, local_params)
// Two helices per pair: start_angle and start_angle + 180.
let path_red = @cad.helix_path(
pos=local_params.pos,
size_per_turn=(radius, radius, height_per_turn),
num_turns=local_params.vert_turns,
num_segs_per_turn=local_params.num_segs,
start_angle~,
)
let path_green = @cad.helix_path(
pos=local_params.pos,
size_per_turn=(radius, radius, height_per_turn),
num_turns=local_params.vert_turns,
num_segs_per_turn=local_params.num_segs,
start_angle=start_angle + 180,
)
first_path = emit_centerline_path_json(
builder,
first_path,
"bfem:coil:red:pair-\{pair_num}",
pair_num,
start_angle,
path_red.points,
)
first_path = emit_centerline_path_json(
builder,
first_path,
"bfem:coil:green:pair-\{pair_num}",
pair_num,
start_angle + 180,
path_green.points,
)
}
builder.write_string("]}")
@fs.write_file(
out_path,
@encoding/utf8.encode(builder.to_string()),
permission=0o644,
create_mode=@fs.CreateMode::CreateOrTruncate,
) catch {
err => {
@cli.eprintln("error: failed to write centerlines JSON: \{err}")
abort("")
}
}
}
///|
/// Export a *single* series conductor path with explicit IN/OUT terminals.
///
/// This is intended to represent the claimed single continuous wire between
/// `bfem:exit-wire:green` (IN) and `bfem:exit-wire:red` (OUT).
///
/// Units: millimeters.
async fn export_conductor_network_json(
params : Params,
out_path : String,
) -> Unit {
let (radius, height) = coil_turn_dimensions(params.num_pairs, params)
let (in_base, out_base, in_term, out_term) = compute_exit_wire_terminals(
(radius, radius, height),
params,
)
// Build a single polyline by stitching together helix centerlines with
// explicit straight-line jump segments.
let series = Array()
// Start at IN terminal (top of the green exit wire) then go down to the base.
push_point(series, in_term)
push_point(series, in_base)
// Precompute all helix point arrays so we can stitch deterministically.
let delta_z = (params.wire_gap + params.wire_width) /
(params.num_pairs.to_double() - 1)
// We build a serpentine through the stacked pairs:
// green(top->bottom) -> jump -> red(bottom->top) -> jump to next layer
// ending back at OUT base.
//
// NOTE: This export defines the *intended* single series path for solver
// workflows; it does not attempt to infer topology from the STEP.
for i in 0.. {
@cli.eprintln("error: failed to write conductor network JSON: \{err}")
abort("")
}
}
}
///|
fn push_point(dst : Array[Vec3], p : Vec3) -> Unit {
if dst.length() == 0 {
dst.push(p)
return
}
let last = dst[dst.length() - 1]
if last != p {
dst.push(p)
}
}
///|
fn emit_terminal_json(
builder : StringBuilder,
first : Bool,
label : String,
name : String,
p : Vec3,
) -> Unit {
if !first {
builder.write_char(',')
}
builder.write_string("{\"label\":\"")
builder.write_string(label)
builder.write_string("\",\"name\":\"")
builder.write_string(name)
builder.write_string("\",\"point\":[\{p.0},\{p.1},\{p.2}]}")
}
///|
fn append_polyline(dst : Array[Vec3], src : Array[Vec3]) -> Unit {
if src.length() == 0 {
return
}
// Avoid repeating the first point if it matches the last.
if dst.length() > 0 {
let last = dst[dst.length() - 1]
let first = src[0]
if last == first {
for i in 1..bottom
/// - red runs from bottom->top
fn orient_bifilar_pair(
green : Array[Vec3],
red : Array[Vec3],
) -> (Array[Vec3], Array[Vec3]) {
let g0 = green[0]
let g1 = green[green.length() - 1]
let r0 = red[0]
let r1 = red[red.length() - 1]
let green_top_to_bottom = if g0.2 >= g1.2 {
green
} else {
reverse_points(green)
}
let red_bottom_to_top = if r0.2 <= r1.2 { red } else { reverse_points(red) }
(green_top_to_bottom, red_bottom_to_top)
}
///|
fn reverse_points(points : Array[Vec3]) -> Array[Vec3] {
let out = Array(capacity=points.length())
let n = points.length()
for i in 0.. (Vec3, Vec3, Vec3, Vec3) {
// Mirror make_cage_context calculations (kept local to avoid geometry deps).
let npairs = params.num_pairs
let num_pairs = npairs.to_double()
let lower_z = params.pos.2 - params.wire_width / 2
let coil_height = 2.0 *
params.vert_turns *
(params.wire_width + params.wire_gap) +
params.wire_width
let extrude_height = coil_height + params.wire_gap + params.wire_width
let rail_angle_delta = @math.PI / num_pairs
let inner_radius = size.0 + params.wire_width / 2 + params.wire_gap
let outer_radius = inner_radius + params.radial_thickness
let outer_dtheta = @math.asin(params.wire_gap / (2 * outer_radius))
let delta_z = (params.wire_gap + params.wire_width) / (num_pairs - 1)
let max_axial_connector_top_zs = extrude_height +
lower_z +
(num_pairs - 1) * delta_z
// Use the final (outermost) i.
let i = npairs - 1
let i_d = i.to_double()
let rotation = -@math.PI * i_d / num_pairs
let line_length = (num_pairs - i_d) * (params.wire_width + params.wire_gap)
let connector_radius = inner_radius - line_length
let connector_dtheta = @math.asin(params.wire_gap / (2 * connector_radius))
let top_angle = rotation - connector_dtheta
// This matches bfem-cage: top_helix_z derived from the pair Z.
let z_layer = lower_z + i_d * delta_z + params.wire_width
let top_helix_z = z_layer + coil_height - 2 * params.wire_width
// Inner flat-top output connector polygon.
let angle_diff = rail_angle_delta - 2 * outer_dtheta
let sx4 = params.pos.0 + connector_radius * @math.cos(top_angle + @math.PI)
let sy4 = params.pos.1 + connector_radius * @math.sin(top_angle + @math.PI)
let sx1 = params.pos.0 +
connector_radius * @math.cos(top_angle + @math.PI - angle_diff)
let sy1 = params.pos.1 +
connector_radius * @math.sin(top_angle + @math.PI - angle_diff)
let ew_inner_radius = connector_radius -
0.6 * params.exit_wire_separation -
params.exit_wire_diameter
let s2 = vertex_at(
center=(params.pos.0, params.pos.1),
radius=ew_inner_radius,
angle=top_angle + @math.PI,
)
let s3 = vertex_at(
center=(params.pos.0, params.pos.1),
radius=ew_inner_radius,
angle=top_angle + @math.PI - angle_diff,
)
let polygon = [(sx4, sy4), (sx1, sy1), s3, s2]
let inner_wire_post_center = center_of_face(polygon)
let mut inner_wire_pos = inner_wire_post_center
let outer_wire_radius = (inner_radius + outer_radius) / 2
let outer_wire_dtheta = @math.asin(params.wire_gap / (2 * outer_wire_radius))
let outer_wire_start_angle = 0.0
let outer_wire_end_angle = rail_angle_delta - 2 * outer_wire_dtheta
let outer_wire_post_center = vertex_at(
center=(params.pos.0, params.pos.1),
radius=outer_wire_radius,
angle=(outer_wire_end_angle - outer_wire_start_angle) / 2,
)
let mut outer_wire_pos = outer_wire_post_center
let centers_distance = vector_length(
outer_wire_post_center, inner_wire_post_center,
)
if centers_distance < params.exit_wire_separation {
let pa = inner_wire_post_center
let va = (polygon[3].0, polygon[3].1)
let pb = outer_wire_post_center
let vb = vertex_at(
center=(params.pos.0, params.pos.1),
radius=(inner_radius + outer_radius) / 2,
angle=outer_wire_start_angle,
)
let f1 = fn(t) { (pa.0 + t * (va.0 - pa.0), pa.1 + t * (va.1 - pa.1)) }
let f2 = fn(t) { (pb.0 + t * (vb.0 - pb.0), pb.1 + t * (vb.1 - pb.1)) }
let d = params.exit_wire_separation
let t = solve_for_t(d, fn(t) { vector_length(f1(t), f2(t)) })
inner_wire_pos = f1(t)
outer_wire_pos = f2(t)
}
let flat_top_height = 2.5 * params.wire_width + params.back_thickness
let in_base = (
inner_wire_pos.0,
inner_wire_pos.1,
top_helix_z + flat_top_height,
)
let out_base = (
outer_wire_pos.0,
outer_wire_pos.1,
top_helix_z + flat_top_height,
)
let in_term = (in_base.0, in_base.1, in_base.2 + params.connector_length)
let out_term = (out_base.0, out_base.1, out_base.2 + params.connector_length)
// Keep the max_axial_connector_top_zs calculation “used” to avoid drift vs
// the geometry code if it is later incorporated into the stitching logic.
let _ = max_axial_connector_top_zs
(in_base, out_base, in_term, out_term)
}
///|
fn emit_centerline_path_json(
builder : StringBuilder,
first_path : Bool,
name : String,
pair_num : Int32,
start_angle_deg : Double,
points : Array[Vec3],
) -> Bool {
if !first_path {
builder.write_char(',')
}
builder.write_string("{\"name\":\"")
// Names are controlled strings (no quotes expected), so minimal escaping.
builder.write_string(name)
builder.write_string("\"")
builder.write_string(",\"pair\":\{pair_num}")
builder.write_string(",\"start_angle_deg\":\{start_angle_deg}")
builder.write_string(",\"points\":[")
let mut first_pt = true
for p in points {
if !first_pt {
builder.write_char(',')
}
first_pt = false
builder.write_string("[\{p.0},\{p.1},\{p.2}]")
}
builder.write_string("]}")
false
}