Closest point on SVG path

Instead of looking for the closest point to the mouse from all the points of the svg, how do I limit to finding just the closest point on a specific path?

For example If I keyPressed 3, it would just look for the closest point to the mouse from path 3?

// https://discourse.processing.org/t/calculate-distance-from-point-to-closest-point-of-shape/7201/7

import geomerative.*;

RShape myshape;
RPoint [] points;
boolean diagpoints = true;
RPoint mouse = new RPoint(0,0);

float mind;
int nearest = -1;

int calc_min_mouse_dist() {
  int minpos = -1;
  mind = 10000;

  for (int i =0; i<points.length; i++) {
    float d = points[i].dist(mouse);
    if ( d < mind ) { 
      mind = d;
      minpos = i;
    }
  }
  return minpos;
}

void setup () {
  size (500,500); 
  RG.init(this);
  myshape = RG.loadShape ("lines.svg");
  points = myshape.getPoints();
  println(points.length);
  //printArray(points);
}

void draw() {
  background(100,200,0);
  
  if(points != null ){
    if ( diagpoints )    for(int i=0; i<points.length; i++)   ellipse(points[i].x, points[i].y,2,2);  
      draw_mouseline();
    }
}

void mouseDragged() {
  mouse= new RPoint(float(mouseX), float(mouseY));
  nearest = calc_min_mouse_dist();
}

void draw_mouseline() {
  fill(200, 0, 0);
  if (nearest > -1 ){
    line(points[nearest].x, points[nearest].y, mouse.x, mouse.y);
    text(int(mind), mouse.x+10, mouse.y-10);
    
    circle(points[nearest].x, points[nearest].y, 10);
  }
}
1 Like

Hi! That’s a great question, could you upload lines.svg to make it easier for us to test?

Sure, here it is: https://www.dropbox.com/s/hgh5mp750rl5plg/lines.svg?dl=0

Cool!

I’m not very used to Geomerative, but I think there is a way of getting “children” paths of the SVG instead of all the points together (this is certainly true on standard Processing loadShape() SVG->PShape loading). And I also know Geomerative does separate paths from typeface characters.

I’m working on changing your example to use .getPointsInPaths(), let’s see if I can do it.

So, my code is very ugly but I guess it demonstrates how to get an array of arrays of points instead of just all points inside the same array…

Use points = myshape.getPointsInPaths(); // RPoint [][] points

import geomerative.*;

RShape myshape;
RPoint [][] points;
int chosenPath = 0;
boolean diagpoints = true;
RPoint mouse = new RPoint(0, 0);

float mind;
int nearest = -1;

int calc_min_mouse_dist() {
  int minpos = -1;
  mind = 10000;
  int ip = chosenPath;
  for (int i =0; i<points[ip].length; i++) {
    float d = points[ip][i].dist(mouse); 
    if ( d < mind  && ip == chosenPath) { 
      mind = d; 
      minpos = i;
    }
  }
  return minpos;
}

void setup () {
  size (500, 500); 
  RG.init(this); 
  myshape = RG.loadShape ("lines.svg"); 
  points = myshape.getPointsInPaths(); // RPoint [][] points
  println(points.length); 
  //printArray(points);
}

void draw() {
  background(100, 200, 0); 
  text("chosenPath = " + chosenPath, 30, 30);
  for (int ip = 0; ip <points.length; ip++) {
    if (points[ip] != null) {
      if ( diagpoints ) for (int i=0; i<points[ip].length; i++)  ellipse(points[ip][i].x, points[ip][i].y, 2, 2); 
      draw_mouseline();
    }
  }
}

void mouseDragged() {
  mouse= new RPoint(float(mouseX), float(mouseY)); 
  nearest = calc_min_mouse_dist();
}

void mouseReleased() { 
  nearest = -1;
}

void draw_mouseline() {
  fill(200, 0, 0); 
  if (nearest > -1) {
    int ip = chosenPath;
    line(points[ip][nearest].x, points[ip][nearest].y, mouse.x, mouse.y); 
    text(int(mind), mouse.x+10, mouse.y-10); 
    circle(points[ip][nearest].x, points[ip][nearest].y, 10);
  }
}

void keyPressed() {
  chosenPath = (chosenPath + 1) % points.length;
}

EDIT: I have removed some unnecessary for loops! :smiley:

4 Likes

This is great, thank you! And don’t worry about ugly code, I’m the master of ugly code!
I’ve just thought, maybe a more initiative way of picking which path to select would be to find the nearest path on mousePressed instead of the keyPressed route?

1 Like

:slight_smile:
I think you could work out something like this:
On mousePressed, calculate the nearest point for each of the paths, then choose the path which has the smallest distance…

1 Like

Hi @sixfeet!

Looking at the Geomerative docs, it seems to have some “closest” functionality built in, but I didn’t manage to work it out (seems like it is related to intersections and might need a line/shape instead of a point).

  // Get the intersection points
  RClosest c = shp.getClosest(cuttingLine);
  RPoint[] ps = c.distance > 0 ? c.closest : c.intersects;

On the other hand, I have implemented the mousePressed thing, but in Python mode :slight_smile:

add_library('geomerative')

chosen_path = 0
diag_points = True
mouse = RPoint(0, 0)
nearest_point = None

def setup():
    global paths
    size(500, 500)
    RG.init(this)
    myshape = RG.loadShape("lines.svg")
    paths = myshape.getPointsInPaths()  # RPoint [][] paths

def draw():
    background(100, 200, 0)
    text("chosen_path = {}".format(chosen_path), 30, 30)
    for points in paths:
        if points:
            if diag_points:
                for p in points:
                    ellipse(p.x, p.y, 2, 2)
            draw_mouseline()

def draw_mouseline():
    global mouse
    mouse = RPoint(float(mouseX), float(mouseY))
    fill(200, 0, 0)
    if nearest_point:
        line(nearest_point.x, nearest_point.y, mouse.x, mouse.y)
        text(int(min_d), mouse.x + 10, mouse.y - 10)
        circle(nearest_point.x, nearest_point.y, 10)

def mouseDragged():
    global nearest_point
    nearest_point = calc_nearest_point(paths[chosen_path])

def calc_nearest_point(path):
    global min_d
    nearest_point = None
    min_d = 10000
    for i, p in enumerate(path):
        d = p.dist(mouse)
        if d < min_d:
            min_d = d
            nearest_point = p
    return nearest_point

def mouseReleased():
    global nearest_point
    nearest_point = None

def mousePressed():
    global chosen_path
    min_d = 1000
    for i, path in enumerate(paths):
        p = calc_nearest_point(path)
        d = p.dist(mouse)
        if d < min_d:
            min_d = d
            nearest_path = i
    chosen_path = nearest_path

def keyPressed():
    global chosen_path
    chosen_path = (chosen_path + 1) % len(paths)
2 Likes

@villares amazing! Is there a reason why you chose python over java?

I’ve been enjoying Python best in the last few years, I feel like it flows better for me, and I find the code very readable. But it is unfortunate that it lacks autocomplete in the Processing IDE.

It does read a lot easier. Trying to convert your code to java but the ease of flow in python is tricking me up porting it to java!

If I have the time I’ll try and port it back. The main tricky thing I introduced, I guess, is that the calculation of the closest point now returns a point and not an index to the point in the array. You might now have to check for null instead of -1 when there is no point.

I’m close, but the mousePressed doesn’t seem right and is breaking it, it either has to be a really tiny min_d to check against or else it fails. :face_with_raised_eyebrow:
Not as robust as your python solution for sure!

import geomerative.*;

RShape myshape;
RPoint [][] paths;
int chosenPath = 0;
boolean diagpaths = true;
RPoint mouse = new RPoint(0, 0);

float min_d;
int nearest,chosen_path, primid = -1;


int calc_nearest_point() {
  int minpos = -1;
  min_d = 10000;
  int ip = chosenPath;
  for (int i =0; i<paths[ip].length; i++) {
    float d = paths[ip][i].dist(mouse); 
    if ( d < min_d  && ip == chosenPath) { 
      min_d = d; 
      minpos = i;
    }
  }
  return minpos;
}

void setup () {
  size (500, 500); 
  RG.init(this); 
  myshape = RG.loadShape ("lines.svg"); 
  paths = myshape.getPointsInPaths(); // RPoint [][] paths
  println(paths.length); 
  //printArray(paths);
}

