Log for the Record. Crash and burn. Read only if you want to see me confused.

Note
I thought I had it figured out without going back to drawing the picture and doing the work. Perhaps I have but I am not convinced, and I’ve lost the thread. Two hours here, wasted. I don’t even know what lesson I’ve learned.

Grr

I think the basic issue is that my offset value needs to take into account the length of the ArcRod, but I’m not really sure quite how. Possibly I need to do the check on the result, not on the offsets. Anyway, we have an initial test that is supposed to deliver two different choices, and the ones I actually want.

    def test_update_90_select(self):
        length = 5
        position = vector(0,6)
        radius = 2
        arc_rod_a = ArcRod(
            length=length,
            target_position=position,
            target_radius=radius,
            target_angle=0)
        parent_finish = vector(0,0)
        start, finish_a1, finish_a2 = arc_rod_a.update_both(parent_finish)
        arc_rod_b = ArcRod(
            length=length,
            target_position=position,
            target_radius=radius,
            target_angle=180)
        start_, finish_b1, finish_b2 = arc_rod_b.update_both(parent_finish)
        assert finish_b1 == finish_a2
        assert finish_b2 == finish_a1

That won’t compile, because the ArcRod does not expect target_angle. Change signature:

class ArcRod:
    def __init__(self, *, length, target_position, target_radius, target_angle):
        self._length = length
        self._target_position = target_position
        self._target_radius = target_radius
        radius_vector = vector(target_radius, 0)
        self._target_offset = radius_vector.rotate_xy(target_angle*math.pi/180)
        print(f'{target_angle=} {self._target_offset=}')

This prints <2,0,0> when the angle is 0 and <-2,0,0> when it is 180. That is good, as 2 is the right side and -2 the left. Now if only we had comparable values. We’re comparing against values like <1.5612494995996002, 4.75, 0>. So we need our known radius stuffed in there.

But we do not know that value until we’re inside the calculation.

I code this:

    def update_both(self, p_finish):
        axis = self._target_position - p_finish
        angle = math.atan2(axis.y, axis.x)
        desired_offset = self._target_offset + vector(self._length,0).rotate_xy(angle)
        offset_1, offset_2 = self.target_offsets(p_finish)
        rotated_1 = offset_1.rotate_xy(angle)
        rotated_2 = offset_2.rotate_xy(angle)
        d1 = desired_offset.distance(rotated_1)
        d2 = desired_offset.distance(rotated_2)
        if d1 > d2:
            rotated_1, rotated_2 = rotated_2, rotated_1
        print(f'{rotated_1=} {rotated_2=} {desired_offset=}')
        return p_finish, p_finish + rotated_1, p_finish + rotated_2

And my test passes. The printed results look good:

target_angle=0 self._target_offset=v_Vector((2.0, 0.0, 0))
rotated_1=v_Vector((1.5612494995996002, 4.75, 0)) rotated_2=v_Vector((-1.5612494995995998, 4.75, 0)) desired_offset=v_Vector((2.0000000000000004, 5.0, 0))

target_angle=180 self._target_offset=v_Vector((-2.0, 2.4492935982947064e-16, 0))
rotated_1=v_Vector((-1.5612494995995998, 4.75, 0)) rotated_2=v_Vector((1.5612494995996002, 4.75, 0)) desired_offset=v_Vector((-1.9999999999999998, 5.0, 0))

This might be OK. It’s good enough to commit: first test_select working.

The Reasoning

I can’t quite explain my reasoning here. I’m not even convinced that this always works. Let’s try a new test, at 45 degrees this time.

    def test_update_45_select(self):
        length = 5
        position = vector(6,0).rotate_xy(45)
        radius = 2
        arc_rod_a = ArcRod(
            length=length,
            target_position=position,
            target_radius=radius,
            target_angle=-45)
        parent_finish = vector(0,0)
        start, finish_a1, finish_a2 = arc_rod_a.update_both(parent_finish)
        arc_rod_b = ArcRod(
            length=length,
            target_position=position,
            target_radius=radius,
            target_angle=135)
        start_, finish_b1, finish_b2 = arc_rod_b.update_both(parent_finish)
        assert finish_b1 == finish_a2
        assert finish_b2 == finish_a1

