变换几何形状,保持对称性和边界尺寸不变

Tranforming a geometrical shape, keeping symmetry and bounding dimensions intact

我正在开发一种工具,用于根据各种模板修改不同的几何形状。这些形状是可以在房间中找到的基本形状。 例如:L型、T型、六角、长方形等

我需要做的是使形状符合所有必要的边,以便在用户修改边时保持形状的对称性和边界尺寸不变。

一个shape的简单实现是这样的,第一个节点从左上角开始,顺时针绕着shape走一圈(我用的是TypeScript):

public class Shape {
    private nodes: Array<Node>;
    private scale: number; // Scale for calculating correct coordinate compared to given length
    ... // A whole lot of transformation methods

然后将其绘制为图形,将每个节点连接到数组中的下一个节点。 (见下文)

例如,如果我将边 C 的长度从 3.5m 更改为 3m,那么我还希望边 E 或 G 更改它们的长度以将边保持为 12m 并向下推 E所以边 D 仍然是完全水平的。 如果我改为将 D 边更改为 2m,则 B 必须将其长度更改为 10m,依此类推。

(我也有斜角的形状,比如一个角被切掉的矩形)

问题

我有以下修改特定边的代码:

    public updateEdgeLength(start: Point, length: number): void  {
        let startNode: Node;
        let endNode: Node;
        let nodesSize = this.nodes.length;

        // Find start node, and then select end node of selected edge.
        for (let i = 0; i < nodesSize; i++) {
            if (this.nodes[i].getX() === start.x && this.nodes[i].getY() === start.y) {
                startNode = this.nodes[i];
                endNode = this.nodes[(i + 1) % nodesSize];
                break;
            }
        }

        // Calculate linear transformation scalar and create a vector of the edge
        let scaledLength = (length * this.scale);
        let edge: Vector = Vector.create([endNode.getX() - startNode.getX(), endNode.getY() - startNode.getY()]);
        let scalar = scaledLength / startNode.getDistance(endNode);

        edge = edge.multiply(scalar);

        // Translate the new vector to its correct position 
        edge = edge.add([startNode.getX(), startNode.getY()]);
        // Calculate tranlation vector
        edge = edge.subtract([endNode.getX(), endNode.getY()]);

        endNode.translate({x: edge.e(1), y: edge.e(2)});

    }

现在我需要一个更一般的情况来找到也需要修改的相应边。我已经开始实施特定于形状的算法,因为我知道哪些节点对应于形状的边缘,但这在未来不会很可扩展。

例如,上面的形状可以像这样实现:

public updateSideLength(edge: Position): void {
    // Get start node coordinates
    let startX = edge.start.getX();
    let startY = edge.start.getY();

    // Find index of start node;
    let index: num;
    for (let i = 0; i < this.nodes.length; i++) {
        let node: Node = this.nodes[i];
        if(node.getX() === startX && node.getY() === startY) {
            index = i;
            break;
        }
    }

    // Determine side
    let side: number;
    if (index === 0 || index === 2) {
        side = this.TOP;
    }
    else if (index === 1 || index === 3 || index === 5) {
        side = this.RIGHT;
    }
    else if (index === 4 || index === 6) {
        side = this.BOTTOM;
    }    
    else if (index === 7) {
        side = this.LEFT;
    }

    adaptSideToBoundingBox(index, side); // adapts all other edges of the side except for the one that has been modified
}

public adaptSideToBoundingBox(exceptionEdge: number, side: number) {
    // Modify all other edges
        // Example: C and G will be modified
        Move C.end Y-coord to D.start Y-coord;
        Move G.start Y-coord to D.end Y-coord;       
}

等等..但是为每个形状 (5 atm.) 和未来的形状实现这个将非常耗时。

所以我想知道是否有更通用的方法来解决这个问题?

谢谢!

保留点对列表和约束它们的键,并使用它在更新时覆盖坐标。

这适用于您提供的示例:

var Point = (function () {
    function Point(x, y, connectedTo) {
        if (connectedTo === void 0) { connectedTo = []; }
        this.x = x;
        this.y = y;
        this.connectedTo = connectedTo;
    }
    return Point;
}());
var Polygon = (function () {
    function Polygon(points, constrains) {
        if (constrains === void 0) { constrains = []; }
        this.points = points;
        this.constrains = constrains;
    }
    return Polygon;
}());
var Sketch = (function () {
    function Sketch(polygons, canvas) {
        if (polygons === void 0) { polygons = []; }
        if (canvas === void 0) { canvas = document.body.appendChild(document.createElement("canvas")); }
        this.polygons = polygons;
        this.canvas = canvas;
        this.canvas.width = 1000;
        this.canvas.height = 1000;
        this.ctx = this.canvas.getContext("2d");
        this.ctx.fillStyle = "#0971CE";
        this.ctx.strokeStyle = "white";
        this.canvas.onmousedown = this.clickHandler.bind(this);
        this.canvas.onmouseup = this.clickHandler.bind(this);
        this.canvas.onmousemove = this.clickHandler.bind(this);
        requestAnimationFrame(this.draw.bind(this));
    }
    Sketch.prototype.clickHandler = function (evt) {
        if (evt.type == "mousedown") {
            if (this.selectedPoint != void 0) {
                this.selectedPoint = null;
            }
            else {
                var score = null;
                var best = null;
                for (var p = 0; p < this.polygons.length; p++) {
                    var polygon = this.polygons[p];
                    for (var pi = 0; pi < polygon.points.length; pi++) {
                        var point = polygon.points[pi];
                        var dist = Math.abs(point.x - evt.offsetX) + Math.abs(point.y - evt.offsetY);
                        if (score == null ? true : dist < score) {
                            score = dist;
                            best = point;
                        }
                    }
                }
                this.selectedPoint = best;
            }
        }
        if (evt.type == "mousemove" && this.selectedPoint != void 0) {
            this.selectedPoint.x = Math.round(evt.offsetX / 5) * 5;
            this.selectedPoint.y = Math.round(evt.offsetY / 5) * 5;
            for (var pi = 0; pi < this.polygons.length; pi++) {
                var polygon = this.polygons[pi];
                if (polygon.points.indexOf(this.selectedPoint) < 0) {
                    continue;
                }
                for (var pa = 0; pa < polygon.constrains.length; pa++) {
                    var constrain = polygon.constrains[pa];
                    if (constrain.a == this.selectedPoint || constrain.b == this.selectedPoint) {
                        constrain.a[constrain.key] = this.selectedPoint[constrain.key];
                        constrain.b[constrain.key] = this.selectedPoint[constrain.key];
                        if (constrain.offset != void 0) {
                            if (constrain.a == this.selectedPoint) {
                                constrain.b[constrain.key] += constrain.offset;
                            }
                            else {
                                constrain.a[constrain.key] -= constrain.offset;
                            }
                        }
                    }
                }
            }
        }
        requestAnimationFrame(this.draw.bind(this));
    };
    Sketch.prototype.draw = function () {
        var ctx = this.ctx;
        //clear
        ctx.fillStyle = "#0971CE";
        ctx.fillRect(0, 0, 1000, 1000);
        //grid
        ctx.strokeStyle = "rgba(255,255,255,0.25)";
        for (var x = 0; x <= this.canvas.width; x += 5) {
            ctx.beginPath();
            ctx.moveTo(x, -1);
            ctx.lineTo(x, this.canvas.height);
            ctx.stroke();
            ctx.closePath();
        }
        for (var y = 0; y <= this.canvas.height; y += 5) {
            ctx.beginPath();
            ctx.moveTo(-1, y);
            ctx.lineTo(this.canvas.width, y);
            ctx.stroke();
            ctx.closePath();
        }
        ctx.strokeStyle = "white";
        ctx.fillStyle = "white";
        //shapes
        for (var i = 0; i < this.polygons.length; i++) {
            var polygon = this.polygons[i];
            for (var pa = 0; pa < polygon.points.length; pa++) {
                var pointa = polygon.points[pa];
                if (pointa == this.selectedPoint) {
                    ctx.beginPath();
                    ctx.fillRect(pointa.x - 2, pointa.y - 2, 4, 4);
                    ctx.closePath();
                }
                ctx.beginPath();
                for (var pb = 0; pb < pointa.connectedTo.length; pb++) {
                    var pointb = pointa.connectedTo[pb];
                    if (polygon.points.indexOf(pointb) < pa) {
                        continue;
                    }
                    ctx.moveTo(pointa.x, pointa.y);
                    ctx.lineTo(pointb.x, pointb.y);
                }
                ctx.stroke();
                ctx.closePath();
            }
        }
    };
    return Sketch;
}());
//==Test==
//Build polygon 1 (House)
var poly1 = new Polygon([
    new Point(10, 10),
    new Point(80, 10),
    new Point(80, 45),
    new Point(130, 45),
    new Point(130, 95),
    new Point(80, 95),
    new Point(80, 135),
    new Point(10, 135),
]);
//Connect dots
for (var x = 0; x < poly1.points.length; x++) {
    var a = poly1.points[x];
    var b = poly1.points[(x + 1) % poly1.points.length];
    a.connectedTo.push(b);
    b.connectedTo.push(a);
}
//Setup constrains
for (var x = 0; x < poly1.points.length; x++) {
    var a = poly1.points[x];
    var b = poly1.points[(x + 1) % poly1.points.length];
    poly1.constrains.push({ a: a, b: b, key: x % 2 == 1 ? 'x' : 'y' });
}
poly1.constrains.push({ a: poly1.points[1], b: poly1.points[5], key: 'x' }, { a: poly1.points[2], b: poly1.points[5], key: 'x' }, { a: poly1.points[1], b: poly1.points[6], key: 'x' }, { a: poly1.points[2], b: poly1.points[6], key: 'x' });
//Build polygon 2 (Triangle)
var poly2 = new Polygon([
    new Point(250, 250),
    new Point(300, 300),
    new Point(200, 300),
]);
//Connect dots
for (var x = 0; x < poly2.points.length; x++) {
    var a = poly2.points[x];
    var b = poly2.points[(x + 1) % poly2.points.length];
    a.connectedTo.push(b);
    b.connectedTo.push(a);
}
//Setup constrains
poly2.constrains.push({ a: poly2.points[0], b: poly2.points[1], key: 'x', offset: 50 }, { a: poly2.points[0], b: poly2.points[1], key: 'y', offset: 50 });
//Generate sketch
var s = new Sketch([poly1, poly2]);
<!-- TYPESCRIPT -->
<!--
class Point {
 constructor(public x: number, public y: number, public connectedTo: Point[] = []) {

 }
}

interface IConstrain {
 a: Point,
 b: Point,
 key: string,
 offset?: number
}

class Polygon {
 constructor(public points: Point[], public constrains: IConstrain[] = []) {

 }
}

class Sketch {
 public ctx: CanvasRenderingContext2D;
 constructor(public polygons: Polygon[] = [], public canvas = document.body.appendChild(document.createElement("canvas"))) {
  this.canvas.width = 1000;
  this.canvas.height = 1000;

  this.ctx = this.canvas.getContext("2d");
  this.ctx.fillStyle = "#0971CE";
  this.ctx.strokeStyle = "white";

  this.canvas.onmousedown = this.clickHandler.bind(this)
  this.canvas.onmouseup = this.clickHandler.bind(this)
  this.canvas.onmousemove = this.clickHandler.bind(this)
  requestAnimationFrame(this.draw.bind(this))
 }
 public selectedPoint: Point
 public clickHandler(evt: MouseEvent) {
  if (evt.type == "mousedown") {
   if (this.selectedPoint != void 0) {
    this.selectedPoint = null;
   } else {
    let score = null;
    let best = null;
    for (let p = 0; p < this.polygons.length; p++) {
     let polygon = this.polygons[p];
     for (let pi = 0; pi < polygon.points.length; pi++) {
      let point = polygon.points[pi];
      let dist = Math.abs(point.x - evt.offsetX) + Math.abs(point.y - evt.offsetY)
      if (score == null ? true : dist < score) {
       score = dist;
       best = point;
      }
     }
    }
    this.selectedPoint = best;
   }
  }
  if (evt.type == "mousemove" && this.selectedPoint != void 0) {
   this.selectedPoint.x = Math.round(evt.offsetX / 5) * 5;
   this.selectedPoint.y = Math.round(evt.offsetY / 5) * 5;
   for (let pi = 0; pi < this.polygons.length; pi++) {
    let polygon = this.polygons[pi];
    if (polygon.points.indexOf(this.selectedPoint) < 0) {
     continue;
    }
    for (let pa = 0; pa < polygon.constrains.length; pa++) {
     let constrain = polygon.constrains[pa];
     if (constrain.a == this.selectedPoint || constrain.b == this.selectedPoint) {
      constrain.a[constrain.key] = this.selectedPoint[constrain.key]
      constrain.b[constrain.key] = this.selectedPoint[constrain.key]
      if (constrain.offset != void 0) {
       if (constrain.a == this.selectedPoint) {
        constrain.b[constrain.key] += constrain.offset
       } else {
        constrain.a[constrain.key] -= constrain.offset
       }
      }
     }
    }
   }
  }
  requestAnimationFrame(this.draw.bind(this))

 }
 public draw() {
  var ctx = this.ctx;
  //clear
  ctx.fillStyle = "#0971CE";
  ctx.fillRect(0, 0, 1000, 1000)
  //grid
  ctx.strokeStyle = "rgba(255,255,255,0.25)"
  for (let x = 0; x <= this.canvas.width; x += 5) {
   ctx.beginPath()
   ctx.moveTo(x, -1)
   ctx.lineTo(x, this.canvas.height)
   ctx.stroke();
   ctx.closePath()
  }
  for (let y = 0; y <= this.canvas.height; y += 5) {
   ctx.beginPath()
   ctx.moveTo(-1, y)
   ctx.lineTo(this.canvas.width, y)
   ctx.stroke();
   ctx.closePath()
  }
  ctx.strokeStyle = "white"
  ctx.fillStyle = "white";
  //shapes
  for (let i = 0; i < this.polygons.length; i++) {
   let polygon = this.polygons[i];
   for (let pa = 0; pa < polygon.points.length; pa++) {
    let pointa = polygon.points[pa];
    if (pointa == this.selectedPoint) {
     ctx.beginPath();
     ctx.fillRect(pointa.x - 2, pointa.y - 2, 4, 4)
     ctx.closePath();
    }
    ctx.beginPath();
    for (var pb = 0; pb < pointa.connectedTo.length; pb++) {
     var pointb = pointa.connectedTo[pb];
     if (polygon.points.indexOf(pointb) < pa) {
      continue;
     }
     ctx.moveTo(pointa.x, pointa.y)
     ctx.lineTo(pointb.x, pointb.y)
    }
    ctx.stroke();
    ctx.closePath();
   }
  }
 }
}

//==Test==
//Build polygon 1 (House)
var poly1 = new Polygon([
 new Point(10, 10),
 new Point(80, 10),
 new Point(80, 45),
 new Point(130, 45),
 new Point(130, 95),
 new Point(80, 95),
 new Point(80, 135),
 new Point(10, 135),
])
//Connect dots
for (let x = 0; x < poly1.points.length; x++) {
 let a = poly1.points[x];
 let b = poly1.points[(x + 1) % poly1.points.length]
 a.connectedTo.push(b)
 b.connectedTo.push(a)
}
//Setup constrains
for (let x = 0; x < poly1.points.length; x++) {
 let a = poly1.points[x];
 let b = poly1.points[(x + 1) % poly1.points.length]
 poly1.constrains.push({ a: a, b: b, key: x % 2 == 1 ? 'x' : 'y' })
}
poly1.constrains.push(
 { a: poly1.points[1], b: poly1.points[5], key: 'x' },
 { a: poly1.points[2], b: poly1.points[5], key: 'x' },
 { a: poly1.points[1], b: poly1.points[6], key: 'x' },
 { a: poly1.points[2], b: poly1.points[6], key: 'x' }
)
//Build polygon 2 (Triangle)
var poly2 = new Polygon([
 new Point(250, 250),
 new Point(300, 300),
 new Point(200, 300),
])
//Connect dots
for (let x = 0; x < poly2.points.length; x++) {
 let a = poly2.points[x];
 let b = poly2.points[(x + 1) % poly2.points.length]
 a.connectedTo.push(b)
 b.connectedTo.push(a)
}
//Setup constrains
poly2.constrains.push(
 { a: poly2.points[0], b: poly2.points[1], key: 'x', offset: 50 },
 { a: poly2.points[0], b: poly2.points[1], key: 'y', offset: 50 },
)
//Generate sketch
var s = new Sketch([poly1, poly2])

-->

更新 - 约束偏移量

根据评论中的反馈,我在约束中添加了一个 "offset" 键来处理不平衡的关系。

三角形的最右上角(至少最初)受到偏移量的约束。