void draw() {
  background(100, 200, 0); 
  text("chosenPath = " + chosenPath, 30, 30);
  for (int ip = 0; ip <paths.length; ip++) {
    if (paths[ip] != null) {
      if ( diagpaths ) for (int i=0; i<paths[ip].length; i++)  ellipse(paths[ip][i].x, paths[ip][i].y, 2, 2); 
      draw_mouseline();
    }
  }
}

void mousePressed(){
  min_d = 50;
  //int ip = chosenPath;
  for (int i =0; i<paths.length; i++) {
    for(int j = 0; j<paths[i].length; j++){
        float d = paths[i][j].dist(mouse);
        if ( d < min_d) { 
          chosenPath = i; 
        }
    }
  }
}

void mouseDragged() {
  mouse= new RPoint(float(mouseX), float(mouseY)); 
  nearest = calc_nearest_point();
}

void mouseReleased() { 
  nearest = -1;
  chosenPath = 0;
}

void draw_mouseline() {
  fill(200, 0, 0); 
  if (nearest > -1) {
    int ip = chosenPath;
    line(paths[ip][nearest].x, paths[ip][nearest].y, mouse.x, mouse.y); 
    text(int(min_d), mouse.x+10, mouse.y-10); 
    circle(paths[ip][nearest].x, paths[ip][nearest].y, 10);
  }
}

//void keyPressed() {
//  chosenPath = (chosenPath + 1) % paths.length;
//}

I think I found the problem! Maybe 2 problems…

  • You forgot to update min_d in the nearest path search
  • You have to update the point for the mouse position before mousePressed(), otherwise you get an old mouse from the last mouseDragged(). I solved this by updating it in draw()

I added some Java “forEach”/“enhanced” loops that are similar to the Python loops, I hope you’ll like them!

import geomerative.*;

RShape myshape;
RPoint [][] paths;
RPoint mouse = new RPoint(0, 0);
boolean diagpaths = true;
float min_d;
int nearest = -1;
int chosenPath = -1;

int calc_nearest_point() {
  int minpos = -1;
  min_d = 10000;
  int ip = chosenPath;
  for (int i =0; i<paths[ip].length; i++) {
    float d = paths[ip][i].dist(mouse); 
    if ( d < min_d) { 
      min_d = d; 
      minpos = i;
    }
  }
  return minpos;
}

void setup () {
  size (500, 500); 
  RG.init(this); 
  myshape = RG.loadShape ("lines.svg"); 
  // Geomerative picks the sketch folder and not the  /data/ folder as usual
  paths = myshape.getPointsInPaths(); // RPoint [][] paths
  println(paths.length); 
  //printArray(paths);
}

void draw() {
  background(100, 200, 0); 
  // this is important to be here, otherwise mousePressed gets the wrong point!!!
  mouse= new RPoint(float(mouseX), float(mouseY));  
  if (chosenPath >=0) text("chosenPath = " + chosenPath, 30, 30);
  for (RPoint [] path : paths) {   
    for (RPoint p : path) {    // I think we might not need to check for null anymore
      if (diagpaths) ellipse(p.x, p.y, 2, 2);
      if (nearest >= 0) draw_mouseline();
    }
  }
}
void mousePressed() {
  min_d = 1000;
  for (int i =0; i<paths.length; i++) {
    for (RPoint p : paths[i]) {
      float d = p.dist(mouse);
      if (d < min_d) {
        min_d = d;
        chosenPath = i;
      }
    }
  }
}

void mouseDragged() {
  nearest = calc_nearest_point();
}

void mouseReleased() { 
  nearest = -1;
  chosenPath = -1;
}

void draw_mouseline() {
  fill(200, 0, 0); 
  if (nearest > -1) {
    int ip = chosenPath;
    line(paths[ip][nearest].x, paths[ip][nearest].y, mouse.x, mouse.y); 
    text(int(min_d), mouse.x+10, mouse.y-10); 
    circle(paths[ip][nearest].x, paths[ip][nearest].y, 10);
  }
  fill(255);
}
3 Likes

Thanks for this! learned a lot from all your examples so thank you very much!

I’ve noticed it runs a lot slower when there are more paths in the svg, is this because of the for loops? The python version seems to run a lot faster?

1 Like

I’ve refactored @villares’ Java Mode version and now it should be at least as fast (or faster :wink:) as his Python Mode original version. :racing_car:

“Closest_Point_on_SVG_Path.pde”:

/**
 * Closest Point on SVG Path (v1.0.6)
 * by SixFeet & Villares
 * mod GoToLoop (2020/Nov/25)
 * https://Discourse.Processing.org/t/closest-point-on-svg-path/25592/17
 */

import geomerative.RG;
import geomerative.RShape;
import geomerative.RPoint;

static final String FILENAME = "lines.svg", INFO = "chosenPathIndex = ";
static final color BG = #64C800, FILL = #C80000, STROKE = 0;
static final int BOLD = 3, DIAM = 10, INFO_OFFSET = 30, MOUSE_OFFSET = 10;

final RPoint mouse = new RPoint();
RPoint[][] paths;

boolean dragging = true, displayPoints = true;
int farOff, chosenPath = -1, nearestPoint = -1;

void settings() {
  RG.init(this);

  final RShape svg = RG.loadShape(FILENAME);
  paths = svg.getPointsInPaths();

  println("\npaths:", paths.length);
  println("x, y, w, h:", svg.getX(), svg.getY(), svg.width, svg.height);

  final RPoint center = svg.getCenter();
  size((int) center.x << 1, (int) center.y << 1);

  center.print();
  println("width, height:", width, height);
}

void setup() {
  fill(FILL);
  stroke(STROKE);
  strokeWeight(BOLD);
}

void draw() {
  mouse.x = mouseX;
  mouse.y = mouseY;

  background(BG);

  if (displayPoints)      displayPoints();
  if (nearestPoint >= 0)  drawMouseLine();
  if (chosenPath >= 0)    text(INFO + chosenPath, INFO_OFFSET, INFO_OFFSET);
}

void keyPressed() {
  dragging = false;
  chosenPath = (chosenPath + 1) % paths.length;
  findNearestPointIndex();
}

void mousePressed() {
  dragging = true;
  displayPoints ^= mouseButton == RIGHT;
  findChosenPathIndex();
}

void mouseMoved() {
  if (!dragging)  mouseDragged();
}

void mouseDragged() {
  if (chosenPath >= 0)  findNearestPointIndex();
  else                  findChosenPathIndex();
}

void mouseReleased() {
  chosenPath = nearestPoint = -1;
}

void displayPoints() {
  for (final RPoint[] pts : paths)  for (final RPoint p : pts)  point(p.x, p.y);
}

void findChosenPathIndex() {
  final int len = paths.length;
  int nearest = MAX_INT, lastChosenPath = -1;

  for (int i = 0; (chosenPath = i) < len; ++i) {
    findNearestPointIndex();

    if (farOff < nearest) {
      nearest = farOff;
      lastChosenPath = i;
    }
  }

  if ((chosenPath = lastChosenPath) >= 0)  findNearestPointIndex();
}

void findNearestPointIndex() {
  final RPoint[] pts = paths[chosenPath];
  final int len = pts.length;

  farOff = MAX_INT;
  nearestPoint = -1;

  for (int i = 0; i < len; ++i) {
    final int d = round(pts[i].dist(mouse));

    if (d < farOff) {
      farOff = d;
      nearestPoint = i;
    }
  }
}

void drawMouseLine() {
  final RPoint p = paths[chosenPath][nearestPoint];

  strokeWeight(1);
  line(p.x, p.y, mouse.x, mouse.y);
  circle(p.x, p.y, DIAM);
  text(farOff, mouse.x + MOUSE_OFFSET, mouse.y - MOUSE_OFFSET);
  strokeWeight(BOLD);
}

“lines.svg”:

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
	 viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
<style type="text/css">
	.st0{fill:none;stroke:#000000;stroke-width:10;stroke-miterlimit:10;}
</style>
<line class="st0" x1="142.5" y1="111.7" x2="358" y2="111.7"/>
<line class="st0" x1="142.5" y1="167.9" x2="358" y2="167.9"/>
<line class="st0" x1="142.5" y1="224.1" x2="358" y2="224.1"/>
<line class="st0" x1="142.5" y1="280.3" x2="358" y2="280.3"/>
<path class="st0" d="M352.9,398.8H147.6c-6.6,0-12-5.4-12-12v-38.3c0-6.6,5.4-12,12-12h205.3c6.6,0,12,5.4,12,12v38.3
	C364.9,393.4,359.5,398.8,352.9,398.8z"/>
</svg>
2 Likes