How to snap to the intersection of lines?

Hi,
I started newly with KiCad (worked with Altium ~10 years) and have the following problem:
I need to generate the route tool path of a PCB and wanted to offset the PCB contour by half of the milling tool diameter.
In the screenshot you can see, that I duplicated the angled lines, rotated them by 90° and snapped them to the inflection points.
Now I wanted to drag the pink horizontal lines (color just for highlighting) and snap them to these, but that doesn’t work. Also if I enlarge the line by changing its start/end points, there is nothing at the intersections.
Do you have any ideas how to achieve that?

Cheers! :slight_smile:

1 Like

You will probably have better luck doing such mechanical/manufacturing prep in a tool designed for that. Export your board edge as dxf and then use a mechanical CAD package to build a tool path from that.

First, I probably would not do this at all in KiCad. The normal place to do this is in CAM software. Apart from directly generating vectors for CNC machines such software often also has functions for offsetting contours by the tool radius, generating lead in and lead out and adding tabs and more of such things.

Second, KiCad’s drawing tools are not very sophisticated. KiCad has no intention of re-creating all the complexity of a mechanical CAD program inside KiCad. Instead, KiCad has import and export functions so you can use your favorite mechanical CAD program.


And now for actually doing this in KiCad.

Well, first of, that does not work. Those lines should not have been rotated by 90 degrees, but by 90 degrees plus or minus half the angle between those lines.

In KiCad, snapping between graphical items only works between graphic items on the same layer.

KiCad does not have a real offset function. Pretty much the only way in KiCad to draw geometrically accurate objects is to use the grid system or enter coordinates manually (for example by editing the properties of a line segment) Horizontal and vertical lines are easy enough to duplicate this way.

Something that works reasonably is:

  1. Draw a circle with it’s center at the inflection point.
  2. Use [Ctrl + D] to duplicate the slanted line. This will keep it’s angle the same.
  3. Place the line so it is just inside or just outside the circle. (Set the grid to a suitable resolution).
  4. Select both line segments.
  5. Right click and Shape Modification / Extend lines to meet
  6. Delete the circle.

You are not going to get “perfect” geometry this way, but a PCB does not need tight tolerances in general. It just needs to fit inside something else. The method I described above is probably adequate for most cases. If you really need more accuracy, then a dedicated mechanical CAD program is the way to go.

1 Like

You can create accurate offset of a slanted line with:

  1. Make a duplicate of the slanted line with [Ctrl + D] (Place it anywhere).
  2. Rotate the duplicate 90 degrees.
  3. Edit its properties and set it length to the radius of your tool.
  4. Move the line, snap its endpoint to the endpoint of the original line.
  5. Make another duplicate of the orinal line, and snap it to the other endpoint of the helpline you created.

The Shape Modification menu has an Extend lines to meet function, but (strangely) it does not have a function to trim lines that are already crossing each other. You can work around this by moving (easy for horizontal or vertical lines) For slanted lines you can set them to a short length (which preserves their angle).

Yet another (and pretty complicated) way is to rotate your object so it is horizonal, then make some modificatons and then rotate it back. You can set the angle used for roations in PCB Editor / Preferences / Preferences / PCB Editor / Editing Options / Step for rotate commands. But be careful here. KiCad works with integers for internal coordinates, and rotations therefore will always introduce rounding errors (on the nano meter level). But none the less. if you thing this method is useful, use it sparingly.

For the angle, you can view the properties of any line segment, and copy it to the clipboard:

There are some tricks you can use with the midpoints of lines.

  1. If you have a line, you can grab it by it’s midpoint when you move it.
  2. If you move another object (for example the endpoint of a second line) then KiCad recognizes the midpoint of the first line as a valid snap point. KiCad shows a circle around the cursor cross when it recognizes a snap point.

I agree these tips and workarounds are both limited and ugly, but they can be useful if you want to modify some small things. There are around 1500 open issues on gitlab for KiCad, around 200 to 300 get closed every month, but just as many get created again. Mechanical CAD is not high on the priority list for KiCad. As said before, for complicated things, use export to and import from a mechanical CAD program.

1 Like

KiCad doesn’t currently have snapping to arbitrary line intersections. I am also not sure there’s an open feature request for it – it is probably worth opening one and we can see how popular it is. Right now we only support snapping to specific points (such as the endpoints of a line segment)

Thanks for alle the ideas.
I finally built the path in our CAD tool and then imported it in to the design.

Also, unfortunately the KiCad in our company is still v6 because we only use the versions that are integrated in the official Debian releases.
So I don’t have access to the shape modification tool yet :disappointed_relieved:

That’s a pretty challenging company policy. I would bring this up to whoever matters at the company for these policies that this is not a way that the KiCad team recommends working. Debian releases lag far behind the other Linux release channels we have due to how Debian operates, which is fine for software that evolves and changes slowly, but KiCad is not that. I would advise anyone using KiCad in the workplace to try to get on an annual upgrade cycle. This doesn’t mean upgrading right when the new version comes out necessarily – for example, if you are extremely risk-averse, then you could upgrade from V6 to V7 when V8 comes out. This will mean moving to a version of V7 that has been tested in the field for a year. Staying multiple years out of date with KiCad is a bad idea, though.

1 Like

You (I would) could make the argument that ‘backports’ ARE integrated into the official Debian releases.
https://backports.debian.org/
more specifically,

I have a server that runs Debian. I stay true to the safest packages for that. Ubuntu, from my understanding, runs more of the testing version of Debian. Backports is ‘safer’ than that.

KiCad is a program that receives many updates and improvements With each mayor KiCad version (expected each year around February, a bit before FOSDEM quite a lot of new features are introduced, and existing features are tweaked continuously. For example the Interactive router works better with each new KiCad version, even though it’s interface does not change (much).

But that said, the big “gaps” in KiCad’s functionality have been plumbed in V6. Before V6, mayor version updates had a significant impact on users, such as changing all the icons in KiCad, or dependencies on external libraries that finally got fixed. And the new S-expression based file formats for V6. KiCad V6, V7 and V8 work very similarly to each other, and although there are many improvements in each new version, they can all be considered “minor”.

One of the biggest things left is “future compatibility”. Once a project is saved in a newer version of KiCad, it can not be opened anymore in an older KiCad version.

I am working on a tool to split geometric entries, currently there is no working splitting for arcs so currently only lines and circles split. You can use this to generate the snapping points. Also implementing tangent construction isnt that hard but havent looked into it.

Like i said a month back kicad really needs these features. Note: i am implementing the intersections without a spatial accelerator. KiCAD has its own functions for this but so far i haven’t been able to wade through the typecasting hell of the python API to be able to use it. So dont select too many entries as the code is a O(N^2) tool but should work fine for dozens in selection.

The code would need to change a bit more to be MIT licensable but still, if you want to use the code as is currently i can post it here:

Licensed under CC BY but i will rewrite one function at some point so it can be MIT at some point

import pcbnew
from functools import wraps
from bisect import insort
from pcbnew import FromMM, ToMM, VECTOR2I, PCB_SHAPE
from math import atan2, acos, sin, cos, pi, sqrt, trunc


class SplitSelection( pcbnew.ActionPlugin ):
    def defaults( self ):
        self.name = "Split selection alpha 0"
        self.category = "Modify PCB"
        self.description = "Split selected items by eachother"

    def Run( self ):
        pcb = pcbnew.GetBoard()
        sel = []
        for d in pcb.GetDrawings():
            if (d.IsSelected()):
                try:
                    obj_shape = d.GetShape()
                except AttributeError:
                    continue
                if obj_shape is pcbnew.S_CIRCLE:
                    sel.append(Circle(d))
                if obj_shape is pcbnew.S_SEGMENT:
                    sel.append(Line(d))
        for i in range(len(sel)-1):
            for j in range(i+1,len(sel)):
              sel[i].splitBy(sel[j])  
        for i in sel:
            i.realize(pcb)
        pcbnew.Refresh()

    
class Circle:
    def __init__( self , circle): 
        self.li = []
        self.obj = circle
        
    def __getitem__( self, item ):
        return self.li[item]
        
    def append( self, item ):
        center = self.obj.GetCenter()
        
        insort(self.li, item, key=lambda a: atan2(a[0]-center[0],a[1]-center[1]))

    def splitBy( self, other ):
        obj_shape = other.obj.GetShape()
        if obj_shape is pcbnew.S_CIRCLE: #and not obj_shape is pcbnew.S_ARC:
            a = circle_circle_intersection( self.obj.GetCenter(), self.obj.GetRadius(), other.obj.GetCenter(), other.obj.GetRadius() )
        if obj_shape is pcbnew.S_SEGMENT:
            a = circle_line_segment_intersection( self.obj.GetCenter(), self.obj.GetRadius(), other.obj.GetStart(), other.obj.GetEnd() )
        if obj_shape is pcbnew.S_ARC:
            a = circle_circle_intersection( self.obj.GetCenter(), self.obj.GetRadius(), other.obj.GetCenter(), other.obj.GetRadius() )
            a = other.acceptPoints(a)
        for result in a:
            self.append(result)
            other.append(result)
        
        
    def realize(self, pcb):
        if len(self.li)<2:
            return
        center = self.obj.GetCenter()
        rad = self.obj.GetRadius()
        
        for i in range(len(self.li)-1):
            shape = pcbnew.PCB_SHAPE(pcb, pcbnew.S_ARC)
            a  = ( atan2( self.li[i][0]-center[0], self.li[i][1]-center[1] ))/2
            a += ( atan2( self.li[i+1][0]-center[0], self.li[i+1][1]-center[1] ))/2
            
            shape.SetArcGeometry(
              pcbnew.VECTOR2I( int(self.li[i][0]),int(self.li[i][1]) ),
              pcbnew.VECTOR2I( int(center[0]+sin(a)*rad), int(center[1]+cos(a)*rad) ),
              pcbnew.VECTOR2I( int(self.li[i+1][0]),int(self.li[i+1][1]) )
            )
            setShapeStyles(shape, self.obj)
            pcb.Add(shape)
            
            #~ shape = lineShape( pcb, self.li[i], center)
            #~ shape.SetLayer(self.obj.GetLayer())
            #~ pcb.Add(shape)
        
        shape = pcbnew.PCB_SHAPE(pcb, pcbnew.S_ARC)
        a  = ( atan2( self.li[-1][0]-center[0], self.li[-1][1]-center[1] )  )/2
        a += ( atan2( self.li[0][0]-center[0], self.li[0][1]-center[1] ) +2*pi )/2
            
        shape.SetArcGeometry(
          pcbnew.VECTOR2I( int(self.li[-1][0]),int(self.li[-1][1]) ),
          pcbnew.VECTOR2I( int(center[0]+sin(a)*rad), int(center[1]+cos(a)*rad) ),
          pcbnew.VECTOR2I( int(self.li[0][0]),int(self.li[0][1]) )
        )
        setShapeStyles(shape, self.obj)
        pcb.Add(shape)
        #~ shape = lineShape( pcb, self.li[-1], center)
        #~ shape.SetLayer(self.obj.GetLayer())
        #~ pcb.Add(shape)
    
        pcb.Remove(self.obj)

    
class Line:
    def __init__( self , line): 
        self.li = []
        self.obj = line
        self.append(tuple(line.GetStart()))
        self.append(tuple(line.GetEnd()))
        
    def __getitem__( self, item ):
        return self.li[item]
        
    def append( self, item ):
        insort(self.li, item)    
        
    def splitBy( self, other ):
        obj_shape = other.obj.GetShape()
        a=[]
        if obj_shape is pcbnew.S_CIRCLE:
            a=circle_line_segment_intersection(other.obj.GetCenter(), other.obj.GetRadius(), self.obj.GetStart(), self.obj.GetEnd() )
        if obj_shape is pcbnew.S_SEGMENT:
            a=line_line_segment_intersection(self.obj.GetStart(), self.obj.GetEnd(), other.obj.GetStart(), other.obj.GetEnd())
        if obj_shape is pcbnew.S_ARC:
            a=line_line_segment_intersection(self.obj.GetStart(), self.obj.GetEnd(), other.obj.GetStart(), other.obj.GetEnd())
            a = other.acceptPoints(a)
        for result in a:
                self.append(result)
                other.append(result)
        
        
    def realize(self, pcb):
        print(self.li)
        for i in range(len(self.li)-1):
            shape = lineShape( pcb, self.li[i], self.li[i+1])
            setShapeStyles(shape, self.obj)
            pcb.Add(shape)
        pcb.Remove(self.obj)
        

SplitSelection().register()

def setShapeStyles(shape, parent):
    shape.SetLayer(parent.GetLayer())
    shape.SetWidth(parent.GetWidth())
    #~ shape.SetDash(parent.GetDash())


def lineShape(pcb, a, b):
    shape = pcbnew.PCB_SHAPE(pcb, pcbnew.SHAPE_T_SEGMENT)
    shape.SetStart(pcbnew.VECTOR2I( int(a[0]),int(a[1]) ))
    shape.SetEnd(pcbnew.VECTOR2I( int(b[0]),int(b[1]) ))
    return shape


def circle_circle_intersection( c1, r1 , c2, r2 ):
    s = (c1[0] - c2[0], c1[1] - c2[1])
    if (s[0]==0 and s[1]==0):
       return []
    d = sqrt(s[0]*s[0] + s[1]*s[1])
    a1 = atan2(s[1], s[0])
    q = (float(d)*d+float(r2)*r2 - float(r1)*r1)/(2. * float(d) * float(r2))
    if (q<-1 or q>1):
        return []
    a2 =  [a1 + sign  *acos(q) for sign in (-1, 1) ];
    return [( c2[0]+r2*cos(theta), c2[1]+r2*sin(theta)) for theta in a2]

def line_line_segment_intersection( pt0, pt1, pt2, pt3 ):
    s1 = (pt1[0] - pt0[0],  pt1[1] - pt0[1])
    s2 = (pt3[0] - pt2[0],  pt3[1] - pt2[1])

    dr = (-s2[0] * s1[1] + s1[0] * s2[1])
    if (dr == 0):
        return []
    s = (-s1[1] * (pt0[0] - pt2[0]) + s1[0] * (pt0[1] - pt2[1])) / dr
    t = ( s2[0] * (pt0[1] - pt2[1]) - s2[1] * (pt0[0] - pt2[0])) / dr

    if (s > 0 and s < 1 and t > 0 and t < 1):
        return [( pt0[0] + t*s1[0], pt0[1] + t*s1[1] )]
    return []
    
def circle_line_segment_intersection(circle_center, circle_radius, pt1, pt2, tangent_tol=1e1):
    """
    adapted from https://stackoverflow.com/questions/30844482/
    """
    
    (p1x, p1y), (p2x, p2y), (cx, cy) = pt1, pt2, circle_center
    (x1, y1), (x2, y2) = (p1x - cx, p1y - cy), (p2x - cx, p2y - cy)
    dx, dy = (x2 - x1), (y2 - y1)
    dr = (dx ** 2 + dy ** 2)**.5
    big_d = x1 * y2 - x2 * y1
    discriminant = circle_radius ** 2 * dr ** 2 - big_d ** 2

    if discriminant < 0:  # No intersection
        return []
    else:  # There may be 0, 1, or 2 intersections with the segment
        intersections = [
            (cx + (big_d * dy + sign * (-1 if dy < 0 else 1) * dx * discriminant**.5) / dr ** 2,
             cy + (-big_d * dx + sign * abs(dy) * discriminant**.5) / dr ** 2)
            for sign in (1, -1) ]
        fraction_along_segment = [(xi - p1x) / dx if abs(dx) > abs(dy) else (yi - p1y) / dy for xi, yi in intersections]
        intersections = [pt for pt, frac in zip(intersections, fraction_along_segment) if 0 <= frac <= 1]
        if len(intersections) == 2 and abs(discriminant) <= tangent_tol:  # If line is tangent to circle, return just one point (as both intersections have same location)
            return [intersections[0]]
        else:
            return intersections

Last time i brought this up i was chastised for wasting development effort on functions nobody needs.

PS: offset is sort of possible with create polygon form selections bounding hull method and changing of line thickness

offset

1 Like

That bounding hull thing is a nice trick to create an outline, or to create something with a specific clearance around some other object. During this bounding hull creation you can also set the width of the gap:

image

That may (or may not) have been me. I had a short look at the topics you started but did not see it there. As an open source project, everybody is of course free to create issues (bug reports or feature requests) on gitlab. I am sometimes conflicted myself whether to recommend someone to report something on gitlab, or to advise against it (or just not respond at all).

Just yesterday I was experimenting a bit with circles on a margin layer, saw unexpected behavior and suspected a bug. I mentioned it on this forum instead of reporting it to gitlab, and after some hints from Piotr, I discovered it was expected and logical behavior. I sure am glad I did not “waste developers time” on that by reporting it on gitlab.

I am also slowly moving in the direction that more drawing capabilities in KiCad would be nice. It would be unlikely KiCad would get a full parametric engine, but making project related things such as wiring diagrams (possible bent wires in the schematic) and things like in this topic would be nice to have.

Programs like QElectroTech are more suitable for wiring diagrams of cabinets, but the one time I made a schematic for a CNC machine, I did it in KiCad because I had no interest in learning another program (which also had to be installed, updated, think about licensing, openness of file formats etc). Being able do all drawings in KiCad has it’s advantages.

It’s always about finding some kind of balance.

I am definitely thinking full parametric solver are out unless we can merge solve spaces algorithm.

But like i said previously, almost all simple drawing packages implement whats simple for the developer. Its as if they think users should be working at low level.

So to be a complete package with circle, arc and line primitives. You need a function to intersect the geometry. Why? Well otherwise its impossible to find the intersection accurately. Intersection finding is foundational since this allows you to manually construct nearly all things that these primitives can make:

  1. tangents, tangents can be found by intersections
  2. offsets, also depend on intersections due to tangents
  3. nearly any other construction you can think of. If you don’t believe me then you should consult treatises of greek mathematicians who studies the problem extensively. Its called geometry for a reason you know.

Image 1: Once you get intersections all this becomes possible.

All this is possible because the grid is no longer a impediment. And no i dont say you should draw stuff like this in KiCAD. I was just playing around with stuff like this because i was testing the intersection tool. Intersection wont make KiCADa mechanical CAD. But everything a mechanical 2d cad can do becomes possible once you open the door to intersection finding, if you are willing to do the work

But the thing is kicad has these functions! They don’t need to be rewritten. The algorithm that decides when things intersect are used by the interactive routing, zone filling and DRC all the time.

So if you look at the code i posted you can pretty much throw 80% away all you need is a single loop. Its just that i have hard time following how the code of the core is structured as the manuals are a bit bad for jumping in as a developer. Anybody already inside the system should be able to do this in a afternoon. My code above is done in 8 hours and it does all heavy lifting and i needed to learn how the kicad api works and how the manual should be interpretted. The fist line of code took most of the time about 6 of the 8 hours.

Though realistically somebody should go through the tool organization the right click menu is a mess, most of the stuff in the rmb menu needs to go elsewhere in the gui.

Also couldn’t the move item dialog be nonmodal.

Like I wrote before:

So KiCad definitely already has the ability to calculate intersections.

This is a sort of logical result of how Open Source projects work. There is no leading comittee that decides up front what function set is going to be implemented and what the GUI will look like. A lot of things grow “organical” from lots of small contributions from different sources. The first addition has to be fit “somewhere” in the GUI, another is later added to it. And then, in a later stadium a bunch of such things start to from a logical group and it becomes clear to put them in another place in the UI. This is of course less efficient, but laying out the big plan first and then working on it for a year before it works is not possible for many Open Source projects. It’s a compromise that is necessary to make progress at all.

I did not look at your code at all. I just can’t cough up the focus and concentration anymore to do (serious) coding myself. The whole Python interface in KiCad is now based on swig and it’s not stable. Plugins break with each mayor KiCad version. Long term goal is to drop SWIG and create a stable Python interface.

This is a good example of how features in KiCad sort of grow organically, and then get formalized later. Implementing SWIG is (apparently) simple. It creates something usable, and it also creates feedback on how important the feature is to KiCad users.

Yeah, i know the swig interface is not stable, its painfully obvious if you search for any info.