BBC's guide to development
  • General

    • About
    • Tools
    • Git(hub)
    • Showpad
    • Hosting
    • Maintenance
    • Security
    • Go live checklist
  • Front-end development

    • Bundlers
    • CSS/SCSS
    • Javascript
    • Vue
    • PHP
    • Mails
    • Dev Faq
  • Functions
  • Mixins
  • General

    • OOP Structure
  • Component Classes

    • Accordion
    • App
    • Component
    • HighwayApp
    • Popup
    • PNG Sequencer
    • Tab
  • Manager Classes

    • BountListenerMgr
    • Cache
    • Configuration
    • InViewStateMgr
    • Instance Manager
    • Event dispatcher
  • Factories

    • SwiperFactory
  • PDF

    • AssetLoader
    • BasePdfDoc
    • TemplatePdfDoc
    • CustomPdfDoc
  • Utility functions

    • canvas
    • Connection Status
    • css
    • dev
    • placeholder
    • dom
    • fetch
    • json
    • object
    • scroll
    • scrollbar
    • spreadsheets
    • string
    • url
  • General

    • ComponentMgr
    • ThreeJsViewer
  • Components

    • ComponentMgr
    • GltfModel
    • Snappable
    • Socket
    • ThreeJsViewer
    • ThreeJsViewerCamera
  • Loaders

    • ConfigurationSerializer
    • GltfBlockParser
  • Utils

    • CanvasInputAdapter
    • CollisionManager
    • SocketGridExpander
    • blender
    • headless
  • General

    • Troubleshooting
    • Legacy
  • Components

    • AssetBar
    • ConfigGenerator
    • ShowpadApp
  • Managers

    • Assets
    • AppsDb
    • Config
  • Utils

    • Connection Status
    • general
    • showpad-interactive
    • showpad-upload
  • Components

    • Accordion
    • BackButton
    • Breadcrumb
    • ByltButton
    • Hamburger
    • Icon
    • Logo
    • Loader
    • Modal
    • Popup
    • Prompt
    • ProgressBar
    • TextLoader
  • Composables

    • useDebugMode
    • useConnectionStatus
  • Utils

    • dom
    • props
  • General

    • General
    • Tracking
  • Components

    • Accordion
    • ActionButton
    • AssetItem
    • AssetList
    • BackButton
    • ConfigGenButton
    • Logo
    • Media
    • Modal
    • Popup
    • Prompt
    • SPButton
    • SPRouterView
    • SPTrackedRouterLink
    • TextLoader
    • View
  • Composables

    • useConnectionStatus
  • Stores

    • useAppsDbStore
    • useBreadcrumbStore
    • useShowpadAPIStore
    • useShowpadSDKStore
    • useSpConfigStore
    • useSpStore
    • useSpTrackingStore
  • The New Kit

    • General
    • Installation & Usage
    • ACF Blocks
    • PHPCS
    • Functions
    • Vite
    • WP Config
    • Staging Deployment
  • Best Practices

    • Page Structure
    • Fonts/Typography
  • Todo
GitHub
  • General

    • About
    • Tools
    • Git(hub)
    • Showpad
    • Hosting
    • Maintenance
    • Security
    • Go live checklist
  • Front-end development

    • Bundlers
    • CSS/SCSS
    • Javascript
    • Vue
    • PHP
    • Mails
    • Dev Faq
  • Functions
  • Mixins
  • General

    • OOP Structure
  • Component Classes

    • Accordion
    • App
    • Component
    • HighwayApp
    • Popup
    • PNG Sequencer
    • Tab
  • Manager Classes

    • BountListenerMgr
    • Cache
    • Configuration
    • InViewStateMgr
    • Instance Manager
    • Event dispatcher
  • Factories

    • SwiperFactory
  • PDF

    • AssetLoader
    • BasePdfDoc
    • TemplatePdfDoc
    • CustomPdfDoc
  • Utility functions

    • canvas
    • Connection Status
    • css
    • dev
    • placeholder
    • dom
    • fetch
    • json
    • object
    • scroll
    • scrollbar
    • spreadsheets
    • string
    • url
  • General

    • ComponentMgr
    • ThreeJsViewer
  • Components

    • ComponentMgr
    • GltfModel
    • Snappable
    • Socket
    • ThreeJsViewer
    • ThreeJsViewerCamera
  • Loaders

    • ConfigurationSerializer
    • GltfBlockParser
  • Utils

    • CanvasInputAdapter
    • CollisionManager
    • SocketGridExpander
    • blender
    • headless
  • General

    • Troubleshooting
    • Legacy
  • Components

    • AssetBar
    • ConfigGenerator
    • ShowpadApp
  • Managers

    • Assets
    • AppsDb
    • Config
  • Utils

    • Connection Status
    • general
    • showpad-interactive
    • showpad-upload
  • Components

    • Accordion
    • BackButton
    • Breadcrumb
    • ByltButton
    • Hamburger
    • Icon
    • Logo
    • Loader
    • Modal
    • Popup
    • Prompt
    • ProgressBar
    • TextLoader
  • Composables

    • useDebugMode
    • useConnectionStatus
  • Utils

    • dom
    • props
  • General

    • General
    • Tracking
  • Components

    • Accordion
    • ActionButton
    • AssetItem
    • AssetList
    • BackButton
    • ConfigGenButton
    • Logo
    • Media
    • Modal
    • Popup
    • Prompt
    • SPButton
    • SPRouterView
    • SPTrackedRouterLink
    • TextLoader
    • View
  • Composables

    • useConnectionStatus
  • Stores

    • useAppsDbStore
    • useBreadcrumbStore
    • useShowpadAPIStore
    • useShowpadSDKStore
    • useSpConfigStore
    • useSpStore
    • useSpTrackingStore
  • The New Kit

    • General
    • Installation & Usage
    • ACF Blocks
    • PHPCS
    • Functions
    • Vite
    • WP Config
    • Staging Deployment
  • Best Practices

    • Page Structure
    • Fonts/Typography
  • Todo
GitHub
  • Snap Maths

Snap Maths

This page explains the mathematics behind calculateSnapMatrix and snapItemToSocket — the two functions that physically position one block against another.

The goal is to give you a mental model accurate enough to reason about edge cases, extend the system, or debug unexpected orientations. Basic familiarity with vectors, dot products, cross products, and 4×4 transformation matrices is assumed. Nothing beyond that.

The Goal

When a user clicks a socket, the configurator must move the incoming block (item) so that its chosen socket (itemSocket) presses face-to-face against the stationary socket (targetSocket). "Face-to-face" means:

  1. The two sockets end up at the same world position.
  2. The item socket's normal points directly opposite to the target socket's normal (they face each other).
  3. The up directions of both sockets align, so the block is not twisted around the snap axis.
  4. An optional rotation offset spins the block around the snap axis by a user-chosen amount.

All four constraints are encoded into a single THREE.Matrix4 that is applied to the Snappable.

Socket Coordinate Frames

Each Socket provides three pieces of data in its local space:

PropertyWhat it is
normalUnit vector pointing away from the block surface — the snap axis
upNormalUnit vector perpendicular to normal — the rotational reference direction
positionWorld position of the socket origin

These are stored in local space and must be transformed to world space before any calculation. For a vector v\mathbf{v}v and a world quaternion qqq, the world-space vector is:

vworld=q⋅v⋅q−1\mathbf{v}_{\text{world}} = q \cdot \mathbf{v} \cdot q^{-1} vworld​=q⋅v⋅q−1

In Three.js this is expressed as:

const targetNormal   = targetSocket.normal.clone().applyQuaternion(targetWorldQuat);
const targetUpNormal = targetSocket.upNormal.clone().applyQuaternion(targetWorldQuat);

After this step, both normal and upNormal are expressed in the same global coordinate system and can be compared and combined.

Building an Orthonormal Frame (Gram-Schmidt)

A coordinate frame is three mutually perpendicular unit vectors. For each socket the code builds one such frame from N\mathbf{N}N (normal) and U\mathbf{U}U (upNormal).

Step 1 — Remove the component of U\mathbf{U}U parallel to N\mathbf{N}N.

upNormal as authored is intended to be perpendicular to normal, but floating-point accumulation and imprecise authoring mean it rarely is exactly. The Gram-Schmidt projection removes the leakage:

U⊥=U−(U⋅N) N\mathbf{U}_{\perp} = \mathbf{U} - (\mathbf{U} \cdot \mathbf{N})\,\mathbf{N} U⊥​=U−(U⋅N)N

The scalar U⋅N\mathbf{U} \cdot \mathbf{N}U⋅N is the projection of U\mathbf{U}U onto N\mathbf{N}N — the amount of U\mathbf{U}U that "points in the same direction as N\mathbf{N}N". Subtracting that amount leaves only the part of U\mathbf{U}U that is truly perpendicular to N\mathbf{N}N.

tU.sub(tN.clone().multiplyScalar(tU.dot(tN)));

Step 2 — Compute the right vector via cross product.

R=U⊥×N\mathbf{R} = \mathbf{U}_{\perp} \times \mathbf{N} R=U⊥​×N

const tR = new THREE.Vector3().crossVectors(tU, tN).normalize();

The cross product of two perpendicular unit vectors produces a third unit vector perpendicular to both, completing a right-handed coordinate system. The three vectors {R, U⊥, N}\{\mathbf{R},\,\mathbf{U}_{\perp},\,\mathbf{N}\}{R,U⊥​,N} are now mutually orthogonal and all unit-length — an orthonormal frame.

Why is this needed? An orthonormal frame encodes a rotation unambiguously. The three vectors become the columns of a rotation matrix. If the vectors were not exactly perpendicular and unit-length, the matrix would contain shear or scale, and the resulting transformation would distort geometry.

Building the Rotation Matrices

With the three orthonormal vectors in hand, each socket's frame is encoded as a 4×4 rotation matrix with the frame axes as columns:

Mtarget=(RxUxNx0RyUyNy0RzUzNz00001)M_{\text{target}} = \begin{pmatrix} R_x & U_x & N_x & 0 \\ R_y & U_y & N_y & 0 \\ R_z & U_z & N_z & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} Mtarget​=​Rx​Ry​Rz​0​Ux​Uy​Uz​0​Nx​Ny​Nz​0​0001​​

const targetMat = new THREE.Matrix4().set(
    tR.x, tU.x, tN.x, 0,
    tR.y, tU.y, tN.y, 0,
    tR.z, tU.z, tN.z, 0,
    0,    0,    0,    1
);

The same is done for the item socket, producing MitemM_{\text{item}}Mitem​. Reading a rotation matrix by columns is a useful mental shorthand: the columns tell you where the local X, Y, Z axes end up in world space after the rotation is applied.

The Anti-Parallel Problem

The obvious approach — "just rotate the item's normal to point at the target" — uses setFromUnitVectors. This works for most directions but has a fatal edge case.

When two vectors are anti-parallel (a⋅b=−1\mathbf{a} \cdot \mathbf{b} = -1a⋅b=−1, pointing in exactly opposite directions), the rotation required to map one onto the other is 180°, but the rotation axis is undefined — it can be any vector perpendicular to both. setFromUnitVectors picks one arbitrarily, and that arbitrary choice introduces an unpredictable twist around the snap axis.

Sockets that face each other are always anti-parallel. This means the naive approach would randomly twist blocks every time.

The orthonormal frame approach sidesteps this entirely. Because both frames include an up direction, the 180° flip axis is fully determined:

