转换几何形状,保持对称和边界尺寸完整

时间:2017-07-21 07:25:42

标签: javascript geometry graph-theory computational-geometry

我正在开发一种工具,用于从各种模板中修改不同的几何形状。形状是基本的,可以在房间里找到。 例如:L形,T形,六边形,矩形等。

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

一个形状就像这样实现,第一个节点从左上角开始,顺时针绕着形状(我使用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.5米改为3米,那么我也希望边缘E或G改变它们的长度以保持边距为12米并且还可以按下E这样边缘D仍然是完全水平的。 如果我将D侧改为2m,则B必须将其长度改为10m,依此类推。

(我的形状也有倾斜的角度,就像一个角落切掉的矩形)

An example of the shapes I have, a T-shape.

问题

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

    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个大气压)和未来的形状实现这个将是非常耗时的。

所以我想知道的是,是否有更一般的方法解决这个问题?

谢谢!

1 个答案:

答案 0 :(得分:1)

保留一个点对列表以及约束它们的键,并使用它来覆盖更新时的坐标。

这适用于您提供的示例:

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])

-->

更新 - 限制抵消

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

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