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

#javascript #geometry

#javascript #геометрия

Вопрос:

Описание

Дана синяя линия, которую можно разместить по своему усмотрению и перемещать с помощью мыши. При перемещении линии линия градиента (здесь в hotpink) оттягивается от исходного положения. Теперь к этой строке предъявляются следующие требования:

Предполагая, что левый край синей линии является точкой 1 (красный круг), и предполагая, что левый край исходного положения синей линии является точкой 2 (лаймовый круг), предполагая, что правая сторона синей линии является точкой 3 (зеленый круг), угол от точки 1 должен быть либо 90или -90 градусов до точки 2 / точки 3.

Я считаю, что термин для этого таков: линия градиентной точки и синяя линия должны быть перпендикулярны.
Направление синей линии не должно меняться, только ее положение!

Пример

Моя проблема

Перемещая синюю линию, я могу рассчитать градус до его исходного положения и нарисовать линию градиентной точки. Тем не менее, я не могу вычислить ближайшую точку от синей линии, которая сделала бы линию градиентной горячей точки перпендикулярной как исходной синей линии, так и новой позиции синей линии. Если кто-нибудь может указать мне правильную формулу или правильный термин для этой проблемы, я был бы благодарен.

Визуальный пример (вырезанные части кода)

Ниже приведен краткий пример, который я собрал вместе. Можно переместить синюю линию, но я не могу заставить точку 1 сохранять определенный угол 90 / -90 градусов до точки 2 / точки 3.

 //REM: Current moving element
var _currentElement = null;

//REM: Drawing for quicker access
var _Drawing = null;

//REM: Starting the drag
function _onDown(event){
  if(event.target amp;amp; event.target.tagName === 'line'){
    let tMatrix = _getMatrix(event.target);
    
    _currentElement = {
      Element: event.target,
      startX: event.clientX,
      startY: event.clientY,
      startE: tMatrix.e,
      startF: tMatrix.f,
      X1: Number(event.target.getAttribute('x1')),
      Y1: Number(event.target.getAttribute('y1')),
      X2: Number(event.target.getAttribute('x2')),
      Y2: Number(event.target.getAttribute('y2')),
      Ratio: 0.4
    }
  }
}

//REM: Dragging
function _onMove(event){
  if(_currentElement){
    _currentElement.endE = _currentElement.startE   ((event.clientX - _currentElement.startX) * _currentElement.Ratio);
    _currentElement.endF = _currentElement.startF   ((event.clientY - _currentElement.startY) * _currentElement.Ratio);

    console.log(
      'Angle (3)',
      _getAngleBetweenThreePoints(
        {x: _currentElement.X1   _currentElement.endE, y: _currentElement.Y1   _currentElement.endF},
        {x: _currentElement.X1, y: _currentElement.Y1},
        {x: _currentElement.X2   _currentElement.endE, y: _currentElement.Y2   _currentElement.endF}
      )
    );

    _setMatrix(_currentElement.Element, 1, 0, 0, 1, _currentElement.endE, _currentElement.endF)
  }
}

//REM: Ending the drag
function _onUp(){
  _currentElement = null
}

//REM: Returns the elements matrix
function _getMatrix(element){
  if(element){
    return element.transform.baseVal.numberOfItems ?
    element.transform.baseVal.getItem(0).matrix :
    element.transform.baseVal.appendItem(_Drawing.createSVGTransform()).matrix
  }
}

//REM: Sets the elements matrix
function _setMatrix(element, a, b, c, d, e, f){
  if(element){
    let tMatrix = _getMatrix(element);
    if(tMatrix){
      tMatrix.a = (typeof a === 'number') ? a : tMatrix.a;
      tMatrix.b = (typeof b === 'number') ? b : tMatrix.b;
      tMatrix.c = (typeof c === 'number') ? c : tMatrix.c;
      tMatrix.d = (typeof d === 'number') ? d : tMatrix.d;
      tMatrix.e = (typeof e === 'number') ? e : tMatrix.e;
      tMatrix.f = (typeof f === 'number') ? f : tMatrix.f;

      element.transform.baseVal.getItem(0).setMatrix(tMatrix)
    }
  }
}

//REM: Transforms client-coords to svg-coords
function _getSVGCoords(clientX, clientY){
    var tCTM = _Drawing.getScreenCTM();
    return tCTM ? {x: (clientX - tCTM.e) / tCTM.a, y: (clientY - tCTM.f ) / tCTM.d} : {x: clientX, y: clientY}
}