any axis perpendicular to N⟶exactly one axis: the one that also aligns U\text{any axis perpendicular to } \mathbf{N} \longrightarrow \text{exactly one axis: the one that also aligns } \mathbf{U} any axis perpendicular to N⟶exactly one axis: the one that also aligns U

There is only one rotation that simultaneously anti-aligns the normals and aligns the up directions, and the frame matrices encode it directly.

The Face-to-Face Flip

Both frames are built from their actual (non-inverted) normals. At this point MtargetM_{\text{target}}Mtarget​ and MitemM_{\text{item}}Mitem​ describe frames with matching N\mathbf{N}N columns — they point in the same direction. To make the item socket face toward the target, the item's normal must be reversed.

This is done with a 180° rotation around the item socket's local up axis (Y in local frame before the frame matrix is applied):

qflip=axisAngle ⁣(y^, π)\mathbf{q}_{\text{flip}} = \text{axisAngle}\!\left(\hat{\mathbf{y}},\, \pi\right) qflip​=axisAngle(y^​,π)

Mflip=rotationMatrix(qflip)M_{\text{flip}} = \text{rotationMatrix}(\mathbf{q}_{\text{flip}}) Mflip​=rotationMatrix(qflip​)

const flipQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
const flipMat  = new THREE.Matrix4().makeRotationFromQuaternion(flipQuat);

The flip is applied in local space first, then the item's world frame is applied on top:

Mflipped=Mitem⋅MflipM_{\text{flipped}} = M_{\text{item}} \cdot M_{\text{flip}} Mflipped​=Mitem​⋅Mflip​

const flippedItemMat = new THREE.Matrix4().multiplyMatrices(itemMat, flipMat);

Note the multiplication order: MflipM_{\text{flip}}Mflip​ acts first (in local space), MitemM_{\text{item}}Mitem​ transforms the result to world space. A 180° rotation around local Y flips the Z axis (normal) while leaving the Y axis (up) unchanged — exactly the face-to-face semantics we want.

Aligning the Frames

Both frames are now in world space and the item frame has its normal flipped. The rotation that maps the item frame onto the target frame is:

Malign=Mtarget⋅Mflipped−1M_{\text{align}} = M_{\text{target}} \cdot M_{\text{flipped}}^{-1} Malign​=Mtarget​⋅Mflipped−1​

const flippedItemMatInv = flippedItemMat.clone().invert();
const alignmentMat      = new THREE.Matrix4().multiplyMatrices(targetMat, flippedItemMatInv);
const combinedQuat      = new THREE.Quaternion().setFromRotationMatrix(alignmentMat);

Why does this formula work?

Think of Mflipped−1M_{\text{flipped}}^{-1}Mflipped−1​ as the transformation from world space into the item socket's (flipped) frame. Composing it with MtargetM_{\text{target}}Mtarget​ gives the full path:

Mtarget⏟item frame→world⋅Mflipped−1⏟world→item frame\underbrace{M_{\text{target}}}_{\text{item frame} \to \text{world}} \cdot \underbrace{M_{\text{flipped}}^{-1}}_{\text{world} \to \text{item frame}} item frame→worldMtarget​​​⋅world→item frameMflipped−1​​​

The result is the single rotation that takes the item's (flipped) coordinate frame and lands it exactly on top of the target's coordinate frame.

Rotation Offset

The user can spin the block around the snap axis after alignment. The offset quaternion is built from the target socket's world normal N^target\hat{\mathbf{N}}_{\text{target}}N^target​:

qoffset=axisAngle ⁣(N^target, θ)\mathbf{q}_{\text{offset}} = \text{axisAngle}\!\left(\hat{\mathbf{N}}_{\text{target}},\, \theta\right) qoffset​=axisAngle(N^target​,θ)

const offsetQuat = new THREE.Quaternion().setFromAxisAngle(targetNormal.normalize(), rotationOffsetRadians);

It is applied by pre-multiplying the combined quaternion:

qfinal=qoffset⋅qalign\mathbf{q}_{\text{final}} = \mathbf{q}_{\text{offset}} \cdot \mathbf{q}_{\text{align}} qfinal​=qoffset​⋅qalign​

const finalQuat = new THREE.Quaternion().multiplyQuaternions(offsetQuat, combinedQuat);

Pre-multiplication means the offset acts in world space after the alignment is applied — the block is first oriented correctly relative to the socket, then spun around the snap axis. Post-multiplying would apply the spin in the block's local space before alignment, producing a different (and wrong) result.

Computing the Translation

Once the final rotation RRR is known, the block must be repositioned so that itemSocket lands at the same world position as targetSocket.

Step 1 — Rotate the item socket's current world position.

pitem′=R⋅pitem\mathbf{p}_{\text{item}}' = R \cdot \mathbf{p}_{\text{item}} pitem′​=R⋅pitem​

const rotatedItemPos = itemWorldPos.clone().applyMatrix4(rotationMatrix);

This computes where the item socket would end up after the rotation is applied, assuming the block pivots around the world origin.

Step 2 — Find the offset needed to bring it to ptarget\mathbf{p}_{\text{target}}ptarget​.

t=ptarget−pitem′\mathbf{t} = \mathbf{p}_{\text{target}} - \mathbf{p}_{\text{item}}' t=ptarget​−pitem′​

const translation = targetWorldPos.clone().sub(rotatedItemPos);

Why subtract pitem′\mathbf{p}_{\text{item}}'pitem′​ and not pitem\mathbf{p}_{\text{item}}pitem​?

The rotation is applied around the world origin. When the block rotates, its socket moves from pitem\mathbf{p}_{\text{item}}pitem​ to pitem′\mathbf{p}_{\text{item}}'pitem′​. The translation must compensate for where the socket ends up after rotation, not where it started.

Step 3 — Compose the final matrix.

Mfinal=T(t)⋅RM_{\text{final}} = T(\mathbf{t}) \cdot R Mfinal​=T(t)⋅R

const finalMatrix = new THREE.Matrix4().multiplyMatrices(translationMatrix, rotationMatrix);

In this composition, RRR acts first (rotating around the origin), then TTT moves the rotated block into final position. This is the standard T⋅RT \cdot RT⋅R decomposition for a transform that rotates then translates.

Why Reset to Origin First

calculateSnapMatrix assumes the Snappable is at the world origin before the matrix is applied. snapItemToSocket enforces this:

item.position.set(0, 0, 0);
item.quaternion.set(0, 0, 0, 1);
item.scale.set(1, 1, 1);
item._content.quaternion.set(0, 0, 0, 1); // clear content rotation too
item.updateMatrixWorld(true);

Why is this necessary? calculateSnapMatrix samples itemSocket.getWorldPosition() and itemSocket.getWorldQuaternion() to read the socket's current world-space data. If the Snappable is sitting somewhere else in the scene, those values would be offset by the Snappable's old transform MoldM_{\text{old}}Mold​:

psocket, world=Mold⋅psocket, local\mathbf{p}_{\text{socket, world}} = M_{\text{old}} \cdot \mathbf{p}_{\text{socket, local}} psocket, world​=Mold​⋅psocket, local​

The returned snap matrix would then implicitly encode an "undo MoldM_{\text{old}}Mold​" correction, which would compound incorrectly with re-placement.

Resetting to the identity transform guarantees:

psocket, world=I⋅psocket, local=psocket, local\mathbf{p}_{\text{socket, world}} = I \cdot \mathbf{p}_{\text{socket, local}} = \mathbf{p}_{\text{socket, local}} psocket, world​=I⋅psocket, local​=psocket, local​

The snap matrix is then purely about geometry alignment, not about history correction.

Content Group Rotation

After placing the Snappable at its snap position, rotationOffset is applied to the _content sub-group — not the Snappable itself. The rotation axis is the target socket's world normal N^target\hat{\mathbf{N}}_{\text{target}}N^target​, but it must be expressed in the Snappable's local space.

Let qsq_sqs​ be the Snappable's world quaternion. The local axis is:

N^local=qs−1⋅N^target⋅qs\hat{\mathbf{N}}_{\text{local}} = q_s^{-1} \cdot \hat{\mathbf{N}}_{\text{target}} \cdot q_s N^local​=qs−1​⋅N^target​⋅qs​

const targetNormalWorld = targetSocket.normal.clone().applyQuaternion(targetWorldQuat).normalize();

const snappableWorldQuat = new THREE.Quaternion();
item.getWorldQuaternion(snappableWorldQuat);
snappableWorldQuat.invert();

const localAxis = targetNormalWorld.clone().applyQuaternion(snappableWorldQuat).normalize();

Why convert to local space?

_content.quaternion lives in the Snappable's local coordinate system. Setting a quaternion using a world-space axis produces the wrong rotation once the Snappable has a non-identity orientation (which it always does after snapping). The conversion "undoes" the Snappable's world orientation so the axis is expressed in the same space as the quaternion being set.

Degenerate Fallback

Before building the full orthonormal frames, the code checks whether the Gram-Schmidt step produced a non-zero result:

∥U⊥∥>0.001   for both sockets\|\mathbf{U}_\perp\| > 0.001 \;\text{ for both sockets} ∥U⊥​∥>0.001 for both sockets

if (tULen > 0.001 && iULen > 0.001) {
    // full orthonormal frame path
} else {
    // fallback: normal-only alignment
    alignmentQuat.setFromUnitVectors(invertedItemNormal, targetNormal);
}

If ∥U⊥∥\|\mathbf{U}_\perp\|∥U⊥​∥ drops below 0.001, U\mathbf{U}U and N\mathbf{N}N were nearly co-linear — the artist placed the up indicator almost exactly on top of the normal axis. In that case the frame-based approach would produce a near-zero right vector R\mathbf{R}R and a garbage rotation matrix, so the code falls back to setFromUnitVectors. This fallback has the anti-parallel ambiguity described above, but it is better than corrupted geometry. The ambiguity only manifests as an unknown spin around the snap axis, which the user can correct with rotation controls.

