UNPKG

awv3

Version:
393 lines (352 loc) 16.9 kB
import * as THREE from 'three'; /* * The following restrictions apply to points: xoffset yoffset. * Restrictions apply to lines in the following order: length angle xoffset yoffset. * Restrictions apply to arcs in the following order: angle radius clockwise. */ export function drawPointBy_S([startHint], restrictions) { let start = startHint.clone(); if (restrictions.xabsolute !== undefined) start.x = restrictions.xabsolute; if (restrictions.yabsolute !== undefined) start.y = restrictions.yabsolute; return { start }; } export function drawLineBy_S_E([start, endHint], restrictions) { let { xoffset, yoffset, xabsolute, yabsolute, angle, length } = restrictions; if (xabsolute !== undefined) xoffset = xabsolute - start.x; if (yabsolute !== undefined) yoffset = yabsolute - start.y; let end = endHint.clone().sub(start); if (xoffset !== undefined) { if (yoffset !== undefined) end.set(xoffset, yoffset, 0); else if (angle !== undefined) end.set(xoffset, xoffset * Math.tan(angle), 0); else if (length !== undefined) end.set(xoffset, Math.sign(end.y) * cath(length, xoffset), 0); else end.set(xoffset, 0, 0); } else if (yoffset !== undefined) { if (angle !== undefined) end.set(yoffset / Math.tan(angle), yoffset, 0); else if (length !== undefined) end.set(Math.sign(end.x) * cath(length, yoffset), yoffset, 0); else end.set(0, yoffset, 0); } else if (angle !== undefined) { setVectorFromAngleLength(end, angle, length !== undefined ? length : end.length()); } else if (length !== undefined) { end.setLength(length); } end.add(start); return { start, end }; } // is unambigously defined if restrictions.angle and restrictions.clockwise are set // uses generalHint to disambiguate side if only restrictions.angle is set // uses generalHint to disambiguate side if restrictions.radius and restrictions.clockwise are set // uses generalHint and assumes smaller (<pi) angle if only restrictions.radius is set // throws an error otherwise function drawArcBy_S_E([start, end, generalHint], restrictions) { const halfChord = start.distanceTo(end) / 2; // calculate radius from the angle const radius = restrictions.angle !== undefined ? halfChord / Math.sin(restrictions.angle / 2) : restrictions.radius !== undefined ? restrictions.radius : /*throw*/ new Error("drawArcBy_S_E mustn't be called without angle or radius restrictions"); if (!(0 < radius && radius < 1e6)) return { start, end }; // perpend is the distance from the center to the start-end chord let perpend = cath(radius, halfChord), side = 1; // calculate side (sign of perpend) from angle and clockwise if (restrictions.angle !== undefined && restrictions.clockwise !== undefined) side = restrictions.angle < Math.PI === restrictions.clockwise ? -1 : 1; // otherwise use generalHint to disambiguate side else side = cross(end.clone().sub(start), generalHint.clone().sub(start)) < 0 ? -1 : 1; const center = setVectorFromOrthoCoords(end.clone().sub(start).normalize(), halfChord, side * perpend).add(start); const clockwise = restrictions.clockwise !== undefined ? restrictions.clockwise : restrictions.angle !== undefined ? restrictions.angle < Math.PI === (side === -1) : side === -1; return { start, end, center, clockwise }; } export function drawArcBy_S_E_C([start, end, centerHint], restrictions) { if (restrictions.angle !== undefined || restrictions.radius !== undefined) return drawArcBy_S_E([start, end, centerHint], restrictions); const perpend = cross(end.clone().sub(start).normalize(), centerHint.clone().sub(start)); const radius = Math.hypot(perpend, start.distanceTo(end) / 2); return drawArcBy_S_E([start, end, centerHint], { radius, clockwise: restrictions.clockwise }); } export function drawArcBy_S_E_CP([start, end, controlPointHint], restrictions) { const mid = start.clone().lerp(end, 0.5); let center = controlPointHint.clone().sub(mid); const perpend = cross(end.clone().sub(mid), center); const k = start.distanceToSquared(mid) / perpend; center.multiplyScalar((-k) * k).add(mid); if (!Number.isFinite((-k) * k)) center.copy(end); return drawArcBy_S_E_C([start, end, center], restrictions); } export function drawArcBy_S_E_M([start, end, middleHint], restrictions) { if (restrictions.angle !== undefined || restrictions.radius !== undefined) return drawArcBy_S_E([start, end, middleHint], restrictions); const MS = start.clone().sub(middleHint); const ME = end.clone().sub(middleHint); const inscribedAngle = Math.abs(Math.atan2(cross(MS, ME), MS.dot(ME))); const angle = 2 * (Math.PI - inscribedAngle); const clockwise = cross(MS, ME) > 0; return drawArcBy_S_E([start, end, middleHint], { angle, clockwise }); } export function drawArcBy_S_T_E([start, startTangent, endHint], restrictions) { // can't delegate to another drawArcBy* because they use end as-is but we must use startTangent as-is and end as a hint let { xoffset, yoffset, xabsolute, yabsolute, angle, radius, clockwise } = restrictions; endHint = new THREE.Vector3( (xabsolute !== undefined ? xabsolute : xoffset !== undefined ? start.x + xoffset : endHint.x), (yabsolute !== undefined ? yabsolute : yoffset !== undefined ? start.y + yoffset : endHint.y), 0, ); const dirHint = endHint.clone().sub(start); if (clockwise === undefined) clockwise = cross(startTangent, dirHint) < 0; if (angle !== undefined && radius !== undefined) { const s = clockwise ? -1 : 1; const center = setVectorFromOrthoCoords(startTangent.clone(), 0, s * radius).add(start); const end = setVectorFromOrthoCoords(startTangent.clone(), Math.sin(angle), s * (1 - Math.cos(angle))) .multiplyScalar(radius) .add(start); return { start, end, center, clockwise }; } else if (angle !== undefined) { const dirAngle = angle / (clockwise ? -2 : 2); // all possible ends lie on a dir ray from start const dir = setVectorFromOrthoCoords(startTangent.clone(), Math.cos(dirAngle), Math.sin(dirAngle)); let end = dirHint.clone().projectOnVector(dir); if (dir.dot(end) < 0) end.negate(); end.add(start); return drawArcBy_S_E([start, end, endHint], { angle, clockwise }); } else if (radius !== undefined) { const center = setVectorFromOrthoCoords(startTangent.clone(), 0, clockwise ? -radius : radius).add(start); const end = endHint.clone().sub(center).setLength(radius).add(center); return { start, end, center, clockwise }; } else { const signedRadius = dirHint.lengthSq() / cross(startTangent, dirHint) / 2; const center = setVectorFromOrthoCoords(startTangent.clone(), 0, signedRadius).add(start); return drawArcBy_S_E_C([start, endHint, center], { radius: Math.abs(signedRadius), clockwise }); } } export function drawCircleBy_C_E([center, endHint], restrictions) { let { radius } = restrictions; if (radius === undefined) radius = center.distanceTo(endHint); return { center, radius }; } export function drawArcBy_Angle_M([vertexPos, startDir, endDir, middlePos]) { // to return var len = undefined; var center = new THREE.Vector3(0, 0, 0); var centerDir = startDir.clone().add(endDir).multiplyScalar(0.5).normalize(); var vertexToMiddle = middlePos.clone().sub(vertexPos); // if radius is too small or middlepos and its projection to centerDir is out of filletAngle if (vertexToMiddle.length() < 1e-2 || vertexToMiddle.dot(centerDir) < 0) return null; // if middlepos is out of filletAngle but its projection to centerDir isn't var alpha = endDir.angleTo(startDir); var angleMidToStart = vertexToMiddle.angleTo(startDir); var angleMidToEnd = vertexToMiddle.angleTo(endDir); // project middlepos onto one of the directions if it is out of filletAngle var maxAngle = Math.max(angleMidToStart, angleMidToEnd, alpha); if (maxAngle !== alpha) { if (angleMidToStart === maxAngle) { len = vertexToMiddle.dot(endDir); } else if (angleMidToEnd === maxAngle) { len = vertexToMiddle.dot(startDir); } } if (len) { center = vertexPos.clone().add(centerDir.clone().multiplyScalar(len / Math.cos(alpha / 2))); // fillet angle < 180 => cos(alpha/2) != 0 return { start: vertexPos.clone().add(startDir.clone().multiplyScalar(len)), end: vertexPos.clone().add(endDir.clone().multiplyScalar(len)), center: center, clockwise: true, }; } var middleNew = middlePos.clone().sub(vertexPos); var sinA2Sq = Math.pow(Math.sin(alpha / 2), 2); var cosA2Sq = 1 - sinA2Sq; var A0 = middleNew.lengthSq(); var A1 = (-2) * middleNew.x; var A2 = (-2) * middleNew.y; //if (centerDir.x === 0) { // var sign = centerDir.dot(new THREE.Vector3(0,1,0)); // var discr = Math.pow(A2, 2) - 4 * cosA2Sq * A0; // var Oy = (-A2 + sign * Math.sqrt(discr)) / (2 * cosA2Sq); // // center.x = 0; // center.y = Oy; //} else if (centerDir.y === 0) { var sign = centerDir.dot(new THREE.Vector3(1, 0, 0)); var discr = Math.pow(A1, 2) - 4 * cosA2Sq * A0; var Ox = (-A1 + sign * Math.sqrt(discr)) / (2 * cosA2Sq); center.x = Ox; center.y = 0; } else { var sign = centerDir.dot(new THREE.Vector3(0, 1, 0)); var proportion = centerDir.x / centerDir.y; var A = (proportion * proportion + 1) * cosA2Sq; var B = A1 * proportion + A2; var C = A0; var discr = Math.pow(B, 2) - 4 * A * C; var Oy = (-B + sign * Math.sqrt(discr)) / (2 * A); center.x = Oy * proportion; center.y = Oy; } var len = center.length() * Math.cos(alpha / 2); center.add(vertexPos); return { start: vertexPos.clone().add(startDir.clone().multiplyScalar(len)), end: vertexPos.clone().add(endDir.clone().multiplyScalar(len)), center: center, clockwise: true, }; } export function drawLineBy_Angle_M([vertexPos, startDir, endDir, middlePos]) { var centerDir = startDir.clone().add(endDir).multiplyScalar(0.5).normalize(); var vertexToMiddle = middlePos.clone().sub(vertexPos); // if radius is too small or middlepos and its projection to centerDir is out of filletAngle if (vertexToMiddle.length() < 1e-2 || vertexToMiddle.dot(centerDir) < 0) return null; var alpha = endDir.angleTo(startDir); var len = vertexToMiddle.dot(centerDir) / Math.cos(alpha / 2); // we have appropriate fillet angle so cos != 0 return { start: vertexPos.clone().add(startDir.clone().multiplyScalar(len)), end: vertexPos.clone().add(endDir.clone().multiplyScalar(len)), }; } //converting arc from "three-points+flag" to "center+radius+angles" representation export function getArcAngles(params) { let vecStart = params.start.clone().sub(params.center); let vecEnd = params.end.clone().sub(params.center); let startAngle = Math.atan2(vecStart.y, vecStart.x); let endAngle = Math.atan2(vecEnd.y, vecEnd.x); let radius = 0.5 * (vecStart.length() + vecEnd.length()); if (params.clockwise) { if (startAngle < endAngle) startAngle += 2 * Math.PI; } else { if (startAngle > endAngle) endAngle += 2 * Math.PI; } const midAngle = (startAngle + endAngle) / 2; const vecMid = new THREE.Vector3(Math.cos(midAngle), Math.sin(midAngle), 0); const mid = params.center.clone().addScaledVector(vecMid, radius); return { center: params.center.clone(), mid, radius, start: startAngle, end: endAngle, bulge: Math.tan((endAngle - startAngle) / 4), }; } //returns intersection point of given two lines //if angularTolerance is set, then null is returned when lines are parallel export function intersectLines(pntA, dirA, pntB, dirB, angularTolerance) { angularTolerance = angularTolerance || 1e-15; //mach.eps. by default dirA = dirA.clone().normalize(); dirB = dirB.clone().normalize(); if ( dirA.length() < 0.9 || dirB.length() < 0.9 //must be 1 ) return null; var PxQ = cross(dirA, dirB); if (Math.abs(PxQ) <= angularTolerance) return null; var BmAxQ = cross(pntB.clone().sub(pntA), dirB); var BmAxP = cross(pntB.clone().sub(pntA), dirA); var paramA = BmAxQ / PxQ; var paramB = BmAxP / PxQ; var intersA = dirA.clone().multiplyScalar(paramA).add(pntA); var intersB = dirB.clone().multiplyScalar(paramB).add(pntB); var inters = intersA.clone().add(intersB).multiplyScalar(0.5); return inters; } export function getClass(geomParams) { if (geomParams.entities) return geomParams.cclass; if (geomParams.center) return geomParams.end ? 'CC_Arc' : 'CC_Circle'; if (geomParams.end) return 'CC_Line'; if (geomParams.start) return'CC_Point'; return undefined; } // calculate a tangent of a line, arc or circle at a point as a normalized (possibly zero) vector export function getTangent(geomParams, point) { if (geomParams.center) { const radVec = point.clone().sub(geomParams.center).normalize(); const tangVec = new THREE.Vector3(-radVec.y, radVec.x, 0); if (geomParams.clockwise) tangVec.negate(); return tangVec; } else if (geomParams.end) { return geomParams.end.clone().sub(geomParams.start).normalize(); } } // modify geomParams inplace to represent movement of a subObject by the displacement // subType can be either 'start', 'end', 'center' or '' (for object itself) export function move(geomParams, subType, displacement) { switch (subType) { case 'start': reverseCurve(geomParams); // fallthrough case 'end': geomParams.end.add(displacement); if (getClass(geomParams) === 'CC_Arc') { const tangent = getTangent(geomParams, geomParams.start); if ( tangent.lengthSq() === 0 // singular arc ) tangent.set(1, 0, 0); const newParams = drawArcBy_S_T_E([geomParams.start, tangent, geomParams.end], {}); if (newParams.center) { // only if the arc is still an arc Object.assign(geomParams, newParams); geomParams.radius = geomParams.center.distanceTo(geomParams.start); } } if (subType === 'start') reverseCurve(geomParams); break; case 'center': if (getClass(geomParams) === 'CC_Arc') { // center point must stay on the middle perpendicular line const mid = geomParams.end.clone().add(geomParams.start).multiplyScalar(0.5); const dir = geomParams.end.clone().sub(geomParams.start); geomParams.center .add(displacement) .sub(mid) .projectOnVector(new THREE.Vector3(-dir.y, dir.x, 0)) .add(mid); geomParams.radius = geomParams.center.distanceTo(geomParams.start); } else { geomParams.center.add(displacement); } break; case '': if (geomParams.start) geomParams.start.add(displacement); if (geomParams.end) geomParams.end.add(displacement); if (geomParams.center) geomParams.center.add(displacement); break; } return geomParams; } // modify params inplace for reversed curve function reverseCurve(geomParams) { if ( geomParams.end !== undefined // Line, Arc ) [geomParams.start, geomParams.end] = [geomParams.end, geomParams.start]; if ( geomParams.clockwise !== undefined //Arc ) geomParams.clockwise = !geomParams.clockwise; return geomParams; } // cross product function cross(a, b) { return a.x * b.y - a.y * b.x; } // cathetus (cf. Math.hypot): y = sqrt(z**2 - x**2) || 0 function cath(z, x) { const y = Math.sqrt(z * z - x * x); return Number.isNaN(y) ? 0 : y; } // set a vector from orthogonal coordinates: xdir = x * xdir + y * xdir^T function setVectorFromOrthoCoords(xdir, x, y) { return xdir.applyMatrix3(new THREE.Matrix3().fromArray([x, y, 0, -y, x, 0, 0, 0, 1])); } // set a vector from angle to OX and length function setVectorFromAngleLength(vector3, angle, length) { return vector3.set(Math.cos(angle), Math.sin(angle), 0).multiplyScalar(length); }