sunrize
Version:
A Multi-Platform X3D Editor
651 lines (521 loc) • 23.2 kB
JavaScript
"use strict";
const
X3DSnapNodeTool = require ("./X3DSnapNodeTool"),
ActionKeys = require ("../../Application/ActionKeys"),
X3D = require ("../../X3D");
class SnapTarget extends X3DSnapNodeTool
{
#transformTools = [ ];
#changing = Symbol ();
async initializeTool ()
{
await super .initializeTool ();
this .tool .type = "SNAP_TARGET";
}
connectTool ()
{
super .connectTool ();
X3DSnapNodeTool .snapTarget = true;
X3DSnapNodeTool .addToolInterest (this, () => this .set_transform_tools ());
this .set_transform_tools ();
}
disconnectTool ()
{
X3DSnapNodeTool .snapTarget = false;
X3DSnapNodeTool .removeToolInterest (this);
super .disconnectTool ();
}
set_transform_tools ()
{
for (const transformTool of this .#transformTools)
transformTool .removeInterest ("set_transform", this);
this .#transformTools .length = 0;
for (const transformTool of X3DSnapNodeTool .tools)
{
if (!(transformTool instanceof X3D .X3DTransformNode))
continue;
this .#transformTools .push (transformTool);
}
for (const transformTool of this .#transformTools)
transformTool .addInterest ("set_transform", this, transformTool);
}
set_transform (transformTool)
{
if (!this ._visible .getValue ())
return;
if (ActionKeys .value === (ActionKeys .Shift | ActionKeys .Control))
return;
if (!transformTool .tool .isActive)
return;
switch (transformTool .tool .activeTool)
{
case "TRANSLATE":
this .set_translation (transformTool);
return;
case "ROTATE":
this .set_rotation (transformTool);
return;
case "SCALE":
this .set_scale (transformTool);
return;
}
}
static #axes = [
new X3D .Vector3 (),
new X3D .Vector3 (),
new X3D .Vector3 (),
];
static #normals = [
new X3D .Vector3 (),
new X3D .Vector3 (),
new X3D .Vector3 (),
];
set_translation (transformTool)
{
if (transformTool .getUserData (this .#changing))
{
transformTool .setUserData (this .#changing, false);
return;
}
// Get absolute position.
const
dynamicSnapDistance = this .getDynamicSnapDistance (),
absolutePosition = this .tool .position .getValue () .copy (),
absoluteMatrix = transformTool .getCurrentMatrix () .multRight (transformTool .getModelMatrix ()),
bbox = transformTool .getSubBBox (new X3D .Box3 ()) .multRight (absoluteMatrix),
center = (this .tool .snapToCenter && !transformTool .tool .keepCenter) ? absoluteMatrix .multVecMatrix (transformTool ._center .getValue () .copy ()) : bbox .center .copy (),
axes = bbox .getAxes (SnapTarget .#axes),
normals = bbox .getNormals (SnapTarget .#normals);
// Determine snap translation.
const
xCenters = [center, bbox .center .copy () .add (axes [0]), bbox .center .copy () .subtract (axes [0])],
xAxes = [axes [0], axes [0], axes [0] .copy () .negate ()],
xNormals = [normals [0], normals [0], normals [0] .copy () .negate ()];
const
yCenters = [center, bbox .center .copy () .add (axes [1]), bbox .center .copy () .subtract (axes [1])],
yAxes = [axes [1], axes [1], axes [1] .copy () .negate ()],
yNormals = [normals [1], normals [1], normals [1] .copy () .negate ()];
const
zCenters = [center, bbox .center .copy () .add (axes [2]), bbox .center .copy () .subtract (axes [2])],
zAxes = [axes [2], axes [2], axes [2] .copy () .negate ()],
zNormals = [normals [2], normals [2], normals [2] .copy () .negate ()];
const
[snapTranslationX] = this .getSnapTranslation (absolutePosition, xCenters, xAxes, xNormals, dynamicSnapDistance),
[snapTranslationY] = this .getSnapTranslation (absolutePosition, yCenters, yAxes, yNormals, dynamicSnapDistance),
[snapTranslationZ] = this .getSnapTranslation (absolutePosition, zCenters, zAxes, zNormals, dynamicSnapDistance);
const snapTranslation = snapTranslationX .add (snapTranslationY) .add (snapTranslationZ);
this .tool .snapped = snapTranslation .magnitude () > 0.0001;
if (snapTranslation .equals (X3D .Vector3 .Zero))
return;
// Snap translation.
const
snapMatrix = new X3D .Matrix4 () .set (snapTranslation),
currentMatrix = absoluteMatrix .multRight (snapMatrix) .multRight (transformTool .getModelMatrix () .copy () .inverse ());
transformTool .setUserData (this .#changing, true);
if (transformTool .tool .keepCenter)
transformTool .setMatrixKeepCenter (currentMatrix);
else
transformTool .setMatrixWithCenter (currentMatrix);
}
static #rotationAxes = {
X_AXIS: 0,
Y_AXIS: 1,
Z_AXIS: 2,
};
set_rotation (transformTool)
{
if (transformTool .getUserData (this .#changing))
{
transformTool .setUserData (this .#changing, false);
return;
}
const
absolutePosition = this .tool .position .getValue () .copy (),
absoluteNormal = this .tool .normal .getValue () .copy () .normalize (),
absoluteMatrix = transformTool .getCurrentMatrix () .multRight (transformTool .getModelMatrix ()),
bbox = transformTool .getSubBBox (new X3D .Box3 ()) .multRight (absoluteMatrix);
// Determine rotation axis and the tho snap axes.
const
index0 = SnapTarget .#rotationAxes [transformTool .tool .activeHandle], // Index of rotation axis
index1 = (index0 + 1) % 3,
index2 = (index0 + 2) % 3;
const axes = [
absoluteMatrix .xAxis .copy (),
absoluteMatrix .yAxis .copy (),
absoluteMatrix .zAxis .copy (),
]; // Rotation axis, equates to grid normal.
const
axis1 = axes [index1] .normalize (), // Snap axis 1
axis2 = axes [index2] .normalize (), // Snap axis 2
axis0 = axis1 .copy () .cross (axis2) .normalize (); // Rotation plane normal
// Determine snap vector, from center to the position of this SnapTarget, projected onto plane of rotation axis.
// If the angle between the normal of this SnapTarget and rotation axis is very small (<10°) the vector from
// center to the position of this SnapTarget is used.
const
center = absoluteMatrix .multVecMatrix (transformTool ._center .getValue () .copy ()),
useNormal = Math .abs (absoluteNormal .dot (axis0)) < Math .cos (X3D .Algorithm .radians (10)),
snapVector = axis0 .copy () .cross (useNormal ? absoluteNormal : absolutePosition .copy () .subtract (center)) .cross (axis0) .normalize ();
// Determine snap point onto plane and axes points onto plane with same distance to center as snap point.
const
dynamicSnapDistance = this .getDynamicSnapDistance () * 2,
distance = Math .max (bbox .center .distance (center), bbox .size .magnitude ()),
snapPoint = snapVector .copy () .multiply (distance),
point1a = axis1 .copy () .multiply (distance),
point1b = axis1 .copy () .negate () .multiply (distance),
point2a = axis2 .copy () .multiply (distance),
point2b = axis2 .copy () .negate () .multiply (distance);
// Determine snap rotation.
const
distance1a = snapPoint .distance (point1a),
distance1b = snapPoint .distance (point1b),
distance1 = Math .min (distance1a, distance1b),
distance2a = snapPoint .distance (point2a),
distance2b = snapPoint .distance (point2b),
distance2 = Math .min (distance2a, distance2b),
snapRotation = new X3D .Rotation4 ();
if (distance1 < distance2)
{
if (distance1 < dynamicSnapDistance)
{
const
invModelMatrix = transformTool .getModelMatrix () .copy () .inverse (),
from = invModelMatrix .multDirMatrix (distance1a < distance1b ? axis1 : axis1 .negate ()),
to = invModelMatrix .multDirMatrix (snapVector);
snapRotation .setFromToVec (from, to);
}
}
else
{
if (distance2 < dynamicSnapDistance)
{
const
invModelMatrix = transformTool .getModelMatrix () .copy () .inverse (),
from = invModelMatrix .multDirMatrix (distance2a < distance2b ? axis2 : axis2 .negate ()),
to = invModelMatrix .multDirMatrix (snapVector);
snapRotation .setFromToVec (from, to);
}
}
this .tool .snapped = Math .abs (snapRotation .angle) > 0.0001;
if (snapRotation .equals (X3D .Rotation4 .Identity))
return;
// Snap rotation.
const currentMatrix = new X3D .Matrix4 ()
.set (transformTool ._translation .getValue (),
transformTool ._rotation .getValue () .copy () .multRight (snapRotation),
transformTool ._scale .getValue (),
transformTool ._scaleOrientation .getValue (),
transformTool ._center .getValue ());
transformTool .setUserData (this .#changing, true);
if (transformTool .tool .keepCenter)
transformTool .setMatrixKeepCenter (currentMatrix);
else
transformTool .setMatrixWithCenter (currentMatrix);
}
static #scaleHandles = {
X_FRONT: 0,
Y_FRONT: 1,
Z_FRONT: 2,
X_BACK: 3,
Y_BACK: 4,
Z_BACK: 5,
SCALE_1: 6,
SCALE_2: 7,
SCALE_3: 8,
SCALE_4: 9,
SCALE_5: 10,
SCALE_6: 11,
SCALE_7: 12,
SCALE_8: 13,
};
set_scale (transformTool)
{
if (transformTool .getUserData (this .#changing))
{
transformTool .setUserData (this .#changing, false);
return;
}
const handle = SnapTarget .#scaleHandles [transformTool .tool .activeHandle];
const currentMatrix = handle < 6
? this .getScaleMatrix (transformTool, handle)
: this .getUniformScaleMatrix (transformTool, handle - 6);
if (!currentMatrix)
return;
transformTool .setUserData (this .#changing, true);
if (transformTool .tool .keepCenter)
transformTool .setMatrixKeepCenter (currentMatrix);
else
transformTool .setMatrixWithCenter (currentMatrix);
}
getSnapTranslation (position, centers, axes, normals, snapDistance)
{
// Return first successful snap translation.
const translations = [ ];
for (let i = 0; i < centers .length; ++ i)
{
const
center = centers [i],
axis = axes [i],
normal = normals [i],
positionPlane = new X3D .Plane3 (position, normal),
axisLine = new X3D .Line3 (center, axis .magnitude () > 0 ? axis .copy () .normalize () : normal),
intersection = new X3D .Vector3 (),
intersected = positionPlane .intersectsLine (axisLine, intersection);
if (!intersected)
continue;
const translation = intersection .subtract (center);
if (translation .magnitude () > snapDistance)
continue;
translations .push ([translation, i]);
}
if (translations .length)
return translations .reduce (([p, pi], [c, ci]) => p .magnitude () < c .magnitude () ? [p, pi] : [c, ci]);
return [new X3D .Vector3 (), undefined];
}
getScaleMatrix (transformTool, handle)
{
const
MIN_DELTA = 1e-6,
MIN_RATIO = 1e-3;
const
dynamicSnapDistance = this .getDynamicSnapDistance (),
absolutePosition = this .tool .position .getValue () .copy (),
absoluteMatrix = transformTool .getCurrentMatrix () .multRight (transformTool .getModelMatrix ()),
subBBox = transformTool .getSubBBox (new X3D .Box3 ()),
subAABBox = new X3D .Box3 (subBBox .size, subBBox .center),
bbox = subAABBox .copy () .multRight (absoluteMatrix),
axes = bbox .getAxes (SnapTarget .#axes),
normals = bbox .getNormals (SnapTarget .#normals);
switch (transformTool .tool .scaleMode)
{
case "SCALE_FROM_CENTER":
{
// Scale one axis in both directions.
const
axis = handle % 3,
aCenters = [bbox .center .copy () .add (axes [axis]), bbox .center .copy () .subtract (axes [axis])],
aAxes = [axes [axis], axes [axis] .copy () .negate ()],
aNormals = [normals [axis], normals [axis] .copy () .negate ()];
const [snapTranslation, aAxis] = this .getSnapTranslation (absolutePosition, aCenters, aAxes, aNormals, dynamicSnapDistance);
this .tool .snapped = snapTranslation .magnitude () > 0.0001;
if (snapTranslation .equals (X3D .Vector3 .Zero))
return undefined;
const
aBefore = aAxes [aAxis],
aAfter = aAxes [aAxis] .copy () .add (snapTranslation),
aDelta = aAfter .distance (aBefore),
aRatio = aAfter .magnitude () / aBefore .magnitude ();
if (Math .abs (aDelta) < MIN_DELTA || Math .abs (aRatio) < MIN_RATIO || isNaN (aRatio) || Math .abs (aRatio) === Infinity)
{
return undefined;
}
let snapScale = new X3D .Vector3 (1, 1, 1);
snapScale [axis] = aRatio;
snapScale = this .getConnectedAxes (transformTool, axis, snapScale);
const
center = subAABBox .center,
snapMatrix = new X3D .Matrix4 () .set (null, null, snapScale, null, center);
snapMatrix .multRight (transformTool .getCurrentMatrix ());
return snapMatrix;
}
case "SCALE_FROM_OPPOSITE_HANDLE":
{
const
axis = handle % 3,
sgn = handle < 3 ? 1 : -1,
aCenters = [bbox .center .copy () .add (axes [axis] .copy () .multiply (sgn))],
aAxes = [axes [axis] .copy () .multiply (sgn)],
aNormals = [normals [axis] .copy () .multiply (sgn)];
const [snapTranslation, aAxis] = this .getSnapTranslation (absolutePosition, aCenters, aAxes, aNormals, dynamicSnapDistance);
this .tool .snapped = snapTranslation .magnitude () > 0.0001;
if (snapTranslation .equals (X3D .Vector3 .Zero))
return undefined;
const
aBefore = aAxes [aAxis] .copy () .multiply (2),
aAfter = aBefore .copy () .add (snapTranslation),
aDelta = aAfter .distance (aBefore),
aRatio = aAfter .magnitude () / aBefore .magnitude ();
if (Math .abs (aDelta) < MIN_DELTA || Math .abs (aRatio) < MIN_RATIO || isNaN (aRatio) || Math .abs (aRatio) === Infinity)
{
return undefined;
}
let snapScale = new X3D .Vector3 (1, 1, 1);
snapScale [axis] = aRatio;
snapScale = this .getConnectedAxes (transformTool, axis, snapScale);
const
subAABBoxAxes = subAABBox .getAxes (SnapTarget .#axes),
center = subAABBoxAxes [axis] .multiply (-sgn) .add (subAABBox .center),
snapMatrix = new X3D .Matrix4 () .set (null, null, snapScale, null, center);
snapMatrix .multRight (transformTool .getCurrentMatrix ());
return snapMatrix;
}
}
}
static #adjacentFaces =
[
[0, 2, 4],
[1, 2, 4],
[1, 3, 4],
[0, 3, 4],
[0, 2, 5],
[1, 2, 5],
[1, 3, 5],
[0, 3, 5],
];
static #oppositePoints = [6, 7, 4, 5, 2, 3, 0, 1];
static #points = [
new X3D .Vector3 (),
new X3D .Vector3 (),
new X3D .Vector3 (),
new X3D .Vector3 (),
new X3D .Vector3 (),
new X3D .Vector3 (),
new X3D .Vector3 (),
new X3D .Vector3 (),
];
getUniformScaleMatrix (transformTool, handle)
{
const
MIN_DELTA = 1e-6,
MIN_RATIO = 1e-3;
const
dynamicSnapDistance = this .getDynamicSnapDistance (),
absolutePosition = this .tool .position .getValue () .copy (),
absoluteMatrix = transformTool .getCurrentMatrix () .multRight (transformTool .getModelMatrix ()),
subBBox = transformTool .getSubBBox (new X3D .Box3 ()),
subAABBox = new X3D .Box3 (subBBox .size, subBBox .center),
bbox = subAABBox .copy () .multRight (absoluteMatrix),
axes = bbox .getAxes (SnapTarget .#axes),
normals = bbox .getNormals (SnapTarget .#normals);
switch (transformTool .tool .scaleMode)
{
case "SCALE_FROM_CENTER":
{
// Scale one axis in both directions.
const
aCenters = [
bbox .center .copy () .add (axes [0]),
bbox .center .copy () .subtract (axes [0]),
bbox .center .copy () .add (axes [1]),
bbox .center .copy () .subtract (axes [1]),
bbox .center .copy () .add (axes [2]),
bbox .center .copy () .subtract (axes [2]),
],
aAxes = [
axes [0],
axes [0] .copy () .negate (),
axes [1],
axes [1] .copy () .negate (),
axes [2],
axes [2] .copy () .negate (),
],
aNormals = [
normals [0],
normals [0] .copy () .negate (),
normals [1],
normals [1] .copy () .negate (),
normals [2],
normals [2] .copy () .negate (),
];
const [snapTranslation, aAxis] = this .getSnapTranslation (absolutePosition, aCenters, aAxes, aNormals, dynamicSnapDistance);
this .tool .snapped = snapTranslation .magnitude () > 0.0001;
if (snapTranslation .equals (X3D .Vector3 .Zero))
return undefined;
const
aBefore = aAxes [aAxis],
aAfter = aAxes [aAxis] .copy () .add (snapTranslation),
aDelta = aAfter .distance (aBefore),
aRatio = aAfter .magnitude () / aBefore .magnitude ();
if (Math .abs (aDelta) < MIN_DELTA || Math .abs (aRatio) < MIN_RATIO || isNaN (aRatio) || Math .abs (aRatio) === Infinity)
{
return undefined;
}
const
snapScale = new X3D .Vector3 (aRatio, aRatio, aRatio),
center = subAABBox .center,
snapMatrix = new X3D .Matrix4 () .set (null, null, snapScale, null, center);
snapMatrix .multRight (transformTool .getCurrentMatrix ());
return snapMatrix;
}
case "SCALE_FROM_OPPOSITE_HANDLE":
{
const
aIndices = SnapTarget .#adjacentFaces [handle],
aCenters = [
bbox .center .copy () .add (axes [0]),
bbox .center .copy () .subtract (axes [0]),
bbox .center .copy () .add (axes [1]),
bbox .center .copy () .subtract (axes [1]),
bbox .center .copy () .add (axes [2]),
bbox .center .copy () .subtract (axes [2]),
]
.filter ((v, i) => aIndices .includes (i)),
aAxes = [
axes [0],
axes [0] .copy () .negate (),
axes [1],
axes [1] .copy () .negate (),
axes [2],
axes [2] .copy () .negate (),
]
.filter ((v, i) => aIndices .includes (i)),
aNormals = [
normals [0],
normals [0] .copy () .negate (),
normals [1],
normals [1] .copy () .negate (),
normals [2],
normals [2] .copy () .negate (),
]
.filter ((v, i) => aIndices .includes (i));
const [snapTranslation, aAxis] = this .getSnapTranslation (absolutePosition, aCenters, aAxes, aNormals, dynamicSnapDistance);
this .tool .snapped = snapTranslation .magnitude () > 0.0001;
if (snapTranslation .equals (X3D .Vector3 .Zero))
return undefined;
const
aBefore = aAxes [aAxis] .copy () .multiply (2),
aAfter = aBefore .copy () .add (snapTranslation),
aDelta = aAfter .distance (aBefore),
aRatio = aAfter .magnitude () / aBefore .magnitude ();
if (Math .abs (aDelta) < MIN_DELTA || Math .abs (aRatio) < MIN_RATIO || isNaN (aRatio) || Math .abs (aRatio) === Infinity)
{
return undefined;
}
const
snapScale = new X3D .Vector3 (aRatio, aRatio, aRatio),
subAABBoxPoints = subAABBox .getPoints (SnapTarget .#points),
center = subAABBoxPoints [SnapTarget .#oppositePoints [handle]],
snapMatrix = new X3D .Matrix4 () .set (null, null, snapScale, null, center);
snapMatrix .multRight (transformTool .getCurrentMatrix ());
return snapMatrix;
}
}
}
static #connectedAxes = {
"X": 0,
"Y": 1,
"Z": 2,
};
getConnectedAxes (transformTool, axis, scale)
{
for (const connectedAxis of transformTool .tool .connectedAxes)
{
const
lhs = SnapTarget .#connectedAxes [connectedAxis [0]],
rhs = SnapTarget .#connectedAxes [connectedAxis [1]];
if (rhs === axis)
scale [lhs] = scale [rhs];
}
return scale;
}
getDynamicSnapDistance ()
{
const
executionContext = this .tool .getValue () .getBody (),
sphereNode = executionContext .getNamedNode ("Sphere") .getValue (),
vectorNode = executionContext .getNamedNode ("Vector") .getValue (),
bbox = sphereNode .getBBox (new X3D .Box3 ()) .multRight (vectorNode .getMatrix ()),
dynamicSnapDistance = Math .max (... bbox .size) / 2;
return dynamicSnapDistance;
}
}
module .exports = SnapTarget;