Страницы

Поиск по вопросам

вторник, 28 января 2020 г.

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

#javascript #svg #d3js


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

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;
      }
    }
  }

    


Ответы

Ответ 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)

Ответ 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;

Комментариев нет:

Отправить комментарий