From c082c13ed50fb28f64b2600a471dbabc45e0597d Mon Sep 17 00:00:00 2001 From: "Val Erastov (xibyte)" Date: Thu, 2 Apr 2020 02:14:33 -0700 Subject: [PATCH] angular measure tool --- web/app/math/math.js | 16 + web/app/math/vec.js | 18 + web/app/sketcher/actions/measureActions.js | 24 +- .../sketcher/components/ConstraintEditor.jsx | 28 +- web/app/sketcher/constr/ANConstraints.js | 6 +- web/app/sketcher/icons/tools/ToolIcons.jsx | 6 + .../icons/tools/measure-angle-between.svg | 26 ++ web/app/sketcher/io.js | 82 +++-- web/app/sketcher/shapes/dim.js | 347 +++++++++++++++++- web/app/sketcher/shapes/sketch-types.js | 3 +- web/app/sketcher/shapes/textHelper.js | 12 +- web/app/sketcher/tools/circle.js | 2 +- web/app/sketcher/tools/dim.js | 132 ++++++- web/app/sketcher/tools/drag.js | 2 +- web/app/sketcher/tools/edit-tools-map.js | 8 +- web/app/sketcher/tools/tool.js | 2 +- web/app/sketcher/viewer2d.js | 3 + 17 files changed, 653 insertions(+), 64 deletions(-) create mode 100644 web/app/sketcher/icons/tools/measure-angle-between.svg diff --git a/web/app/math/math.js b/web/app/math/math.js index 23917a58..94e354bd 100644 --- a/web/app/math/math.js +++ b/web/app/math/math.js @@ -1,6 +1,8 @@ import Vector from 'math/vector'; import BBox from './bbox' import * as vec from './vec'; +import {perp2d} from "./vec"; +import {eqTol} from "../brep/geom/tolerance"; export const TOLERANCE = 1E-6; export const TOLERANCE_SQ = TOLERANCE * TOLERANCE; @@ -284,6 +286,20 @@ export function pointToLineSignedDistance(ax, ay, bx, by, px, py) { return vx * nx + vy * ny; } +export function lineLineIntersection2d(p1, p2, v1, v2) { + + // const n1 = perp2d(v1); + const n2 = perp2d(v2); + const cos = vec.dot(n2, v1); + if (eqTol(cos, 0)) { + return null; + } + const u1 = vec.dot(n2, vec.sub(p2, p1)) / cos; + // const u2 = vec.dot(n1, vec.sub(p1, p2)) / vec.dot(n1, v2); + + return [p1[0] + v1[0] * u1, p1[1] + v1[1] * u1]; +} + export const DEG_RAD = Math.PI / 180.0; export const sq = (a) => a * a; diff --git a/web/app/math/vec.js b/web/app/math/vec.js index 65fccc89..e0e15ea2 100644 --- a/web/app/math/vec.js +++ b/web/app/math/vec.js @@ -103,6 +103,10 @@ export function __normalize(v, out) { return __div(v, mag, out) } +export function cross2d(v1, v2) { + return v1[0] * v2[1] - v1[1] * v2[0]; +} + export function _normalize(v) { return __normalize(v, v); } @@ -155,6 +159,20 @@ export function distance(v1, v2) { return Math.sqrt(distanceSq(v1, v2)); } +export function perp2d(v) { + return __perp2d(v, []); +} + +export function _perp2d(v) { + return __perp2d(v, v); +} + +export function __perp2d([x, y], out) { + out[0] = -y; + out[1] = x; + return out; +} + export function normal3(ccwSequence) { let a = ccwSequence[0]; let b = ccwSequence[1]; diff --git a/web/app/sketcher/actions/measureActions.js b/web/app/sketcher/actions/measureActions.js index fbeb614d..5596e8a5 100644 --- a/web/app/sketcher/actions/measureActions.js +++ b/web/app/sketcher/actions/measureActions.js @@ -1,7 +1,12 @@ -import {MirrorGeneratorIcon} from "../icons/generators/GeneratorIcons"; -import {MirrorGeneratorSchema} from "../generators/mirrorGenerator"; -import {AddCircleDimTool, AddFreeDimTool, AddHorizontalDimTool, AddVerticalDimTool} from "../tools/dim"; import { + AddAngleBetweenDimTool, + AddCircleDimTool, + AddFreeDimTool, + AddHorizontalDimTool, + AddVerticalDimTool +} from "../tools/dim"; +import { + MeasureAngleBetweenAngle, MeasureCircleToolIcon, MeasureFreeToolIcon, MeasureHorizontalToolIcon, @@ -62,4 +67,17 @@ export default [ } }, + + { + id: 'MeasureAngleBetween', + shortName: 'Measure Angle Between', + kind: 'Tool', + description: 'Measure angle between', + icon: MeasureAngleBetweenAngle, + + invoke: (ctx) => { + ctx.viewer.toolManager.takeControl(new AddAngleBetweenDimTool(ctx.viewer, ctx.viewer.dimLayer)); + } + + }, ]; \ No newline at end of file diff --git a/web/app/sketcher/components/ConstraintEditor.jsx b/web/app/sketcher/components/ConstraintEditor.jsx index 579d7402..33c51d84 100644 --- a/web/app/sketcher/components/ConstraintEditor.jsx +++ b/web/app/sketcher/components/ConstraintEditor.jsx @@ -10,7 +10,6 @@ import Field from "ui/components/controls/Field"; import Label from "../../../../modules/ui/components/controls/Label"; import {SketcherAppContext} from "./SketcherApp"; import {EMPTY_OBJECT} from "../../../../modules/gems/objects"; -import identity from 'lodash'; export function ConstraintEditor() { @@ -19,7 +18,7 @@ export function ConstraintEditor() { const [values, setValues] = useState(null); useEffect(() => { - setValues(req && {...req.constraint.constants}) + setValues(req && {...req.constraint.constants}); return () => { if (req) { viewer.unHighlight(req.constraint.objects); @@ -72,10 +71,10 @@ export function ConstraintEditor() { .sort().map(name => { const def = constraint.schema.constants[name]; const presentation = def.presentation || EMPTY_OBJECT; - const onChange = value => setValue(name, (presentation.transformIn||identity)(value)); - - const val = (presentation.transformOut||identity)(values[name]); const type = presentation.type || def.type; + const onChange = value => setValue(name, (presentation.transformIn||transformInByType(type))(value)); + + const val = (presentation.transformOut||transformOutByType(type))(values[name]); return { @@ -113,4 +112,21 @@ export function editConstraint(rqStream, constraint, onApply) { onApply(); } }); -} \ No newline at end of file +} + +const NO_TR = v => v; + +function transformInByType(type) { + if ('boolean' === type) { + return value => value ? 'true' : 'false'; + } + return NO_TR; +} + +function transformOutByType(type) { + if ('boolean' === type) { + return value => value === 'true'; + } + return NO_TR; +} + diff --git a/web/app/sketcher/constr/ANConstraints.js b/web/app/sketcher/constr/ANConstraints.js index b8546b1a..a1468db4 100644 --- a/web/app/sketcher/constr/ANConstraints.js +++ b/web/app/sketcher/constr/ANConstraints.js @@ -964,6 +964,8 @@ export class AlgNumConstraint { val = expressionResolver(val); if (def.type === 'number') { val = parseFloat(val); + } else if (def.type === 'boolean') { + val = val === 'true' || val === true; } if (def.transform) { val = def.transform(val); @@ -996,9 +998,9 @@ export class AlgNumConstraint { this.constantKeys.map(name => { let val = this.schema.constants[name].initialValue(this.objects); if (typeof val === 'number') { - val = val.toFixed(2) + ''; + val = val.toFixed(2); } - this.updateConstant(name, val); + this.updateConstant(name, val + ''); }); } } diff --git a/web/app/sketcher/icons/tools/ToolIcons.jsx b/web/app/sketcher/icons/tools/ToolIcons.jsx index e82a09c6..6b419743 100644 --- a/web/app/sketcher/icons/tools/ToolIcons.jsx +++ b/web/app/sketcher/icons/tools/ToolIcons.jsx @@ -4,6 +4,7 @@ import measureCircleContent from './measure-circle-tool.svg'; import measureFreeContent from './measure-free-tool.svg'; import measureHorizontalContent from './measure-horizontal-tool.svg'; import measureVerticalContent from './measure-vertical-tool.svg'; +import measureAngleBetweenContent from './measure-angle-between.svg'; import rectangleContent from './rectangle-tool.svg'; import bezierContent from './bezier-tool.svg'; import ellipseContent from './ellipse-tool.svg'; @@ -34,6 +35,11 @@ export function MeasureVerticalToolIcon(props) { return } +export function MeasureAngleBetweenAngle(props) { + + return +} + export function RectangleToolIcon(props) { return diff --git a/web/app/sketcher/icons/tools/measure-angle-between.svg b/web/app/sketcher/icons/tools/measure-angle-between.svg new file mode 100644 index 00000000..2782f45d --- /dev/null +++ b/web/app/sketcher/icons/tools/measure-angle-between.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + diff --git a/web/app/sketcher/io.js b/web/app/sketcher/io.js index fde26fe9..e88996a2 100644 --- a/web/app/sketcher/io.js +++ b/web/app/sketcher/io.js @@ -8,7 +8,7 @@ import {Circle} from './shapes/circle' import {Ellipse} from './shapes/ellipse' import {EllipticalArc} from './shapes/elliptical-arc' import {BezierCurve} from './shapes/bezier-curve' -import {DiameterDimension, Dimension, HDimension, VDimension} from './shapes/dim' +import {AngleBetweenDimension, DiameterDimension, Dimension, HDimension, VDimension} from './shapes/dim' import {Constraints} from './parametric' import Vector from 'math/vector'; import exportTextData from 'gems/exportTextData'; @@ -78,31 +78,32 @@ IO.prototype._loadSketch = function(sketch) { } } } - var layer = viewer.createLayer(name, Styles.DEFAULT); + let layer = viewer.createLayer(name, Styles.DEFAULT); viewer.layers.push(layer); return layer; } const version = sketch.version || 1; - var T = Types; - var maxEdge = 0; - var sketchLayers = sketch.layers; - var boundary = sketch.boundary; - var boundaryNeedsUpdate = !(boundary === undefined || boundary == null); + let T = Types; + let maxEdge = 0; + let sketchLayers = sketch.layers; + let boundary = sketch.boundary; + let boundaryNeedsUpdate = !(boundary === undefined || boundary == null); + const dimensions = []; if (sketchLayers !== undefined) { - for (var l = 0; l < sketchLayers.length; ++l) { - var ioLayer = sketchLayers[l]; - var layerName = ioLayer.name; - var boundaryProcessing = layerName === IO.BOUNDARY_LAYER_NAME && boundaryNeedsUpdate; - var layer = getLayer(this.viewer, layerName); + for (let l = 0; l < sketchLayers.length; ++l) { + let ioLayer = sketchLayers[l]; + let layerName = ioLayer.name; + let boundaryProcessing = layerName === IO.BOUNDARY_LAYER_NAME && boundaryNeedsUpdate; + let layer = getLayer(this.viewer, layerName); // if (!!ioLayer.style) layer.style = ioLayer.style; layer.readOnly = !!ioLayer.readOnly; - var layerData = ioLayer.data; + let layerData = ioLayer.data; for (let i = 0; i < layerData.length; ++i) { - var obj = layerData[i]; - var skobj = null; - var _class = obj._class; - var aux = !!obj.aux; - var role = obj.role; + let obj = layerData[i]; + let skobj = null; + let _class = obj._class; + let aux = !!obj.aux; + let role = obj.role; //support legacy format if (!role) { @@ -150,17 +151,8 @@ IO.prototype._loadSketch = function(sketch) { const cp1 = endPoint(obj.cp1); const cp2 = endPoint(obj.cp2); skobj = new BezierCurve(a, b, cp1, cp2); - } else if (_class === T.HDIM) { - skobj = new HDimension(obj.a, obj.b); - obj.offset !== undefined && (skobj.offset = obj.offset); - } else if (_class === T.VDIM) { - skobj = new VDimension(obj.a, obj.b); - obj.offset !== undefined && (skobj.offset = obj.offset); - } else if (_class === T.DIM) { - skobj = new Dimension(obj.a, obj.b); - obj.offset !== undefined && (skobj.offset = obj.offset); - } else if (_class === T.DDIM) { - skobj = new DiameterDimension(obj.obj); + } else { + dimensions.push(obj); } if (skobj != null) { skobj.role = role; @@ -192,14 +184,27 @@ IO.prototype._loadSketch = function(sketch) { } } - for (let i = 0; i < this.viewer.dimLayer.objects.length; ++i) { - obj = this.viewer.dimLayer.objects[i]; - if (obj._class === T.DIM || obj._class === T.HDIM || obj._class === T.VDIM) { - obj.a = index[obj.a]; - obj.b = index[obj.b]; - } else if (obj._class === T.DDIM) { - obj.obj = index[obj.obj]; + for (let obj of dimensions) { + let _class = obj._class; + let skobj = null; + if (_class === T.HDIM) { + skobj = new HDimension(index[obj.a], index[obj.b]); + obj.offset !== undefined && (skobj.offset = obj.offset); + } else if (_class === T.VDIM) { + skobj = new VDimension(index[obj.a], index[obj.b]); + obj.offset !== undefined && (skobj.offset = obj.offset); + } else if (_class === T.DIM) { + skobj = new Dimension(index[obj.a], index[obj.b]); + obj.offset !== undefined && (skobj.offset = obj.offset); + } else if (_class === T.DDIM) { + skobj = new DiameterDimension(index[obj.obj]); + skobj.angle = obj.angle; + } else if (_class === T.ANGLE_BW) { + skobj = new AngleBetweenDimension(index[obj.a], index[obj.b]); + skobj.offset = obj.offset; } + this.viewer.dimLayer.add(skobj); + index[obj.id] = skobj; } if (boundaryNeedsUpdate) { @@ -391,6 +396,11 @@ IO.prototype._serializeSketch = function(metadata) { to.offset = obj.offset; } else if (obj._class === T.DDIM) { to.obj = obj.obj.id; + to.angle = obj.obj.angle; + } else if (obj._class === T.ANGLE_BW) { + to.a = obj.a.id; + to.b = obj.b.id; + to.offset = obj.offset; } const children = nonPointChildren(obj).map(c => c.id); if (children.length !== 0) { diff --git a/web/app/sketcher/shapes/dim.js b/web/app/sketcher/shapes/dim.js index 89fe8ee2..5d5160bc 100644 --- a/web/app/sketcher/shapes/dim.js +++ b/web/app/sketcher/shapes/dim.js @@ -1,5 +1,6 @@ import * as math from '../../math/math' -import {pointToLineSignedDistance} from '../../math/math' +import * as vec from '../../math/vec' +import {DEG_RAD, lineLineIntersection2d, makeAngle0_360, pointToLineSignedDistance} from '../../math/math' import Vector from 'math/vector'; import {SketchObject} from './sketch-object' import {Styles} from "../styles"; @@ -12,6 +13,8 @@ const ARROW_H_PX = 4; const ARROW_TO_TEXT_PAD_PX = 2; const TEXT_H_OFFSET = 3; const OUTER_ARROW_TO_TEXT_PAD_PX = 6; +const EXT_LINEAR_WIDTH_PX = 7; +const EXT_ANGULAR_WIDTH_PX = 10; function drawArrow(ctx, x, y, nx, ny, arrowW, arrowH) { ctx.beginPath(); @@ -21,6 +24,14 @@ function drawArrow(ctx, x, y, nx, ny, arrowW, arrowH) { ctx.fill(); } +function drawArrowForArc(ctx, px, py, x, y, nx, ny, arrowW, arrowH) { + ctx.beginPath(); + ctx.moveTo(x, y, 0); + ctx.lineTo(px + nx * arrowH, py + ny * arrowH ); + ctx.lineTo(px - nx * arrowH, py - ny * arrowH); + ctx.fill(); +} + function drawExtensionLine(ctx, x, y, nx, ny, width, tip, arrowW) { ctx.beginPath(); ctx.moveTo(x + ny * arrowW, y + -nx * arrowW); @@ -119,7 +130,7 @@ class LinearDimension extends SketchObject { function drawRef(start, x, y) { var vec = new Vector(x - start.x, y - start.y); vec._normalize(); - vec._multiply(7 * unscale); + vec._multiply(EXT_LINEAR_WIDTH_PX * unscale); ctx.moveTo(start.x, start.y ); ctx.lineTo(x, y); @@ -375,9 +386,337 @@ export class DiameterDimension extends SketchObject { } DiameterDimension.prototype._class = 'TCAD.TWO.DiameterDimension'; +export class AngleBetweenDimension extends SketchObject { -function getTextOff(scale) { - return 3 * scale; + constructor(a, b) { + super(); + this.a = a; + this.b = b; + this.offset = 20; + this.pickA = []; + this.pickB = []; + this.textHelper = new TextHelper(); + this.configuration = [this.a.a, this.a.b, this.b.a, this.b.b]; + this.pickInfo = []; + } + + visitParams(callback) { + } + + getReferencePoint() { + return this.a; + } + + translateImpl(dx, dy) { + + const [_ax, _ay] = this.pickA; + const [_bx, _by] = this.pickB; + + let _vx = - (_by - _ay); + let _vy = _bx - _ax; + + const d = math.distance(_ax, _ay, _bx, _by); + + //normalize + let _vxn = _vx / d; + let _vyn = _vy / d; + + this.offset += (dx * _vxn + dy * _vyn); + } + + drawImpl(ctx, scale, viewer) { + const marked = this.markers.length !== 0; + + if (marked) { + ctx.save(); + viewer.setStyle(Styles.HIGHLIGHT, ctx); + } + this.drawDimension(ctx, scale, viewer) + + if (marked) { + ctx.restore(); + } + } + + drawDimension(ctx, scale, viewer) { + + const unscale = viewer.unscale; + let off = this.offset; + const MIN_OFFSET_PX = 20; + if (off * scale < MIN_OFFSET_PX) { + off = MIN_OFFSET_PX * unscale; + } + const textOff = unscale * TEXT_H_OFFSET; + + let [aa, ab, ba, bb] = this.configuration; + + let aAng = makeAngle0_360(Math.atan2(ab.y - aa.y, ab.x - aa.x)); + let bAng = makeAngle0_360(Math.atan2(bb.y - ba.y, bb.x - ba.x)); + let ang = makeAngle0_360(bAng - aAng); + if (ang > Math.PI) { + this.configuration.reverse(); + [aa, ab, ba, bb] = this.configuration; + aAng = makeAngle0_360(Math.atan2(ab.y - aa.y, ab.x - aa.x)); + bAng = makeAngle0_360(Math.atan2(bb.y - ba.y, bb.x - ba.x)); + ang = makeAngle0_360(bAng - aAng); + } + // this.a.syncGeometry(); + // this.b.syncGeometry && this.b.syncGeometry(); + + let avx = Math.cos(aAng); + let avy = Math.sin(aAng); + let bvx = Math.cos(bAng); + let bvy = Math.sin(bAng); + + + this.center = findCenter(aa, ab, ba, bb, avx, avy, bvx, bvy); + + if (!this.center) { + return; + } + + const [cx, cy] = this.center; + + // if (distanceSquared(aa.x, aa.y, cx, cy) > distanceSquared(ab.x, ab.y, cx, cy)) { + // aAng = makeAngle0_360(aAng + Math.PI); + // avx *= -1; + // avy *= -1; + // } + // if (distanceSquared(ba.x, ba.y, cx, cy) > distanceSquared(bb.x, bb.y, cx, cy)) { + // bAng = makeAngle0_360(bAng + Math.PI); + // bvx *= -1; + // bvy *= -1; + // + // } + const halfAng = 0.5 * ang; + + let _ax = cx + off * avx; + let _ay = cy + off * avy; + let _bx = cx + off * bvx; + let _by = cy + off * bvy; + + + const _vxn = Math.cos(aAng + halfAng); + const _vyn = Math.sin(aAng + halfAng); + + const mx = cx + off * _vxn; + const my = cy + off * _vyn; + + const arrowWpx = ARROW_W_PX; + const arrowW = arrowWpx * unscale; + const arrowH = ARROW_H_PX * unscale; + + const txt = (1 / DEG_RAD * ang).toFixed(2) + '°'; + + this.textHelper.prepare(txt, ctx, viewer); + + let sinPhi = arrowW / off; + const cosPhi = Math.sqrt(1 - sinPhi * sinPhi); + + if (cosPhi !== cosPhi) { + return; + } + + let arrLxV = avx * cosPhi - avy * sinPhi; + let arrLyV = avx * sinPhi + avy * cosPhi; + + let arrLx = cx + off*(arrLxV); + let arrLy = cy + off*(arrLyV); + + sinPhi *= -1; + + let arrRxV = bvx * cosPhi - bvy * sinPhi; + let arrRyV = bvx * sinPhi + bvy * cosPhi; + + let arrRx = cx + off*(arrRxV); + let arrRy = cy + off*(arrRyV); + + + const availableArea = math.distance(arrLx, arrLy, arrRx, arrRy); + + const modelTextWidth = this.textHelper.modelTextWidth; + + const innerMode = modelTextWidth <= availableArea; + + let tx, ty; + + if (innerMode) { + + ctx.beginPath(); + ctx.arc(cx, cy, off, Math.atan2(arrLyV, arrLxV), Math.atan2(arrRyV, arrRxV)); + ctx.stroke(); + + drawArrowForArc(ctx, arrLx, arrLy, _ax, _ay, -arrLxV, -arrLyV, arrowW, arrowH); + drawArrowForArc(ctx, arrRx, arrRy, _bx, _by, arrRxV, arrRyV, arrowW, arrowH); + + const h = modelTextWidth/2; + tx = (mx + _vxn * textOff) + (- _vyn) * h; + ty = (my + _vyn * textOff) + ( _vxn) * h; + this.textHelper.draw(tx, ty, _vxn, _vyn, ctx, unscale, viewer, textOff, true); + + } else { + + ctx.beginPath(); + ctx.arc(cx, cy, off, aAng, bAng); + ctx.stroke(); + + //sin is inverted by this time + + arrLxV = avx * cosPhi - avy * sinPhi; + arrLyV = avx * sinPhi + avy * cosPhi; + + arrLx = cx + off*(arrLxV); + arrLy = cy + off*(arrLyV); + + sinPhi *= -1; + + arrRxV = bvx * cosPhi - bvy * sinPhi; + arrRyV = bvx * sinPhi + bvy * cosPhi; + + arrRx = cx + off*(arrRxV); + arrRy = cy + off*(arrRyV); + + drawArrowForArc(ctx, arrLx, arrLy, _ax, _ay, -arrLxV, -arrLyV, arrowW, arrowH); + drawArrowForArc(ctx, arrRx, arrRy, _bx, _by, arrRxV, arrRyV, arrowW, arrowH); + + const longExt = modelTextWidth + 2 * OUTER_ARROW_TO_TEXT_PAD_PX * unscale; + const shortExt = OUTER_ARROW_TO_TEXT_PAD_PX * unscale; + + ctx.beginPath(); + ctx.moveTo(arrLx, arrLy); + ctx.lineTo(arrLx + arrLyV * longExt, arrLy - arrLxV * longExt); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(arrRx, arrRy); + ctx.lineTo(arrRx - arrRyV * shortExt, arrRy + arrRxV * shortExt); + ctx.stroke(); + + + tx = arrLx - ( -arrLyV) * OUTER_ARROW_TO_TEXT_PAD_PX * unscale + arrLxV * textOff; + ty = arrLy - ( arrLxV) * OUTER_ARROW_TO_TEXT_PAD_PX * unscale + arrLyV * textOff; + + this.textHelper.draw(tx, ty, arrLxV, arrLyV, ctx, unscale, viewer, textOff, true); + } + + this.setPickInfo(cx, cy, _ax, _ay, _bx, _by, off); + + function drawRef(a, b, px, py, vx, vy) { + + const abx = b.x - a.x; + const aby = b.y - a.y; + + const apx = px - a.x; + const apy = py - a.y; + + const dot = abx * apx + aby * apy; + + + if (dot < 0) { + ctx.save(); + viewer.setStyle(Styles.CONSTRUCTION, ctx); + ctx.beginPath(); + ctx.moveTo(a.x, a.y); + ctx.lineTo(px - vx * EXT_ANGULAR_WIDTH_PX * unscale, py - vy * EXT_ANGULAR_WIDTH_PX * unscale); + ctx.stroke(); + ctx.restore(); + } else if (apx * apx + apy * apy > abx * abx + aby * aby) { + ctx.save(); + viewer.setStyle(Styles.CONSTRUCTION, ctx); + ctx.beginPath(); + ctx.moveTo(b.x, b.y); + ctx.lineTo(px + vx * EXT_ANGULAR_WIDTH_PX * unscale, py + vy * EXT_ANGULAR_WIDTH_PX * unscale); + ctx.stroke(); + ctx.restore(); + } + } + + drawRef(aa, ab, _ax, _ay, avx, avy); + drawRef(ba, bb, _bx, _by, bvx, bvy); + } + + setPickInfo(cx, cy, ax, ay, bx, by, rad) { + for (let i = 0; i < arguments.length; ++i) { + this.pickInfo[i] = arguments[i]; + } + } + + normalDistance(aim, scale) { + + const textDist = this.textHelper.normalDistance(aim); + + if (textDist !== -1) { + return textDist; + } + + if (this.pickInfo.length === 0) { + return; + } + + const [cx, cy, ax, ay, bx, by, rad] = this.pickInfo; + + function isPointInsideSector(x, y) { + const ca = [ax - cx, ay - cy]; + const cb = [bx - cx, by - cy]; + const ct = [x - cx, y - cy]; + + vec._normalize(ca); + vec._normalize(cb); + vec._normalize(ct); + const cosAB = vec.dot(ca, cb); + const cosAT = vec.dot(ca, ct); + + const isInside = cosAT >= cosAB; + const abInverse = vec.cross2d(ca, cb) < 0; + const atInverse = vec.cross2d(ca, ct) < 0; + + let result; + if (abInverse) { + result = !atInverse || !isInside; + } else { + result = !atInverse && isInside; + } + return result; + } + + const isInsideSector = isPointInsideSector(aim.x, aim.y); + if (isInsideSector) { + return Math.abs(math.distance(aim.x, aim.y, cx, cy) - rad); + } else { + return Math.min( + math.distance(aim.x, aim.y, ax, ay), + math.distance(aim.x, aim.y, bx, by) + ); + } + } } +export function findCenter(aa, ab, ba, bb, avx, avy, bvx, bvy) { + let center = lineLineIntersection2d([aa.x, aa.y], [ba.x, ba.y], [avx, avy], [bvx, bvy]); + + if (!center) { + let commonPt = null; + aa.visitLinked(p => { + if (ba === p || bb === p) { + commonPt = aa; + } + }); + if (!commonPt) { + ab.visitLinked(p => { + if (ba === p || bb === p) { + commonPt = ab; + } + }); + + } + if (!commonPt) { + return null; + } + center = commonPt.toVectorArray(); + } + return center; +} + +AngleBetweenDimension.prototype._class = 'TCAD.TWO.AngleBetweenDimension'; + diff --git a/web/app/sketcher/shapes/sketch-types.js b/web/app/sketcher/shapes/sketch-types.js index 3b129fea..89d6bacb 100644 --- a/web/app/sketcher/shapes/sketch-types.js +++ b/web/app/sketcher/shapes/sketch-types.js @@ -10,5 +10,6 @@ export const SketchTypes = { DIM : 'TCAD.TWO.Dimension', HDIM : 'TCAD.TWO.HDimension', VDIM : 'TCAD.TWO.VDimension', - DDIM : 'TCAD.TWO.DiameterDimension' + DDIM : 'TCAD.TWO.DiameterDimension', + ANGLE_BW : 'TCAD.TWO.AngleBetweenDimension' }; \ No newline at end of file diff --git a/web/app/sketcher/shapes/textHelper.js b/web/app/sketcher/shapes/textHelper.js index df497ffc..925252c8 100644 --- a/web/app/sketcher/shapes/textHelper.js +++ b/web/app/sketcher/shapes/textHelper.js @@ -15,7 +15,7 @@ export class TextHelper { this.txt = txt; } - draw(tx, ty, nx, ny, ctx, unscale, viewer, flipOffset) { + draw(tx, ty, nx, ny, ctx, unscale, viewer, flipOffset, staticFlip = false) { ctx.save(); let rot = makeAngle0_360(Math.atan2(-nx, ny)); @@ -31,10 +31,12 @@ export class TextHelper { let dty = [modelTextHeight * nx, ny * modelTextHeight]; if (flip) { - const dx = ny * modelTextWidth - nx * 2 *flipOffset; - const dy = -nx * modelTextWidth - ny * 2*flipOffset; - tx += dx; - ty += dy; + tx += ny * modelTextWidth; + ty += -nx * modelTextWidth; + const k = staticFlip ? 1.5 : -1; + tx += k * nx * 2 *flipOffset; + ty += k * ny * 2 *flipOffset; + _negate(dtx); _negate(dty); } diff --git a/web/app/sketcher/tools/circle.js b/web/app/sketcher/tools/circle.js index 03efdb48..dfb39915 100644 --- a/web/app/sketcher/tools/circle.js +++ b/web/app/sketcher/tools/circle.js @@ -50,11 +50,11 @@ export class EditCircleTool extends Tool { this.circle = new Circle( new EndPoint(p.x, p.y) ); - if (needSnap) this.viewer.parametricManager.coincidePoints(this.circle.c, p); this.pointPicked(this.circle.c.x, this.circle.c.y); this.sendHint('specify radius'); this.viewer.activeLayer.add(this.circle); this.viewer.parametricManager.prepare([this.circle]); + if (needSnap) this.viewer.parametricManager.coincidePoints(this.circle.c, p); this.viewer.refresh(); } diff --git a/web/app/sketcher/tools/dim.js b/web/app/sketcher/tools/dim.js index afe7c8fe..93b0d0b9 100644 --- a/web/app/sketcher/tools/dim.js +++ b/web/app/sketcher/tools/dim.js @@ -1,4 +1,4 @@ -import {HDimension, VDimension, Dimension, DiameterDimension} from '../shapes/dim' +import {AngleBetweenDimension, DiameterDimension, Dimension, findCenter, HDimension, VDimension,} from '../shapes/dim' import Vector from 'math/vector'; import {EndPoint} from '../shapes/point' import {Tool} from './tool' @@ -6,6 +6,8 @@ import {DragTool} from "./drag"; import {isInstanceOf} from "../actions/matchUtils"; import {Segment} from "../shapes/segment"; import {DEFAULT_SEARCH_BUFFER} from "../viewer2d"; +import {distance} from "../../math/math"; +import {_negate, cross2d} from "../../math/vec"; export class AddDimTool extends Tool { @@ -97,14 +99,14 @@ export class AddCircleDimTool extends Tool { return o._class === 'TCAD.TWO.Circle' || o._class === 'TCAD.TWO.Arc'; }); - if (objects.length != 0) { + if (objects.length !== 0) { this.dim.obj = objects[0]; this.viewer.capture('tool', [this.dim.obj], true); } else { this.dim.obj = null; this.viewer.withdrawAll('tool'); } - if (this.dim.obj != null) { + if (this.dim.obj !== null) { this.dim.angle = Math.atan2(p.y - this.dim.obj.c.y, p.x - this.dim.obj.c.x); } this.viewer.refresh(); @@ -122,3 +124,127 @@ export class AddCircleDimTool extends Tool { } } +export class AddAngleTool extends Tool { + + constructor(name, viewer, layer, dimCreation) { + super(name, viewer); + this.layer = layer; + this.a = null; + this.dim = null; + this.dimCreation = dimCreation; + } + + mousemove(e) { + const p = this.viewer.screenToModel(e); + + const result = this.viewer.search(p.x, p.y, DEFAULT_SEARCH_BUFFER, true, false, []).filter(o => o._class === 'TCAD.TWO.Segment'); + const [segment] = result; + + if (this.dim) { + let [center, configuration] = this.classify(p.x, p.y); + if (configuration) { + this.dim.configuration = configuration; + } + if (!center) { + center = segment.a; + } + + const [cx, cy] = center; + this.dim.offset = distance(cx, cy, p.x, p.y); + + } else { + if (segment) { + this.viewer.capture('tool', [segment], true); + } else { + this.viewer.withdrawAll('tool'); + } + } + + + this.viewer.refresh(); + } + + mouseup(e) { + + const p = this.viewer.screenToModel(e); + + const [segment] = this.viewer.captured.tool; + this.viewer.withdrawAll('tool'); + + if (this.a === null) { + if (!segment) { + this.viewer.toolManager.releaseControl(); + this.viewer.refresh(); + } + this.a = segment; + } else if (this.dim == null) { + if (!segment) { + this.viewer.toolManager.releaseControl(); + this.viewer.refresh(); + } + + this.dim = this.dimCreation(this.a, segment); + let [center, configuration] = this.classify(p.x, p.y); + if (configuration) { + this.dim.configuration = configuration; + } + if (!center) { + center = segment.a; + } + const [cx, cy] = center; + this.dim.offset = distance(cx, cy, p.x, p.y); + this.layer.add(this.dim); + this.viewer.refresh(); + } else { + this.viewer.toolManager.releaseControl(); + this.viewer.refresh(); + } + } + + classify(px, py) { + + const line1 = this.dim.a, line2 = this.dim.b; + + const v1 = [line1.ny, - line1.nx]; + const v2 = [line2.ny, - line2.nx]; + + const isec = findCenter(line1.a, line1.b, line2.a, line2.b, v1[0], v1[1], v2[0], v2[1]); + if (!isec) { + return []; + } + + const [cx, cy] = isec; + const v = [px - cx, py - cy]; + + const insideSector = (v, v1, v2) => cross2d(v1, v) > 0 && cross2d(v2, v) < 0; + + if (insideSector(v, v1, v2)) { + return [isec, [line1.a, line1.b, line2.a, line2.b]]; + } + + if (insideSector(v, v2, _negate(v1))) { + return [isec, [line2.a, line2.b, line1.b, line1.a]]; + } + _negate(v1); + + if (insideSector(v, _negate(v1), _negate(v2))) { + return [isec, [line1.b, line1.a, line2.b, line2.a]]; + } + _negate(v1); + _negate(v2); + + if (insideSector(v, _negate(v2), v1)) { + return [isec, [line2.b, line2.a, line1.a, line1.b]]; + } + + + return [isec]; + } + +} + +export class AddAngleBetweenDimTool extends AddAngleTool { + constructor(viewer, layer) { + super('angle between dimension', viewer, layer, (a, b) => new AngleBetweenDimension(a, b)); + } +} diff --git a/web/app/sketcher/tools/drag.js b/web/app/sketcher/tools/drag.js index 6c10cfd0..05f03324 100644 --- a/web/app/sketcher/tools/drag.js +++ b/web/app/sketcher/tools/drag.js @@ -25,7 +25,7 @@ export class DragTool extends Tool { this.obj.translate(dx, dy); } // this.viewer.parametricManager.setConstantsFromGeometry(this.obj); - if (!Tool.dumbMode(e) || this.obj.constraints.length !== 0) { + if (!Tool.dumbMode(e) && this.obj.constraints.length !== 0) { // this.viewer.parametricManager.prepare(); this.viewer.parametricManager.solve(true); } diff --git a/web/app/sketcher/tools/edit-tools-map.js b/web/app/sketcher/tools/edit-tools-map.js index 2361784e..d8670ad0 100644 --- a/web/app/sketcher/tools/edit-tools-map.js +++ b/web/app/sketcher/tools/edit-tools-map.js @@ -1,9 +1,10 @@ import {Ellipse} from '../shapes/ellipse' -import {EllipticalArc} from '../shapes/elliptical-arc' import {Circle} from '../shapes/circle' import {EditCircleTool} from './circle' import {DragTool} from './drag' import {EllipseTool, STATE_RADIUS} from './ellipse' +import {AngleBetweenDimension} from "../shapes/dim"; +import {AddAngleBetweenDimTool} from "./dim"; export function GetShapeEditTool(viewer, obj, alternative) { if (obj instanceof Circle && !alternative) { @@ -18,6 +19,11 @@ export function GetShapeEditTool(viewer, obj, alternative) { tool.ellipse = obj; tool.state = STATE_RADIUS; return tool; + } else if (obj instanceof AngleBetweenDimension && !alternative) { + const tool = new AddAngleBetweenDimTool(viewer, viewer.dimLayer); + tool.a = obj.a; + tool.dim = obj; + return tool; } else { return new DragTool(obj, viewer); } diff --git a/web/app/sketcher/tools/tool.js b/web/app/sketcher/tools/tool.js index 4520695e..cf231f0f 100644 --- a/web/app/sketcher/tools/tool.js +++ b/web/app/sketcher/tools/tool.js @@ -64,7 +64,7 @@ export class Tool { } static dumbMode(e) { - return e.ctrlKey || e.metaKey; + return e.ctrlKey || e.metaKey || e.altKey; } } diff --git a/web/app/sketcher/viewer2d.js b/web/app/sketcher/viewer2d.js index c2ff86e2..b6d1fb58 100644 --- a/web/app/sketcher/viewer2d.js +++ b/web/app/sketcher/viewer2d.js @@ -240,6 +240,9 @@ class Viewer { this.__prevStyle = null; + this.interactiveScale = this.scale / this.retinaPxielRatio; + this.unscale = 1 / this.interactiveScale; + this.__drawWorkspace(ctx, this._workspace, Viewer.__SKETCH_DRAW_PIPELINE); this.__drawWorkspace(ctx, this._serviceWorkspace, Viewer.__SIMPLE_DRAW_PIPELINE); };