i’m looking for some addon to add a spiral on a toroidal shape
anyone has an addon for this ?
should be able to adjust the number of turns and curve bevel size
thanks
happy bl
i’m looking for some addon to add a spiral on a toroidal shape
anyone has an addon for this ?
should be able to adjust the number of turns and curve bevel size
thanks
happy bl
Interesting problem. I’ll post the code I have so far at the bottom, but it needs alot of cleanup and a few features added to be more useful.
Uniform shapes allow fairly quick and smooth creation of a bezier curve wrapping around the torus. You will notice a twist issue in the curve at the end point along the x-axis.
Because this script is creating a bezier path through the center of the torus (to be used in a follow path modifier) and calculating how many points are on a sample slice through the torus the additional points can quickly cause blender to become unresponsive during some of the operations. You can disable the modifier in viewport (without removing or applying it) to aid in response times.
As mentioned above a bezier follow path is created in the process prior to the final bezier coil so odd shapes are followed reasonably accurately.
Due to the fact that (at the moment) when making the final coil path (add coil button) each wrap is generated in an even circular pattern (instead of adding points at the specific torus loop edges) sharp corners are not currently accurately followed.
This would require storing the angle of each point in the sample slice plane from the center of the slice plane and when creating the coil rotate by that specific angle list.
Currently there are no checks to verify the selected “torus” object actually has a hole through it.
The “torus” object must currently have the hole through the z-axis. (This would be a good option for someone to add requiring additional property in property group and modifying most if not all operations to check which axis rotation should occur through)
The “torus” object must have a unity world_matrix
. (There is an option/operator to set force the object matrix if not already but this will reset scaling, rotation, translation of the object and is not enforced. The other operators will be allowed but the results will be broken.)
For anyone who wants to play with it or clean it up / add functionality / fix improve etc. as I will likely not take this further.
bl_info = {
"name": "Test Addon",
"author": "Nezumi",
"version": (0, 0, 1),
"blender": (3, 1, 0),
"location": "View3D > UI > Test Panel",
"description": "",
"warning": "",
"wiki_url": "",
"category": "Development",
"support": "TESTING"
}
import bpy
from mathutils import Vector, Euler, Matrix
from math import radians
import bmesh
def point_filter(self, object):
return object.type == 'MESH'
class TEST_PG_props(bpy.types.PropertyGroup):
my_obj: bpy.props.PointerProperty(
type=bpy.types.Object, poll=point_filter
)
my_wraps: bpy.props.IntProperty(
name="wraps",
description="# wraps for 360 deg of torus",
default=4,
min=1,
soft_max=100,
)
pts_per_wrap: bpy.props.IntProperty(
name="points per wrap",
description="# points in cross section of torus",
default=4,
min=1,
soft_max=100,
)
coil_radius: bpy.props.FloatProperty(
name="diameter",
description="diameter of wire coil",
default=0.1,
min=0.0,
soft_max=2,
)
def recurLayerCollection(layerColl, collName):
found = None
if (layerColl.name == collName):
return layerColl
for layer in layerColl.children:
found = recurLayerCollection(layer, collName)
if found:
return found
def ensure_collection(context, master_coll, target_coll_name):
target_coll = recurLayerCollection(master_coll, target_coll_name)
if target_coll:
context.view_layer.active_layer_collection = target_coll
else:
# create a new collection in the master scene collection
target_coll = bpy.data.collections.new(target_coll_name)
context.scene.collection.children.link(target_coll)
target_coll = recurLayerCollection(master_coll, target_coll_name)
def make_mesh(context, name="", coll_name="", verts=[], edges=[], faces=[]):
my_props = context.scene.test_pg
mesh_data = bpy.data.meshes.new(name)
mesh_obj = bpy.data.objects.new(mesh_data.name, mesh_data)
mesh_obj.location = Vector(my_props.my_obj.location)
if coll_name:
coll = bpy.data.collections.get(coll_name)
else:
coll = context.scene.collection
coll.objects.link(mesh_obj)
context.view_layer.objects.active = mesh_obj
mesh_data.from_pydata(verts, edges, faces)
return mesh_obj
def make_slice_plane(context):
act_obj = context.view_layer.objects.active
my_props = context.scene.test_pg
coll = bpy.data.collections.get(f'{my_props.my_obj.name}_scratch')
# make mesh slice plane
bbox_x = []
bbox_y = []
bbox_z = []
for i in my_props.my_obj.bound_box:
bbox_x.append(i[0])
bbox_y.append(i[1])
bbox_z.append(i[2])
my_x = max(abs(i) for i in bbox_x)
my_y = max(abs(i) for i in bbox_y)
my_z = max(abs(i) for i in bbox_z)
ref_plane_w = max([my_x, my_y]) * 2
ref_plane_h = (my_z + 0.1) * 2
vts = [
(0, 0, ref_plane_h),
(0, 0, -ref_plane_h),
(ref_plane_w, 0, ref_plane_h),
(ref_plane_w, 0, -ref_plane_h),
]
eds = []
fcs = [
[0, 1, 3, 2, ],
]
s_plane = make_mesh(
context,
name=f'{my_props.my_obj.name}_slice_plane',
coll_name=f'{my_props.my_obj.name}_scratch',
verts=vts,
edges=eds,
faces=fcs)
mod = s_plane.modifiers.new('Boolean', 'BOOLEAN')
mod.operation = 'INTERSECT'
mod.object = my_props.my_obj
mod.solver = 'FAST'
# create single wrap for vert count
coil_ref = s_plane.copy()
coil_ref.data = s_plane.data.copy()
coil_ref.name = f'{my_props.my_obj.name}_coil_ref'
coil_ref.data.name = f'{my_props.my_obj.name}_coil_ref'
coll.objects.link(coil_ref)
bpy.ops.object.select_all(action='DESELECT')
coil_ref.select_set(True)
context.view_layer.objects.active = coil_ref
bpy.ops.object.modifier_apply(modifier="Boolean")
bpy.ops.object.convert(target='CURVE')
coil_ref.select_set(False)
if act_obj:
context.view_layer.objects.active = act_obj
act_obj.select_set(True)
def key_rotation(context, obj, axis=0, step=1):
rot = 0
for frm in range(context.scene.frame_end):
context.scene.frame_set(frm+1)
obj.rotation_euler[axis] = rot
obj.keyframe_insert(data_path="rotation_euler", frame=frm+1)
rot += radians(360/step)
def make_bez_path(context, bez_name="", duration=0, verts=[]):
my_props = context.scene.test_pg
my_curve_data = bpy.data.curves.new(bez_name, 'CURVE')
my_curve_data.dimensions = '3D'
my_curve_data.resolution_u = 1
my_curve_data.use_path = True
my_curve_data.path_duration = duration
bez = my_curve_data.splines.new('BEZIER')
bez.bezier_points.add(len(verts)-1)
for i in range(len(verts)):
x, y, z = verts[i]
bez.bezier_points[i].co = (x, y, z)
bez.bezier_points[i].handle_left_type = 'AUTO'
bez.bezier_points[i].handle_right_type = 'AUTO'
bez.use_cyclic_u = True
my_curve_obj = bpy.data.objects.new(bez_name, my_curve_data)
my_curve_obj.location = Vector(my_props.my_obj.location)
return my_curve_obj
def rotate_slice_plane(context):
act_obj = context.view_layer.objects.active
my_props = context.scene.test_pg
coll = bpy.data.collections[f'{my_props.my_obj.name}_scratch']
coords = []
ref_coil = bpy.data.objects[f'{my_props.my_obj.name}_coil_ref']
pts = my_props.my_wraps * len(ref_coil.data.splines[0].points)
s_plane = bpy.data.objects[f'{my_props.my_obj.name}_slice_plane']
x, y, z = (radians(0), radians(0), radians(360/pts))
R = Euler((x, y, z)).to_matrix().to_4x4()
context.scene.frame_start = 1
context.scene.frame_end = pts
cur_frame = context.scene.frame_start
rot = 0
# keyframe slice_plane
key_rotation(context, s_plane, axis=2, step=pts)
# calculate ctr of torus at slice_plane
depsgraph = bpy.context.evaluated_depsgraph_get()
for frm in range(context.scene.frame_end):
context.scene.frame_set(frm+1)
context.view_layer.update()
ob = s_plane.evaluated_get(depsgraph)
local_bbox_center = 0.125 * sum(
(Vector(b) for b in ob.bound_box),
Vector())
global_bbox_center = ob.matrix_world @ local_bbox_center
coords.append(global_bbox_center)
ob.matrix_world = R @ ob.matrix_world
# make bezier curve as path at ctr of torus
my_curve_obj = make_bez_path(
context,
bez_name=f'{my_props.my_obj.name}_torus_path_ctr',
duration=pts,
verts=coords
)
coll.objects.link(my_curve_obj)
if act_obj:
context.view_layer.objects.active = act_obj
act_obj.select_set(True)
def add_line_segment(context):
act_obj = context.view_layer.objects.active
my_props = context.scene.test_pg
my_curve_obj = context.view_layer.objects[
f'{my_props.my_obj.name}_torus_path_ctr']
ref_coil = context.view_layer.objects[f'{my_props.my_obj.name}_coil_ref']
coll = bpy.data.collections[f'{my_props.my_obj.name}_scratch']
# add line segment (2 points from ctr torus extended on x-axis 0.01)
vts = [
(0.0, 0.0, 0.0),
(0.1, 0.0, 0.0),
]
eds = [
[0, 1, ],
]
fcs = [
]
coil_path = make_mesh(
context,
name=f'{my_props.my_obj.name}_coil_path',
coll_name=f'{my_props.my_obj.name}_scratch',
verts=vts,
edges=eds,
faces=fcs)
mod = coil_path.modifiers.new('Shrinkwrap', 'SHRINKWRAP')
mod.wrap_method = 'PROJECT'
mod.wrap_mode = 'OUTSIDE_SURFACE'
mod.target = my_props.my_obj
mod.offset = my_props.coil_radius
coil_path_const = coil_path.constraints.new('FOLLOW_PATH')
coil_path_const.target = my_curve_obj
coil_path_const.use_curve_follow = True
# animate follow path constraint
bpy.ops.object.select_all(action='DESELECT')
coil_path.select_set(True)
context.view_layer.objects.active = coil_path
bpy.ops.constraint.followpath_path_animate(
constraint="Follow Path",
owner='OBJECT')
coil_path.select_set(False)
# rotate empty around follow axis (y-axis)
key_rotation(
context,
coil_path,
axis=1,
step=len(ref_coil.data.splines[0].points))
if act_obj:
context.view_layer.objects.active = act_obj
act_obj.select_set(True)
def add_coil(context):
act_obj = context.view_layer.objects.active
my_props = context.scene.test_pg
coll_name = f'{my_props.my_obj.name}_scratch'
coll = bpy.data.collections[coll_name]
coil_path = coll.objects[f'{my_props.my_obj.name}_coil_path']
ctr_path = coll.objects[f'{my_props.my_obj.name}_torus_path_ctr']
bm = bmesh.new()
# calculate coil path around torus
coords = []
depsgraph = bpy.context.evaluated_depsgraph_get()
for frm in range(context.scene.frame_end):
context.scene.frame_set(frm+1)
bm.from_object(coil_path, depsgraph)
rme = bpy.data.meshes.new("Rib")
bm.to_mesh(rme)
coords.append(coil_path.matrix_world @ rme.vertices[1].co)
bm.clear()
bpy.ops.outliner.orphans_purge()
# make bezier curve as coil path around torus
my_coil_obj = make_bez_path(
context,
bez_name=f'{my_props.my_obj.name}_coil',
verts=coords
)
my_coil_obj.data.resolution_u = 4
my_coil_obj.data.bevel_depth = my_props.coil_radius / 2
coll.objects.link(my_coil_obj)
if act_obj:
context.view_layer.objects.active = act_obj
act_obj.select_set(True)
class OBJECT_OT_make_slice_plane(bpy.types.Operator):
bl_idname = 'object.make_slice_plane'
bl_label = "Make Slice Plane"
@classmethod
def poll(cls, context):
my_props = context.scene.test_pg
cond1 = my_props.my_obj
return cond1
def execute(self, context):
my_props = context.scene.test_pg
print(f'{self.bl_idname} button pressed')
master_coll = context.view_layer.layer_collection
current_coll = recurLayerCollection(
master_coll,
context.collection.name)
target_coll_name = f'{my_props.my_obj.name}_scratch'
ensure_collection(context, master_coll, target_coll_name)
my_coll = bpy.data.collections[target_coll_name]
if len(my_coll.objects) > 0:
while my_coll.objects:
bpy.data.objects.remove(my_coll.objects[0], do_unlink=True)
bpy.ops.outliner.orphans_purge()
make_slice_plane(context)
return {'FINISHED'}
class OBJECT_OT_rotate_slice_plane(bpy.types.Operator):
bl_idname = 'object.rotate_slice_plane'
bl_label = "Rotate Slice Plane"
@classmethod
def poll(cls, context):
my_props = context.scene.test_pg
cond1 = my_props.my_obj
return cond1
def execute(self, context):
my_props = context.scene.test_pg
print(f'{self.bl_idname} button pressed')
rotate_slice_plane(context)
return {'FINISHED'}
class OBJECT_OT_add_line_segment(bpy.types.Operator):
bl_idname = 'object.add_line_segment'
bl_label = "Add line segment"
@classmethod
def poll(cls, context):
my_props = context.scene.test_pg
cond1 = my_props.my_obj
return cond1
def execute(self, context):
my_props = context.scene.test_pg
print(f'{self.bl_idname} button pressed')
add_line_segment(context)
return {'FINISHED'}
class OBJECT_OT_add_coil(bpy.types.Operator):
bl_idname = 'object.add_coil'
bl_label = "add coil"
@classmethod
def poll(cls, context):
my_props = context.scene.test_pg
cond1 = my_props.my_obj
return cond1
def execute(self, context):
my_props = context.scene.test_pg
print(f'{self.bl_idname} button pressed')
add_coil(context)
return {'FINISHED'}
class OBJECT_OT_apply_scale(bpy.types.Operator):
bl_idname = 'object.apply_scale'
bl_label = "Apply Scale"
@classmethod
def poll(cls, context):
my_props = context.scene.test_pg
cond1 = my_props.my_obj
return cond1
def execute(self, context):
my_props = context.scene.test_pg
print(f'{self.bl_idname} button pressed')
act_obj = context.view_layer.objects.active
bpy.ops.object.select_all(action='DESELECT')
my_props.my_obj.select_set(True)
context.view_layer.objects.active = my_props.my_obj
bpy.ops.object.transform_apply(
location=False,
rotation=False,
scale=True)
context.view_layer.objects.active = act_obj
act_obj.select_set(True)
return {'FINISHED'}
class OBJECT_OT_reset_mw(bpy.types.Operator):
"""Reset matrix world translation, scale, rotation affected"""
bl_idname = 'object.reset_mw'
bl_label = "Reset matrix world"
@classmethod
def poll(cls, context):
my_props = context.scene.test_pg
cond1 = my_props.my_obj
return cond1
def execute(self, context):
my_props = context.scene.test_pg
print(f'{self.bl_idname} button pressed')
my_props.my_obj.matrix_world = Matrix()
return {'FINISHED'}
class VIEW3D_PT_test(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Test Panel"
bl_idname = "VIEW3D_PT_test_panel"
bl_label = "Main Panel"
def draw(self, context):
my_props = context.scene.test_pg
layout = self.layout
box = layout.box()
col = box.column(align=True)
col.label(text="Target Torus")
col.prop(my_props, "my_obj", text="")
if my_props.my_obj:
if my_props.my_obj != context.object:
col.label(text="caution not active object")
if my_props.my_obj.scale != Vector((1.0, 1.0, 1.0)):
col.label(text="warning scale non unity")
col.operator('object.apply_scale')
if my_props.my_obj.matrix_world != Matrix():
col.label(text="matrix world non unity")
col.operator('object.reset_mw')
box = layout.box()
col = box.column(align=False)
col.label(text="Coil wraps per 360 deg of torus")
col.prop(my_props, "my_wraps", text="Wraps")
col.prop(my_props, "coil_radius", text="diameter")
try:
coil_ref = bpy.data.objects.get(f'{my_props.my_obj.name}_coil_ref')
multiplier = len(coil_ref.data.splines[0].points)
except AttributeError:
multiplier = 0
if multiplier > 0:
col.label(text=f'Coil Points: {multiplier * my_props.my_wraps}')
else:
col.label(text=f'Coil Points: not calculated')
col.operator('object.make_slice_plane')
col.operator('object.rotate_slice_plane')
col.operator('object.add_line_segment')
col.operator('object.add_coil')
classes = [
VIEW3D_PT_test,
TEST_PG_props,
OBJECT_OT_apply_scale,
OBJECT_OT_reset_mw,
OBJECT_OT_make_slice_plane,
OBJECT_OT_rotate_slice_plane,
OBJECT_OT_add_line_segment,
OBJECT_OT_add_coil,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.test_pg = bpy.props.PointerProperty(
type=TEST_PG_props)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
del bpy.types.Scene.test_pg
if __name__ == "__main__":
register()
thanks for trying to solve such weird shape !
i began an operator script
have to test the curve adding or mesh not certain yet how to start it
i was not really thinking in term of different shape
more the sort of torus we find on the market for electrical coils
but interesting
thanks
happy bl
Use the Curve-Extra Objects addon (included)
add a torus…
add…Curve > Curve Spirals > Torus
Adjust and /or use a shrinkwrap modifier…( make sure to click on the tab in the shrinkwrap > Apply to Spline
or it will be a flat ribbon…
*Note: my machine it too old i cannot use BL 3.0 *
*only last version of 2.9 *
add torus curve !
this seems to add a simple circle curve it is not a 3D torus ?
how do you make it 3D ?
thanks
happy bl
@ RSEhlers
tried you simplified method but could not get it to work
which BL are you using ?
when i try to add spiral curve torus
i get like a circle curve are there other parameters to get a spiral ?
can you upload a sample file
thanks for feedback
happy bl
@ nezumi.blend
got script to work in 2.9
now the torus you select is it a mesh torus or some curve torus ?
thanks
happy bl
for the new addon
when i try with mesh cube or mesh coil torus
i get an error
AttributeError: ‘BooleanModifier’ object has no attribute ‘solver’
do you know how to correct that or how to make it work ?
thanks
happy bl
In the POP-up window lower Left…select TURNS…adjust till you get the proper radis for the spiral curve to fit the original torus ( MESH TORUS).
Add a Shink-Wrap Modifier and adjust it to get a good fit…adjust the Geometry of the curve in the curve tab for Bevel > Round > Depth…TWEAK till all is good…
Apply the Shrinkwrap and convert the spiral curve to mesh if you want to Boolean > Union
are you saying you need first to start with a mesh torus and a spiral around it
then use your addon ?
or add cube torus with a spiral shrinkwrap around ?
thanks
happy bl
YES add a mesh torus…(I don’t know what a Cube torus even is??) select a single vertex on an edge and cursor to selection…then add the Curve Spiral Torus…
if i do that i get this
probably wrong or my set up is not as yours
where should the curve should be ?
and you uploaded a new modified script in original thread
so i will re donwload it
thanks
happy bl
when you add spiral curve on tours mesh you place it at origin of torus
how many turns do you use for the torus curve or not important ?
then convert to mesh and use shrinkwrap around mesh torus!
thanks
happy bl
Yes Origins are the Same…as well as location on cursor
Turns are just the amount of loops the Curve Torus is going to wrap around…
The important things are the Inner Radius the Radius and the Number of steps…Match all of them to the original Mesh Torus ( I can’t even find something that produces the Box you showed…( Where are you adding that from? )
I also find that the ends of the curve torus will split apart… making it cyclic will join them but you end up with a bulge…so the best way is to Extrude one and snap to the other… then in the curve parameters use SMOOTHING and it will clean it up…
sorry for delay
can you show image with let say a square torus and the spiral cuve
just before you use your script
at least that would show me the initial set up
the square torus is just one example of a torus shape
there can several different shape - round square
thanks for feedback