From 21f96516b04f2e4a6754b5d7e160b04ff30dc81c Mon Sep 17 00:00:00 2001 From: Val Erastov Date: Sat, 31 Dec 2016 01:49:30 -0800 Subject: [PATCH] construct offset based on constraints --- web/app/3d/cad-utils.js | 16 +--- web/app/math/math.js | 14 +++ web/app/sketcher/constr/constraints.js | 17 ++++ web/app/sketcher/parametric.js | 38 ++++++++- web/app/sketcher/tools/loop-pick.js | 45 ++++++++-- web/app/sketcher/tools/offset.js | 113 ++++++++++++++++++++++--- 6 files changed, 209 insertions(+), 34 deletions(-) diff --git a/web/app/3d/cad-utils.js b/web/app/3d/cad-utils.js index f56d672f..1f1e53ce 100644 --- a/web/app/3d/cad-utils.js +++ b/web/app/3d/cad-utils.js @@ -348,20 +348,8 @@ export function normalOfCCWSeqTHREE(ccwSequence) { return b.sub(a).cross(c.sub(a)).normalize(); } - -// 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 area = math.area; +export const isCCW = math.isCCW; export function calculateExtrudedLid(sourcePolygon, normal, direction, expansionFactor) { var lid = []; diff --git a/web/app/math/math.js b/web/app/math/math.js index 3b02eb37..758d1dde 100644 --- a/web/app/math/math.js +++ b/web/app/math/math.js @@ -175,4 +175,18 @@ export function isPointInsidePolygon( inPt, inPolygon ) { 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; diff --git a/web/app/sketcher/constr/constraints.js b/web/app/sketcher/constr/constraints.js index 6b75b473..8bf31a95 100644 --- a/web/app/sketcher/constr/constraints.js +++ b/web/app/sketcher/constr/constraints.js @@ -9,6 +9,8 @@ function createByConstraintName(name, params, values) { return new Equal(params); case "equalsTo": return new EqualsTo(params, values[0]); + case "Diff": + return new Diff(params, values[0]); case "MinLength": return new MinLength(params, values[0]); 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 */ function P2LDistance(params, distance) { diff --git a/web/app/sketcher/parametric.js b/web/app/sketcher/parametric.js index 59c936cf..6efe5ea1 100644 --- a/web/app/sketcher/parametric.js +++ b/web/app/sketcher/parametric.js @@ -402,7 +402,7 @@ ParametricManager.prototype.radius = function(objs, promptCallback) { } }; -ParametricManager.prototype.linkObjects = function(objs) { +ParametricManager.prototype._linkObjects = function(objs) { var i; var masterIdx = -1; 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]); this._add(c); } +}; + +ParametricManager.prototype.linkObjects = function(objs) { + this._linkObjects(objs); 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 */ Constraints.Lock = function(p, c) { this.p = p; diff --git a/web/app/sketcher/tools/loop-pick.js b/web/app/sketcher/tools/loop-pick.js index 53ba8a26..99b5d7f1 100644 --- a/web/app/sketcher/tools/loop-pick.js +++ b/web/app/sketcher/tools/loop-pick.js @@ -9,12 +9,14 @@ export class LoopPickTool extends Tool { this.loops = new Map(); this.marked = new Set(); this.pickedLoop = null; - } + this.pointToObject = new Map(); + } restart() { this.sendHint('pick a polygon'); this.reindexLoops(); this.marked.clear(); + this.pointToObject.clear(); this.pickedLoop = null; }; @@ -56,8 +58,8 @@ export class LoopPickTool extends Tool { const graph = { connections : (p) => { - const conns = [this.otherEnd(p)]; - p.linked.forEach(l => conns.push(this.otherEnd(l))); + const conns = p.linked.slice(); + conns.push(this.otherEnd(p)); return conns; }, @@ -69,22 +71,37 @@ export class LoopPickTool extends Tool { 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 point of loop) { + for (let point of loop.points) { 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) { this.clearMarked(); this.pickedLoop = null; const p = this.viewer.screenToModel(e); this.pickedLoop = this.pickLoop(p); if (this.pickedLoop != null) { - for (let p of this.pickedLoop) { - this.mark(p.parent); + for (let obj of this.pickedLoop.edges) { + this.mark(obj); } } this.viewer.refresh(); @@ -115,3 +132,17 @@ export class LoopPickTool extends Tool { 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) +} diff --git a/web/app/sketcher/tools/offset.js b/web/app/sketcher/tools/offset.js index 9d1e5f7c..bbbfdf9a 100644 --- a/web/app/sketcher/tools/offset.js +++ b/web/app/sketcher/tools/offset.js @@ -1,5 +1,11 @@ import {LoopPickTool} from './loop-pick' +import {Constraints} from '../parametric' 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 { @@ -8,20 +14,103 @@ export class OffsetTool extends LoopPickTool { } onMousedown(e) { - const delta = prompt('offset distance?'); - const offsetPolygon = math.polygonOffsetByDelta(this.pickedLoop, parseInt(delta)); - const length = offsetPolygon.length; - const segments = []; - for (let p = length - 1, q = 0; q < length; p = q++) { - const a = offsetPolygon[p]; - const b = offsetPolygon[q]; - const segment = this.viewer.addSegment(a.x, a.y, b.x, b.y, this.viewer.activeLayer); - segments.push(segment); + const loopPoints = this.pickedLoop.points; + const loopEdges = this.pickedLoop.edges; + const length = loopPoints.length; + + for (let obj of loopEdges) { + if (!SUPPORTED_OBJECTS.has(obj._class)) { + alert(obj._class + " isn't supported for offsets"); + return; + } } - 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.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; +} \ No newline at end of file