///|
pub async fn Design::create_blender_file(
self : Design,
out_path : String,
keep_modifiers? : Bool,
segments~ : Int32 = 0, // <=0: retain cubic Beziers, >0: convert to polylines with this many segments
) -> Unit {
let bpy_code = self.to_blender_python(segments~, keep_modifiers?)
// Use a unique directory: concurrent examples/tests must not race on a
// fixed /tmp script path.
let temp_dir = @fs.tmpdir(prefix="moonbit-blender-")
defer @fs.rmdir(temp_dir, recursive=true)
let temp_py_path = temp_dir + "/script.py"
write_text_file(bpy_code, Some(temp_py_path))
let script_path = match @env.get_env_var("MOONBIT_STEP_ROOT") {
Some(root) =>
if root.has_suffix("/") {
root + "scripts/validate-bpy.py"
} else {
root + "/scripts/validate-bpy.py"
}
None => "./scripts/validate-bpy.py"
}
// Now run: ./scripts/validate-bpy.py {temp_py_path} --blend out_path.
// Capture the output instead of inheriting stdout: callers may be tests
// that talk a structured protocol on stdout.
let args = [temp_py_path, "--blend", out_path]
let (code, output) = @process.collect_output_merged(script_path, args)
if code != 0 {
abort(
"failed to write '\{out_path}': blender exited with status \{code}:\n\{output.text()}",
)
}
}
///|
pub async fn Design::write_blender_python(
self : Design,
out_path : String?,
segments~ : Int32 = 0, // <=0: retain cubic Beziers, >0: convert to polylines with this many segments
keep_modifiers? : Bool,
) -> Unit {
let bpy_code = self.to_blender_python(segments~, keep_modifiers?)
write_text_file(bpy_code, out_path)
}
///|
fn py_string(s : String) -> String {
let escaped = s.replace(old="\\", new="\\\\").replace(old="'", new="\\'")
"'" + escaped + "'"
}
///|
fn mm_to_m(x : Double) -> Double {
x * 0.001
}
///|
fn py_bool(b : Bool) -> String {
if b {
"True"
} else {
"False"
}
}
///|
fn py_vec3(v : Vec3) -> String {
"(\{v.0}, \{v.1}, \{v.2})"
}
///|
fn py_mm(x : Double) -> String {
"\{mm_to_m(x)}"
}
///|
fn py_mm_vec2(v : Vec2) -> String {
"(\{mm_to_m(v.0)}, \{mm_to_m(v.1)})"
}
///|
fn py_mm_vec3(v : Vec3) -> String {
"(\{mm_to_m(v.0)}, \{mm_to_m(v.1)}, \{mm_to_m(v.2)})"
}
///|
fn py_vec3_list(pts : Array[Vec3]) -> String {
"[" + pts.map(py_vec3).join(", ") + "]"
}
///|
fn py_mm_vec2_list(pts : Array[Vec2]) -> String {
"[" + pts.map(py_mm_vec2).join(", ") + "]"
}
///|
fn py_mm_vec3_list(pts : Array[Vec3]) -> String {
"[" + pts.map(py_mm_vec3).join(", ") + "]"
}
///|
fn py_mm_vec2_list_list(loops : Array[Array[Vec2]]) -> String {
"[" + loops.map(py_mm_vec2_list).join(", ") + "]"
}
///|
fn py_bezier_anchor(a : BezierAnchor) -> String {
let pos = py_mm_vec2(a.position)
let hi = py_mm_vec2(a.handle_in)
let ho = py_mm_vec2(a.handle_out)
"{'pos': \{pos}, 'hi': \{hi}, 'ho': \{ho}}"
}
///|
fn py_bezier_path(path : BezierPath) -> String {
let anchors = "[" + path.anchors.map(py_bezier_anchor).join(", ") + "]"
"{'anchors': \{anchors}, 'closed': \{py_bool(path.closed)}, 'clear': \{py_bool(path.clear)}}"
}
///|
fn py_bezier_paths(paths : Array[BezierPath]) -> String {
"[" + paths.map(py_bezier_path).join(", ") + "]"
}
///|
fn py_color(color : Rgb) -> String {
"(\{color.r}, \{color.g}, \{color.b}, 1.0)"
}
///|
fn next_var(prefix : String, idx : Int32) -> (String, Int32) {
(prefix + idx.to_string(), idx + 1)
}
///|
fn emit_node_python(
builder : StringBuilder,
node : SceneNode,
idx : Int32,
segments~ : Int32,
) -> (String, Int32) {
match node.shape {
Cuboid(c) => {
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_cuboid_object(\{py_string(node.name)}, \{py_mm(c.sx)}, \{py_mm(c.sy)}, \{py_mm(c.sz)}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
Cylinder(c) => {
// Blender has no analytic cylinder path here; approximate with the
// faceted extrusion used by `tube()`.
let ext = ExtrudedProfile::{
profile: {
outer: Profile2D::circle(c.diameter, segments=64),
holes: [],
},
height_mm: c.height,
round_bore: None,
}
emit_node_python(
builder,
{ ..node, shape: Shape::ExtrudedProfile(ext), },
idx,
segments~,
)
}
CalibrationChamferBlock(b) => {
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_chamfer_block_object(\{py_string(node.name)}, \{py_mm(b.length_mm)}, \{py_mm(b.width_mm)}, \{py_mm(b.height_mm)}, \{py_mm(b.chamfer_size_mm)}, \{py_mm(b.fillet_radius_mm)}, \{b.fillet_segments}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
ExtrudedProfile(e) => {
let (obj_var, next_idx) = next_var("obj", idx)
let loops = [e.profile.outer, ..e.profile.holes]
let h2 = e.height_mm / 2.0
let (ox, oy, oz) = rotate_point((0.0, 0.0, h2), node.rx, node.ry, node.rz)
let tx = node.tx + ox
let ty = node.ty + oy
let tz = node.tz + oz
builder.write_string(
(
$| \{obj_var} = finalize_object(create_extruded_profile_object(\{py_string(node.name)}, \{py_mm_vec2_list_list(loops)}, \{py_mm(e.height_mm)}, collection), get_material(\{py_color(node.color)}), (\{py_mm(tx)}, \{py_mm(ty)}, \{py_mm(tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
ExtrudedArcProfile(e) => {
let (verts, faces) = extruded_arc_mesh(e)
let face_str = "[" +
faces
.map(fn(f) { "[" + f.map(fn(i) { i.to_string() }).join(",") + "]" })
.join(",") +
"]"
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_mesh_object(\{py_string(node.name)}, \{py_mm_vec3_list(verts)}, \{face_str}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
SquareHoleWasher(w) => {
let (verts, faces) = square_hole_mesh(w)
let face_str = "[" +
faces
.map(fn(f) { "[" + f.map(fn(i) { i.to_string() }).join(",") + "]" })
.join(",") +
"]"
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_mesh_object(\{py_string(node.name)}, \{py_mm_vec3_list(verts)}, \{face_str}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
HexHeadWasherFace(h) =>
match hhwf_proxy(h) {
Some(p) => {
let (verts, faces) = chamfered_prism_mesh(p)
let face_str = "[" +
faces
.map(fn(f) { "[" + f.map(fn(i) { i.to_string() }).join(",") + "]" })
.join(",") +
"]"
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_mesh_object(\{py_string(node.name)}, \{py_mm_vec3_list(verts)}, \{face_str}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
None => (next_var("obj", idx).0, idx)
}
TabWasher(w) => {
let (verts, faces) = tab_washer_mesh(w)
let face_str = "[" +
faces
.map(fn(f) { "[" + f.map(fn(i) { i.to_string() }).join(",") + "]" })
.join(",") +
"]"
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_mesh_object(\{py_string(node.name)}, \{py_mm_vec3_list(verts)}, \{face_str}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
ExtrudedBezierProfile(e) => {
let (obj_var, next_idx) = next_var("obj", idx)
let paths_data = py_bezier_paths(e.paths)
let h2 = e.height_mm / 2.0
let (ox, oy, oz) = rotate_point((0.0, 0.0, h2), node.rx, node.ry, node.rz)
let tx = node.tx + ox
let ty = node.ty + oy
let tz = node.tz + oz
builder.write_string(
(
$| \{obj_var} = finalize_object(create_extruded_bezier_object(\{py_string(node.name)}, \{paths_data}, \{py_mm(e.height_mm)}, collection), get_material(\{py_color(node.color)}), (\{py_mm(tx)}, \{py_mm(ty)}, \{py_mm(tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
RevolvedProfile(rev) => {
// Blender gets a polygonal revolve; STEP uses the analytic compiler.
let (verts, faces) = rev_mesh(rev, angular_segments=48, arc_segments=12)
let face_str = "[" +
faces
.map(fn(f) { "[" + f.map(fn(i) { i.to_string() }).join(",") + "]" })
.join(",") +
"]"
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_mesh_object(\{py_string(node.name)}, \{py_mm_vec3_list(verts)}, \{face_str}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
ChamferedPrism(prism) => {
let (verts, faces) = chamfered_prism_mesh(prism)
let face_str = "[" +
faces
.map(fn(f) { "[" + f.map(fn(i) { i.to_string() }).join(",") + "]" })
.join(",") +
"]"
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_mesh_object(\{py_string(node.name)}, \{py_mm_vec3_list(verts)}, \{face_str}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
SocketedSolid(solid) => {
let (verts, faces) = socketed_solid_mesh(solid)
let face_str = "[" +
faces
.map(fn(f) { "[" + f.map(fn(i) { i.to_string() }).join(",") + "]" })
.join(",") +
"]"
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_mesh_object(\{py_string(node.name)}, \{py_mm_vec3_list(verts)}, \{face_str}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
ConicalNut(solid) => {
let (verts, faces) = conical_nut_mesh(solid)
let face_str = "[" +
faces
.map(fn(f) { "[" + f.map(fn(i) { i.to_string() }).join(",") + "]" })
.join(",") +
"]"
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_mesh_object(\{py_string(node.name)}, \{py_mm_vec3_list(verts)}, \{face_str}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
SquareNut(solid) => {
let (verts, faces) = square_nut_mesh(solid)
let face_str = "[" +
faces
.map(fn(f) { "[" + f.map(fn(i) { i.to_string() }).join(",") + "]" })
.join(",") +
"]"
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_mesh_object(\{py_string(node.name)}, \{py_mm_vec3_list(verts)}, \{face_str}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
AcornNut(solid) => {
let (verts, faces) = acorn_nut_mesh(solid)
let face_str = "[" +
faces
.map(fn(f) { "[" + f.map(fn(i) { i.to_string() }).join(",") + "]" })
.join(",") +
"]"
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_mesh_object(\{py_string(node.name)}, \{py_mm_vec3_list(verts)}, \{face_str}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
SlitTube(solid) => {
let (verts, faces) = slit_tube_mesh(solid)
let face_str = "[" +
faces
.map(fn(f) { "[" + f.map(fn(i) { i.to_string() }).join(",") + "]" })
.join(",") +
"]"
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_mesh_object(\{py_string(node.name)}, \{py_mm_vec3_list(verts)}, \{face_str}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
SweptProfile(s) => {
let (obj_var, next_idx) = next_var("obj", idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(create_swept_profile_object(\{py_string(node.name)}, \{py_mm_vec3_list(s.path.points)}, \{py_vec3_list(s.path.normals)}, \{py_vec3_list(s.path.tangents)}, \{py_mm_vec3(s.profile.center)}, \{py_mm_vec3_list(s.profile.vertices)}, \{py_vec3(s.profile.normal_dir)}, \{py_vec3(s.profile.right_dir)}, \{py_bool(s.make_start_cap)}, \{py_bool(s.make_end_cap)}, collection), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, next_idx)
}
Union(u) => {
let child_vars : Array[String] = []
let mut cur_idx = idx
for child in u.nodes {
let (child_var, next_idx) = emit_node_python(
builder,
child,
cur_idx,
segments~,
)
child_vars.push(child_var)
cur_idx = next_idx
}
let (list_var, next_idx) = next_var("union_objs", cur_idx)
builder.write_string(
(
$| \{list_var} = [\{child_vars.join(", ")}]
$|
),
)
let (obj_var, final_idx) = next_var("obj", next_idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(join_objects(\{list_var}, \{py_string(node.name)}), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, final_idx)
}
Difference(d) => {
let (base_var, next_idx) = emit_node_python(
builder,
d.base,
idx,
segments~,
)
let cutter_vars : Array[String] = []
let mut cur_idx = next_idx
for cutter in d.cutters {
let (cutter_var, cutter_idx) = emit_node_python(
builder,
cutter,
cur_idx,
segments~,
)
cutter_vars.push(cutter_var)
cur_idx = cutter_idx
}
let (list_var, next_idx) = next_var("diff_cutters", cur_idx)
builder.write_string(
(
$| \{list_var} = [\{cutter_vars.join(", ")}]
$|
),
)
let (obj_var, final_idx) = next_var("obj", next_idx)
builder.write_string(
(
$| \{obj_var} = finalize_object(boolean_difference(\{base_var}, \{list_var}, \{py_string(node.name)}), get_material(\{py_color(node.color)}), (\{py_mm(node.tx)}, \{py_mm(node.ty)}, \{py_mm(node.tz)}), (\{node.rx}, \{node.ry}, \{node.rz}))
$|
),
)
(obj_var, final_idx)
}
Fillet(_) | Chamfer(_) =>
abort(
"edge fillet/chamfer is not supported by the Blender exporter; use Design::to_step_evaluated() (OCCT)",
)
}
}
///|
/// Convert the Design to a Blender 5 Python3 script that will recreate the model
/// using the Blender Python3 API. This function keeps the STEP model as high-level
/// as possible, allowing Blender to handle as many modifier operations itself
/// as it can.
pub fn Design::to_blender_python(
self : Design,
segments~ : Int32 = 0, // <=0: retain cubic Beziers, >0: convert to polylines with this many segments
keep_modifiers~ : Bool = false,
) -> String {
let builder = StringBuilder()
builder.write_string(PREAMBLE)
if keep_modifiers {
builder.write_string(BOOLEAN_DIFFERENCE_KEEP_MODIFIERS)
} else {
builder.write_string(BOOLEAN_DIFFERENCE_APPLY_MODIFIERS)
}
builder.write_string(
(
#|
#|def create_model_objects(collection):
#|
),
)
let mut obj_index = 0
for node in self.nodes {
let (_, next_idx) = emit_node_python(builder, node, obj_index, segments~)
obj_index = next_idx
}
builder.write_string(postscript(self.name))
builder.to_string()
}
///|
const PREAMBLE =
#|#!/usr/bin/env python3
#|"""
#|Blender 5.0 Python3 script generated by `gmlewis/step` and MoonBit.
#|Run this script from within Blender's scripting environment.
#|This script is idempotent meaning that it can be run multiple times and
#|only the created model will be replaced in the current scene.
#|"""
#|
#|import bpy
#|import math
#|import sys
#|
#|config = {}
#|_material_cache = {}
#|
#|
#|def delete_default_cube():
#| obj = bpy.data.objects.get("Cube")
#| if obj is not None:
#| bpy.data.objects.remove(obj, do_unlink=True)
#| print("Removed default cube.")
#|
#|
#|def set_up_collection(collection_name):
#| """
#| Set up the collection for the new model.
#| - If it exists, delete all objects within it.
#| - If it doesn't exist, create it.
#| """
#| scene = bpy.context.scene
#| collection = bpy.data.collections.get(collection_name)
#|
#| if collection is not None:
#| print(f"Collection '{collection_name}' exists. Removing all objects...")
#| objects_to_delete = list(collection.objects)
#| for obj in objects_to_delete:
#| bpy.data.objects.remove(obj, do_unlink=True)
#| print(f" Removed {len(objects_to_delete)} objects.")
#| else:
#| print(f"Creating new collection '{collection_name}'...")
#| collection = bpy.data.collections.new(collection_name)
#| scene.collection.children.link(collection)
#|
#| return collection
#|
#|
#|def get_material(color):
#| """Return a material for the given RGBA color tuple."""
#| key = f"mat_{int(color[0] * 255)}_{int(color[1] * 255)}_{int(color[2] * 255)}"
#| if key in _material_cache:
#| return _material_cache[key]
#| mat = bpy.data.materials.get(key)
#| if mat is None:
#| mat = bpy.data.materials.new(name=key)
#| mat.use_nodes = True
#| # Set viewport color so it's visible without rendering
#| mat.diffuse_color = color
#| bsdf = mat.node_tree.nodes.get("Principled BSDF")
#| if bsdf is not None:
#| bsdf.inputs["Base Color"].default_value = color
#| _material_cache[key] = mat
#| return mat
#|
#|
#|def assign_material(obj, material):
#| if obj is None or material is None:
#| return
#| if obj.data is None:
#| return
#| if hasattr(obj.data, "materials"):
#| if obj.data.materials:
#| obj.data.materials[0] = material
#| else:
#| obj.data.materials.append(material)
#|
#|
#|def apply_transform(obj, location, rotation_deg):
#| if obj is None:
#| return
#| obj.location = location
#| obj.rotation_euler = (
#| math.radians(rotation_deg[0]),
#| math.radians(rotation_deg[1]),
#| math.radians(rotation_deg[2]),
#| )
#|
#|
#|def finalize_object(obj, material, location, rotation_deg):
#| if obj is None:
#| return None
#| apply_transform(obj, location, rotation_deg)
#| # Curve objects might need to be converted to mesh before boolean operations,
#| # but material can be assigned either before or after.
#| # We assign it here to the original object.
#| assign_material(obj, material)
#| return obj
#|
#|
#|def create_mesh_object(name, verts, faces, collection):
#| if len(verts) == 0 or len(faces) == 0:
#| return None
#| mesh = bpy.data.meshes.new(name + "_mesh")
#| mesh.from_pydata(verts, [], faces)
#| mesh.update()
#| obj = bpy.data.objects.new(name, mesh)
#| collection.objects.link(obj)
#| return obj
#|
#|
#|def polygon_area_2d(loop):
#| area = 0.0
#| n = len(loop)
#| if n < 3:
#| return 0.0
#| for i in range(n):
#| x1, y1 = loop[i]
#| x2, y2 = loop[(i + 1) % n]
#| area += x1 * y2 - x2 * y1
#| return area / 2.0
#|
#|
#|def ensure_ccw(loop):
#| return loop if polygon_area_2d(loop) >= 0.0 else list(reversed(loop))
#|
#|
#|def ensure_cw(loop):
#| return loop if polygon_area_2d(loop) <= 0.0 else list(reversed(loop))
#|
#|
#|def create_extruded_profile_object(name, loops, height, collection):
#| if not loops or len(loops[0]) < 3:
#| return None
#| outer = ensure_ccw(loops[0])
#| holes = [ensure_cw(h) for h in loops[1:] if len(h) >= 3]
#| curve = bpy.data.curves.new(name + "_curve", type="CURVE")
#| curve.dimensions = "2D"
#| curve.fill_mode = "BOTH"
#| curve.extrude = height / 2.0
#| for loop in [outer] + holes:
#| spline = curve.splines.new("POLY")
#| spline.points.add(len(loop) - 1)
#| for i, (x, y) in enumerate(loop):
#| spline.points[i].co = (x, y, 0.0, 1.0)
#| spline.use_cyclic_u = True
#| obj = bpy.data.objects.new(name, curve)
#| collection.objects.link(obj)
#| return obj
#|
#|
#|def create_extruded_bezier_object(name, bezier_paths, height, collection):
#| """Create an extruded object from native cubic Bezier paths."""
#| if not bezier_paths:
#| return None
#| curve = bpy.data.curves.new(name + "_curve", type="CURVE")
#| curve.dimensions = "2D"
#| curve.fill_mode = "BOTH"
#| curve.extrude = height / 2.0
#| for path_data in bezier_paths:
#| anchors = path_data['anchors']
#| closed = path_data['closed']
#| # clear = path_data['clear'] # Used for holes, handled by winding order
#| if len(anchors) < 2:
#| continue
#| spline = curve.splines.new("BEZIER")
#| spline.bezier_points.add(len(anchors) - 1)
#| for i, anchor in enumerate(anchors):
#| bp = spline.bezier_points[i]
#| px, py = anchor['pos']
#| hix, hiy = anchor['hi']
#| hox, hoy = anchor['ho']
#| bp.co = (px, py, 0.0)
#| bp.handle_left = (hix, hiy, 0.0)
#| bp.handle_right = (hox, hoy, 0.0)
#| bp.handle_left_type = 'FREE'
#| bp.handle_right_type = 'FREE'
#| spline.use_cyclic_u = closed
#| obj = bpy.data.objects.new(name, curve)
#| collection.objects.link(obj)
#| return obj
#|
#|
#|def build_cuboid_mesh(sx, sy, sz):
#| verts = [
#| (0.0, 0.0, 0.0),
#| (sx, 0.0, 0.0),
#| (sx, sy, 0.0),
#| (0.0, sy, 0.0),
#| (0.0, 0.0, sz),
#| (sx, 0.0, sz),
#| (sx, sy, sz),
#| (0.0, sy, sz),
#| ]
#| faces = [
#| [0, 1, 2, 3],
#| [4, 5, 6, 7],
#| [0, 1, 5, 4],
#| [1, 2, 6, 5],
#| [2, 3, 7, 6],
#| [3, 0, 4, 7],
#| ]
#| return verts, faces
#|
#|
#|def create_cuboid_object(name, sx, sy, sz, collection):
#| verts, faces = build_cuboid_mesh(sx, sy, sz)
#| return create_mesh_object(name, verts, faces, collection)
#|
#|
#|def build_chamfer_block_mesh(length_mm, width_mm, height_mm, chamfer_size_mm, fillet_radius_mm, fillet_segments):
#| rings = [(0.0, 0.0)]
#| if fillet_radius_mm > 0.0:
#| r = fillet_radius_mm
#| start_z = height_mm - r
#| rings.append((start_z, 0.0))
#| segs = 8 if fillet_segments <= 0 else fillet_segments
#| for i in range(1, segs + 1):
#| phi = i * (math.pi / 2.0) / segs
#| offset = r - r * math.cos(phi)
#| z = height_mm - r + r * math.sin(phi)
#| rings.append((z, offset))
#| elif chamfer_size_mm > 0.0:
#| c = chamfer_size_mm
#| rings.append((height_mm - c, 0.0))
#| rings.append((height_mm, c))
#| else:
#| rings.append((height_mm, 0.0))
#|
#| verts = []
#| for rz, ro in rings:
#| px0 = ro
#| px1 = length_mm - ro
#| py0 = ro
#| py1 = width_mm - ro
#| verts.extend([(px0, py0, rz), (px1, py0, rz), (px1, py1, rz), (px0, py1, rz)])
#|
#| faces = []
#| faces.append([0, 3, 2, 1])
#| top_base = (len(rings) - 1) * 4
#| faces.append([top_base + 0, top_base + 1, top_base + 2, top_base + 3])
#| for ri in range(len(rings) - 1):
#| a = ri * 4
#| b = (ri + 1) * 4
#| faces.append([a + 0, a + 1, b + 1, b + 0])
#| faces.append([a + 1, a + 2, b + 2, b + 1])
#| faces.append([a + 2, a + 3, b + 3, b + 2])
#| faces.append([a + 3, a + 0, b + 0, b + 3])
#| return verts, faces
#|
#|
#|def create_chamfer_block_object(name, length_mm, width_mm, height_mm, chamfer_size_mm, fillet_radius_mm, fillet_segments, collection):
#| verts, faces = build_chamfer_block_mesh(length_mm, width_mm, height_mm, chamfer_size_mm, fillet_radius_mm, fillet_segments)
#| return create_mesh_object(name, verts, faces, collection)
#|
#|
#|def v_sub(a, b):
#| return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
#|
#|def v_cross(a, b):
#| return (
#| a[1] * b[2] - a[2] * b[1],
#| a[2] * b[0] - a[0] * b[2],
#| a[0] * b[1] - a[1] * b[0],
#| )
#|
#|def v_dot(a, b):
#| return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
#|
#|def v_len(a):
#| return math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2])
#|
#|def v_norm(a):
#| l = v_len(a)
#| if l == 0.0:
#| return (1.0, 0.0, 0.0)
#| return (a[0] / l, a[1] / l, a[2] / l)
#|
#|def v_scale(a, s):
#| return (a[0] * s, a[1] * s, a[2] * s)
#|
#|def rotation_matrix(rx, ry, rz):
#| rad_x = math.radians(rx)
#| rad_y = math.radians(ry)
#| rad_z = math.radians(rz)
#| sx, cx = math.sin(rad_x), math.cos(rad_x)
#| sy, cy = math.sin(rad_y), math.cos(rad_y)
#| sz, cz = math.sin(rad_z), math.cos(rad_z)
#| return (
#| (cy * cz, sx * sy * cz - cx * sz, cx * sy * cz + sx * sz),
#| (cy * sz, sx * sy * sz + cx * cz, cx * sy * sz - sx * cz),
#| (-sy, sx * cy, cx * cy),
#| )
#|
#|def apply_matrix(m, p):
#| return (
#| m[0][0] * p[0] + m[0][1] * p[1] + m[0][2] * p[2],
#| m[1][0] * p[0] + m[1][1] * p[1] + m[1][2] * p[2],
#| m[2][0] * p[0] + m[2][1] * p[1] + m[2][2] * p[2],
#| )
#|
#|
#|def polygon_normal_3d(pts, indices):
#| if len(indices) < 3:
#| return (0.0, 0.0, 0.0)
#| ax = ay = az = 0.0
#| n = len(indices)
#| for i in range(n):
#| x1, y1, z1 = pts[indices[i]]
#| x2, y2, z2 = pts[indices[(i + 1) % n]]
#| ax += y1 * z2 - z1 * y2
#| ay += z1 * x2 - x1 * z2
#| az += x1 * y2 - y1 * x2
#| return (ax / 2.0, ay / 2.0, az / 2.0)
#|
#|
#|def build_swept_profile_mesh(path_points, path_normals, path_tangents, profile_center, profile_vertices, profile_normal_dir, profile_right_dir, make_start_cap, make_end_cap):
#| if len(path_points) < 2 or len(profile_vertices) < 3:
#| return [], []
#| prof_z = v_norm(profile_normal_dir)
#| prof_x = v_norm(profile_right_dir)
#| prof_y = v_norm(v_cross(prof_z, prof_x))
#| m_global_to_prof = (
#| (prof_x[0], prof_x[1], prof_x[2]),
#| (prof_y[0], prof_y[1], prof_y[2]),
#| (prof_z[0], prof_z[1], prof_z[2]),
#| )
#| local_verts = [apply_matrix(m_global_to_prof, v_sub(v, profile_center)) for v in profile_vertices]
#|
#| pts = []
#| num_path_pts = len(path_points)
#| num_prof_verts = len(profile_vertices)
#| for i in range(num_path_pts):
#| p_origin = path_points[i]
#| p_tangent = v_norm(path_tangents[i])
#| p_normal = v_norm(path_normals[i])
#| p_right = v_norm(v_cross(p_tangent, p_normal))
#| m_path_to_global = (
#| (p_right[0], p_normal[0], p_tangent[0]),
#| (p_right[1], p_normal[1], p_tangent[1]),
#| (p_right[2], p_normal[2], p_tangent[2]),
#| )
#| for v in local_verts:
#| gv = apply_matrix(m_path_to_global, v)
#| pts.append((gv[0] + p_origin[0], gv[1] + p_origin[1], gv[2] + p_origin[2]))
#|
#| faces = []
#| for i in range(num_path_pts - 1):
#| offset1 = i * num_prof_verts
#| offset2 = (i + 1) * num_prof_verts
#| for j in range(num_prof_verts):
#| k = (j + 1) % num_prof_verts
#| b1 = offset1 + j
#| b2 = offset1 + k
#| t1 = offset2 + j
#| t2 = offset2 + k
#| faces.append([b1, b2, t2, t1])
#|
#| def add_cap(indices, expected_out):
#| n = polygon_normal_3d(pts, indices)
#| if v_dot(n, expected_out) < 0.0:
#| indices = list(reversed(indices))
#| faces.append(indices)
#|
#| if make_start_cap:
#| expected = v_scale(v_norm(path_tangents[0]), -1.0)
#| add_cap(list(range(num_prof_verts)), expected)
#| if make_end_cap:
#| expected = v_norm(path_tangents[-1])
#| base = (num_path_pts - 1) * num_prof_verts
#| add_cap([base + i for i in range(num_prof_verts)], expected)
#| return pts, faces
#|
#|
#|def create_swept_profile_object(name, path_points, path_normals, path_tangents, profile_center, profile_vertices, profile_normal_dir, profile_right_dir, make_start_cap, make_end_cap, collection):
#| verts, faces = build_swept_profile_mesh(
#| path_points,
#| path_normals,
#| path_tangents,
#| profile_center,
#| profile_vertices,
#| profile_normal_dir,
#| profile_right_dir,
#| make_start_cap,
#| make_end_cap,
#| )
#| return create_mesh_object(name, verts, faces, collection)
#|
#|
#|def join_objects(objs, name):
#| objs = [o for o in objs if o is not None]
#| if not objs:
#| return None
#| if len(objs) == 1:
#| obj = objs[0]
#| obj.name = name
#| return obj
#| bpy.ops.object.select_all(action="DESELECT")
#| for obj in objs:
#| obj.select_set(True)
#| bpy.context.view_layer.objects.active = objs[0]
#| bpy.ops.object.join()
#| obj = bpy.context.view_layer.objects.active
#| obj.name = name
#| return obj
#|
#|def ensure_mesh_object(obj):
#| if obj is None:
#| return None
#| if obj.type == "MESH":
#| return obj
#| bpy.ops.object.select_all(action="DESELECT")
#| obj.select_set(True)
#| bpy.context.view_layer.objects.active = obj
#| try:
#| bpy.ops.object.convert(target="MESH")
#| except Exception as exc:
#| raise RuntimeError(f"Failed to convert '{obj.name}' to mesh: {exc}")
#| new_obj = bpy.context.view_layer.objects.active
#| if new_obj is None or new_obj.type != "MESH":
#| raise RuntimeError(f"Conversion to mesh failed for '{obj.name}'.")
#| return new_obj
#|
///|
const BOOLEAN_DIFFERENCE_KEEP_MODIFIERS =
#|def boolean_difference(base_obj, cutters, name):
#| if base_obj is None:
#| return None
#| base_obj = ensure_mesh_object(base_obj)
#| cutters = [ensure_mesh_object(c) for c in cutters if c is not None]
#| if not cutters:
#| base_obj.name = name
#| return base_obj
#| bpy.ops.object.select_all(action="DESELECT")
#| base_obj.select_set(True)
#| bpy.context.view_layer.objects.active = base_obj
#| for cutter in cutters:
#| if cutter is None:
#| continue
#| if cutter.type != "MESH":
#| raise RuntimeError(f"Cutter '{cutter.name}' is not a mesh after conversion.")
#| mod = base_obj.modifiers.new(name=f"Bool_{cutter.name}", type='BOOLEAN')
#| mod.operation = 'DIFFERENCE'
#| mod.object = cutter
#| if hasattr(mod, "show_expanded"):
#| mod.show_expanded = False
#| cutter.hide_set(True)
#| cutter.hide_render = True
#| base_obj.name = name
#| return base_obj
#|
///|
const BOOLEAN_DIFFERENCE_APPLY_MODIFIERS =
#|def boolean_difference(base_obj, cutters, name):
#| if base_obj is None:
#| return None
#| base_obj = ensure_mesh_object(base_obj)
#| cutters = [ensure_mesh_object(c) for c in cutters if c is not None]
#| if not cutters:
#| base_obj.name = name
#| return base_obj
#| bpy.ops.object.select_all(action="DESELECT")
#| base_obj.select_set(True)
#| bpy.context.view_layer.objects.active = base_obj
#| for cutter in cutters:
#| if cutter is None:
#| continue
#| if cutter.type != "MESH":
#| raise RuntimeError(f"Cutter '{cutter.name}' is not a mesh after conversion.")
#| mod = base_obj.modifiers.new(name="Bool", type='BOOLEAN')
#| mod.operation = 'DIFFERENCE'
#| mod.object = cutter
#| try:
#| bpy.ops.object.modifier_apply(modifier=mod.name)
#| except Exception as exc:
#| raise RuntimeError(f"Boolean difference failed for '{cutter.name}': {exc}")
#| for cutter in cutters:
#| bpy.data.objects.remove(cutter, do_unlink=True)
#| base_obj.name = name
#| return base_obj
#|
///|
fn postscript(name : String) -> String {
(
$|
$| obj_count = len(collection.objects)
$| if obj_count == 0:
$| raise RuntimeError("No objects were created in the collection.")
$| bpy.ops.object.select_all(action="DESELECT")
$| for obj in collection.objects:
$| obj.select_set(True)
$| if collection.objects:
$| bpy.context.view_layer.objects.active = collection.objects[0]
$|
$|
$|def main():
$| try:
$| delete_default_cube()
$| # Create or reuse a named collection (based upon the name of this model).
$| # Delete all children of the collection in the case that it already exists.
$| collection_name = config.get('collectionName', '\{name} Collection')
$| print(f"\{'\\'}nSetting up collection '{collection_name}'...")
$| collection = set_up_collection(collection_name)
$|
$| create_model_objects(collection)
$|
$| print("Done.")
$| except Exception as exc:
$| print(f"ERROR: {exc}", file=sys.stderr)
$| sys.exit(1)
$|
$|
$|if __name__ == "__main__":
$| main()
)
}