https://github.com/mirefek/geo_logic
Tip revision: c8c9b715cae0acfb07585c25659b1333d075cc5b authored by mirefek on 25 July 2021, 19:43:55 UTC
bugfix
bugfix
Tip revision: c8c9b71
gtool_constr.py
from gtool import GTool
from geo_object import *
import itertools
from primitive_constr import circumcircle
from primitive_pred import not_collinear
"""
Construction tools are ComboPoint, ComboLine, ComboPerpLine, ComboCircle and ComboCircumCircle
They all support automatic addition of new points which is handled by an abstract class
GToolConstr.
"""
class GToolConstr(GTool):
# point can be now a GUI index, or tuple (self.smart_intersection, a, b)
def lies_on(self, p, cl):
if isinstance(p, tuple) and p[0] == self.smart_intersection:
return cl in p[1:]
else: return GTool.lies_on(self, p, cl)
# check for applying touchpoint on line and a circle
def is_tangent(self, c, l):
c,l = map(self.vis.gi_to_li, (c,l))
if self.vis.li_to_type(c) == Line: c,l = l,c
if self.vis.li_to_type(c) != Circle or self.vis.li_to_type(l) != Line:
return False
label = self.tools.is_tangent_cl
return self.vis.logic.get_constr(label, (c,l)) is not None
# numerical intersection only if it is guarantied that there is one
def intersect(self, cl1, cln1, cl2, cln2):
if cln1 is cln2: return None
if isinstance(cln1, Line) and isinstance(cln2, Line):
x = intersection_ll(cln1, cln2)
if x is None: return None
else: return (x,)
if isinstance(cln2, Line): cln1, cln2 = cln2, cln1
if isinstance(cln1, Line): res = intersection_lc(cln1, cln2)
else: res = intersection_cc(cln1, cln2)
if len(res) == 0: return None
if len(res) == 1 and not self.is_tangent(cl1, cl2): return None
return tuple(res)
# find an intersection near the mouse coordinates
def select_intersection(self, coor, point_on = None):
if point_on is None:
cl1,cln1 = self.select_cl(coor, permanent = False)
if cl1 is None: return None,None
else: cl1,cln1 = point_on
candidates = []
for cl2,cln2 in itertools.chain(
self.vis.selectable_lines,
self.vis.selectable_circles,
):
if cl2 == cl1: continue
dist_cl = cln2.dist_from(coor)
if dist_cl >= self.find_radius: continue
intersections = self.intersect(cl1, cln1, cl2, cln2)
if intersections is None: continue
for x in intersections:
dist_x = np.linalg.norm(x-coor)
if dist_x >= self.find_radius: continue
candidates.append((dist_x, dist_cl, x, cl2, cln2))
if not candidates:
if point_on is None: self.hl_selected.pop()
return None, None
min_dist_x = min(dist_x for dist_x,_,_,_,_ in candidates)
_,x,cl2,cln2 = min((
(dist_cl,x,cl,cln)
for dist_x,dist_cl,x,cl,cln in candidates
if eps_identical(dist_x, min_dist_x)
))
x = Point(x)
self.hl_propose(x, permanent = True)
self.hl_select(cl2)
return (self.smart_intersection, x, cl1, cl2), x
# find a point, or create a new one
def select_pi(self, coor, point_on = None):
if point_on is None: filter_f = None
else:
def filter_f(p,pn): return self.lies_on(p, point_on[0])
p,pn = self.select_point(coor, filter_f = filter_f)
if p is not None: return p,pn
return self.select_intersection(coor, point_on = point_on)
# create a new intersection in the logic system
# if there are two options, it attempts to smartly decide whether to guide
# the position of the intersection by a point or not
def smart_intersection(self, x, cl1, cl2, update = True):
if self.is_tangent(cl1, cl2):
if self.vis.gi_to_type(cl1) == Line: cl1, cl2 = cl2, cl1
return self.run_tool("touchpoint", cl1, cl2, update = update)
cln1 = self.vis.gi_to_num(cl1)
cln2 = self.vis.gi_to_num(cl2)
if isinstance(cln1, Line) and isinstance(cln2, Line):
return self.run_tool("intersection", cl1, cl2, update = update)
if isinstance(cln2, Line):
cl1, cl2 = cl2, cl1
cln1, cln2 = cln2, cln1
x1,x2 = self.intersect(cl1, cln1, cl2, cln2)
r = np.linalg.norm(x1 - x2) / 4
if isinstance(cln1, Line):
l = cl1
lr = np.linalg.norm(x1 - x2) - 2*r
else: l = None
candidates = []
l_candidates = []
for p,pn in self.vis.selectable_points:
d1,d2 = (np.linalg.norm(x12-pn.a) for x12 in (x1,x2))
d = min(d1, d2)
if l is not None and self.lies_on(p, l):
if not eps_smaller(abs(d1-d2), lr) : l_candidates.append((d,p))
elif not eps_bigger(d, r): candidates.append((d,p))
if l_candidates: candidates = l_candidates
if candidates:
_,p = min(candidates)
return self.run_m_tool("intersection", x, cl1, cl2, p, update = update)
return self.run_m_tool("intersection", x, cl1, cl2, update = update)
class ComboPoint(GToolConstr):
icon_name = "point"
key_shortcut = 'x'
label = "Point Tool"
def update_basic(self, coor):
p, pn = self.select_point(coor)
if p is not None:
self.confirm_next = self.update_midpoint, p, pn
return
cl,cln = self.select_cl(coor)
if cl is not None:
self.drag = self.drag_intersection, cl, cln
p,pn = self.select_intersection(coor, point_on = (cl,cln))
if p is None:
self.confirm = self.run_m_tool, "m_point_on", Point(coor), cl
else: self.confirm = p
return
self.confirm = self.run_m_tool, "free_point", Point(coor)
# after a click on a point
def update_midpoint(self, coor, p, pn):
p2, pn2 = self.select_point(coor)
if p2 is not None:
if p2 == p or pn2.identical_to(pn): return
self.hl_propose(Point((pn.a+pn2.a)/2), permanent = False)
self.hl_add_helper((pn.a, pn2.a))
self.confirm = self.run_tool, "midpoint", p, p2
self.drag = self.drag_foot, p,pn, p2,pn2
return
# foot to a line / circle
cl, cln = self.select_cl(
coor,
filter_f = lambda l,ln: isinstance(ln, Circle) or not ln.contains(pn.a)
)
if cl is not None:
if isinstance(cln, Circle):
if eps_identical(cln.c, pn.a): return
# Point on a circle -> select arc direction or diameter
if self.lies_on(p, cl):
op_point = Point(2*cln.c - pn.a)
if np.linalg.norm(op_point.a - coor) < self.find_radius:
self.hl_propose(op_point)
self.confirm = self.run_tool, "opposite_point", p, cl
else:
self.hl_selected.pop()
pos1 = vector_direction(pn.a - cln.c)
pos2 = vector_direction(coor - cln.c)
pos_arc = ((pos2-pos1) % 2) < 1
if pos_arc: self.hl_select((cl, pos1, pos1+1))
else: self.hl_select((cl, pos1+1, pos1), permanent = False)
self.confirm_next = self.select_arc_midpoint, p,pn, cl,cln, pos_arc
return
foot = cln.closest_on(pn.a)
if np.linalg.norm(coor-foot) < 4*self.find_radius:
self.hl_propose(Point(foot))
self.hl_add_helper((pn.a, foot))
self.confirm = self.run_tool, "foot", p, cl
return
self.hl_propose(Point((pn.a+coor)/2))
self.hl_add_helper((pn.a, coor))
# after clicking a point and a circle containing it
def select_arc_midpoint(self, coor, p1,pn1, circ,circn, pos_arc):
p2,pn2 = self.select_point(
coor,
filter_f = lambda p,pn: self.lies_on(p,circ)
)
if p2 is not None and p2 == p1:
self.hl_select(circ)
self.hl_propose(Point(2*circn.c - pn1.a))
self.confirm = self.run_tool, "opposite_point", p1, circ
return
v1 = pn1.a - circn.c
if p2 is None:
if eps_identical(coor, circn.c): return
v2 = coor - circn.c
v2 *= circn.r / np.linalg.norm(v2)
else: v2 = pn2.a - circn.c
if eps_identical(v1, v2): return
if not pos_arc: v1,v2 = v2,v1
v = -vector_perp_rot(v1 - v2)
v *= circn.r / np.linalg.norm(v)
p = Point(circn.c + v)
self.hl_propose(p)
self.hl_select((circ, vector_direction(v1), vector_direction(v2)))
if p2 is not None:
if pos_arc: self.confirm = self.run_tool, "midpoint_arc", p1, p2, circ
else: self.confirm = self.run_tool, "midpoint_arc", p2, p1, circ
# after clicking a point and draging from another point (foot to a line)
def drag_foot(self, coor, p,pn, p1,pn1):
p2,pn2 = self.select_point(coor)
if p2 is not None:
if p2 == p1 or pn2.identical_to(pn1): return
self.confirm = self.run_tool, "foot", p, p1, p2
coor = pn2.a
if eps_identical(pn1.a, coor): return
l = line_passing_np_points(pn1.a, coor)
foot = Point(l.closest_on(pn.a))
self.hl_propose(foot)
self.hl_add_helper(l, (pn.a, foot.a))
# dragging from a line / circle -> circle center or an intersection
def drag_intersection(self, coor, cl1,cln1):
## circle center
if isinstance(cln1, Circle): # close to the circle center
self.hl_add_helper(Point(cln1.c))
if np.linalg.norm(coor - cln1.c) < self.find_radius:
self.hl_propose(Point(cln1.c))
self.confirm = self.run_tool, "center_of", cl1
return
cl2,cln2 = self.select_cl(coor)
if cl2 is None:
# futher from to circle center but also from other clines
if isinstance(cln1, Circle) and \
np.linalg.norm(coor - cln1.c) < cln1.r:
self.hl_propose(Point(cln1.c))
self.confirm = self.run_tool, "center_of", cl1
return
if cl2 == cl1: return
## intersection
intersections = self.intersect(cl1, cln1, cl2, cln2)
if intersections is None: return
x = Point(min(intersections, key = lambda x: np.linalg.norm(x-coor)))
self.hl_propose(x)
self.confirm = self.smart_intersection, x, cl1, cl2
class ComboLine(GToolConstr):
icon_name = "line"
key_shortcut = 'l'
label = "(Parallel) Line"
def update_basic(self, coor):
p,pn = self.select_pi(coor)
if p is not None:
self.drag = self.drag_parallel, p, pn
self.confirm_next = self.update_point2, p, pn
return
l,ln = self.select_line(coor)
if l is not None:
self.confirm_next = self.select_parallel, (l,), ln.n
# after clicking on a point
def update_point2(self, coor, p1, pn1):
# simple line P -> P
p2,pn2 = self.select_pi(coor)
if p2 is not None:
if p2 != p1 and not pn2.identical_to(pn1):
l = line_passing_points(pn1, pn2)
self.confirm = self.run_tool, "line", p1, p2
self.drag = self.drag_angle_bisector, p1,pn1, p2,pn2
self.hl_propose(l, permanent = False)
return
# tangents
c,cn = self.select_circle(
coor,
filter_f = (lambda c,cn: self.lies_on(p1,c) or
eps_smaller(cn.r, np.linalg.norm(cn.c - pn1.a)))
)
if c is not None:
if isinstance(cn, Circle):
if eps_identical(coor, cn.c): free_pos = 0
else: free_pos = vector_direction(coor-cn.c)
if self.lies_on(p1, c):
v = pn1.a - cn.c
pos = vector_direction(v)
pos_diff = abs((free_pos - pos+1)%2-1)
if pos_diff < 0.3:
self.confirm = self.run_tool, "tangent_at", p1, c
self.hl_propose(Line(v, np.dot(v, pn1.a)))
return
else:
dia_rad = np.linalg.norm(pn1.a - cn.c)/2
if eps_zero(dia_rad): return
diacirc = Circle((pn1.a + cn.c)/2, dia_rad)
touchpoint_cand = intersection_cc(cn, diacirc)
if len(touchpoint_cand) < 2: return
i,touchpoint = min(
enumerate(touchpoint_cand),
key = lambda x: np.linalg.norm(x[1]-coor)
)
pos = vector_direction(touchpoint-cn.c)
pos_diff = abs((free_pos - pos+1)%2-1)
if pos_diff < 0.2:
tangent_name = "tangent{}".format(i)
self.confirm = self.run_tool, tangent_name, p1, c
self.hl_propose(
Point(touchpoint),
line_passing_np_points(touchpoint, pn1.a),
)
return
# free line passing a point
free_l = line_passing_np_points(pn1.a, coor)
self.confirm = self.run_m_tool, "m_line", free_l, p1
self.hl_propose(free_l)
# numerical helper
def angle_bisector(self, A, B, C):
v1 = A - B
v2 = C - B
v1 /= np.linalg.norm(v1)
v2 /= np.linalg.norm(v2)
if np.dot(v1, v2) > 0:
n = vector_perp_rot(v1+v2)
else:
n = v1-v2
return Line(n, np.dot(B, n))
# after clicking a point and dragging from another
def drag_angle_bisector(self, coor, p,pn, p1,pn1):
p2,pn2 = self.select_pi(coor)
if p2 is not None:
if p2 == p1:
self.confirm = self.run_tool, "line", p, p1
self.hl_propose(line_passing_points(pn,pn1))
return
coor = pn2.a
self.hl_add_helper((pn.a,pn1.a))
if eps_identical(coor, pn.a): return
self.hl_add_helper((pn.a, coor))
self.hl_propose(self.angle_bisector(pn1.a, pn.a, coor))
if p2 is not None:
self.confirm = self.run_tool, "angle_bisector_int", p1,p,p2
# after dragging from a point -> virtual line
def drag_parallel(self, coor, p1, pn1):
p2,pn2 = self.select_pi(coor)
if p2 is None:
self.hl_add_helper(line_passing_np_points(pn1.a, coor))
elif p2 != p1 and not pn2.identical_to(pn1):
ln = line_passing_points(pn1, pn2)
self.hl_add_helper(ln)
self.confirm_next = self.select_parallel, (p1,p2), ln.n
# after clicking a line / drawing a line by dragging -> parallel
def select_parallel(self, coor, line_def, normal_vec):
p,pn = self.select_pi(coor)
if p is None:
new_l = Line(normal_vec, np.dot(normal_vec, coor))
self.hl_propose(new_l)
direction = ("direction_of", *line_def)
self.confirm = self.run_m_tool, "m_line_with_dir", new_l, direction
else:
new_l = Line(normal_vec, np.dot(normal_vec, pn.a))
self.hl_propose(new_l)
args = line_def+(p,)
self.confirm = self.run_tool, "paraline", *args
# Very similar to ComboLine
class ComboPerpLine(GToolConstr):
icon_name = "perpline"
key_shortcut = 't'
label = "Perpendicular Line"
def update_basic(self, coor):
p,pn = self.select_pi(coor)
if p is not None:
self.confirm_next = self.update_point2, p, pn
self.drag = self.drag_perp, p, pn
return
l,ln = self.select_line(coor)
if l is not None:
self.confirm_next = self.select_perp, (l,), ln.v
# numerical helper
def perp_bisector(self, p1, p2):
v = p1 - p2
return Line(v, np.dot(v, p1+p2)/2)
# after clicking on a point
def update_point2(self, coor, p1, pn1):
p2,pn2 = self.select_pi(coor)
if p2 is None:
l = self.perp_bisector(pn1.a, coor)
self.hl_add_helper((pn1.a, coor))
self.hl_propose(l)
elif p2 != p1 and not pn2.identical_to(pn1):
l = self.perp_bisector(pn1.a, pn2.a)
self.confirm = self.run_tool, "perp_bisector", p1, p2
self.drag = self.drag_angle_bisector, p1,pn1, p2,pn2
self.hl_add_helper((pn1.a, pn2.a))
self.hl_propose(l, permanent = False)
# numerical helper
def angle_bisector(self, A, B, C):
v1 = A - B
v2 = C - B
v1 /= np.linalg.norm(v1)
v2 /= np.linalg.norm(v2)
if np.dot(v1, v2) > 0:
n = v1+v2
else:
n = vector_perp_rot(v1-v2)
return Line(n, np.dot(B, n))
# after clicking a point and dragging from another
def drag_angle_bisector(self, coor, p,pn, p1,pn1):
self.hl_add_helper((pn.a,pn1.a))
p2,pn2 = self.select_pi(coor)
if p2 is not None:
if p2 == p1:
self.confirm = self.run_tool, "perp_bisector", p, p1
self.hl_propose(self.perp_bisector(pn.a, pn1.a))
return
coor = pn2.a
if eps_identical(coor, pn.a): return
self.hl_add_helper((pn.a, coor))
self.hl_propose(self.angle_bisector(pn1.a, pn.a, coor))
if p2 is not None:
self.confirm = self.run_tool, "angle_bisector_ext", p1,p,p2
# after dragging from a point -> virtual line
def drag_perp(self, coor, p1, pn1):
p2,pn2 = self.select_pi(coor)
if p2 is None:
l = line_passing_np_points(pn1.a, coor)
self.hl_add_helper(l)
self.hl_propose(Line(l.v, np.dot(l.v, coor)))
elif p2 != p1 and not pn2.identical_to(pn1):
l = line_passing_points(pn1, pn2)
self.hl_add_helper(l)
self.hl_propose(Line(l.v, np.dot(l.v, pn2.a)), permanent = False)
self.confirm_next = self.select_perp, (p1,p2), l.v
# after clicking a line / drawing a line by dragging -> perpendicular
def select_perp(self, coor, line_def, normal_vec):
p,pn = self.select_pi(coor)
if p is None:
new_l = Line(normal_vec, np.dot(normal_vec, coor))
self.hl_propose(new_l)
direction = "perp_direction", *line_def
self.confirm = self.run_m_tool, "m_line_with_dir", new_l, direction
else:
nl = Line(normal_vec, np.dot(normal_vec, pn.a))
self.hl_propose(nl)
self.confirm = self.run_tool, "perpline", *(line_def+(p,))
class ComboCircle(GToolConstr):
icon_name = "circle"
key_shortcut = 'c'
label = "Circle with Center"
def update_basic(self, coor):
p,pn = self.select_pi(coor)
if p is not None:
self.confirm_next = self.update_p, p, pn
self.drag = self.drag_radius, p, pn
return
c,cn = self.select_circle(coor)
if c is not None:
r = ("radius_of", c)
self.confirm_next = self.update_center, r,cn.r, None
# after clicking on a point
def update_p(self, coor, center, center_n):
p, pn = self.select_pi(coor)
if p is not None:
if p != center and not pn.identical_to(center_n):
new_c = Circle(center_n.a, np.linalg.norm(center_n.a-pn.a))
self.hl_propose(new_c)
self.confirm = self.run_tool, "circle", center, p
return
l, ln = self.select_line(
coor, filter_f = lambda l,ln: not ln.contains(center_n.a)
)
if ln is not None: # circle touching a line
foot = ln.closest_on(center_n.a)
if np.linalg.norm(coor-foot) < 4*self.find_radius:
circle = Circle(center_n.a, np.linalg.norm(foot - center_n.a))
foot = Point(foot)
self.hl_propose(circle, foot)
self.confirm = self.make_tangent_to_line, center, l
return
# copy circle radius
new_c = Circle(center_n.a, np.linalg.norm(center_n.a-coor))
self.hl_propose(new_c)
self.confirm = self.run_m_tool, "m_circle_with_center", new_c, center
def make_tangent_to_line(self, center, l):
foot, = self.run_tool("foot", center, l, update = False)
return self.run_tool("circle", center, foot)
# after dragging from a point
def drag_radius(self, coor, p1, pn1):
p2, pn2 = self.select_pi(coor)
if p2 is not None and not pn2.identical_to(pn1):
rn = np.linalg.norm(pn2.a-pn1.a)
new_c = Circle(pn2.a, rn)
self.hl_propose(new_c, permanent = False)
r = "dist", p1, p2
self.confirm_next = self.update_center, r,rn, (pn1.a,pn2.a)
elif not eps_identical(coor, pn1.a):
c = Circle(coor, np.linalg.norm(coor-pn1.a))
self.hl_propose(c)
# after selecting a radius
def update_center(self, coor, r,rn, radius_helper):
if radius_helper is not None:
self.hl_add_helper(radius_helper)
center, center_n = self.select_pi(coor)
if center is None:
cn = Circle(coor, rn)
self.hl_propose(cn)
self.confirm = self.run_m_tool, "m_circle_with_radius", cn, r
else:
c = Circle(center_n.a, rn)
self.hl_propose(c)
args = r[1:]+(center,)
self.confirm = self.run_tool, "compass", *args
class ComboCircumCircle(GToolConstr):
icon_name = "circumcircle"
key_shortcut = 'o'
label = "Circumcircle tool"
def update_basic(self, coor):
p1,pn1 = self.select_pi(coor)
if p1 is not None:
self.confirm_next = self.update_p, p1, pn1
# after selecting one point
def update_p(self, coor, p1, pn1):
p2,pn2 = self.select_pi(coor)
if p2 is None:
center = (pn1.a + coor)/2
c = Circle(center, np.linalg.norm(center-coor))
self.hl_propose(c)
self.confirm = self.run_m_tool, "m_circle_passing1", c, p1
elif p1 != p2 and not pn2.identical_to(pn1):
center = (pn1.a + pn2.a)/2
c = Circle(center, np.linalg.norm(center-pn2.a))
self.hl_propose(c, permanent = False)
self.confirm_next = self.update_pp, p1,pn1,p2,pn2
# after selecting two points
def update_pp(self, coor, p1,pn1, p2,pn2):
p3,pn3 = self.select_pi(coor)
c = None
if p3 is None:
pn3 = Point(coor)
if not_collinear(pn1,pn2,pn3):
c = circumcircle(pn1,pn2,pn3)
self.confirm = self.run_m_tool, "m_circle_passing2", c, p1,p2
elif p3 == p2 or p3 == p1:
center = (pn1.a + pn2.a)/2
c = Circle(center, np.linalg.norm(center-pn2.a))
self.confirm = self.run_tool, "diacircle", p1,p2
elif not_collinear(pn1,pn2,pn3):
c = circumcircle(pn1,pn2,pn3)
self.confirm = self.run_tool, "circumcircle", p1,p2,p3
if c is not None: self.hl_propose(c)