public static IObservable <CourseEditorState> Create(CourseEditorActions actions, CourseData courseData, Ref <ImmutableTransform> cameraTransform) { var commands = actions.CreateProp.Select(StoreAction.Create <Unit>("createProp")) .Merge(actions.CreatePropOnLocation.Select(StoreAction.Create <ImmutableTransform>("createPropOnLocation"))) .Merge(actions.DeleteProp.Select(StoreAction.Create <PropId>("deleteProp"))) .Merge(actions.DeleteSelectedProp.Select(StoreAction.Create <Unit>("deleteSelectedProp"))) .Merge(actions.UpdateProp.Select(StoreAction.Create <Tuple <PropId, ImmutableTransform> >("updateProp"))) .Merge(actions.HighlightProp.Select(StoreAction.Create <Maybe <PropId> >("highlightProp"))) .Merge(actions.SelectProp.Select(StoreAction.Create <Maybe <PropId> >("selectProp"))) .Merge(actions.SelectPropType.Select(StoreAction.Create <PropType>("selectPropType"))) .Merge(actions.ReorderProps.Select(StoreAction.Create <IImmutableList <PropId> >("reorderProps"))) .Merge(actions.Undo.Select(StoreAction.Create <Unit>("undo"))) .Merge(actions.Redo.Select(StoreAction.Create <Unit>("redo"))); var initialHistoricalState = courseData.ToCourseEditorState(); var updatesWithHistory = commands .Scan(new AppState <HistoricalCourseEditorState>(initialHistoricalState), (currentAppState, command) => { var currentState = currentAppState.CurrentState; AppState <HistoricalCourseEditorState> newAppState = currentAppState; if (command.Id.Equals("createProp")) { var propTransform = cameraTransform.Deref().TranslateLocally(new Vector3(0, 0, 30)); newAppState = History.AddNewState(currentAppState, currentState.AddProp(propTransform)); } else if (command.Id.Equals("createPropOnLocation")) { newAppState = History.AddNewState(currentAppState, currentState.AddProp((ImmutableTransform)command.Arguments)); } else if (command.Id.Equals("updateProp")) { var args = (Tuple <PropId, ImmutableTransform>)command.Arguments; var prop = currentState.Props[args._1]; var isTransformChanged = !prop.Transform.Equals(args._2); if (isTransformChanged) { var updatedProp = prop.UpdateTransform(args._2); newAppState = History.AddNewState(currentAppState, currentState.UpdateProp(updatedProp)); } else { newAppState = currentAppState; } } else if (command.Id.Equals("deleteProp")) { var propId = (PropId)command.Arguments; if (currentState.PropOrder.Contains(propId)) { newAppState = History.AddNewState(currentAppState, currentState.DeleteProp(propId)); } } else if (command.Id.Equals("deleteSelectedProp")) { if (currentState.SelectedProp.IsJust) { newAppState = History.AddNewState(currentAppState, currentState.DeleteProp(currentState.SelectedProp.Value)); } } else if (command.Id.Equals("selectProp")) { var selectedPropId = (Maybe <PropId>)command.Arguments; if (selectedPropId.IsNothing || (selectedPropId.IsJust && currentState.Props.ContainsKey(selectedPropId.Value))) { newAppState = History.AddNewState(currentAppState, currentState.SelectProp(selectedPropId)); } } else if (command.Id.Equals("selectPropType")) { newAppState = History.AddNewState(currentAppState, currentState.SelectPropType((PropType)command.Arguments)); } else if (command.Id.Equals("reorderProps")) { newAppState = History.AddNewState(currentAppState, currentState.UpdatePropOrder((IImmutableList <PropId>)command.Arguments)); } else if (command.Id.Equals("undo")) { newAppState = History.Undo(currentAppState); } else if (command.Id.Equals("redo")) { newAppState = History.Redo(currentAppState); } else { newAppState = currentAppState; } return(newAppState); }) .Select(appState => { return(appState.CurrentState.UpdateUndoRedoAvailability( !appState.UndoStack.IsEmpty, !appState.RedoStack.IsEmpty)); }) .StartWith(initialHistoricalState) .Publish(); var initialTransientState = new TransientCourseEditorState(courseData.Name, Maybe.Nothing <PropId>(), TransformTool.Move); var transientUpdates = actions.HighlightProp.Select(StoreAction.Create <Maybe <PropId> >("highlightProp")) .Merge(actions.SelectTransformTool.Select(StoreAction.Create <TransformTool>("selectTransformTool"))) .Merge(actions.UpdateName.Select(StoreAction.Create <string>("updateName"))) .Scan(initialTransientState, (state, command) => { if (command.Id.Equals("highlightProp")) { return(state.HighlightProp((Maybe <PropId>)command.Arguments)); } else if (command.Id.Equals("selectTransformTool")) { // TODO Prevent transform tool selection when no prop is selected return(state.SelectTransformTool((TransformTool)command.Arguments)); } else if (command.Id.Equals("updateName")) { return(state.UpdateName((string)command.Arguments)); } return(state); }) .DistinctUntilChanged() .StartWith(initialTransientState) .CombineLatest(updatesWithHistory, (transientState, histState) => { // Check if the highlighted prop still exists var highlightedProp = transientState.HighlightedProp; var isHighlightedItemDeleted = highlightedProp.IsJust && !histState.Props.ContainsKey(highlightedProp.Value); if (isHighlightedItemDeleted) { return(transientState.HighlightProp(Maybe.Nothing <PropId>())); } return(transientState); }); var updates = updatesWithHistory.CombineLatest( transientUpdates, (histState, transientState) => new CourseEditorState(histState, transientState)) .DistinctUntilChanged() .Replay(1); updates.Connect(); updatesWithHistory.Connect(); return(updates); }
public static void InitializeCourseEditor(CourseEditorActions actions, IObservable <CourseEditorState> store, Camera camera, Ref <ImmutableTransform> cameraTransform, RingProps renderableProps, PropRenderer <PropId> propRenderer, IClock clock) { var courseUpdates = store .Do(state => { var rProps = state.Props.Select(kvPair => { var propId = kvPair.Key; var prop = kvPair.Value; return(new KeyValuePair <PropId, RenderableProp>( propId, new RenderableProp(prop.Transform, renderableProps.Factory[prop.PropType].Spawn))); }) .ToDictionary(); propRenderer.Update(rProps); }).Replay(1); courseUpdates.Connect(); var keyboardEvents = UnityRxKeyboard.CreateKeyboard(); var highlight = ObjectPlacement.ObjectHighlight(camera, LayerMaskUtil.FullMask) .Select(go => { if (go.IsJust) { var id = go.Value.GetComponent <Id>(); if (id != null && id.Value.StartsWith("__CourseEditor-")) { return(Maybe.Just(new PropId(id.Value.Replace("__CourseEditor-", "")))); } return(Maybe.Nothing <PropId>()); } return(Maybe.Nothing <PropId>()); }); var leftMouseClick = keyboardEvents .KeyDown() .Where(c => c == KeyCode.Mouse0) .Select(c => Unit.Default); var selection = ObjectPlacement.ObjectSelection(highlight, leftMouseClick); IObservable <Unit> undo; IObservable <Unit> redo; // Editor already binds to default undo/redo keys so we need a different // mapping for them if (Application.isEditor) { undo = UnityObservable.CreateUpdate <Unit>(observer => { if (UnityEngine.Input.GetKeyDown(KeyCode.Z)) { observer.OnNext(Unit.Default); } }); redo = UnityObservable.CreateUpdate <Unit>(observer => { if (UnityEngine.Input.GetKeyDown(KeyCode.Y)) { observer.OnNext(Unit.Default); } }); } else { var historyCommands = UnityObservable.CreateUpdate <string>(observer => { if (UnityEngine.Input.GetKey(KeyCode.LeftControl) && UnityEngine.Input.GetKeyDown(KeyCode.Y)) { observer.OnNext("redo"); } else if (UnityEngine.Input.GetKey(KeyCode.LeftControl) && UnityEngine.Input.GetKeyDown(KeyCode.Z)) { observer.OnNext("undo"); } }); undo = historyCommands .Where(c => c.Equals("undo")) .Select(c => Unit.Default); redo = historyCommands .Where(c => c.Equals("redo")) .Select(c => Unit.Default); } var createProp = UnityObservable.CreateUpdate <Unit>(observer => { if (UnityEngine.Input.GetKey(KeyCode.LeftControl) && UnityEngine.Input.GetKeyDown(KeyCode.F)) { observer.OnNext(Unit.Default); } }); var deleteProp = keyboardEvents .KeyDown() .Where(key => key == KeyCode.Delete) .Select(key => Unit.Default); Func <KeyCode, Vector3> key2Translation = key => { if (key == KeyCode.W) { return(new Vector3(0, 0, 1)); } else if (key == KeyCode.S) { return(new Vector3(0, 0, -1)); } else if (key == KeyCode.A) { return(new Vector3(-1, 0, 0)); } else if (key == KeyCode.D) { return(new Vector3(1, 0, 0)); } else if (key == KeyCode.Q) { return(new Vector3(0, -1, 0)); } else if (key == KeyCode.E) { return(new Vector3(0, 1, 0)); } return(Vector3.zero); }; Func <KeyCode, Vector3> key2Rotation = key => { if (key == KeyCode.W) { return(new Vector3(1, 0, 0)); } else if (key == KeyCode.S) { return(new Vector3(-1, 0, 0)); } else if (key == KeyCode.D) { return(new Vector3(0, 1, 0)); } else if (key == KeyCode.A) { return(new Vector3(0, -1, 0)); } return(Vector3.zero); }; var selectedTransformTool = courseUpdates .Select(state => state.SelectedTransformTool) .DistinctUntilChanged(EnumComparer <TransformTool> .Instance); var switchTransformTool = selectedTransformTool.Select(tool => { return(UnityObservable.CreateUpdate <Unit>(observer => { if (UnityEngine.Input.GetKeyDown(KeyCode.LeftAlt) || UnityEngine.Input.GetKeyDown(KeyCode.RightAlt)) { observer.OnNext(Unit.Default); } }) .Select(_ => CourseEditor.TransformTools.GetNext(tool))); }).Switch(); var ticks = UnityRxObservables.UpdateTicks(() => clock.DeltaTime); var keysHeldStream = UnityRxKeyboard.CreateKeyboard(new[] { KeyCode.W, KeyCode.S, KeyCode.A, KeyCode.D, KeyCode.Q, KeyCode.E, KeyCode.Mouse1 }).KeysHeld(); var combinedTransformation = ticks.CombineLatest( keysHeldStream.CombineLatest(selectedTransformTool, (keysHeld, transformTool) => new { KeysHeld = keysHeld, TransformTool = transformTool }), (deltaTime, data) => { var rotationSpeed = 80f; var movementSpeed = 16f; var transform = ImmutableTransform.Identity; // Prevent collision with spectator camera movement. if (!data.KeysHeld.Contains(KeyCode.Mouse1)) { if (data.TransformTool == TransformTool.Rotate) { var rotation = data.KeysHeld .Aggregate(Vector3.zero, (current, key) => current + key2Rotation(key)) .normalized *rotationSpeed *deltaTime; transform = transform.Rotate(rotation); } else { var translation = data.KeysHeld .Aggregate(Vector3.zero, (current, key) => current + key2Translation(key)) .normalized *movementSpeed *deltaTime; transform = transform.Translate(translation); } } return(transform); }); var combinedTransformation2 = combinedTransformation .Window(() => { return(combinedTransformation .Where(t => t.Equals(ImmutableTransform.Identity))); }); var currentselectedProp = courseUpdates .Select( state => { var prop = state.SelectedProp.IsJust ? Maybe.Just(state.Props[state.SelectedProp.Value]) : Maybe.Nothing <EditorProp>(); return(prop); }) .DistinctUntilChanged(); var moveProp = currentselectedProp.CombineLatest(combinedTransformation2, (selectedProp, transformationCommand) => { if (selectedProp.IsJust) { return(transformationCommand.Scan( new Tuple <PropId, ImmutableTransform>(selectedProp.Value.Id, selectedProp.Value.Transform), (accumulatedMovement, transformation) => { var transformUpdate = accumulatedMovement._2 .Rotate(transformation.Rotation) .Translate(transformation.Position, cameraTransform.V.Rotation) .Scale(transformation.Scale); return new Tuple <PropId, ImmutableTransform>(accumulatedMovement._1, transformUpdate); })); } else { return(Observable.Empty <Tuple <PropId, ImmutableTransform> >()); } }); moveProp.Switch() .Subscribe(gameObjectMoveCommand => { var propId = gameObjectMoveCommand._1; var newTransform = gameObjectMoveCommand._2; var go = propRenderer.GetProp(propId); go.SetTransform(newTransform); }); moveProp .Select(moveCommand => moveCommand.TakeLast(1)) .Switch() .Subscribe(moveCommand => actions.UpdateProp.OnNext(moveCommand)); var moveToNextProp = UnityObservable.CreateUpdate <string>(observer => { if (UnityEngine.Input.GetKey(KeyCode.LeftControl) && UnityEngine.Input.GetKeyDown(KeyCode.Tab)) { observer.OnNext("previous"); } else if (UnityEngine.Input.GetKeyDown(KeyCode.Tab)) { observer.OnNext("next"); } }); courseUpdates .Select(state => { if (state.PropOrder.Count > 0) { return(moveToNextProp.Select(command => { if (state.SelectedProp.IsJust) { if (command.Equals("next")) { return state.PropOrder.GetNext(state.SelectedProp.Value); } else if (command.Equals("previous")) { return state.PropOrder.GetPrevious(state.SelectedProp.Value); } } return state.PropOrder.First(); })); } return(Observable.Empty <PropId>()); }) .Switch() .Subscribe(newlySelectedProp => { actions.SelectProp.OnNext(Maybe.Just(newlySelectedProp)); actions.MoveToProp.OnNext(newlySelectedProp); }); // TODO When player presses 'V' move prop to camera perspective. createProp.Subscribe(_ => actions.CreateProp.OnNext(Unit.Default)); deleteProp.Subscribe(_ => actions.DeleteSelectedProp.OnNext(Unit.Default)); undo.Subscribe(_ => actions.Undo.OnNext(Unit.Default)); redo.Subscribe(_ => actions.Redo.OnNext(Unit.Default)); highlight.Subscribe(propId => actions.HighlightProp.OnNext(propId)); selection.Subscribe(propId => actions.SelectProp.OnNext(Maybe.Just(propId))); switchTransformTool.Subscribe(tool => actions.SelectTransformTool.OnNext(tool)); var moveToProp = courseUpdates.Select(state => { return(actions.MoveToProp.Select(propId => state.Props[propId])); }).Switch(); moveToProp.Subscribe(prop => { Vector3 cameraRotation = cameraTransform.V.Rotation.eulerAngles; var propRotation = prop.Transform.Rotation.eulerAngles; cameraTransform.V = prop.Transform .TranslateLocally(new Vector3(0, 0, -30)) .UpdateRotation(cameraRotation.X(propRotation.x).Y(propRotation.y)); }); // TODO This code can be much simpler var guiUpdates = courseUpdates .Scan(new Diff <ObjectSelectionState?>(null, null), (previousDiff, state) => { Func <Maybe <PropId>, Maybe <GameObject> > findGameObject = propId => { return(propId.IsJust ? Maybe.Of(propRenderer.GetProp(propId.Value)) : Maybe.Nothing <GameObject>()); }; var @new = new ObjectSelectionState(findGameObject(state.HighlightedProp), findGameObject(state.SelectedProp)); var old = previousDiff.New; if (old.HasValue) { if (old.Value.HighlightedObject.IsJust && old.Value.HighlightedObject.Value == null) { old = new ObjectSelectionState(Maybe.Nothing <GameObject>(), old.Value.SelectedObject); } if (old.Value.SelectedObject.IsJust && old.Value.SelectedObject.Value == null) { old = new ObjectSelectionState(old.Value.HighlightedObject, Maybe.Nothing <GameObject>()); } } return(new Diff <ObjectSelectionState?>(old, @new)); }); guiUpdates.Subscribe(guiState => { if (guiState.Old.HasValue) { guiState.Old.Value.HighlightedObject.Do(obj => obj.GetComponentOfInterface <IHighlightable>().UnHighlight()); guiState.Old.Value.SelectedObject.Do(obj => obj.GetComponentOfInterface <ISelectable>().UnSelect()); } guiState.New.Value.HighlightedObject.Do(obj => obj.GetComponentOfInterface <IHighlightable>().Highlight()); guiState.New.Value.SelectedObject.Do(obj => obj.GetComponentOfInterface <ISelectable>().Select()); }); }