221 lines
9.0 KiB
JavaScript
221 lines
9.0 KiB
JavaScript
const Vector3D = require('./math/Vector3')
|
|
const Line3D = require('./math/Line3')
|
|
const Matrix4x4 = require('./math/Matrix4')
|
|
const OrthoNormalBasis = require('./math/OrthoNormalBasis')
|
|
const Plane = require('./math/Plane')
|
|
|
|
// # class Connector
|
|
// A connector allows to attach two objects at predefined positions
|
|
// For example a servo motor and a servo horn:
|
|
// Both can have a Connector called 'shaft'
|
|
// The horn can be moved and rotated such that the two connectors match
|
|
// and the horn is attached to the servo motor at the proper position.
|
|
// Connectors are stored in the properties of a CSG solid so they are
|
|
// ge the same transformations applied as the solid
|
|
const Connector = function (point, axisvector, normalvector) {
|
|
this.point = new Vector3D(point)
|
|
this.axisvector = new Vector3D(axisvector).unit()
|
|
this.normalvector = new Vector3D(normalvector).unit()
|
|
}
|
|
|
|
Connector.prototype = {
|
|
normalized: function () {
|
|
let axisvector = this.axisvector.unit()
|
|
// make the normal vector truly normal:
|
|
let n = this.normalvector.cross(axisvector).unit()
|
|
let normalvector = axisvector.cross(n)
|
|
return new Connector(this.point, axisvector, normalvector)
|
|
},
|
|
|
|
transform: function (matrix4x4) {
|
|
let point = this.point.multiply4x4(matrix4x4)
|
|
let axisvector = this.point.plus(this.axisvector).multiply4x4(matrix4x4).minus(point)
|
|
let normalvector = this.point.plus(this.normalvector).multiply4x4(matrix4x4).minus(point)
|
|
return new Connector(point, axisvector, normalvector)
|
|
},
|
|
|
|
// Get the transformation matrix to connect this Connector to another connector
|
|
// other: a Connector to which this connector should be connected
|
|
// mirror: false: the 'axis' vectors of the connectors should point in the same direction
|
|
// true: the 'axis' vectors of the connectors should point in opposite direction
|
|
// normalrotation: degrees of rotation between the 'normal' vectors of the two
|
|
// connectors
|
|
getTransformationTo: function (other, mirror, normalrotation) {
|
|
mirror = mirror ? true : false
|
|
normalrotation = normalrotation ? Number(normalrotation) : 0
|
|
let us = this.normalized()
|
|
other = other.normalized()
|
|
// shift to the origin:
|
|
let transformation = Matrix4x4.translation(this.point.negated())
|
|
// construct the plane crossing through the origin and the two axes:
|
|
let axesplane = Plane.anyPlaneFromVector3Ds(
|
|
new Vector3D(0, 0, 0), us.axisvector, other.axisvector)
|
|
let axesbasis = new OrthoNormalBasis(axesplane)
|
|
let angle1 = axesbasis.to2D(us.axisvector).angle()
|
|
let angle2 = axesbasis.to2D(other.axisvector).angle()
|
|
let rotation = 180.0 * (angle2 - angle1) / Math.PI
|
|
if (mirror) rotation += 180.0
|
|
transformation = transformation.multiply(axesbasis.getProjectionMatrix())
|
|
transformation = transformation.multiply(Matrix4x4.rotationZ(rotation))
|
|
transformation = transformation.multiply(axesbasis.getInverseProjectionMatrix())
|
|
let usAxesAligned = us.transform(transformation)
|
|
// Now we have done the transformation for aligning the axes.
|
|
// We still need to align the normals:
|
|
let normalsplane = Plane.fromNormalAndPoint(other.axisvector, new Vector3D(0, 0, 0))
|
|
let normalsbasis = new OrthoNormalBasis(normalsplane)
|
|
angle1 = normalsbasis.to2D(usAxesAligned.normalvector).angle()
|
|
angle2 = normalsbasis.to2D(other.normalvector).angle()
|
|
rotation = 180.0 * (angle2 - angle1) / Math.PI
|
|
rotation += normalrotation
|
|
transformation = transformation.multiply(normalsbasis.getProjectionMatrix())
|
|
transformation = transformation.multiply(Matrix4x4.rotationZ(rotation))
|
|
transformation = transformation.multiply(normalsbasis.getInverseProjectionMatrix())
|
|
// and translate to the destination point:
|
|
transformation = transformation.multiply(Matrix4x4.translation(other.point))
|
|
// let usAligned = us.transform(transformation);
|
|
return transformation
|
|
},
|
|
|
|
axisLine: function () {
|
|
return new Line3D(this.point, this.axisvector)
|
|
},
|
|
|
|
// creates a new Connector, with the connection point moved in the direction of the axisvector
|
|
extend: function (distance) {
|
|
let newpoint = this.point.plus(this.axisvector.unit().times(distance))
|
|
return new Connector(newpoint, this.axisvector, this.normalvector)
|
|
}
|
|
}
|
|
|
|
const ConnectorList = function (connectors) {
|
|
this.connectors_ = connectors ? connectors.slice() : []
|
|
}
|
|
|
|
ConnectorList.defaultNormal = [0, 0, 1]
|
|
|
|
ConnectorList.fromPath2D = function (path2D, arg1, arg2) {
|
|
if (arguments.length === 3) {
|
|
return ConnectorList._fromPath2DTangents(path2D, arg1, arg2)
|
|
} else if (arguments.length === 2) {
|
|
return ConnectorList._fromPath2DExplicit(path2D, arg1)
|
|
} else {
|
|
throw new Error('call with path2D and either 2 direction vectors, or a function returning direction vectors')
|
|
}
|
|
}
|
|
|
|
/*
|
|
* calculate the connector axisvectors by calculating the "tangent" for path2D.
|
|
* This is undefined for start and end points, so axis for these have to be manually
|
|
* provided.
|
|
*/
|
|
ConnectorList._fromPath2DTangents = function (path2D, start, end) {
|
|
// path2D
|
|
let axis
|
|
let pathLen = path2D.points.length
|
|
let result = new ConnectorList([new Connector(path2D.points[0],
|
|
start, ConnectorList.defaultNormal)])
|
|
// middle points
|
|
path2D.points.slice(1, pathLen - 1).forEach(function (p2, i) {
|
|
axis = path2D.points[i + 2].minus(path2D.points[i]).toVector3D(0)
|
|
result.appendConnector(new Connector(p2.toVector3D(0), axis,
|
|
ConnectorList.defaultNormal))
|
|
}, this)
|
|
result.appendConnector(new Connector(path2D.points[pathLen - 1], end,
|
|
ConnectorList.defaultNormal))
|
|
result.closed = path2D.closed
|
|
return result
|
|
}
|
|
|
|
/*
|
|
* angleIsh: either a static angle, or a function(point) returning an angle
|
|
*/
|
|
ConnectorList._fromPath2DExplicit = function (path2D, angleIsh) {
|
|
function getAngle (angleIsh, pt, i) {
|
|
if (typeof angleIsh === 'function') {
|
|
angleIsh = angleIsh(pt, i)
|
|
}
|
|
return angleIsh
|
|
}
|
|
let result = new ConnectorList(
|
|
path2D.points.map(function (p2, i) {
|
|
return new Connector(p2.toVector3D(0),
|
|
Vector3D.Create(1, 0, 0).rotateZ(getAngle(angleIsh, p2, i)),
|
|
ConnectorList.defaultNormal)
|
|
}, this)
|
|
)
|
|
result.closed = path2D.closed
|
|
return result
|
|
}
|
|
|
|
ConnectorList.prototype = {
|
|
setClosed: function (closed) {
|
|
this.closed = !!closed // FIXME: what the hell ?
|
|
},
|
|
appendConnector: function (conn) {
|
|
this.connectors_.push(conn)
|
|
},
|
|
/*
|
|
* arguments: cagish: a cag or a function(connector) returning a cag
|
|
* closed: whether the 3d path defined by connectors location
|
|
* should be closed or stay open
|
|
* Note: don't duplicate connectors in the path
|
|
* TODO: consider an option "maySelfIntersect" to close & force union all single segments
|
|
*/
|
|
followWith: function (cagish) {
|
|
const CSG = require('./CSG') // FIXME , circular dependency connectors => CSG => connectors
|
|
|
|
this.verify()
|
|
function getCag (cagish, connector) {
|
|
if (typeof cagish === 'function') {
|
|
cagish = cagish(connector.point, connector.axisvector, connector.normalvector)
|
|
}
|
|
return cagish
|
|
}
|
|
|
|
let polygons = []
|
|
let currCag
|
|
let prevConnector = this.connectors_[this.connectors_.length - 1]
|
|
let prevCag = getCag(cagish, prevConnector)
|
|
// add walls
|
|
this.connectors_.forEach(function (connector, notFirst) {
|
|
currCag = getCag(cagish, connector)
|
|
if (notFirst || this.closed) {
|
|
polygons.push.apply(polygons, prevCag._toWallPolygons({
|
|
toConnector1: prevConnector, toConnector2: connector, cag: currCag}))
|
|
} else {
|
|
// it is the first, and shape not closed -> build start wall
|
|
polygons.push.apply(polygons,
|
|
currCag._toPlanePolygons({toConnector: connector, flipped: true}))
|
|
}
|
|
if (notFirst === this.connectors_.length - 1 && !this.closed) {
|
|
// build end wall
|
|
polygons.push.apply(polygons,
|
|
currCag._toPlanePolygons({toConnector: connector}))
|
|
}
|
|
prevCag = currCag
|
|
prevConnector = connector
|
|
}, this)
|
|
return CSG.fromPolygons(polygons).reTesselated().canonicalized()
|
|
},
|
|
/*
|
|
* general idea behind these checks: connectors need to have smooth transition from one to another
|
|
* TODO: add a check that 2 follow-on CAGs are not intersecting
|
|
*/
|
|
verify: function () {
|
|
let connI
|
|
let connI1
|
|
for (let i = 0; i < this.connectors_.length - 1; i++) {
|
|
connI = this.connectors_[i]
|
|
connI1 = this.connectors_[i + 1]
|
|
if (connI1.point.minus(connI.point).dot(connI.axisvector) <= 0) {
|
|
throw new Error('Invalid ConnectorList. Each connectors position needs to be within a <90deg range of previous connectors axisvector')
|
|
}
|
|
if (connI.axisvector.dot(connI1.axisvector) <= 0) {
|
|
throw new Error('invalid ConnectorList. No neighboring connectors axisvectors may span a >=90deg angle')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {Connector, ConnectorList}
|