ComponentMgr
Orchestrates block placement, snapping, selection, rotation, pointer interaction, and collision in a Three.js scene. Extends THREE.EventDispatcher.
Getting started
import { ComponentMgr } from './configurator';
const componentMgr = new ComponentMgr({
camera,
scene
});
Custom Ghost Processor
The built-in processor clones the Snappable and applies opacity = 0.5 to all of its materials.
If you need different ghost visuals — a wireframe outline, a custom colour tint, or a simplified mesh — provide your own function:
const componentMgr = new ComponentMgr({
camera, scene,
ghostProcessor: (snappable) => {
const ghost = snappable.clone(true);
// apply your own material changes to ghost here
return ghost;
}
});
The function receives the original Snappable and must return a new Snappable instance.
The returned object is added to the scene and repositioned each frame by tick().
It is removed when clearCurrent() is called or a block is placed.
Warning
Do not add the ghost to the scene yourself — ComponentMgr manages its lifecycle.
API
constructor({ camera, scene, isGhostEnabled, ghostProcessor })
The constructor method initialises the new ComponentMgr instance
Parameters
camera: Camera used for raycastingscene: Scene where blocks are addedisGhostEnabled: Whether to show a ghost preview when hovering socketsghostProcessor: Custom function to create the ghost clone:(snappable) => Snappable
Properties / Getters
objects
All placed blocks
sockets
All sockets across all placed blocks
currentItem
Block currently being previewed
selectedBlock
Currently selected placed block
childrenMap
Parent-to-children mapping
isGhostEnabled
get/set — enables or disables ghost preview
isCategoryFilteringDisabled
get/set — when true, all sockets are treated as wildcards regardless of their category strings.
Useful during development to test block geometry and snapping positions without worrying about compatibility rules, or for a "free build" mode where the user can connect any piece to any other. Does not affect already-placed and locked sockets; it only changes which target sockets are highlighted as compatible on the next tick().
isSocketVisible
get — current socket visibility state
isSocketUpVisible
get — current up indicator visibility state
isDraggingBlock
true while a block drag is in progress
dragBlock
The block currently being dragged, or null
Methods
add(snappable)
Places a Snappable into the scene. Registers its sockets, applies current debug/visibility settings, and dispatches block-placed.
Parameters
snappable: The block to place
Dispatches
block-placed
remove(snappable, isRemoveChildren?)
Removes a Snappable from the scene. Unlocks all its sockets and cleans up all internal references.
Parameters
snappable: The block to removeisRemoveChildren: Whether to remove all child blocks recursively (default:true)
Dispatches
block-removed, andblock-deselectedif the removed block was selected
setCurrent(snappable)
Sets the block being previewed/dragged. Creates a ghost clone and shows it over hovered sockets. If the scene is empty when called, the block is placed immediately at the origin.
Parameters
snappable: Activate block in the scene
Note
- Resets
selectedSocketIndexto0
clearCurrent()
Clears the current preview block and removes the ghost from the scene.
selectBlock(snappable)
Selects a placed block (must already be in componentMgr.objects).
Parameters
snappable: Activate placed block in the scene
Dispatches
block-selected
clearSelection()
Deselects the currently selected block.
Dispatches
block-deselected
removeSelectedBlock()
Calls remove() on the currently selected block. No-op if nothing is selected.
setSelectedSocketIndex(index)
Sets which socket on the current item is used as the snap point. Index must be within [0, sockets.length - 1].
Parameters
index: Socket index (default:0)
setSocketVisibility(isVisible)
Shows or hides all socket meshes across all placed blocks.
Parameters
isVisible: Visibility state (default:true)
setSocketUpVisibility(isVisible)
Shows or hides all up indicator objects across all sockets.
Parameters
isVisible: Visibility state (default:true)
setSocketColors(colors)
Overrides the socket state colours used by all Socket instances. Calls Socket.updateColors(colors).
Parameters
colors: Objectdefault: Initial state (default:#ffffff)hovered: Hovered state (default:#0000ff)clicked: Clicked state (default:#00ff00)compatible: Compatible state (default:#ffff00)locked: Locked state (default:#ff0000)blocked: Blocked state (default:#333333)
rotateCurrentItem(deltaSteps) → {success, steps, degrees, radians}
Adds deltaSteps × 45° to the current item's rotation.
Parameters
deltaSteps: Number of 45° steps to add to the current item's rotation
Returns
- Object:
{success: boolean, steps: number, degrees: number, radians: number}or{success: false, reason: 'no-current-item'}
setCurrentItemRotationSteps(steps) → {success, steps, degrees, radians}
Sets the current item's rotation to an absolute step count.
Parameters
steps—number
Returns
- Object:
{success: boolean, steps: number, degrees: number, radians: number}or{success: false, reason: 'no-current-item'}
setCurrentItemRotationRadians(radians) → {success, steps, degrees, radians}
Sets the current item's rotation to an absolute radian value.
Parameters
radians: Rotation angle in radians
Returns
- Object:
{success: boolean, steps: number, degrees: number, radians: number}or{success: false, reason: 'no-current-item'}
getCurrentItemRotationSteps() → number
Returns the current item's rotation rounded to the nearest 45° step. Returns 0 if no current item.
Returns
number: The current item's rotation in steps
getCurrentItemRotationRadians() → number
Returns the current item's rotation in radians. Returns 0 if no current item.
Returns
number: The current item's rotation in radians
rotateSelectedBlock(deltaSteps) → {success, steps, degrees, radians, blockId}
Adds deltaSteps × 45° to the selected block's rotation and re-snaps all descendants.
Parameters
deltaSteps: Number of 45° steps to add to the selected block's rotation
Returns
- Object:
{success: boolean, steps: number, degrees: number, radians: number, blockId: string}or{success: false, reason: 'no-selected-block'}
setSelectedBlockRotationRadians(radians) → {success, steps, degrees, radians, blockId}
Sets the selected block's rotation to an absolute radian value and re-snaps descendants.
Parameters
radians: Rotation angle in radians
Returns
- Object:
{success: boolean, steps: number, degrees: number, radians: number, blockId: string}or{success: false, reason: 'no-selected-block'}
getSelectedBlockRotationSteps() → number
Returns the selected block's rotation rounded to the nearest 45° step. Returns 0 if no block is selected.
Returns
number: The selected block's rotation in steps
getSelectedBlockRotationRadians() → number
Returns the selected block's rotation in radians. Returns 0 if no block is selected.
Returns
number: The selected block's rotation in radians
applyRotationOffset(snappable, radians)
Single source of truth for rotating a Snappable and updating all its descendants. Sets rotationOffset on the block, then re-snaps every descendant to its parent socket.
Parameters
snappable: The block to rotateradians: Absolute rotation offset in radians
canRotateSelectedBlock() → boolean
Returns true if the selected block has a parent socket anchor (i.e. it was snapped to another block and can be rotated around the snap axis). Root blocks placed at the origin return false.
Returns
boolean
updatePointer(ndcX, ndcY)
Updates the raycaster position. Call on every pointermove (or mousemove).
Parameters
ndcX: Normalized device coordinates (0-1)ndcY: Normalized device coordinates (0-1)
onPointerDown(ndcX, ndcY, screenX, screenY) → boolean
Records pointer-down state.
Call on pointerdown (or mousedown).
If the raycast hits the currently selected block, starts a block drag and returns true, otherwise returns false. When true, the app can disable camera controls and show a drag ghost (see Block drag).
onPointerUp(ndcX, ndcY, screenX, screenY) → boolean
Processes pointer up.
If a block drag was in progress, it ends first (block is moved to the hovered valid socket or snapped back to its original socket; root blocks restore world position) and returns true — selection logic is skipped.
Otherwise, if no socket was clicked this frame, checks if a block mesh was clicked and dispatches block-selected or block-deselected. Ignores drags > 5 px threshold. Returns false when no drag was ended.
The 5 px drag threshold is measured in screen pixels between the pointer down and pointer up positions.
Its purpose is to distinguish a deliberate click from the end of a camera orbit drag.
If the user panned the camera and released over a block, the selection should not fire. Any movement larger than 5 px is treated as a drag and the click is suppressed. This value is fixed and not currently configurable. Block drag does not use this threshold
Drag starts on pointer down when the raycast hits the selected block.
createGhostForBlock(block) → Snappable
Returns a ghost clone of the given Snappable using the same ghostProcessor as the placement preview. The app can add it to the scene, sync its transform from dragBlock each frame, and remove it on block-drag-end.
Parameters
block: The block to create a ghost for
Returns
Snappable: The ghost clone with transparent materials
tick()
Must be called once per frame inside your render loop. Drives:
Snappable.tick()on all placed blocks (socket colour animations)- Raycasting against sockets and blocks
- Socket state updates (hovered, clicked, compatible, blocked)
- Ghost preview positioning
- Block drag position update (when a placed block is being dragged: raycast excluding the dragged block, snap to compatible socket or plane-follow)
- Collision detection for hovered sockets
Think of it like a gym membership: it only does anything if you actually show up every frame. Forgetting to call it doesn't throw an error; the configurator just quietly sits on the couch while your render loop runs laps around it, wondering why nothing is responding.
zFightFix(cameraPosition)
Propagates a z-fight mitigation fix to the ghost and all placed blocks, passing down the camera position for depth-offset updates.
Parameters
cameraPosition:THREE.Vector3representing the active camera's position
enableDebugMode()
Replaces all block materials with a shared grey debug material. Enables arrow helpers on all sockets showing normals and up directions.
disableDebugMode()
Restores original materials and removes socket arrow helpers.
setDebugOpacity(opacity)
Controls the opacity of all socket materials in debug mode. Range: [0, 1].
Parameters
opacity: Sets the opacity of all socket materials in debug mode. Range:[0, 1]
setCollisionDebug(isEnabled)
Enables BVH visualisation on all placed blocks via CollisionManager.visualizeCollisions().
Parameters
isEnabled: Enables/disables BVH visualisation on all placed blocks
setCollisionDebugParams(options)
Passes options to CollisionManager.setDebugOptions(). See CollisionManager for option keys.
Parameters
options:objectshowAllCollisionAreas(boolean): Show all overlap boxes, not just the firstmaxCollisionAreas(number): Maximum number of overlap areas shownrefreshEveryNFrames(number): How often (in frames) to refresh the area list
setCollisionVisualDebug(isEnabled)
Enables/disables the in-scene visual overlay (red sphere + ring at the collision hotspot, coloured overlays on colliding meshes).
Parameters
isEnabled: Enables/disables the in-scene visual overlay
setCollisionVisualDebugParams(options)
Configures the visual debug overlay:
Parameters
options:objectshowAllCollisionAreas(boolean): Show all overlap boxes, not just the firstmaxCollisionAreas(number): Maximum number of overlap areas shownrefreshEveryNFrames(number): How often (in frames) to refresh the area list
dispose()
Removes the ghost, clears all collision visualisations, disposes debug materials, and nulls internal references. Call when tearing down the scene.
Events
block-placed
After a block is added to the scene (after add())
block-removed
After a block is removed from the scene (after remove())
block-selected
After a block is selected (after selectBlock())
block-deselected
After a block is deselected (after clearSelection() or when selected block is removed)
block-drag-start
When a block drag starts (after onPointerDown())
block-drag-end
When a block drag ends (after onPointerUp())
Exported Utility
calculateSnapMatrix(targetSocket, itemSocket, rotationOffsetRadians?) → THREE.Matrix4
Computes the world-space transformation matrix that aligns itemSocket face-to-face with targetSocket, with an optional rotation offset around the snap axis.
Parameters
targetSocket—Socket— the stationary socket to snap toitemSocket—Socket— the socket on the block being movedrotationOffsetRadians—number, default0
Returns
THREE.Matrix4
Import: import { calculateSnapMatrix } from './configurator'
snapItemToSocket(item, targetSocket, itemSocket, rotationOffsetRadians?)
Snaps an item to a target socket, handling reset-to-base and rotation offset internally. This ensures all rotations are calculated from a consistent base state (origin/identity).
Parameters
item—THREE.Object3D— the object to snap (will be modified)targetSocket—Socket— the socket to snap to (on the stationary object)itemSocket—Socket— the socket being moved (on the object to transform)rotationOffsetRadians—number, default0
Import: import { snapItemToSocket } from './configurator'