awv3
Version:
⚡ AWV3 embedded CAD
393 lines (352 loc) • 16.9 kB
JavaScript
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);
}