mirror of
https://github.com/xibyte/jsketcher
synced 2025-12-07 08:53:25 +01:00
construct offset based on constraints
This commit is contained in:
parent
702d01a62a
commit
21f96516b0
6 changed files with 209 additions and 34 deletions
|
|
@ -348,20 +348,8 @@ export function normalOfCCWSeqTHREE(ccwSequence) {
|
||||||
return b.sub(a).cross(c.sub(a)).normalize();
|
return b.sub(a).cross(c.sub(a)).normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const area = math.area;
|
||||||
// http://en.wikipedia.org/wiki/Shoelace_formula
|
export const isCCW = math.isCCW;
|
||||||
export function area(contour) {
|
|
||||||
var n = contour.length;
|
|
||||||
var a = 0.0;
|
|
||||||
for ( var p = n - 1, q = 0; q < n; p = q ++ ) {
|
|
||||||
a += contour[ p ].x * contour[ q ].y - contour[ q ].x * contour[ p ].y;
|
|
||||||
}
|
|
||||||
return a * 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCCW(path2D) {
|
|
||||||
return area(path2D) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateExtrudedLid(sourcePolygon, normal, direction, expansionFactor) {
|
export function calculateExtrudedLid(sourcePolygon, normal, direction, expansionFactor) {
|
||||||
var lid = [];
|
var lid = [];
|
||||||
|
|
|
||||||
|
|
@ -175,4 +175,18 @@ export function isPointInsidePolygon( inPt, inPolygon ) {
|
||||||
return inside;
|
return inside;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// http://en.wikipedia.org/wiki/Shoelace_formula
|
||||||
|
export function area(contour) {
|
||||||
|
var n = contour.length;
|
||||||
|
var a = 0.0;
|
||||||
|
for ( var p = n - 1, q = 0; q < n; p = q ++ ) {
|
||||||
|
a += contour[ p ].x * contour[ q ].y - contour[ q ].x * contour[ p ].y;
|
||||||
|
}
|
||||||
|
return a * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCCW(path2D) {
|
||||||
|
return area(path2D) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
export const sq = (a) => a * a;
|
export const sq = (a) => a * a;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ function createByConstraintName(name, params, values) {
|
||||||
return new Equal(params);
|
return new Equal(params);
|
||||||
case "equalsTo":
|
case "equalsTo":
|
||||||
return new EqualsTo(params, values[0]);
|
return new EqualsTo(params, values[0]);
|
||||||
|
case "Diff":
|
||||||
|
return new Diff(params, values[0]);
|
||||||
case "MinLength":
|
case "MinLength":
|
||||||
return new MinLength(params, values[0]);
|
return new MinLength(params, values[0]);
|
||||||
case "perpendicular":
|
case "perpendicular":
|
||||||
|
|
@ -201,6 +203,21 @@ function EqualsTo(params, value) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Diff(params, value) {
|
||||||
|
|
||||||
|
this.params = params;
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
this.error = function() {
|
||||||
|
return this.params[0].get() - this.params[1].get() - this.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.gradient = function(out) {
|
||||||
|
out[0] = 1;
|
||||||
|
out[1] = -1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** @constructor */
|
/** @constructor */
|
||||||
function P2LDistance(params, distance) {
|
function P2LDistance(params, distance) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -402,7 +402,7 @@ ParametricManager.prototype.radius = function(objs, promptCallback) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ParametricManager.prototype.linkObjects = function(objs) {
|
ParametricManager.prototype._linkObjects = function(objs) {
|
||||||
var i;
|
var i;
|
||||||
var masterIdx = -1;
|
var masterIdx = -1;
|
||||||
for (i = 0; i < objs.length; ++i) {
|
for (i = 0; i < objs.length; ++i) {
|
||||||
|
|
@ -423,6 +423,10 @@ ParametricManager.prototype.linkObjects = function(objs) {
|
||||||
var c = new Constraints.Coincident(objs[i], objs[masterIdx]);
|
var c = new Constraints.Coincident(objs[i], objs[masterIdx]);
|
||||||
this._add(c);
|
this._add(c);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ParametricManager.prototype.linkObjects = function(objs) {
|
||||||
|
this._linkObjects(objs);
|
||||||
this.notify();
|
this.notify();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -888,6 +892,38 @@ Constraints.Coincident.prototype.getObjects = function() {
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------------------------ //
|
// ------------------------------------------------------------------------------------------------------------------ //
|
||||||
|
|
||||||
|
/** @constructor */
|
||||||
|
Constraints.RadiusOffset = function(arc1, arc2, offset) {
|
||||||
|
this.arc1 = arc1;
|
||||||
|
this.arc2 = arc2;
|
||||||
|
this.offset = offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
Constraints.RadiusOffset.prototype.NAME = 'RadiusOffset';
|
||||||
|
Constraints.RadiusOffset.prototype.UI_NAME = 'Radius Offset';
|
||||||
|
|
||||||
|
Constraints.RadiusOffset.prototype.getSolveData = function() {
|
||||||
|
return [
|
||||||
|
['Diff', [this.arc1.r, this.arc2.r], [this.offset]]
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
Constraints.RadiusOffset.prototype.serialize = function() {
|
||||||
|
return [this.NAME, [this.arc1.id, this.arc2.id, this.offset]];
|
||||||
|
};
|
||||||
|
|
||||||
|
Constraints.Factory[Constraints.RadiusOffset.prototype.NAME] = function(refs, data) {
|
||||||
|
return new Constraints.RadiusOffset(refs(data[0]), refs(data[1]), data[2]);
|
||||||
|
};
|
||||||
|
|
||||||
|
Constraints.RadiusOffset.prototype.getObjects = function() {
|
||||||
|
return [this.arc1, this.arc2];
|
||||||
|
};
|
||||||
|
|
||||||
|
Constraints.RadiusOffset.prototype.SettableFields = {'offset' : "Enter the offset"};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------------------------ //
|
||||||
|
|
||||||
/** @constructor */
|
/** @constructor */
|
||||||
Constraints.Lock = function(p, c) {
|
Constraints.Lock = function(p, c) {
|
||||||
this.p = p;
|
this.p = p;
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@ export class LoopPickTool extends Tool {
|
||||||
this.loops = new Map();
|
this.loops = new Map();
|
||||||
this.marked = new Set();
|
this.marked = new Set();
|
||||||
this.pickedLoop = null;
|
this.pickedLoop = null;
|
||||||
|
this.pointToObject = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
restart() {
|
restart() {
|
||||||
this.sendHint('pick a polygon');
|
this.sendHint('pick a polygon');
|
||||||
this.reindexLoops();
|
this.reindexLoops();
|
||||||
this.marked.clear();
|
this.marked.clear();
|
||||||
|
this.pointToObject.clear();
|
||||||
this.pickedLoop = null;
|
this.pickedLoop = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -56,8 +58,8 @@ export class LoopPickTool extends Tool {
|
||||||
const graph = {
|
const graph = {
|
||||||
|
|
||||||
connections : (p) => {
|
connections : (p) => {
|
||||||
const conns = [this.otherEnd(p)];
|
const conns = p.linked.slice();
|
||||||
p.linked.forEach(l => conns.push(this.otherEnd(l)));
|
conns.push(this.otherEnd(p));
|
||||||
return conns;
|
return conns;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -69,22 +71,37 @@ export class LoopPickTool extends Tool {
|
||||||
return points.length;
|
return points.length;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const loops = Graph.findAllLoops(graph, (p) => p.id, (a, b) => a.id == b.id);
|
const loopPoints = Graph.findAllLoops(graph, (p) => p.id, (a, b) => a.id == b.id);
|
||||||
|
const loops = loopPoints.map(l => this.cleanLoop(l));
|
||||||
for (let loop of loops) {
|
for (let loop of loops) {
|
||||||
for (let point of loop) {
|
for (let point of loop.points) {
|
||||||
this.loops.set(point, loop);
|
this.loops.set(point, loop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanLoop(loop) {
|
||||||
|
const points = [];
|
||||||
|
const edges = [];
|
||||||
|
for (var i = 0; i < loop.length; i++) {
|
||||||
|
const a = loop[i];
|
||||||
|
const b = loop[(i + 1) % loop.length];
|
||||||
|
if (a.parent == b.parent) {
|
||||||
|
points.push(a);
|
||||||
|
edges.push(b.parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {points, edges};
|
||||||
|
}
|
||||||
|
|
||||||
mousemove(e) {
|
mousemove(e) {
|
||||||
this.clearMarked();
|
this.clearMarked();
|
||||||
this.pickedLoop = null;
|
this.pickedLoop = null;
|
||||||
const p = this.viewer.screenToModel(e);
|
const p = this.viewer.screenToModel(e);
|
||||||
this.pickedLoop = this.pickLoop(p);
|
this.pickedLoop = this.pickLoop(p);
|
||||||
if (this.pickedLoop != null) {
|
if (this.pickedLoop != null) {
|
||||||
for (let p of this.pickedLoop) {
|
for (let obj of this.pickedLoop.edges) {
|
||||||
this.mark(p.parent);
|
this.mark(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.viewer.refresh();
|
this.viewer.refresh();
|
||||||
|
|
@ -115,3 +132,17 @@ export class LoopPickTool extends Tool {
|
||||||
onMousedown(e) {};
|
onMousedown(e) {};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEdgeFor(a, b) {
|
||||||
|
if (isEdgeFor(a.parent, a, b)) {
|
||||||
|
return a.parent
|
||||||
|
} else if (isEdgeFor(b.parent, a, b)) {
|
||||||
|
return b.parent;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEdgeFor(obj, a, b) {
|
||||||
|
return (obj.a.id == a.id && obj.b.id == b.id) || (obj.a.id == b.id && obj.b.id == b.id)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import {LoopPickTool} from './loop-pick'
|
import {LoopPickTool} from './loop-pick'
|
||||||
|
import {Constraints} from '../parametric'
|
||||||
import * as math from '../../math/math';
|
import * as math from '../../math/math';
|
||||||
|
import Vector from '../../math/vector';
|
||||||
|
import {swap} from '../../utils/utils'
|
||||||
|
import ClipperLib from '../../../lib/clipper'
|
||||||
|
import {EndPoint} from '../shapes/point'
|
||||||
|
import {Arc} from '../shapes/arc'
|
||||||
|
|
||||||
export class OffsetTool extends LoopPickTool {
|
export class OffsetTool extends LoopPickTool {
|
||||||
|
|
||||||
|
|
@ -8,20 +14,103 @@ export class OffsetTool extends LoopPickTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMousedown(e) {
|
onMousedown(e) {
|
||||||
const delta = prompt('offset distance?');
|
const loopPoints = this.pickedLoop.points;
|
||||||
const offsetPolygon = math.polygonOffsetByDelta(this.pickedLoop, parseInt(delta));
|
const loopEdges = this.pickedLoop.edges;
|
||||||
const length = offsetPolygon.length;
|
const length = loopPoints.length;
|
||||||
const segments = [];
|
|
||||||
for (let p = length - 1, q = 0; q < length; p = q++) {
|
for (let obj of loopEdges) {
|
||||||
const a = offsetPolygon[p];
|
if (!SUPPORTED_OBJECTS.has(obj._class)) {
|
||||||
const b = offsetPolygon[q];
|
alert(obj._class + " isn't supported for offsets");
|
||||||
const segment = this.viewer.addSegment(a.x, a.y, b.x, b.y, this.viewer.activeLayer);
|
return;
|
||||||
segments.push(segment);
|
|
||||||
}
|
}
|
||||||
for (var i = 0; i < segments.length; i++) {
|
|
||||||
this.viewer.parametricManager.linkObjects([segments[i].b, segments[(i + 1) % segments.length].a]);
|
|
||||||
}
|
}
|
||||||
|
let delta = parseInt(prompt('offset distance?', 100));
|
||||||
|
const absDelta = Math.abs(delta);
|
||||||
|
|
||||||
|
const edges = [];
|
||||||
|
const startPoint = findLowestPoint(loopPoints);
|
||||||
|
const start = loopPoints.indexOf(startPoint);
|
||||||
|
if (start == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pos(i) {
|
||||||
|
return (i + start) % length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!math.isCCW([loopPoints[pos(0)], loopPoints[pos(1)], loopPoints[pos(length - 1)]])) {
|
||||||
|
delta *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arcs = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < length; ++i) {
|
||||||
|
const a = loopPoints[pos(i)];
|
||||||
|
const b = loopPoints[pos(i + 1)];
|
||||||
|
const normal = new Vector(-(b.y - a.y), (b.x - a.x))._normalize();
|
||||||
|
const offVector = normal._multiply(delta);
|
||||||
|
const origEdge = loopEdges[pos(i)];
|
||||||
|
const aOffX = a.x + offVector.x;
|
||||||
|
const aOffY = a.y + offVector.y;
|
||||||
|
const bOffX = b.x + offVector.x;
|
||||||
|
const bOffY = b.y + offVector.y;
|
||||||
|
if (origEdge._class == 'TCAD.TWO.Segment') {
|
||||||
|
const segment = this.viewer.addSegment(aOffX, aOffY,
|
||||||
|
bOffX, bOffY, this.viewer.activeLayer);
|
||||||
|
this.viewer.parametricManager._add(new Constraints.Parallel(origEdge, segment));
|
||||||
|
this.viewer.parametricManager._add(new Constraints.P2LDistance(a, segment, absDelta));
|
||||||
|
edges.push(segment);
|
||||||
|
} else if (origEdge._class == 'TCAD.TWO.Arc') {
|
||||||
|
const arc = new Arc(
|
||||||
|
new EndPoint(aOffX, aOffY),
|
||||||
|
new EndPoint(bOffX, bOffY),
|
||||||
|
new EndPoint(origEdge.c.x + offVector.x, origEdge.c.y + offVector.y)
|
||||||
|
);
|
||||||
|
arc.stabilize(this.viewer);
|
||||||
|
this.viewer.parametricManager._linkObjects([origEdge.c, arc.c]);
|
||||||
|
this.viewer.parametricManager._add(new Constraints.RadiusOffset(origEdge, arc, delta));
|
||||||
|
this.viewer.activeLayer.add(arc);
|
||||||
|
edges.push(arc);
|
||||||
|
arcs.push(arc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
arcs.forEach(e => e.c.aux = true);
|
||||||
|
for (let i = 0; i < edges.length; i++) {
|
||||||
|
this.viewer.parametricManager._linkObjects([edges[i].b, edges[(i + 1) % edges.length].a]);
|
||||||
|
}
|
||||||
|
loopEdges.forEach(e => e.aux = true);
|
||||||
|
this.viewer.parametricManager.refresh();
|
||||||
|
loopEdges.forEach(e => e.aux = false);
|
||||||
|
arcs.forEach(e => e.c.aux = false);
|
||||||
this.viewer.toolManager.releaseControl();
|
this.viewer.toolManager.releaseControl();
|
||||||
this.viewer.refresh();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function segmentToVector(segment) {
|
||||||
|
return new Vector(segment.b.x - segment.a.x, segment.b.y - segment.a.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPPORTED_OBJECTS = new Set();
|
||||||
|
SUPPORTED_OBJECTS.add('TCAD.TWO.Segment');
|
||||||
|
SUPPORTED_OBJECTS.add('TCAD.TWO.Arc');
|
||||||
|
|
||||||
|
function SimpleSegment(a, b) {
|
||||||
|
this.a = a;
|
||||||
|
this.b = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLowestPoint(poly) {
|
||||||
|
let hero = {x: Number.MAX_VALUE, y: Number.MAX_VALUE};
|
||||||
|
for (let point of poly) {
|
||||||
|
if (point.y < hero.y) {
|
||||||
|
hero = point;
|
||||||
|
} else if (hero.y == hero.y) {
|
||||||
|
if (point.x < hero.x) {
|
||||||
|
hero = point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hero;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue