Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
326 changes: 326 additions & 0 deletions Sources/UntoldEditor/Editor/EditorUndoManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
//
// EditorUndoManager.swift
// UntoldEditor
//
// Copyright (C) Untold Engine Studios
// Licensed under the GNU LGPL v3.0 or later.
// See the LICENSE file or <https://www.gnu.org/licenses/> for details.
//

import Foundation
import simd
import SwiftUI
import UntoldEngine

protocol EditorUndoCommand {
var name: String { get }
func undo()
func redo()
}

struct EditorTransformSnapshot: Equatable {
var position: simd_float3
var rotation: simd_float3
var scale: simd_float3

init(entityId: EntityID) {
position = getLocalPosition(entityId: entityId)
rotation = getAxisRotations(entityId: entityId)
scale = getScale(entityId: entityId)
}

func apply(to entityId: EntityID) {
guard hasComponent(entityId: entityId, componentType: LocalTransformComponent.self) else {
return
}

translateTo(entityId: entityId, position: position)
applyAxisRotations(entityId: entityId, axis: rotation)
scaleTo(entityId: entityId, scale: scale)
}
}

struct EditorNameChangeCommand: EditorUndoCommand {
let entityId: EntityID
let oldName: String
let newName: String

var name: String {
"Rename Entity"
}

func undo() {
setEntityName(entityId: entityId, name: oldName)
}

func redo() {
setEntityName(entityId: entityId, name: newName)
}
}

struct EditorTransformChangeCommand: EditorUndoCommand {
let entityId: EntityID
let before: EditorTransformSnapshot
let after: EditorTransformSnapshot

var name: String {
"Transform Entity"
}

func undo() {
before.apply(to: entityId)
}

func redo() {
after.apply(to: entityId)
}
}

struct EditorValueChangeCommand<Value: Equatable>: EditorUndoCommand {
let name: String
let oldValue: Value
let newValue: Value
let apply: (Value) -> Void

func undo() {
apply(oldValue)
}

func redo() {
apply(newValue)
}
}

struct EditorPostFXSnapshot: Equatable {
var colorGradingEnabled: Bool
var exposure: Float
var brightness: Float
var contrast: Float
var saturation: Float
var temperature: Float
var tint: Float
var bloomEnabled: Bool
var bloomThreshold: Float
var bloomIntensity: Float
var vignetteEnabled: Bool
var vignetteIntensity: Float
var vignetteRadius: Float
var vignetteSoftness: Float
var chromaticAberrationEnabled: Bool
var chromaticAberrationIntensity: Float
var depthOfFieldEnabled: Bool
var focusDistance: Float
var focusRange: Float
var maxBlur: Float
var ssaoEnabled: Bool
var ssaoRadius: Float
var ssaoBias: Float
var ssaoIntensity: Float

init() {
colorGradingEnabled = ColorGradingParams.shared.enabled
exposure = ColorGradingParams.shared.exposure
brightness = ColorGradingParams.shared.brightness
contrast = ColorGradingParams.shared.contrast
saturation = ColorGradingParams.shared.saturation
temperature = ColorGradingParams.shared.temperature
tint = ColorGradingParams.shared.tint

bloomEnabled = BloomThresholdParams.shared.enabled
bloomThreshold = BloomThresholdParams.shared.threshold
bloomIntensity = BloomThresholdParams.shared.intensity

vignetteEnabled = VignetteParams.shared.enabled
vignetteIntensity = VignetteParams.shared.intensity
vignetteRadius = VignetteParams.shared.radius
vignetteSoftness = VignetteParams.shared.softness

chromaticAberrationEnabled = ChromaticAberrationParams.shared.enabled
chromaticAberrationIntensity = ChromaticAberrationParams.shared.intensity

depthOfFieldEnabled = DepthOfFieldParams.shared.enabled
focusDistance = DepthOfFieldParams.shared.focusDistance
focusRange = DepthOfFieldParams.shared.focusRange
maxBlur = DepthOfFieldParams.shared.maxBlur

ssaoEnabled = SSAOParams.shared.enabled
ssaoRadius = SSAOParams.shared.radius
ssaoBias = SSAOParams.shared.bias
ssaoIntensity = SSAOParams.shared.intensity
}

func apply() {
ColorGradingParams.shared.enabled = colorGradingEnabled
ColorGradingParams.shared.exposure = exposure
ColorGradingParams.shared.brightness = brightness
ColorGradingParams.shared.contrast = contrast
ColorGradingParams.shared.saturation = saturation
ColorGradingParams.shared.temperature = temperature
ColorGradingParams.shared.tint = tint

BloomThresholdParams.shared.enabled = bloomEnabled
BloomThresholdParams.shared.threshold = bloomThreshold
BloomThresholdParams.shared.intensity = bloomIntensity

VignetteParams.shared.enabled = vignetteEnabled
VignetteParams.shared.intensity = vignetteIntensity
VignetteParams.shared.radius = vignetteRadius
VignetteParams.shared.softness = vignetteSoftness

ChromaticAberrationParams.shared.enabled = chromaticAberrationEnabled
ChromaticAberrationParams.shared.intensity = chromaticAberrationIntensity

DepthOfFieldParams.shared.enabled = depthOfFieldEnabled
DepthOfFieldParams.shared.focusDistance = focusDistance
DepthOfFieldParams.shared.focusRange = focusRange
DepthOfFieldParams.shared.maxBlur = maxBlur

SSAOParams.shared.enabled = ssaoEnabled
SSAOParams.shared.radius = ssaoRadius
SSAOParams.shared.bias = ssaoBias
SSAOParams.shared.intensity = ssaoIntensity
}
}

