Plotting and Dragging An Arc

I’m working on a piece of code to drag and draw an arc. Once the width of the arc is set I can then adjust the height by dragging the mouse. It replicates the two-point arc function found in many pieces of CAD software. However, I’m having problems with the arc plotting code. When I run it, dragging the arc height simply expands the whole arc (width and height), when the arc width(cord) should remain constant with only the height changing.

Here’s a quick drawing of what I’m trying to achieve (the top red arc):

pt1->pt2 or c is the width/cord of the arc
pt3 or h is the height of the arc which I’m dragging out with the mouse
I calculate the radius of the circle that fits all three points, then work out the angle from the center of the circle, between pt1 and pt2.
This angle is then divided by a number of segments and the corresponding points for the arc looped through and returned for drawing.

I’ve been testing with a simple ellipse function, so I know that my surround code works (mouse dragging, drawing, etc). I know this should be straightforward but I’m going around in circles (literally and figuratively :)). I think I’ve looked at it too long. Any help, much appreciated!

Current Arc Code:

# height(h), width(c) and midpoint(m) between 
# pt1 and pt2 of the arc are passed from the dragging function
# along with the number of segments

def plot_arc_r_dim(h, c, m, segs):
 
    d = (c * c) / (8 * h)
    theta = (2 * math.atan(h / (2 * d))) / segs
    # theta = math.pi / segs
    dx = 0
    dy = 0
    dz = 0

    r = (h / 2) + d
    mx = m[0]
    my = m[1]

    verts = []

    # anti-clockwise
    for i in range(segs + 1):
        dx = mx + math.cos(theta * i) * r
        dy = my + math.sin(theta * i) * r
        verts.append(Vector([dx, dy, dz]))

    return verts

Hi,

2 things come to my mind :

  • Code depends a lot on h and c but we don’t know here if it’s well computed
  • Depending on the drag position, arc should start at different angle whereas here in your code, it always start at 0 (theta*i)

Hi,

Thanks for the comment. c is simply a distances between pt1 and pt2 which are set in world space and h is a distance from a vector halfway between pt1 and pt2 and pt3.

Rotation shouldn’t have any bearing on this as I build arc starting at 0 degrees, then transform with a rotation matrix later on.

Hi,
As you show in your exemple, you have 3 cases :

  • h < c / 2
  • h = c / 2
  • h > c / 2

In the image r = h + d, but in the code r = (h/2) + d. Why ??
In the first case, I found
d = ((c/2)**2 - h**2) / (2*h) (withr = h+d and d**2 = r**2 - (c/2)**2)
theta = atan(d / (c/2))


If you need more help, just tell.

1 Like

Thanks for your comment Elreenys, I appreciate the time you took to write and draw your solution. It didn’t really solve the main issue unfortunately. I posted the code and image after a long day, so made a few errors but finding the radius has not been an issue. I’ve fixed the code now so that arcs work correctly when h is <= chord/2. I just need to work a solution to cases where h is > chord/2.

I’ve moved the code out of blender for testing (using python pillow for drawing). Here’s a very rough, un-factored bit of code. If you can see a good solution for the second case, I’d appreciate the help. I’ll post myself, if I come up with anything.

nb. this code is very rough but the import calculation is in the middle

import math
import PIL.ImageDraw as ImageDraw, PIL.Image as Image, PIL.ImageShow as ImageShow


im = Image.new("RGB", (800, 600))
draw = ImageDraw.Draw(im)


def draw_pts(pts):

    for p in pts:
        draw.ellipse(
            [(p[0], p[1]), (p[0] + 5, p[1] + 5)], width=2, fill=(255, 255, 255)
        )


# screen center
tx = 400
ty = 300

# pts
pt1 = [tx - 100, ty]  # chord start
pt2 = [tx + 100, ty]  # chord end
pt3 = [tx, ty + 50]  # arc height
draw_pts([pt1, pt2, pt3])

# calc dimensions (mid pt between pt1 & pt2,
# chord, height, radius, dist from chord to circle center)
m = [(pt2[0] - pt1[0]) / 2, pt2[1]]
c = pt2[0] - pt1[0]
h = pt3[1] - m[1]
r = (h / 2) + ((c * c) / (8 * h))
d = r - h
# circle center
mx = pt3[0]
my = pt3[1] - r

# start and end angles
segs = 12
st = math.atan2(pt1[1] - my, pt1[0] - mx)
et = math.atan2(pt2[1] - my, pt2[0] - mx)
step = st / segs
i = st
j = 360

# -theta to theta
while i > et:
    dx = mx + math.cos(i) * r
    dy = my + math.sin(i) * r
    draw.ellipse([(dx, dy), (dx + 5, dy + 5)], outline=255, width=1, fill=255)
    i -= step

    # failsafe for testing
    j -= 1
    if j < 0:
        break