//REM: Returns angle from p1 to p2 and p3
function _getAngleBetweenThreePoints(p1, p2, p3){
  let tAngle = 0;

  if(p1 amp;amp; p2 amp;amp; p3){
    let tTemplate = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    tTemplate.setAttribute('r', 1);
    
    let tC1 = _Drawing.appendChild(tTemplate.cloneNode(false));
    tC1.setAttribute('fill', 'red');
    tC1.setAttribute('cx', p1.x);
    tC1.setAttribute('cy', p1.y);
    
    let tC2 = _Drawing.appendChild(tTemplate.cloneNode(false));
    tC2.setAttribute('fill', 'lime');
    tC2.setAttribute('cx', p2.x);
    tC2.setAttribute('cy', p2.y);
    
    let tC3 = _Drawing.appendChild(tTemplate.cloneNode(false));
    tC3.setAttribute('fill', 'green');
    tC3.setAttribute('cx', p3.x);
    tC3.setAttribute('cy', p3.y);
    
    let tLine = document.getElementById('line1');
    if(!tLine){
      tLine = _Drawing.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'line'));
      tLine.id = 'line1';
      tLine.setAttribute('stroke-dasharray', '5,5')
    };
    
    tLine.setAttribute('x1', p1.x);
    tLine.setAttribute('y1', p1.y);
    tLine.setAttribute('x2', p2.x);
    tLine.setAttribute('y2', p2.y);

    tAngle = (Math.atan2(p3.y - p1.y, p3.x - p1.x) - Math.atan2(p2.y - p1.y, p2.x - p1.x)) * 180 / Math.PI
  }

  return tAngle
};

//REM: Assiging events
window.onload = function(){
  _Drawing = document.querySelector('svg');

  document.body.addEventListener('mousedown', _onDown, false);
  document.body.addEventListener('mousemove', _onMove, false);
  document.body.addEventListener('mouseup', _onUp, false);
  document.body.addEventListener('mouseleave', _onUp, false)
}; 
 line,
circle{
  pointer-events: none;
  stroke-width: 1
}

#movable{
  pointer-events: all;
  stroke-width: 10
}

#line1{
  stroke: hotpink
} 
 <svg xmlns = 'http://www.w3.org/2000/svg' viewBox = '0 0 300 300'>
  <line x1 = '50' y1 = '50' x2 = '160' y2 = '110' stroke = 'blue' id = 'movable'></line>
</svg> 

Ожидаемый результат

Перемещая синюю линию, я рисую градиентную линию горячей точки. Эта линия градиента должна быть перпендикулярна исходному положению синей линии, а также новому положению синей линии. Направление самой синей линии может не измениться, только ее положение. Если линия будет горизонтальной, будут разрешены только прямые вверх и прямые вниз. Если бы линия была вертикальной, разрешались бы только прямые влево и вправо. Если линия диагональная, будут разрешены только движения, которые являются прямыми в выравнивании этой линии (например, железная дорога). Так, например, имея мою диагональную синюю линию и двигаясь прямо вверх, я хочу вычислить ближайшую позицию от левого края, которая сделает линию градиентной точки перпендикулярной как исходному положению синей линии, так и новому положению синей линии.

Это поведение очень похоже на Adobe PDF Measuring Tool.

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

1. 1. Вы хотите ограничить перемещение этой синей линии (только перемещение перпендикулярно самой себе)? Или вы хотите нарисовать градиент перпендикулярно? 2. Когда у вас есть два вектора, они ортогональны, если их скалярное произведение равно нулю. Это может помочь.

2. @Josef Wittmann: я хочу ограничить перемещение синей линии, чтобы розовая линия всегда была перпендикулярной. Так что вроде как и то, и другое. Adobe PDF предлагает ту же функциональность, которая там называется инструментом расстояния вместе с привязкой к конечным точкам. Я проверю вашу подсказку. Спасибо.

Ответ №1:

Я создал класс, который дает вам x, y того, где нужно разместить синюю линию.

 class cTracker {
constructor(x1, y1, x2, y2) {
    x1 = parseInt(x1);
    y1 = parseInt(y1);
    x2 = parseInt(x2);
    y2 = parseInt(y2);
    
    const extendLine = 10000;
    
    let blueLineRadian = Math.atan2(x1 - x2, y1 - y2);
    this.m_blueLineRadian = blueLineRadian;
    
    
    let magentaLineRadian = blueLineRadian   (Math.PI / 2);
    
    this.m_blueLineCos = Math.cos(blueLineRadian) * extendLine;
    this.m_blueLineSin = Math.sin(blueLineRadian) * extendLine;
    
    this.m_magentaLineX1 = x1   Math.sin(magentaLineRadian) * extendLine;
    this.m_magentaLineY1 = y1   Math.cos(magentaLineRadian) * extendLine;
    
    this.m_magentaLineX2 = x1   Math.sin(magentaLineRadian   Math.PI) * extendLine;
    this.m_magentaLineY2 = y1   Math.cos(magentaLineRadian   Math.PI) * extendLine;
    
}
// -------------------------------------------------------------------------
//
// -------------------------------------------------------------------------
getCursorPosition(x, y) {
    this.m_x1 = x - this.m_blueLineSin;
    this.m_y1 = y - this.m_blueLineCos;
    this.m_x2 = x   this.m_blueLineSin;
    this.m_y2 = y   this.m_blueLineCos;

        
    return this.intersect(this.m_magentaLineX1, this.m_magentaLineY1, this.m_magentaLineX2, this.m_magentaLineY2, this.m_x1, this.m_y1, this.m_x2, this.m_y2);
}

                                              
  // -------------------------------------------------------------------------
  //
  // -------------------------------------------------------------------------
  intersect(x1, y1, x2, y2, x3, y3, x4, y4) {
      
      var denominator = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1));
      
      var a                                   = y1 - y3;
      var b                                   = x1 - x3;
      var numerator1                          = ((x4 - x3) * a) - ((y4 - y3) * b);
      var numerator2                          = ((x2 - x1) * a) - ((y2 - y1) * b);
      a                                       = numerator1 / denominator;
      
      return {m_x : x1   (a * (x2 - x1)), m_y : y1   (a * (y2 - y1))};
  }
 }
 

Чтобы использовать его, создайте момент с x1, y1, x2, y2 синей линии

 let movableLine = document.getElementById('movable');
    
let x1 = parseInt(movableLine.getAttribute("x1"));
let y1 = parseInt(movableLine.getAttribute("y1"));
let x2 = parseInt(movableLine.getAttribute("x2"));
let y2 = parseInt(movableLine.getAttribute("y2"));

this.m_tracker = new cTracker(x1, y1, x2, y2);
 

Чтобы получить X, Y о том, где разместить синюю линию, вы просто делаете следующее…

 let xySVG = this._getSVGCoords(event.clientX, event.clientY);         
let xy = this.m_tracker.getCursorPosition(xySVG.x, xySVG.y);

console.log(xy); // This is where the blue line needs to be placed...
 

Вот рабочая скрипка: https://jsfiddle.net/syxfv63z/2 /

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

1. Это все еще необходимо для того, чтобы рассчитать угол в 90 градусов, как вы хотели. Что вам нужно сделать, так это сохранить угол между p1 и p2 постоянным, но длину линии можно изменять. Угол p2 и p3 останется статической линией, как вы хотите, как показано в предоставленной вами ссылке. Я так понимаю, вы хотите, чтобы формула ограничивала угол p1 и p2?

2. Попробуйте эту скрипку… jsfiddle.net/9qgfs6k4/6 это то, чего ты добиваешься? Способ перемещения мыши относительно перетаскиваемой линии в настоящее время не идеален из-за того, как он вычисляет положение мыши, и добавленное вами соотношение не помогает. У меня есть некоторый код, который делает это правильно, и я сделаю обновление, как только вы подтвердите, что именно так вы хотели, чтобы это работало.

3. x1 == x2 || y1 == y2 ? 0 : Math.tan(-radians) вероятно, неверно для данного y1 == y2 случая. Это ограничивает перемещения по горизонтали, но должно ограничивать перемещения по вертикали.

4. Это потому, что tan(pi / 2) возвращает огромное число для бесконечности, поэтому эти условия существуют и почему вы получаете большой скачок. Хитрость заключается в том, чтобы поменять местами event.clientX с event.clientY, когда x1 == x2, и это в какой-то степени сработает, но на самом деле это не решает проблему точности отслеживания мыши, о которой я упоминал. Я не закончил это, потому что я просто хотел знать, правильно ли я понял, что вам нужно, прежде чем продолжить, и у вас создалось впечатление, что вы решили движение мыши. Я сделаю обновление в ближайшее время.

5. Я обновил его, и теперь он работает иначе, чем то, что я опубликовал ранее. Я удалил большую часть вашего кода как минимум, чтобы вы могли понять, что отличается. Я также добавил визуальные элементы отладки, чтобы получить представление о том, что происходит. Это будет корректно обрабатывать вертикальные и горизонтальные линии.

Ответ №2:

Чтобы получить движение перпендикулярно синей линии, выполните следующие действия:

  1. Получить вектор ориентации синих линий.
  2. Поверните его на 90 градусов (перпендикулярно его поверхности).
  3. Нормализуйте его, например, сделайте его длину равной 1 (назовите ее n ).
  4. Теперь получите вектор движения мыши ( current - start , вызовите его m ).
  5. Теперь посчитайте n * dot(n,m) . Это ваш вектор движения в n направлении.

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

1. Спасибо, это помогло мне правильно создать градиентную линию. Ответ Джона вроде как включает в себя все, что мне нужно, так что в конце концов я согласился.