The practical fix is to ensure the up indicator object in Blender is placed off to the side of the socket (perpendicular to the socket's facing direction) rather than in front of or behind it.

Step-by-Step Summary

calculateSnapMatrix(target,item,θ)\boxed{\text{calculateSnapMatrix}(\text{target}, \text{item}, \theta)} calculateSnapMatrix(target,item,θ)​

1.Nt, Ut=world-space normal and up of targetSocket1.\quad \mathbf{N}_t,\,\mathbf{U}_t = \text{world-space normal and up of } \textit{targetSocket} 1.Nt​,Ut​=world-space normal and up of targetSocket

2.Ni, Ui=world-space normal and up of itemSocket2.\quad \mathbf{N}_i,\,\mathbf{U}_i = \text{world-space normal and up of } \textit{itemSocket} 2.Ni​,Ui​=world-space normal and up of itemSocket

3.Ut←Ut−(Ut⋅Nt)Nt  ;Rt=Ut×Nt(Gram-Schmidt)3.\quad \mathbf{U}_t \leftarrow \mathbf{U}_t - (\mathbf{U}_t \cdot \mathbf{N}_t)\mathbf{N}_t \;;\quad \mathbf{R}_t = \mathbf{U}_t \times \mathbf{N}_t \quad\text{(Gram-Schmidt)} 3.Ut​←Ut​−(Ut​⋅Nt​)Nt​;Rt​=Ut​×Nt​(Gram-Schmidt)

4.Ui←Ui−(Ui⋅Ni)Ni  ;Ri=Ui×Ni4.\quad \mathbf{U}_i \leftarrow \mathbf{U}_i - (\mathbf{U}_i \cdot \mathbf{N}_i)\mathbf{N}_i \;;\quad \mathbf{R}_i = \mathbf{U}_i \times \mathbf{N}_i 4.Ui​←Ui​−(Ui​⋅Ni​)Ni​;Ri​=Ui​×Ni​

5.Mt=[Rt∣Ut∣Nt]  ;Mi=[Ri∣Ui∣Ni](rotation matrices)5.\quad M_t = [\mathbf{R}_t \mid \mathbf{U}_t \mid \mathbf{N}_t] \;;\quad M_i = [\mathbf{R}_i \mid \mathbf{U}_i \mid \mathbf{N}_i] \quad\text{(rotation matrices)} 5.Mt​=[Rt​∣Ut​∣Nt​];Mi​=[Ri​∣Ui​∣Ni​](rotation matrices)

6.Mflipped=Mi⋅Roty(π)(face-to-face flip)6.\quad M_{\text{flipped}} = M_i \cdot \text{Rot}_y(\pi) \quad\text{(face-to-face flip)} 6.Mflipped​=Mi​⋅Roty​(π)(face-to-face flip)

7.Malign=Mt⋅Mflipped−1(frame alignment)7.\quad M_{\text{align}} = M_t \cdot M_{\text{flipped}}^{-1} \quad\text{(frame alignment)} 7.Malign​=Mt​⋅Mflipped−1​(frame alignment)

8.qfinal=axisAngle(Nt, θ)⋅qalign(rotation offset)8.\quad \mathbf{q}_{\text{final}} = \text{axisAngle}(\mathbf{N}_t,\,\theta) \cdot \mathbf{q}_{\text{align}} \quad\text{(rotation offset)} 8.qfinal​=axisAngle(Nt​,θ)⋅qalign​(rotation offset)

9.t=ptarget−Rfinal⋅pitem(translation)9.\quad \mathbf{t} = \mathbf{p}_{\text{target}} - R_{\text{final}} \cdot \mathbf{p}_{\text{item}} \quad\text{(translation)} 9.t=ptarget​−Rfinal​⋅pitem​(translation)

10.Mfinal=T(t)⋅Rfinal10.\quad M_{\text{final}} = T(\mathbf{t}) \cdot R_{\text{final}} 10.Mfinal​=T(t)⋅Rfinal​

snapItemToSocket(item,target,item socket,θ)\boxed{\text{snapItemToSocket}(\text{item}, \text{target}, \text{item socket}, \theta)} snapItemToSocket(item,target,item socket,θ)​

1.item←0, I(reset to origin)1.\quad \text{item} \leftarrow \mathbf{0},\, I \quad\text{(reset to origin)} 1.item←0,I(reset to origin)

2.apply calculateSnapMatrix(target,item socket,0)2.\quad \text{apply } \text{calculateSnapMatrix}(\text{target}, \text{item socket}, 0) 2.apply calculateSnapMatrix(target,item socket,0)

3.N^local=qsnappable−1⋅N^target, world(convert axis to local space)3.\quad \hat{\mathbf{N}}_{\text{local}} = q_{\text{snappable}}^{-1} \cdot \hat{\mathbf{N}}_{\text{target, world}} \quad\text{(convert axis to local space)} 3.N^local​=qsnappable−1​⋅N^target, world​(convert axis to local space)

4._content.quaternion=axisAngle(N^local, θ)4.\quad \text{\_content.quaternion} = \text{axisAngle}(\hat{\mathbf{N}}_{\text{local}},\, \theta) 4._content.quaternion=axisAngle(N^local​,θ)

Edit this page
Last Updated: 4/27/26, 12:56 PM
Contributors: Nicolas Jaenen