Как примагнитить точку к линии?

104
30 января 2021, 05:50

Попасть в линию очень тяжело. Подскажите сделать, что кликнув рядом с линией понять, что кликнули рядом и поставить точку на линию? Так же, если мы меняем масштаб, прилипание так же сработало.

drawing() {
    this.svg_margin.left = 50;
    this.svg_margin.right = 50;
    this.svg_margin.top = 50;
    this.svg_margin.bottom = 50;
    this.draw_width = this.svg_width - this.svg_margin.left - this.svg_margin.right;
    this.draw_height = this.svg_height - this.svg_margin.top - this.svg_margin.bottom;
    let svg = d3.select('#svg-container')
      .append('svg')
      .attr('width', this.svg_width)
      .attr('height', this.svg_height)
      .style('border', '3px dashed lightgrey')
      .style('background-color', '#4caf5026');
    const x_scale = d3.scaleLinear().domain([0, this.draw_width]).range([0, this.draw_width]);
    const y_scale = d3.scaleLinear().domain([this.draw_height, 0]).range([0, this.draw_height]);
    // Создаём рабочую область
    const svg_work_space = svg.append('g')
      .attr('class', 'svg-work-space')
      .style('fill', 'transparent');
    const line = d3.line()
      .x((d) => x_scale(d.x))
      .y((d) => y_scale(d.y));
    let points  = [];
    let rect = svg_work_space.append('rect')
      .attr("x", 100)
      .attr("y",100)
      .attr("width",100)
      .attr("height",100)
      .attr("fill",'none')
      .style('stroke-width', 5)
      .attr('stroke', 'green');
    let c = svg_work_space
      .append("circle")
      .attr('cx', 100)
      .attr('cy', 100)
      .attr('fill', 'red')
      .attr('r', 1);

    svg.on('click', function() {
      let new_point = d3.mouse(this);
      let new_coordinate = {
        x: Math.round( x_scale.invert(new_point[0])),
        y: Math.round( y_scale.invert(new_point[1]))
      };
      let line ={
        x1:200,
        y1:100,
        x2:200,
        y2:200,
      };

      points.push(new_coordinate);
      svg_work_space.selectAll("circle")
        .data(points)
        .enter()
        .append("circle")
        .attr('cx', (d) => x_scale(d.x))
        .attr('cy', (d) => y_scale(d.y))
        .attr('fill', 'red')
        .attr('r', 5);

      // Проверяем принадлежит ли точка стороне
      belongsToTheSide(new_coordinate.x, new_coordinate.y, line);
    });
    function belongsToTheSide(x,y, line) {
      let equation = (x - line.x1)*(line.y2 - line.y1)-(line.x2 - line.x1)*(y-line.y1);
      console.log('equation:',equation);
      if (equation === 0 ) {
        console.log('Точка принадлежит прямой');
        return true;
      } else if ( equation > 0) {
        console.log('Точка находится справа от прямой');
        return false;
      } else if ( equation < 0) {
        console.log('Точка находится слева от прямой');
        return false;
      }
    }
  }
Answer 1

Вот алгоритм работающий через предварительный перебор точек с большИм шагом и последующий поиск в пространстве рядом с найденной точкой, с уменьшением шага.

Этот метод подойдет когда кривая, до которой необходимо искать кратчайшее расстояние, меняется со временем.

Производительность можно сильно повысить, если вешать обработчик не на все окно, а на толстую и полностью прозрачную копию линии, у которой установлена ширина, которая бы Вас устроила в качестве зоны прилипания.

—-

PS: вот в этом ответе используется другой метод, через разбиение Вороного, и там как раз реализовано добавление точки в линию. Используйте этот метод если кривая не меняется, или меняется не часто.

// максимальная дистанция от мышки до линии при которой будет появляться точка 
let d = 20;  
 
// положение svg-шки на экране 
let r = svg.getBoundingClientRect(); 
 
// невидимая svg точка, при помощи нее будут происходить трансформации координат,  
// чтобы перевести координаты мыши из системы координат окна в систему координат  
// svg пути(учесть всю иерархию transform-ов), к которому магнитим линию 
let pt = svg.createSVGPoint();   
 
// масштаб 
let z = 1;  
 
