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);
        }
示例#2
0
        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());
            });
        }