im.show()

In the last case, r = (c ** 2) / (8 * h) + (h / 2), same as before in fact.
But d = h - r this time.
I’m too tired, I will continue tomorow.

1 Like

Here a code that works pretty well in xy plan but needs some adjustments with angle.

import bpy
from math import cos, sin, pi, acos

from mathutils import Vector, Matrix

# a and b 2 points
pt_a = Vector((-16,12,0))
pt_b = Vector((25,30,0))

bpy.ops.mesh.primitive_uv_sphere_add(location=pt_a)
bpy.ops.mesh.primitive_uv_sphere_add(location=pt_b)

vect_ab = pt_b - pt_a
dist_pts = vect_ab.length
print("length = ",dist_pts)

height = 50
radius = (dist_pts**2)/(8*height) + (height/2)
print("radius = ", radius)

difference = height - radius

# if c is mid vector ab
pt_c = (pt_b + pt_a)/2 

print("mid point c: ", pt_c)
bpy.ops.mesh.primitive_uv_sphere_add(location=pt_c)

# in local base c(u, v, w)
ub = dist_pts / 2 # = k * u
vb = 0
wb = 0

# local a
ua = -dist_pts / 2
va = 0
wa = 0
uvwa = Vector((ua, va, wa))

# local m
um = 0
vm = difference # d or -d, depends on the orientation
wm = 0

uvwm = Vector((um,vm,wm))
print("center : ",uvwm)
xb = pt_b[0]
yb = pt_b[1]
zb = pt_b[2]

xc = pt_c[0]
yc = pt_c[1]
zc = pt_c[2]


vec_ab = pt_b - pt_a
rotation = vec_ab.to_2d().angle_signed(Vector((1,0)), "error")
print("angle diff = ",rotation*180/pi)

# need to find the translation vector
ma = Matrix.Rotation(rotation, 4, 'Z')
trans = ma @ Vector((0,0,0))
loc = pt_c - trans

# and apply it to find the center of circle
mloc = Matrix.Translation(loc)
pos_m = mloc @ ma @ uvwm
print("m ", pos_m)
bpy.ops.mesh.primitive_cone_add(location=pos_m)

rho = acos((dist_pts/2) / radius)

total_angle = pi + 2 * rho
theta = 3*pi + rho
steps = total_angle / 8


def draw_circle(angle, center):
    x = center[0] + radius * cos(angle) 
    y = center[1] + radius * sin(angle)
    return x,y

i = theta
while i >= 2*pi - rho:
    gx, gy = draw_circle(i + rotation, pos_m)
    bpy.ops.mesh.primitive_cube_add(location=(gx,gy,0))
    i -= steps

Thanks for the post @Elreenys I couldn’t get your code to do what I wanted it to, however, I’ve carried on with my own code and found a solution (code below). There are still a couple of things to tidy up (there’s a bit of a jump you can see in the video when changing direction) but it’s almost there.

Code working in add-on:

Here’s a version of the code that I’ve used:

import math
import PIL.ImageDraw as ImageDraw, PIL.Image as Image, PIL.ImageShow as ImageShow


im = Image.new("RGB", (800, 600))
draw = ImageDraw.Draw(im)


def draw_pts(pts, col):
    for p in pts:
        draw.ellipse([(p[0], p[1]), (p[0] + 5, p[1] + 5)], width=1, fill=col)


# screen center
tx = 400
ty = 300

height = -200
neg = False

if height < 0:
    neg = True


height = abs(height)

# pts
pt1 = [tx - 100, ty]  # chord start
pt2 = [tx + 100, ty]  # chord end
pt3 = [tx, ty + height]  # arc height
draw_pts([pt1, pt2, pt3], (255, 255, 255))

# calc dimensions (mid pt between pt1 & pt2,
# chord, height, radius, dist from chord to circle center)
m = [(pt2[0] - pt1[0]) / 2, pt2[1]]
c = pt2[0] - pt1[0]
h = pt3[1] - m[1]
r = (h / 2) + ((c * c) / (8 * h))
d = r - h

# height is > than c/2
if h > c / 2:
    d = r - (2 * r - h)

# circle center
mx = pt3[0]
my = pt3[1] - r

# start and end angles
segs = 12
et = math.atan2(pt2[1] - my, pt2[0] - mx)
# 180 degrees from et
st = math.pi - et

if neg:
    et -= math.pi
    st -= math.pi


step = (st - et) / segs
i = st

verts = []
# -theta to theta
while i > et:
    dx = mx + math.cos(i) * r
    dy = my + math.sin(i) * r

    if neg:
        dy -= d * 2

    verts.append([dx, dy])
    i -= step

draw_pts(verts, (255, 0, 0))


im.show()

Nice :slightly_smiling_face:

1 Like