addEventListener('mousemove', e => { 
 
  // задаем невидимой точке координаты мыши 
  pt.x = e.clientX; 
  pt.y = e.clientY; 
 
  // переводим эту точку в систему координат path 
  let p = pt.matrixTransform(path.getScreenCTM().inverse()); 
 
  // применяем алгоритм поиска ближайшей точки 
  p = closestPoint(path, [p.x, p.y]); 
 
  // задаем  координаты найденной точки невидимой точке  
  pt.x = p[0]; 
  pt.y = p[1]; 
 
  // переводим обратно в экранные координаты 
  let p2 = pt.matrixTransform(path.getScreenCTM()); 
 
  circle.setAttribute('cx', p2.x-r.x) 
  circle.setAttribute('cy', p2.y-r.y) 
  circle.setAttribute('opacity', p.distance < d/z ? 1 : 0) 
 
  // показываем точку только если расстояние до нее меньше порогового значения 
  svg.style.cursor = p.distance < d/z ? 'pointer':'default' 
}) 
 
// поиск ближайшей точки, принадлежащей линии 
function closestPoint(pathNode, point) { 
 
  let pathLen=pathNode.getTotalLength(), precis=8, best, bestLen, bestDist=Infinity; 
 
  // перебираем точки вдоль пути с большИм шагом, находим ту,  
  // до которой минимальное расстояние 
  for (let scan, scanLen = 0, scanDist; scanLen <= pathLen; scanLen += precis)  
    if ((scanDist = dist(scan = pathNode.getPointAtLength(scanLen))) < bestDist)  
      best = scan, bestLen = scanLen, bestDist = scanDist; 
 
  precis /= 2; 
 
  // сканируем пространство вблизи найденной точки влево и вправо уменьшая шаг между  
  // пробами, сохраняя на каждой итерации наилучший результат (кратчайшее расстояние) 
  while (precis > 0.5) { 
    let bef, aft, befLen, aftLen, befDist, aftDist; 
    if ((befLen = bestLen - precis) >= 0 &&  
        (befDist = dist(bef = pathNode.getPointAtLength(befLen))) < bestDist)  
      best = bef, bestLen = befLen, bestDist = befDist; 
    else if ((aftLen = bestLen + precis) <= pathLen &&  
             (aftDist = dist(aft = pathNode.getPointAtLength(aftLen))) < bestDist)  
      best = aft, bestLen = aftLen, bestDist = aftDist; 
    else  
      precis /= 2; 
  } 
 
  best = [best.x, best.y]; 
  best.distance = Math.sqrt(bestDist); 
  return best; 
 
  // евклидово расстояние между точками 
  function dist(p) { 
    let dx = p.x - point[0], dy = p.y - point[1]; 
    return dx * dx + dy * dy; 
  } 
} 
 
addEventListener('wheel', e => { 
  z *= 1+0.1*Math.sign(e.deltaY); 
  path.setAttribute('transform', `scale(${z})`) 
  circle.setAttribute('opacity', 0) 
}, false)
<svg id="svg" width="600" height="175"> 
<path id="path" fill="none" stroke="red"  d="M25,54C25,54,74,88,97,86C120,84,113,45,138,44C163,43,192,83,217,83C242,83,239,44,260,43C281,42,294,79,317,79C340,79,347,49,371,43C395,37,411,54,432,52C453,50,454,28,473,32C492,36,501,73,522,72C543,71,565,19,577,26C589,33,582,108,582,108" transform="scale(1)"></path> 
<circle id="circle" r="5" fill="none" stroke="blue" cx="-10" ></circle> 
<svg>

Answer 2

Можно найти расстояние от точки до прямой без итераций, через длину ортогональной проекции. Для этого достаточно немного доработать belongsToTheSide

//находим нормализованный вектор направления прямой
//если одна прямая используется много раз - можно вычислить единожды 
dirx = line.x2 - line.x1;
diry = line.y2 - line.y1;
len = Math.sqrt(dirx * dirx + diry * diry);
dirx = dirx / len;
diry = diry / len; 
//находим расстояние (знак говорит об ориентации, если расстояние больше нужного порога
dist = (x - line.x1) * diry - (y-line.y1) * dirx;
READ ALSO
Задержка перед сменой слайда в Swiper Slider

Задержка перед сменой слайда в Swiper Slider

Имеется Swiper sliderПосле клика по кнопке Next или свайпа необходимо добавить анимацию перед сменой слайда

121
Почему падает браузер?

Почему падает браузер?

Почему при написании этого кода в консоль у меня падает браузер?

97
Это магия callback-ов? [дубликат]

Это магия callback-ов? [дубликат]

Работаю с Vuejs, наткнулся скорей всего на магию callback-ов, как мне кажется

102
При скроле менять класс у body

При скроле менять класс у body

Никак не получается решить задачку:

102