Here’s the print. The two points chosen are approximatelyL

-45: <4, 2.8> and 134: <1.2, 5.6>

target_angle=-45 self._target_offset=v_Vector((1.4142135623730951, -1.414213562373095, 0))
rotated_1=v_Vector((3.8237521487706445, 3.221633049366716, 0)) rotated_2=v_Vector((1.166806744997788, 4.86195043370741, 0)) desired_offset=v_Vector((4.040823506461744, 2.8403040602974974, 0))

target_angle=135 self._target_offset=v_Vector((-1.414213562373095, 1.4142135623730951, 0))
rotated_1=v_Vector((1.166806744997788, 4.86195043370741, 0)) rotated_2=v_Vector((3.8237521487706445, 3.221633049366716, 0)) desired_offset=v_Vector((1.2123963817155539, 5.668731185043687, 0))

I’ve lost the thread. I don’t think this is working but I’m not sure. I tried to set up a 45 degree drawing in the linkage animator and it is thrashing. Bad input values or bad code? I’m not at all sure.

Time to stop fighting. Withdraw from the field, start again tomorrow.

Grr. Listing included so I can refer to it from the other room while on iPad.



def opposite_angle(side_a, side_b, side_c):
    a_squared = side_a * side_a
    b_squared = side_b * side_b
    c_squared = side_c * side_c
    b_c = side_b * side_c
    cos_angle_opposite_side_a = (b_squared + c_squared - a_squared) / (2 * b_c)
    if cos_angle_opposite_side_a > 1:
        cos_angle_opposite_side_a = 1
    print(f'{cos_angle_opposite_side_a=}')
    return math.acos(cos_angle_opposite_side_a)


class ArcRod:
    def __init__(self, *, length, target_position, target_radius, target_angle):
        self._length = length
        self._target_position = target_position
        self._target_radius = target_radius
        radius_vector = vector(target_radius, 0)
        self._target_offset = radius_vector.rotate_xy(target_angle*math.pi/180)
        # print(f'{target_angle=} {self._target_offset=}')

    def update(self, parent_start, parent_finish):
        start, finish_1, finish_2 = self.update_both(parent_finish)
        return start, finish_1

    def init_draw(self, canvas, start, finish):
        self._cached_line = canvas.create_line(
            start.x, start.y,
            finish.x, finish.y,
            width=4, fill="blue"
        )

    def draw(self, canvas, start, finish):
        canvas.coords(self._cached_line,
                      start.x, start.y,
                      finish.x, finish.y)

    def update_both(self, p_finish):
        axis = self._target_position - p_finish
        angle = math.atan2(axis.y, axis.x)
        desired_offset = self._target_offset + vector(self._length,0).rotate_xy(angle)
        offset_1, offset_2 = self.target_offsets(p_finish)
        rotated_1 = offset_1.rotate_xy(angle)
        rotated_2 = offset_2.rotate_xy(angle)
        d1 = desired_offset.distance(rotated_1)
        d2 = desired_offset.distance(rotated_2)
        if d1 > d2:
            rotated_1, rotated_2 = rotated_2, rotated_1
        # print(f'{rotated_1=} {rotated_2=} {desired_offset=}')
        return p_finish, p_finish + rotated_1, p_finish + rotated_2

    def target_offsets(self, p_finish):
        opposite_side = self._target_radius
        adjacent_side_1 = self._length
        adjacent_side_2 = p_finish.distance(self._target_position)
        angle = opposite_angle(opposite_side, adjacent_side_1, adjacent_side_2)
        offset_1 = vector(adjacent_side_1, 0).rotate_xy(angle)
        offset_2 = vector(adjacent_side_1, 0).rotate_xy(-angle)
        return offset_1, offset_2