我正在开发一种工具,用于从各种模板中修改不同的几何形状。形状是基本的,可以在房间里找到。 例如: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,依此类推。
(我的形状也有倾斜的角度,就像一个角落切掉的矩形)
问题
我有以下用于修改特定边缘的代码:
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个大气压)和未来的形状实现这个将是非常耗时的。
所以我想知道的是,是否有更一般的方法解决这个问题?
谢谢!
答案 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])
-->
更新 - 限制抵消
根据评论中的反馈,我在约束中添加了一个“偏移”键来处理不平衡的关系。
三角形最右上角(至少最初)受到偏移约束。