struct EditorPostFXChangeCommand: EditorUndoCommand {
let name: String
let before: EditorPostFXSnapshot
let after: EditorPostFXSnapshot

func undo() {
before.apply()
}

func redo() {
after.apply()
}
}

final class EditorUndoManager: ObservableObject {
static let shared = EditorUndoManager()

@Published private(set) var canUndo = false
@Published private(set) var canRedo = false

private var undoStack: [EditorUndoCommand] = []
private var redoStack: [EditorUndoCommand] = []
private var transformEditStart: [EntityID: EditorTransformSnapshot] = [:]
private var isReplaying = false

var onStateRestored: (() -> Void)?

func register(_ command: EditorUndoCommand) {
guard isReplaying == false else {
return
}

undoStack.append(command)
redoStack.removeAll()
updateAvailability()
}

func registerNameChange(entityId: EntityID, oldName: String, newName: String) {
guard oldName != newName else {
return
}

register(EditorNameChangeCommand(entityId: entityId, oldName: oldName, newName: newName))
}

func registerTransformChange(entityId: EntityID, before: EditorTransformSnapshot, after: EditorTransformSnapshot) {
guard before != after else {
return
}

register(EditorTransformChangeCommand(entityId: entityId, before: before, after: after))
}

func beginTransformEdit(entityId: EntityID) {
guard isReplaying == false,
hasComponent(entityId: entityId, componentType: LocalTransformComponent.self),
transformEditStart[entityId] == nil
else {
return
}

transformEditStart[entityId] = EditorTransformSnapshot(entityId: entityId)
}

func commitTransformEdit(entityId: EntityID) {
guard let before = transformEditStart.removeValue(forKey: entityId),
hasComponent(entityId: entityId, componentType: LocalTransformComponent.self)
else {
return
}

registerTransformChange(
entityId: entityId,
before: before,
after: EditorTransformSnapshot(entityId: entityId)
)
}

func registerValueChange<Value: Equatable>(
name: String,
oldValue: Value,
newValue: Value,
apply: @escaping (Value) -> Void
) {
guard oldValue != newValue else {
return
}

register(EditorValueChangeCommand(name: name, oldValue: oldValue, newValue: newValue, apply: apply))
}

func registerPostFXChange(name: String, before: EditorPostFXSnapshot, after: EditorPostFXSnapshot) {
guard before != after else {
return
}

register(EditorPostFXChangeCommand(name: name, before: before, after: after))
}

func undo() {
guard let command = undoStack.popLast() else {
return
}

replay {
command.undo()
}
redoStack.append(command)
updateAvailability()
}

func redo() {
guard let command = redoStack.popLast() else {
return
}

replay {
command.redo()
}
undoStack.append(command)
updateAvailability()
}

func clear() {
undoStack.removeAll()
redoStack.removeAll()
transformEditStart.removeAll()
updateAvailability()
}

private func replay(_ operation: () -> Void) {
isReplaying = true
operation()
isReplaying = false
onStateRestored?()
}

private func updateAvailability() {
canUndo = undoStack.isEmpty == false
canRedo = redoStack.isEmpty == false
}
}
8 changes: 8 additions & 0 deletions Sources/UntoldEditor/Editor/EditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ public struct EditorView: View {
.allowsHitTesting(false)
}
.onAppear {
EditorUndoManager.shared.onStateRestored = {
editor_entities = getAllGameEntities()
selectionManager.objectWillChange.send()
sceneGraphModel.refreshHierarchy()
}

sceneGraphModel.refreshHierarchy()

// Listen for asset instance loading completion
Expand Down Expand Up @@ -343,6 +349,7 @@ public struct EditorView: View {
destroyAllEntities()
removeGizmo()
EditorComponentsState.shared.clear()
EditorUndoManager.shared.clear()
deserializeScene(sceneData: sceneData)
editorController?.currentSceneURL = nil
editor_entities = getAllGameEntities()
Expand All @@ -360,6 +367,7 @@ public struct EditorView: View {
destroyAllEntities()
removeGizmo()
EditorComponentsState.shared.clear()
EditorUndoManager.shared.clear()

let light = createEntity()
setEntityName(entityId: light, name: "Directional Light")
Expand Down
2 changes: 0 additions & 2 deletions Sources/UntoldEditor/Editor/EngineStatsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ struct EngineStatsOverlayView: View {
}
}

@ViewBuilder
private func simplifiedOverlay(_ snapshot: EngineStatsSnapshot) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text("Engine Stats")
Expand All @@ -127,7 +126,6 @@ struct EngineStatsOverlayView: View {
.padding(12)
}

@ViewBuilder
private func advancedOverlay(_ snapshot: EngineStatsSnapshot) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text("Engine Stats (Advanced)")
Expand Down
Loading
Loading