# Unity 3D Meccano Editor - Detailed Class Documentation This document provides a deep dive into the most important classes in the Meccano Editor system. It explains their roles, how they fit into the broader architecture, and includes relevant code snippets to illustrate their inner workings. --- ## 1. The Command Layer The Command Layer is the backbone of the application's state management. It encapsulates all user actions (like creating, moving, or deleting a block) so that they can be tracked, reversed, and serialized. ### `ICommand` Interface Every action that modifies the world must implement this interface. ```csharp public interface ICommand { void execute(); void undo(); CommandDTO toDTO(); } ``` ### `CreateBlockCommand` This class implements `ICommand` to handle the instantiation of a new block. Notice how it stores all necessary parameters (prefab ID, position, rotation) so that it can be reversed (`undo`) or serialized (`toDTO`). ```csharp public class CreateBlockCommand : ICommand { private string prefabId; private Vector3 position; private Quaternion rotation; private string blockId; // Captured after creation // Dependencies private BlockFactory factory; private Environment environment; public void execute() { // Creates the physical object in the scene GameObject objectCreated = factory.createblock(prefabId, position, rotation, blockId, layerDiff); // We capture the generated UUID so we can target it during Undo if (objectCreated != null) { Block blockComponent = objectCreated.GetComponent(); blockId = blockComponent.getBlockId(); } } public void undo() { // Completely removes the block from the environment registry and destroys it environment.removeBlock(blockId); } public CommandDTO toDTO() { return new CommandDTO.CommandDTOBuilder() .SetCommandName("CreateCommand") .AddParameter("prefabId", prefabId) .AddParameter("position", position) .AddParameter("rotation", rotation) .Build(); } } ``` ### `CommandManager` This Singleton maintains the history of executed commands. It uses two `LinkedList`s to handle the `undo` and `redo` operations efficiently. ```csharp public class CommandManager { private LinkedList commandStack = new LinkedList(); private LinkedList redoStack = new LinkedList(); private int maxCapacity = 100; // Prevents memory leaks on long sessions public void executeCommand(ICommand command) { command.execute(); commandStack.AddFirst(command); redoStack.Clear(); // Any new action invalidates the redo stack if (commandStack.Count > maxCapacity) { commandStack.RemoveLast(); } } public void undo() { if (commandStack.Count > 0) { ICommand command = commandStack.First.Value; commandStack.RemoveFirst(); command.undo(); redoStack.AddFirst(command); } } } ``` --- ## 2. Dependency Injection The project uses a custom reflection-based DI container to avoid tightly coupling classes together and relying too heavily on Singletons for internal systems. ### `Injector` This class runs very early in the Unity lifecycle (`[DefaultExecutionOrder(-1000)]`). It searches for all `MonoBehaviour` instances, collects those implementing `IDependencyProvider`, and injects their provided values into any fields marked with `[Inject]`. ```csharp [DefaultExecutionOrder(-1000)] public class Injector : MonoBehaviour { void Awake() { var monoBehaviours = FindMonoBehaviours(); // 1. Collect IDependencyProvider components and register what they provide via [Provide] var providers = monoBehaviours.OfType(); foreach (var provider in providers) { RegisterProvider(provider); } // 2. Inject all injectable MonoBehaviours (fields with [Inject]) var injectables = monoBehaviours.Where(IsInjectable); foreach (var injectable in injectables) { Inject(injectable); } // 3. Setup watchers for RuntimeAnchors SetupAnchorWatchers(injectables); } // ... } ``` --- ## 3. The Environment and Save System The `Environment` is the source of truth for the physical world. The Save system works by "listening" to the Environment. ### `Environment` It maintains a `Dictionary` of all active blocks mapped by a UUID. ```csharp public class Environment : MonoBehaviour { private Dictionary blockRegistry = new Dictionary(); // Observers listen to these to react to the world state changing public UnityEvent onBlockAddedEvent; public UnityEvent onBlockRemovedEvent; public UnityEvent onChangeEvent; public void addBlock(Block block, string existingUUID = null) { // 1. Assign ID: Use existing (Undo/Redo) or generate a new one (Fresh placement) string uuid = string.IsNullOrEmpty(existingUUID) ? Guid.NewGuid().ToString() : existingUUID; block.setBlockId(uuid); // 2. Register in the dictionary blockRegistry[uuid] = block; applyChanges(); // Invokes onChangeEvent onBlockAddedEvent.Invoke(block); } } ``` ### `EnvironmentObserver` This class demonstrates the "Event Sourcing" approach to saving. When `autoSaving` is enabled, it listens to the `onChangeEvent`. Instead of saving GameObjects, it loops through the current registry and builds a fresh stack of `CreateBlockCommand`s, then serializes them. ```csharp public class EnvironmentObserver : MonoBehaviour { private void Start() { environment.onChangeEvent.AddListener(onChange); } public void save() { commandSaveHandler.clearCommandList(); // Loop through everything in the Environment Dictionary blockRegistry = environment.getBlockRegistry(); foreach (var blockEntry in blockRegistry) { if (blockEntry.Value != null) { // Create a command representing the current state of the block string prefabId = blockEntry.Value.getBlockData()?.blockName; Vector3 position = blockEntry.Value.transform.position; Quaternion rotation = blockEntry.Value.transform.rotation; ICommand createCommand = new CreateBlockCommand(prefabId, position, rotation); // Convert to a Data Transfer Object and add to the save queue commandSaveHandler.addLastCommand(createCommand.toDTO()); } } // Write the queue to JSON commandSaveHandler.saveCommands(); } } ``` --- ## 4. Object Interaction: GhostManager The `GhostManager` handles the visual drag-and-drop feedback. It creates a semi-transparent "Ghost" of the object being manipulated. ```csharp public class GhostManager : MonoBehaviour { // ... State tracking variables private IGhostMovementStrategy currentStrategy = new FreeMoveStrategy(); public void SetupBlock(BlockData blockData = null) { // Instantiate a fake "ghost" object for the user to drag around GameObject prefab = selectedBlockData.blockPrefab; ghostObject = Instantiate(prefab, rayInteractor.GetHitPosition(), prefab.transform.rotation); // Make the ghost semi-transparent and ignore physics raycasts SetLayerRecursively(ghostObject, LayerMask.NameToLayer("Ignore Raycast")); InitialiseGhostFromObject(ghostObject); // Turns materials translucent canPlace = true; } private void Update() { if (!HasActiveGhost()) return; UpdateSurfaceTracking(); // Trigger Snapping logic to find nearby sockets snappingSystem.UpdateSockets(currentSurfacePoint, ghostObject.transform); // Create a context object to pass into the movement strategy var ctx = new GhostMovementContext(...); // Strategy pattern: Allows swapping between free-move, axis-locked, etc. currentStrategy.UpdateMovement(ref ctx); // Validate the placement (e.g. are we colliding with something?) isValidPlacement = validator != null ? validator.IsValidPlacement(ghostObject) : true; UpdateVisualFeedback(isValidPlacement); // Turns green or red } private void PlaceBlock() { if (!isValidPlacement) return; // We do NOT instantiate the real block here! We delegate to the CommandHandler. if (block != null) commandHandler.createBlock( block.getBlockData()?.blockName, ghostObject.transform.position, ghostObject.transform.rotation); } } ``` --- ## 5. Sockets & Mechanical Connections The mechanical logic relies on `SocketPoint`s representing holes and pins, and `JointBlock`s which create physical constraints between them. ### `SocketPoint` Defines compatibility via radii and a `SocketType` enum. ```csharp public class SocketPoint : MonoBehaviour, ISocket { [SerializeField] private SocketType socketType = SocketType.Regular; [SerializeField] private float socketRadius = 0.1f; [SerializeField] private List acceptableTypes = new List(); [SerializeField] private float radiusTolerance = 0.05f; public bool CanAccept(ISocket otherSocket) { // Must be empty if (isOccupied) return false; // Must match type if (acceptableTypes.Count > 0 && !acceptableTypes.Contains(otherSocket.GetSocketType())) return false; // The hole must be larger than or equal to the pin, within a tolerance float radiusDiff = socketRadius - otherSocket.GetSocketRadius(); return radiusDiff >= -radiusTolerance && radiusDiff <= radiusTolerance; } } ``` ### `JointBlock` When two pieces are successfully snapped together, `JointBlock` takes over to simulate the mechanical connection by creating Unity Physics joints. ```csharp public class JointBlock : MonoBehaviour { [SerializeField] private JointBlockType jointType = JointBlockType.FixedPivot; public void CreatePhysicsJoint(GameObject targetObj, SocketPoint socket) { Rigidbody jointRb = GetComponent(); if (jointRb == null) jointRb = gameObject.AddComponent(); Rigidbody targetRb = targetObj.GetComponent(); if (targetRb == null) targetRb = targetObj.AddComponent(); // Create appropriate joint type Joint joint = CreateJointByType(targetRb, socket); if (joint != null) { createdJoints.Add(joint); } } private Joint CreateJointByType(Rigidbody connectedBody, SocketPoint socket) { switch (jointType) { case JointBlockType.HingePivot: HingeJoint joint = gameObject.AddComponent(); joint.connectedBody = connectedBody; joint.axis = transform.InverseTransformDirection(transform.forward); joint.anchor = transform.InverseTransformPoint(socket.GetTransform().position); return joint; case JointBlockType.FixedPivot: default: return gameObject.AddComponent(); } } } ```