@needle-tools/engine 5.1.4 → 5.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/{needle-engine.bundle-CEba8eTv.js → needle-engine.bundle-CxsZfV6C.js} +5245 -5166
- package/dist/{needle-engine.bundle-BvxvjlSN.min.js → needle-engine.bundle-DUb8msPR.min.js} +140 -140
- package/dist/{needle-engine.bundle-GUaSFX8u.umd.cjs → needle-engine.bundle-D_h1sDs-.umd.cjs} +137 -137
- package/dist/needle-engine.d.ts +39 -30
- package/dist/needle-engine.js +242 -242
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/api.d.ts +1 -1
- package/lib/engine/api.js +1 -1
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/debug/debug_console.js +39 -17
- package/lib/engine/debug/debug_console.js.map +1 -1
- package/lib/engine/debug/debug_spatial_console.js +14 -0
- package/lib/engine/debug/debug_spatial_console.js.map +1 -1
- package/lib/engine/engine_init.js +2 -2
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_license.d.ts +7 -7
- package/lib/engine/engine_license.js +71 -71
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_networking_blob.js +3 -3
- package/lib/engine/engine_utils_qrcode.js +2 -2
- package/lib/engine/engine_utils_screenshot.js +1 -0
- package/lib/engine/engine_utils_screenshot.js.map +1 -1
- package/lib/engine/engine_utils_screenshot.xr.js +1 -1
- package/lib/engine/engine_utils_screenshot.xr.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
- package/lib/engine/webcomponents/needle menu/needle-menu.js +18 -5
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.ar-overlay.js +33 -7
- package/lib/engine/webcomponents/needle-engine.ar-overlay.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +2 -2
- package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine/xr/NeedleXRSession.js +10 -4
- package/lib/engine/xr/NeedleXRSession.js.map +1 -1
- package/lib/engine/xr/NeedleXRSync.js +47 -47
- package/lib/engine/xr/NeedleXRSync.js.map +1 -1
- package/lib/engine/xr/TempXRContext.js +2 -2
- package/lib/engine-components/DragControls.d.ts +9 -0
- package/lib/engine-components/DragControls.js +174 -73
- package/lib/engine-components/DragControls.js.map +1 -1
- package/lib/engine-components/DragControlsConstraints.d.ts +11 -2
- package/lib/engine-components/DragControlsConstraints.js +18 -11
- package/lib/engine-components/DragControlsConstraints.js.map +1 -1
- package/lib/engine-components/OrbitControls.js +32 -6
- package/lib/engine-components/OrbitControls.js.map +1 -1
- package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
- package/package.json +1 -1
- package/plugins/common/license.js +4 -4
- package/plugins/vite/license.js +4 -4
- package/src/engine/api.ts +1 -1
- package/src/engine/debug/debug_console.ts +33 -17
- package/src/engine/debug/debug_spatial_console.ts +15 -0
- package/src/engine/engine_init.ts +2 -2
- package/src/engine/engine_license.ts +68 -68
- package/src/engine/engine_networking_blob.ts +3 -3
- package/src/engine/engine_utils_qrcode.ts +2 -2
- package/src/engine/engine_utils_screenshot.ts +1 -0
- package/src/engine/engine_utils_screenshot.xr.ts +1 -1
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu.ts +19 -5
- package/src/engine/webcomponents/needle-engine.ar-overlay.ts +33 -5
- package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
- package/src/engine/webcomponents/needle-engine.ts +2 -2
- package/src/engine/xr/NeedleXRSession.ts +10 -3
- package/src/engine/xr/NeedleXRSync.ts +47 -50
- package/src/engine/xr/TempXRContext.ts +2 -2
- package/src/engine-components/DragControls.ts +189 -80
- package/src/engine-components/DragControlsConstraints.ts +20 -12
- package/src/engine-components/OrbitControls.ts +31 -4
- package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
|
@@ -585,6 +585,13 @@ __decorate([
|
|
|
585
585
|
], DragControls.prototype, "dragEnded", void 0);
|
|
586
586
|
/** Scratch quaternion for {@link MultiTouchDragHandler}'s per-frame delta rotation. */
|
|
587
587
|
const _mtRotDelta = new Quaternion();
|
|
588
|
+
/** Scratch vectors for the on-screen twist → yaw mapping (view-plane twist path). */
|
|
589
|
+
const _mtCross = new Vector3();
|
|
590
|
+
const _mtToEye = new Vector3();
|
|
591
|
+
const _mtCamOffset = new Vector3();
|
|
592
|
+
/** Scratch vectors for aiming the SnapToSurfaceConstraint ray in MultiTouchDragHandler.onDragUpdate. */
|
|
593
|
+
const _mtSnapRayOrigin = new Vector3();
|
|
594
|
+
const _mtSnapRayDir = new Vector3();
|
|
588
595
|
// #region MultiTouchDragHandler
|
|
589
596
|
/**
|
|
590
597
|
* Handles two touch points affecting one object.
|
|
@@ -606,6 +613,9 @@ class MultiTouchDragHandler {
|
|
|
606
613
|
_keepRotationConstraint = new KeepRotationConstraint();
|
|
607
614
|
/** GrabPointPlaneConstraint returned by the active strategy (if any); used to sync snapResolution. */
|
|
608
615
|
_planeConstraint = null;
|
|
616
|
+
/** SnapToSurfaceConstraint returned by the active strategy (SnapToSurfaces mode); its snap ray
|
|
617
|
+
* is re-aimed every frame in onDragUpdate so it targets what the fingers point at. */
|
|
618
|
+
_snapToSurfaceConstraint = null;
|
|
609
619
|
_activeConstraints = [];
|
|
610
620
|
constructor(dragControls, gameObject, pointerA, pointerB) {
|
|
611
621
|
this.context = dragControls.context;
|
|
@@ -689,6 +699,7 @@ class MultiTouchDragHandler {
|
|
|
689
699
|
};
|
|
690
700
|
const strategyConstraints = this.handlerA.currentStrategy.getConstraints?.(constraintCx) ?? [];
|
|
691
701
|
this._planeConstraint = strategyConstraints.find(c => c instanceof GrabPointPlaneConstraint) ?? null;
|
|
702
|
+
this._snapToSurfaceConstraint = strategyConstraints.find(c => c instanceof SnapToSurfaceConstraint) ?? null;
|
|
692
703
|
const hasStrategyConstraints = strategyConstraints.length > 0;
|
|
693
704
|
this._activeConstraints = [
|
|
694
705
|
...strategyConstraints,
|
|
@@ -699,6 +710,12 @@ class MultiTouchDragHandler {
|
|
|
699
710
|
this._activeConstraints.push(this._keepRotationConstraint);
|
|
700
711
|
}
|
|
701
712
|
this._scaleLimitConstraint.init(constraintCx);
|
|
713
|
+
// Yaw-locked strategies (XZPlane) expose their world-space rotation axis. For screen
|
|
714
|
+
// two-finger input we drive an intuitive on-screen twist → yaw mapping around it (see
|
|
715
|
+
// alignManipulator). XR keeps the free 3D-delta path for now.
|
|
716
|
+
this._yawConstraintAxis = !isSpatialInput(this._deviceMode)
|
|
717
|
+
? (this.handlerA.currentStrategy.yawConstraintAxis ?? null)
|
|
718
|
+
: null;
|
|
702
719
|
// align _manipulatorObject in the same way it would if this was a drag update
|
|
703
720
|
this.alignManipulator();
|
|
704
721
|
// and then parent it to the space object so it follows along.
|
|
@@ -713,7 +730,15 @@ class MultiTouchDragHandler {
|
|
|
713
730
|
return;
|
|
714
731
|
}
|
|
715
732
|
// we want to initialize the drag points for these handlers again.
|
|
716
|
-
// one of them will be removed, but we don't know here which one
|
|
733
|
+
// one of them will be removed, but we don't know here which one.
|
|
734
|
+
// Re-derive each handler's grab point from its current pointer ray BEFORE recentering:
|
|
735
|
+
// during the two-pointer drag the object moved relative to each pointer, so the grab
|
|
736
|
+
// point captured at pointer-down is stale — without the rebase the surviving handler
|
|
737
|
+
// would yank the object until that old grab point is back under the pointer (visible
|
|
738
|
+
// jump on release). recenter() then re-captures start transforms and re-initializes
|
|
739
|
+
// constraints using the rebased grab point.
|
|
740
|
+
this.handlerA.rebaseGrabPointToPointer();
|
|
741
|
+
this.handlerB.rebaseGrabPointToPointer();
|
|
717
742
|
this.handlerA.recenter();
|
|
718
743
|
this.handlerB.recenter();
|
|
719
744
|
// destroy helper objects
|
|
@@ -742,6 +767,15 @@ class MultiTouchDragHandler {
|
|
|
742
767
|
_currABDir = new Vector3();
|
|
743
768
|
/** True only for the very first `alignManipulator` call; triggers the one-time lookAt seed. */
|
|
744
769
|
_isFirstAlignFrame = true;
|
|
770
|
+
/** World-space yaw axis when the active strategy locks rotation to a single axis (XZPlane),
|
|
771
|
+
* for screen input only. Null ⇒ use the free 3D-delta rotation path. Set in onDragStart. */
|
|
772
|
+
_yawConstraintAxis = null;
|
|
773
|
+
/** Camera forward captured while computing the view-plane twist (scratch). */
|
|
774
|
+
_viewForward = new Vector3();
|
|
775
|
+
/** A→B projected into the view plane on the previous frame; reference for the twist delta. */
|
|
776
|
+
_prevViewTwistDir = new Vector3();
|
|
777
|
+
/** A→B projected into the view plane on the current frame (scratch). */
|
|
778
|
+
_currViewTwistDir = new Vector3();
|
|
745
779
|
alignManipulator() {
|
|
746
780
|
if (!this.handlerA || !this.handlerB) {
|
|
747
781
|
console.error("alignManipulator called on MultiTouchDragHandler without valid handlers. This is likely a bug.", this);
|
|
@@ -763,34 +797,41 @@ class MultiTouchDragHandler {
|
|
|
763
797
|
// separately and apply it directly to the dragged object in onDragUpdate.
|
|
764
798
|
const dist = this._tempVec1.distanceTo(this._tempVec2);
|
|
765
799
|
this._currentScaleRatio = dist / this._initialDistance;
|
|
766
|
-
// Rotation:
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
//
|
|
800
|
+
// Rotation: accumulate a per-frame delta so translation never contaminates rotation.
|
|
801
|
+
// Two paths:
|
|
802
|
+
// - Yaw-locked screen drag (XZPlane, _yawConstraintAxis set): map the on-screen finger
|
|
803
|
+
// twist directly to yaw around the constraint axis, so the gesture stays usable from
|
|
804
|
+
// any view angle — a side-on view no longer collapses it. See _applyViewPlaneTwist.
|
|
805
|
+
// - Free rotation (XR / other modes): accumulate the full 3D delta of the A→B direction
|
|
806
|
+
// via setFromUnitVectors (camera tilt has no effect after the initial seed).
|
|
772
807
|
this._currABDir.subVectors(this._tempVec2, this._tempVec1);
|
|
773
808
|
if (this._currABDir.lengthSq() > 1e-10) {
|
|
774
|
-
this._currABDir.normalize();
|
|
775
809
|
if (this._isFirstAlignFrame) {
|
|
776
|
-
// Seed the
|
|
777
|
-
//
|
|
778
|
-
//
|
|
810
|
+
// Seed the manipulator orientation once. Only the manipulator's *change* since
|
|
811
|
+
// this frame drives the object (the follow-object offset captured in onDragStart
|
|
812
|
+
// absorbs the absolute seed), so the seed value itself is arbitrary — lookAt just
|
|
813
|
+
// gives a well-defined starting frame. Seed both rotation-source histories too.
|
|
779
814
|
const camera = this.context.mainCamera;
|
|
780
815
|
this.tempLookMatrix.lookAt(this._tempVec3, this._tempVec2, camera.worldUp);
|
|
781
816
|
this._manipulatorObject.quaternion.setFromRotationMatrix(this.tempLookMatrix);
|
|
782
817
|
this._isFirstAlignFrame = false;
|
|
818
|
+
this._prevABDirection.copy(this._currABDir).normalize();
|
|
819
|
+
this._computeViewPlaneAB(this._prevViewTwistDir);
|
|
820
|
+
}
|
|
821
|
+
else if (this._yawConstraintAxis) {
|
|
822
|
+
this._applyViewPlaneTwist(this._yawConstraintAxis);
|
|
783
823
|
}
|
|
784
824
|
else {
|
|
785
825
|
// Accumulate the rotation delta from previous A→B to current A→B.
|
|
786
826
|
// Guard against the degenerate ~180° flip case where setFromUnitVectors
|
|
787
827
|
// is undefined (dot ≈ -1).
|
|
828
|
+
this._currABDir.normalize();
|
|
788
829
|
if (this._prevABDirection.dot(this._currABDir) > -0.9999) {
|
|
789
830
|
_mtRotDelta.setFromUnitVectors(this._prevABDirection, this._currABDir);
|
|
790
831
|
this._manipulatorObject.quaternion.premultiply(_mtRotDelta);
|
|
791
832
|
}
|
|
833
|
+
this._prevABDirection.copy(this._currABDir);
|
|
792
834
|
}
|
|
793
|
-
this._prevABDirection.copy(this._currABDir);
|
|
794
835
|
}
|
|
795
836
|
this._manipulatorObject.updateMatrix();
|
|
796
837
|
this._manipulatorObject.updateMatrixWorld(true);
|
|
@@ -801,6 +842,49 @@ class MultiTouchDragHandler {
|
|
|
801
842
|
// Gizmos.DrawWireSphere(wp, this._initialScale.length() * dist / this._initialDistance, 0x00ff00, 0, false);
|
|
802
843
|
}
|
|
803
844
|
}
|
|
845
|
+
/** Projects the current world-space A→B direction (_currABDir) onto the plane perpendicular
|
|
846
|
+
* to the camera forward — i.e. the grab line as it appears on screen — into `out`.
|
|
847
|
+
* Also refreshes _viewForward. */
|
|
848
|
+
_computeViewPlaneAB(out) {
|
|
849
|
+
const camera = this.context.mainCamera;
|
|
850
|
+
this._viewForward.copy(camera.worldForward);
|
|
851
|
+
out.copy(this._currABDir);
|
|
852
|
+
out.addScaledVector(this._viewForward, -out.dot(this._viewForward));
|
|
853
|
+
}
|
|
854
|
+
/** Maps the frame-to-frame on-screen twist of the two grab points to a yaw delta around
|
|
855
|
+
* `axis` and premultiplies it onto the manipulator. Decouples gesture magnitude (screen
|
|
856
|
+
* twist) from the applied axis (plane normal), so yaw stays controllable from any view. */
|
|
857
|
+
_applyViewPlaneTwist(axis) {
|
|
858
|
+
this._computeViewPlaneAB(this._currViewTwistDir);
|
|
859
|
+
const prev = this._prevViewTwistDir;
|
|
860
|
+
const curr = this._currViewTwistDir;
|
|
861
|
+
const eps = 1e-8;
|
|
862
|
+
// Grab line points into the screen (fingers overlap) — no usable twist this frame.
|
|
863
|
+
if (curr.lengthSq() < eps)
|
|
864
|
+
return;
|
|
865
|
+
if (prev.lengthSq() >= eps) {
|
|
866
|
+
// Signed twist of the grab line in the view plane, measured about the axis pointing
|
|
867
|
+
// toward the camera so a CCW-on-screen twist is positive.
|
|
868
|
+
_mtToEye.copy(this._viewForward).multiplyScalar(-1);
|
|
869
|
+
_mtCross.crossVectors(prev, curr);
|
|
870
|
+
const screenTwist = Math.atan2(_mtCross.dot(_mtToEye), prev.dot(curr));
|
|
871
|
+
// Sign by which side of the plane the camera sits on. This is stable through a
|
|
872
|
+
// side-on view (where "CCW on screen" is otherwise ill-defined) and correctly
|
|
873
|
+
// inverts the gesture when viewing the plane from below. It also agrees with the
|
|
874
|
+
// visual sense wherever that is well-defined, so tilting the view never flips it.
|
|
875
|
+
const camPos = this.context.mainCamera.worldPosition;
|
|
876
|
+
_mtCamOffset.copy(camPos).sub(this._tempVec3);
|
|
877
|
+
const side = axis.dot(_mtCamOffset) >= 0 ? -1 : 1;
|
|
878
|
+
const yaw = screenTwist * side;
|
|
879
|
+
if (yaw !== 0) {
|
|
880
|
+
_mtRotDelta.setFromAxisAngle(axis, yaw);
|
|
881
|
+
this._manipulatorObject.quaternion.premultiply(_mtRotDelta);
|
|
882
|
+
}
|
|
883
|
+
if (debug)
|
|
884
|
+
Gizmos.DrawLabel(this._tempVec3, `twist ${yaw.toFixed(3)}`, 0.03);
|
|
885
|
+
}
|
|
886
|
+
prev.copy(curr);
|
|
887
|
+
}
|
|
804
888
|
onDragUpdate() {
|
|
805
889
|
// At this point we've run both the other handlers, but their effects have been suppressed because they can't handle
|
|
806
890
|
// two events at the same time. They're basically providing us with two Object3D's and we can combine these here
|
|
@@ -840,6 +924,23 @@ class MultiTouchDragHandler {
|
|
|
840
924
|
// scale-aware constraints (e.g. XZPlaneDragStrategy's bounds-bottom height lock).
|
|
841
925
|
// Pass 1 when keepScale is true so the correction is a no-op.
|
|
842
926
|
this.handlerA.currentStrategy.onTwoPointerScaleUpdate?.(keepScale ? 1 : this._currentScaleRatio);
|
|
927
|
+
// SnapToSurfaces: aim the constraint's snap ray from the pointer origins through the
|
|
928
|
+
// current grab midpoint, so the two-finger snap targets what the fingers point at —
|
|
929
|
+
// the same semantics as the single-pointer surface raycast (which uses the pointer ray).
|
|
930
|
+
if (this._snapToSurfaceConstraint) {
|
|
931
|
+
const spaceA = this.handlerA.followObject.parent;
|
|
932
|
+
const spaceB = this.handlerB.followObject.parent;
|
|
933
|
+
if (spaceA && spaceB) {
|
|
934
|
+
spaceA.getWorldPosition(_mtSnapRayOrigin);
|
|
935
|
+
_mtSnapRayOrigin.add(spaceB.getWorldPosition(_mtSnapRayDir)).multiplyScalar(0.5);
|
|
936
|
+
// _manipulatorObject is a scene child positioned at the world-space midpoint of
|
|
937
|
+
// the two grab points (set in alignManipulator earlier this frame).
|
|
938
|
+
_mtSnapRayDir.copy(this._manipulatorObject.position).sub(_mtSnapRayOrigin);
|
|
939
|
+
if (_mtSnapRayDir.lengthSq() > 1e-10) {
|
|
940
|
+
this._snapToSurfaceConstraint.setRay(_mtSnapRayOrigin, _mtSnapRayDir.normalize());
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
843
944
|
applyFollowObjectConstraints(this._followObject, this._activeConstraints);
|
|
844
945
|
// TODO should use unscaled time here // some test for lerp speed depending on distance
|
|
845
946
|
const t = Mathf.clamp01(this.context.time.deltaTime * lerpStrength * lerpFactor); // / (currentDist - 1 + 0.01));
|
|
@@ -877,9 +978,6 @@ class SnapToSurfacesDragStrategy {
|
|
|
877
978
|
_lastSurfacePlaneRefreshTime = 0;
|
|
878
979
|
_hasLastSurfaceHitPoint = false;
|
|
879
980
|
_lastSurfaceHitPoint = new Vector3();
|
|
880
|
-
/** Original grab point (object local space) captured at drag start. Used to preserve the
|
|
881
|
-
* surface-plane component of the grab offset when snapping to surfaces. */
|
|
882
|
-
_originalHitPointInLocalSpace = new Vector3();
|
|
883
981
|
/** Context stored at initialize() time so getConstraints() can pass it to the multi-touch constraint. */
|
|
884
982
|
_context = null;
|
|
885
983
|
reset() {
|
|
@@ -889,17 +987,12 @@ class SnapToSurfacesDragStrategy {
|
|
|
889
987
|
this._draggedOverObjectLastNormal.set(0, 0, 0);
|
|
890
988
|
this._lastSurfacePlaneRefreshTime = 0;
|
|
891
989
|
this._hasLastSurfaceHitPoint = false;
|
|
892
|
-
this._originalHitPointInLocalSpace.set(0, 0, 0);
|
|
893
990
|
}
|
|
894
991
|
requiresPlaneIntersection = true;
|
|
895
992
|
initialize(cx, hitWP, _rayDirection) {
|
|
896
993
|
this._context = cx.context;
|
|
897
994
|
cx.setPlaneViewAligned(hitWP, false);
|
|
898
995
|
this.reset();
|
|
899
|
-
// Capture the exact point the user grabbed (in object local space). This is used
|
|
900
|
-
// to preserve the surface-plane component of the grab offset while only adjusting
|
|
901
|
-
// the normal-direction component to rest the object on the target surface.
|
|
902
|
-
this._originalHitPointInLocalSpace.copy(cx.hitPointInLocalSpace);
|
|
903
996
|
}
|
|
904
997
|
update(cx, ray, dragSource, draggedObject) {
|
|
905
998
|
const didHaveSurfaceHitPointLastFrame = this._hasLastSurfaceHitPoint;
|
|
@@ -928,69 +1021,52 @@ class SnapToSurfacesDragStrategy {
|
|
|
928
1021
|
didHit = true;
|
|
929
1022
|
this._hasLastSurfaceHitPoint = true;
|
|
930
1023
|
this._lastSurfaceHitPoint.copy(hit.point);
|
|
931
|
-
const dragTimeThreshold = 0.15;
|
|
932
|
-
const dragTimeSatisfied = this._draggedOverObjectDuration >= dragTimeThreshold;
|
|
933
|
-
const dragDistance = 0.001;
|
|
934
|
-
const dragDistanceSatisfied = cx.totalMovement.length() >= dragDistance;
|
|
935
1024
|
// TODO: if the "hit.normal" is undefined we use the hit.face.normal which is still localspace
|
|
936
1025
|
const worldNormal = getTempVector(hit.normal || hit.face.normal).applyQuaternion(hit.object.worldQuaternion);
|
|
937
|
-
//
|
|
938
|
-
|
|
1026
|
+
// Wait until the drag has actually started (enough hover time or pointer movement)
|
|
1027
|
+
// before snapping to the surface, so a click that never becomes a drag doesn't move
|
|
1028
|
+
// the object. Bail out BEFORE touching any snap state: the surface/normal bookkeeping
|
|
1029
|
+
// below must only advance once a real drag is underway, otherwise the change-detection
|
|
1030
|
+
// trigger is consumed on a pre-threshold frame and the anchor/plane refresh is skipped
|
|
1031
|
+
// on the first frame that actually moves the object (leaving it on the stale
|
|
1032
|
+
// view-aligned plane until the 1 s periodic refresh).
|
|
1033
|
+
const dragTimeSatisfied = this._draggedOverObjectDuration >= 0.15;
|
|
1034
|
+
const dragDistanceSatisfied = cx.totalMovement.length() >= 0.001;
|
|
1035
|
+
if (!(dragTimeSatisfied || dragDistanceSatisfied)) {
|
|
1036
|
+
return null; // abort frame update — drag hasn't really started yet
|
|
1037
|
+
}
|
|
1038
|
+
// Refresh the anchor + drag plane when the surface object or its normal changes, and
|
|
1039
|
+
// periodically on flat same-normal surfaces (e.g. stepping across multi-level floors).
|
|
1040
|
+
// _draggedOverObjectLastNormal is stored in WORLD space to match worldNormal — it used
|
|
1041
|
+
// to store the local face normal, so on any rotated surface the dot never reached 1 and
|
|
1042
|
+
// this fired every frame.
|
|
1043
|
+
const surfaceChanged = this._draggedOverObjectLastSetUp !== this._draggedOverObject
|
|
939
1044
|
|| this._draggedOverObjectLastNormal.dot(worldNormal) < 0.999999;
|
|
940
|
-
|
|
941
|
-
const needsPlaneRefresh = needsAnchorUpdate
|
|
1045
|
+
const needsRefresh = surfaceChanged
|
|
942
1046
|
|| (cx.context.time.time - this._lastSurfacePlaneRefreshTime) >= 1.0;
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
// The anchor preserves the original grab point's surface-plane components; only the
|
|
946
|
-
// normal-direction component is adjusted so the object rests flush on the surface.
|
|
947
|
-
if (needsAnchorUpdate) {
|
|
1047
|
+
if (needsRefresh) {
|
|
1048
|
+
this._lastSurfacePlaneRefreshTime = cx.context.time.time;
|
|
948
1049
|
this._draggedOverObjectLastSetUp = this._draggedOverObject;
|
|
949
|
-
this._draggedOverObjectLastNormal.copy(
|
|
1050
|
+
this._draggedOverObjectLastNormal.copy(worldNormal);
|
|
1051
|
+
// Snap the OBJECT ORIGIN under the cursor: the origin's lateral position follows the
|
|
1052
|
+
// pointer→surface hit while the object's contact face rests on the surface. Where the
|
|
1053
|
+
// user grabbed is ignored, so placement is consistent regardless of grab point.
|
|
1054
|
+
// The origin is (0,0,0) in local space, so origin·normal = 0 and the anchor collapses
|
|
1055
|
+
// to the contact face projected onto the normal — the same anchor the multi-touch
|
|
1056
|
+
// SnapToSurfaceConstraint uses, so single- and two-pointer snapping now match.
|
|
950
1057
|
const center = getTempVector();
|
|
951
1058
|
const size = getTempVector();
|
|
952
1059
|
cx.bounds.getCenter(center);
|
|
953
1060
|
cx.bounds.getSize(size);
|
|
954
|
-
//
|
|
1061
|
+
// Contact face of the bounds along the surface normal (the face that rests on it).
|
|
1062
|
+
// NOTE: bounds are local/axis-aligned, so this assumes the object is upright w.r.t. the
|
|
1063
|
+
// surface normal — exact for the common keepRotation case, approximate if tilted.
|
|
955
1064
|
center.sub(size.multiplyScalar(0.5).multiply(worldNormal));
|
|
956
|
-
// Scalar projection of contact point onto the surface normal.
|
|
957
1065
|
const contactAlongNormal = center.dot(worldNormal);
|
|
958
|
-
|
|
959
|
-
const grabAlongNormal = this._originalHitPointInLocalSpace.dot(worldNormal);
|
|
960
|
-
// Build the new anchor: keep the grab point's surface-plane offset, shift only
|
|
961
|
-
// its normal component to match the contact surface (object rests on surface).
|
|
962
|
-
cx.hitPointInLocalSpace
|
|
963
|
-
.copy(this._originalHitPointInLocalSpace)
|
|
964
|
-
.addScaledVector(worldNormal, contactAlongNormal - grabAlongNormal);
|
|
1066
|
+
cx.hitPointInLocalSpace.copy(worldNormal).multiplyScalar(contactAlongNormal);
|
|
965
1067
|
cx.hitNormalInLocalSpace.copy(hit.face.normal);
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
// object's position, wait until there's been enough movement or time.
|
|
969
|
-
// The anchor is already set correctly above so movement will begin without a jump.
|
|
970
|
-
if (!(dragTimeSatisfied || dragDistanceSatisfied)) {
|
|
971
|
-
return null; // abort frame update
|
|
972
|
-
}
|
|
973
|
-
// Update the drag plane when the surface/normal changes or periodically.
|
|
974
|
-
if (needsPlaneRefresh) {
|
|
975
|
-
this._lastSurfacePlaneRefreshTime = cx.context.time.time;
|
|
976
|
-
// ensure plane is far enough up that we don't drag into the surface
|
|
977
|
-
// Which offset we use here depends on the face normal direction we hit
|
|
978
|
-
// If we hit the bottom, we want to use the top, and vice versa
|
|
979
|
-
// To do this dynamically, we can find the intersection between our local bounds and the hit face normal (which is already in local space)
|
|
980
|
-
const center = getTempVector();
|
|
981
|
-
const size = getTempVector();
|
|
982
|
-
cx.bounds.getCenter(center);
|
|
983
|
-
cx.bounds.getSize(size);
|
|
984
|
-
center.add(size.multiplyScalar(0.5).multiply(hit.face.normal));
|
|
985
|
-
const offset = getTempVector(cx.hitPointInLocalSpace).add(center);
|
|
986
|
-
cx.followObject.localToWorld(offset);
|
|
987
|
-
// See https://linear.app/needle/issue/NE-5004
|
|
988
|
-
// const offsetWP = this._followObject.worldPosition.sub(offset);
|
|
989
|
-
const point = hit.point; //.sub(offsetWP);
|
|
990
|
-
// Gizmos.DrawWireSphere(point, 2, 0xff0000, .3);
|
|
991
|
-
// Gizmos.DrawDirection(point, worldNormal, 0xffff00, 1);
|
|
992
|
-
// console.log(hit.normal)
|
|
993
|
-
cx.dragPlane.setFromNormalAndCoplanarPoint(worldNormal, point);
|
|
1068
|
+
// Drag plane sits on the surface; the origin-anchor lands on the pointer→surface hit.
|
|
1069
|
+
cx.dragPlane.setFromNormalAndCoplanarPoint(worldNormal, hit.point);
|
|
994
1070
|
}
|
|
995
1071
|
}
|
|
996
1072
|
}
|
|
@@ -1003,8 +1079,9 @@ class SnapToSurfacesDragStrategy {
|
|
|
1003
1079
|
getConstraints(ctx) {
|
|
1004
1080
|
// Single-pointer (boundsAtScaleOne === null): the existing update() + plane-intersection
|
|
1005
1081
|
// path handles surface snapping; no constraint needed here.
|
|
1006
|
-
// Multi-touch (boundsAtScaleOne !== null): inject a constraint that
|
|
1007
|
-
//
|
|
1082
|
+
// Multi-touch (boundsAtScaleOne !== null): inject a constraint that snaps the follow
|
|
1083
|
+
// object onto the surface the fingers point at. MultiTouchDragHandler aims its ray
|
|
1084
|
+
// (pointer origins → grab midpoint) every frame, mirroring the single-pointer raycast.
|
|
1008
1085
|
if (!ctx.boundsAtScaleOne || !this._context)
|
|
1009
1086
|
return [];
|
|
1010
1087
|
const c = new SnapToSurfaceConstraint(this._context);
|
|
@@ -1030,6 +1107,8 @@ class XZPlaneDragStrategy {
|
|
|
1030
1107
|
requiresPlaneIntersection = true;
|
|
1031
1108
|
/** The fixed XZ plane established at drag start. Used as the clamping target. */
|
|
1032
1109
|
xzPlane = new Plane();
|
|
1110
|
+
/** Two-finger twist maps to yaw around this axis — the plane normal (parent's world up). */
|
|
1111
|
+
get yawConstraintAxis() { return this.xzPlane.normal; }
|
|
1033
1112
|
_inFallback = false;
|
|
1034
1113
|
/** Stable world-space point on xzPlane, captured once when entering fallback mode. */
|
|
1035
1114
|
_fallbackAnchor = new Vector3();
|
|
@@ -1234,6 +1313,28 @@ class DragPointerHandler {
|
|
|
1234
1313
|
this._gridSnapConstraint = new GridSnapConstraint(0);
|
|
1235
1314
|
this._keepRotationConstraint = new KeepRotationConstraint();
|
|
1236
1315
|
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Re-derives the grab point from the current pointer ray ∩ drag plane so that when a
|
|
1318
|
+
* two-pointer drag hands control back to this single pointer, the object keeps its current
|
|
1319
|
+
* offset to the pointer instead of snapping the original (now stale) grab point back under it.
|
|
1320
|
+
* Only needed for plane-intersection strategies — Attached-style modes re-base fully via
|
|
1321
|
+
* {@link recenter}. Call BEFORE {@link recenter} so the recomputed grab-start distance and
|
|
1322
|
+
* the constraint re-initialization use the rebased grab point.
|
|
1323
|
+
*/
|
|
1324
|
+
rebaseGrabPointToPointer() {
|
|
1325
|
+
if (!this._currentStrategy.requiresPlaneIntersection)
|
|
1326
|
+
return;
|
|
1327
|
+
if (!this.gameObject || !this._followObject.parent)
|
|
1328
|
+
return;
|
|
1329
|
+
const dragSource = this._followObject.parent;
|
|
1330
|
+
const ray = new Ray(dragSource.worldPosition, dragSource.worldForward);
|
|
1331
|
+
if (!ray.intersectPlane(this._dragPlane, this._tempVec))
|
|
1332
|
+
return;
|
|
1333
|
+
// Keep the new grab point on the constraint plane (it differs from _dragPlane in the
|
|
1334
|
+
// XZPlane side-view fallback) so GrabPointPlaneConstraint doesn't add a one-time offset.
|
|
1335
|
+
this._grabPointPlaneConstraint?.plane.projectPoint(this._tempVec, this._tempVec);
|
|
1336
|
+
this._hitPointInLocalSpace.copy(this.gameObject.worldToLocal(this._tempVec));
|
|
1337
|
+
}
|
|
1237
1338
|
recenter() {
|
|
1238
1339
|
if (!this._followObject.parent) {
|
|
1239
1340
|
console.warn("Error: space follow object doesn't have parent but recenter() is called. This is likely a bug");
|