diff --git a/DMXEditor.xcodeproj/project.pbxproj b/DMXEditor.xcodeproj/project.pbxproj index 5fc74ec..0193ce0 100644 --- a/DMXEditor.xcodeproj/project.pbxproj +++ b/DMXEditor.xcodeproj/project.pbxproj @@ -11,9 +11,17 @@ A711E41527BEE9CA009E1205 /* DMXEditorDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = A711E41427BEE9CA009E1205 /* DMXEditorDocument.swift */; }; A711E41927BEE9CB009E1205 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A711E41827BEE9CB009E1205 /* Assets.xcassets */; }; A711E41C27BEE9CB009E1205 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A711E41B27BEE9CB009E1205 /* Preview Assets.xcassets */; }; + A74CC46D291E74D70069F387 /* Frame.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74CC46C291E74D70069F387 /* Frame.swift */; }; + A74CC471291E8E290069F387 /* FrameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74CC470291E8E290069F387 /* FrameView.swift */; }; + A74CC4772924F7E90069F387 /* DMXTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74CC4762924F7E90069F387 /* DMXTransition.swift */; }; A770FBAD285CF06800BBA4AF /* AppleScriptRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = A770FBAC285CF06800BBA4AF /* AppleScriptRunner.swift */; }; A7AAEE8727CD2AC400B7BBA7 /* OLAHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7AAEE8627CD2AC400B7BBA7 /* OLAHandler.swift */; }; A7AAEE8927CD36EB00B7BBA7 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7AAEE8827CD36EB00B7BBA7 /* GeneralSettingsView.swift */; }; + A7B0345729AA71C900CB9426 /* CurveEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B0345629AA71C900CB9426 /* CurveEditorView.swift */; }; + A7B0345929AA7A1C00CB9426 /* StepPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B0345829AA7A1C00CB9426 /* StepPicker.swift */; }; + A7E20048292E2DB700676956 /* CurveEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E20047292E2DB700676956 /* CurveEditor.swift */; }; + A7E2004A292E2F1200676956 /* GeometryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E20049292E2F1200676956 /* GeometryExtensions.swift */; }; + A7E2004C292E2F1800676956 /* Dragging.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E2004B292E2F1800676956 /* Dragging.swift */; }; A7E7BC2627BEEACE000A83BB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E7BC1927BEEACE000A83BB /* SettingsView.swift */; }; A7E7BC2727BEEACE000A83BB /* MultiSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E7BC1A27BEEACE000A83BB /* MultiSlider.swift */; }; A7E7BC2827BEEACE000A83BB /* DMXData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E7BC1B27BEEACE000A83BB /* DMXData.swift */; }; @@ -23,7 +31,6 @@ A7E7BC2C27BEEACE000A83BB /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E7BC1F27BEEACE000A83BB /* Settings.swift */; }; A7E7BC2D27BEEACE000A83BB /* NumberField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E7BC2027BEEACE000A83BB /* NumberField.swift */; }; A7E7BC2E27BEEACE000A83BB /* SingleSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E7BC2127BEEACE000A83BB /* SingleSlider.swift */; }; - A7E7BC2F27BEEACE000A83BB /* SlideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E7BC2227BEEACE000A83BB /* SlideView.swift */; }; A7E7BC3027BEEACE000A83BB /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E7BC2327BEEACE000A83BB /* Device.swift */; }; A7E7BC3227BEEACE000A83BB /* RGBPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E7BC2527BEEACE000A83BB /* RGBPicker.swift */; }; A7E7BC3627C13406000A83BB /* MultiSliderEditable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E7BC3527C13406000A83BB /* MultiSliderEditable.swift */; }; @@ -40,9 +47,17 @@ A711E41B27BEE9CB009E1205 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; A711E41D27BEE9CB009E1205 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A711E41E27BEE9CB009E1205 /* DMXEditor.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DMXEditor.entitlements; sourceTree = ""; }; + A74CC46C291E74D70069F387 /* Frame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Frame.swift; sourceTree = ""; }; + A74CC470291E8E290069F387 /* FrameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameView.swift; sourceTree = ""; }; + A74CC4762924F7E90069F387 /* DMXTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMXTransition.swift; sourceTree = ""; }; A770FBAC285CF06800BBA4AF /* AppleScriptRunner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppleScriptRunner.swift; sourceTree = ""; }; A7AAEE8627CD2AC400B7BBA7 /* OLAHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OLAHandler.swift; sourceTree = ""; }; A7AAEE8827CD36EB00B7BBA7 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; + A7B0345629AA71C900CB9426 /* CurveEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurveEditorView.swift; sourceTree = ""; }; + A7B0345829AA7A1C00CB9426 /* StepPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepPicker.swift; sourceTree = ""; }; + A7E20047292E2DB700676956 /* CurveEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurveEditor.swift; sourceTree = ""; }; + A7E20049292E2F1200676956 /* GeometryExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeometryExtensions.swift; sourceTree = ""; }; + A7E2004B292E2F1800676956 /* Dragging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dragging.swift; sourceTree = ""; }; A7E7BC1927BEEACE000A83BB /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; A7E7BC1A27BEEACE000A83BB /* MultiSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiSlider.swift; sourceTree = ""; }; A7E7BC1B27BEEACE000A83BB /* DMXData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DMXData.swift; sourceTree = ""; }; @@ -52,7 +67,6 @@ A7E7BC1F27BEEACE000A83BB /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; A7E7BC2027BEEACE000A83BB /* NumberField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberField.swift; sourceTree = ""; }; A7E7BC2127BEEACE000A83BB /* SingleSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleSlider.swift; sourceTree = ""; }; - A7E7BC2227BEEACE000A83BB /* SlideView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlideView.swift; sourceTree = ""; }; A7E7BC2327BEEACE000A83BB /* Device.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = ""; }; A7E7BC2527BEEACE000A83BB /* RGBPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RGBPicker.swift; sourceTree = ""; }; A7E7BC3527C13406000A83BB /* MultiSliderEditable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSliderEditable.swift; sourceTree = ""; }; @@ -115,12 +129,16 @@ A711E42427BEEA01009E1205 /* Models */ = { isa = PBXGroup; children = ( + A7E20049292E2F1200676956 /* GeometryExtensions.swift */, + A7E2004B292E2F1800676956 /* Dragging.swift */, A711E41427BEE9CA009E1205 /* DMXEditorDocument.swift */, A7E7BC1D27BEEACE000A83BB /* ProjectData.swift */, A7E7BC2327BEEACE000A83BB /* Device.swift */, A7E7BC1B27BEEACE000A83BB /* DMXData.swift */, A7E7BC1F27BEEACE000A83BB /* Settings.swift */, A7E7BC1E27BEEACE000A83BB /* Slide.swift */, + A74CC46C291E74D70069F387 /* Frame.swift */, + A74CC4762924F7E90069F387 /* DMXTransition.swift */, ); name = Models; sourceTree = ""; @@ -134,6 +152,9 @@ A7E7BC2127BEEACE000A83BB /* SingleSlider.swift */, A7E7BC3527C13406000A83BB /* MultiSliderEditable.swift */, A7E7BC3727C171E6000A83BB /* SingleSliderEditable.swift */, + A7E20047292E2DB700676956 /* CurveEditor.swift */, + A7B0345629AA71C900CB9426 /* CurveEditorView.swift */, + A7B0345829AA7A1C00CB9426 /* StepPicker.swift */, ); name = Components; sourceTree = ""; @@ -143,9 +164,9 @@ children = ( A7E7BC1C27BEEACE000A83BB /* EditView.swift */, A7E7BC1927BEEACE000A83BB /* SettingsView.swift */, - A7E7BC2227BEEACE000A83BB /* SlideView.swift */, A7E7BC3927C2C48C000A83BB /* SlideEditView.swift */, A7AAEE8827CD36EB00B7BBA7 /* GeneralSettingsView.swift */, + A74CC470291E8E290069F387 /* FrameView.swift */, ); path = Views; sourceTree = ""; @@ -234,26 +255,33 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A7E2004C292E2F1800676956 /* Dragging.swift in Sources */, A7E7BC3D27C5158E000A83BB /* KeynoteHandler.swift in Sources */, A7AAEE8727CD2AC400B7BBA7 /* OLAHandler.swift in Sources */, A711E41327BEE9CA009E1205 /* DMXEditorApp.swift in Sources */, A7E7BC2827BEEACE000A83BB /* DMXData.swift in Sources */, A7E7BC2627BEEACE000A83BB /* SettingsView.swift in Sources */, + A7B0345729AA71C900CB9426 /* CurveEditorView.swift in Sources */, A7E7BC3227BEEACE000A83BB /* RGBPicker.swift in Sources */, + A74CC471291E8E290069F387 /* FrameView.swift in Sources */, A7E7BC3A27C2C48C000A83BB /* SlideEditView.swift in Sources */, A7E7BC3827C171E6000A83BB /* SingleSliderEditable.swift in Sources */, A7E7BC2727BEEACE000A83BB /* MultiSlider.swift in Sources */, A7E7BC2C27BEEACE000A83BB /* Settings.swift in Sources */, + A7E2004A292E2F1200676956 /* GeometryExtensions.swift in Sources */, A7E7BC3027BEEACE000A83BB /* Device.swift in Sources */, + A74CC46D291E74D70069F387 /* Frame.swift in Sources */, A7E7BC2927BEEACE000A83BB /* EditView.swift in Sources */, + A7B0345929AA7A1C00CB9426 /* StepPicker.swift in Sources */, A7E7BC2E27BEEACE000A83BB /* SingleSlider.swift in Sources */, + A74CC4772924F7E90069F387 /* DMXTransition.swift in Sources */, + A7E20048292E2DB700676956 /* CurveEditor.swift in Sources */, A711E41527BEE9CA009E1205 /* DMXEditorDocument.swift in Sources */, A7E7BC2D27BEEACE000A83BB /* NumberField.swift in Sources */, A7E7BC3627C13406000A83BB /* MultiSliderEditable.swift in Sources */, A7AAEE8927CD36EB00B7BBA7 /* GeneralSettingsView.swift in Sources */, A770FBAD285CF06800BBA4AF /* AppleScriptRunner.swift in Sources */, A7E7BC2B27BEEACE000A83BB /* Slide.swift in Sources */, - A7E7BC2F27BEEACE000A83BB /* SlideView.swift in Sources */, A7E7BC2A27BEEACE000A83BB /* ProjectData.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -404,6 +432,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = de.inckmann.DMXEditor; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -438,6 +467,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = de.inckmann.DMXEditor; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/DMXEditor/CurveEditor.swift b/DMXEditor/CurveEditor.swift new file mode 100644 index 0000000..88e8c6b --- /dev/null +++ b/DMXEditor/CurveEditor.swift @@ -0,0 +1,42 @@ +// +// CurveEditor.swift +// DMXEditor +// +// Created by Maximilian Inckmann on 23.11.22. +// +import Foundation +import SwiftUI + +struct CurveEditor: View { + @Binding var controlPoint0: RelativePoint + @Binding var controlPoint1: RelativePoint + @Binding var initialPoint0: CGSize + @Binding var initialPoint1: CGSize + + var body: some View { + VStack { + HStack{ + Text("Value") + .rotationEffect(Angle(degrees: 270)) + .fixedSize() + + CurveEditorView(controlPoint0: $controlPoint0, controlPoint1: $controlPoint1, initialPoint0: $initialPoint0, initialPoint1: $initialPoint1) + .border(.white) + .frame(maxWidth: 350, maxHeight: 350) + .aspectRatio(contentMode: .fit) + .padding(.trailing) + + Spacer() + } + + Text("Time") + } + .frame(width: 370, height: 350) + } +} + +struct CurveEditor_Previews: PreviewProvider { + static var previews: some View { + CurveEditor(controlPoint0: .constant(.zero), controlPoint1: .constant(.zero), initialPoint0: .constant(.init(width: 0.4, height: 0.3)), initialPoint1: .constant(.init(width: 0.6, height: 0.6))) + } +} diff --git a/DMXEditor/CurveEditorView.swift b/DMXEditor/CurveEditorView.swift new file mode 100644 index 0000000..639b0ec --- /dev/null +++ b/DMXEditor/CurveEditorView.swift @@ -0,0 +1,96 @@ +// +// CurveEditorView.swift +// DMXEditor +// +// Created by Maximilian Inckmann on 25.02.23. +// + +import SwiftUI + +struct CurveShape: Shape { + let cp0, cp1: RelativePoint + func path(in rect: CGRect) -> Path { + Path { p in + p.move(to: CGPoint(x: 0, y: rect.size.height)) + p.addCurve(to: CGPoint(x: rect.size.width, y: 0), + control1: cp0 * rect.size, + control2: cp1 * rect.size) + } + } +} + +struct ControlPointHandle: View { + private let size: CGFloat = 20 + var body: some View { + Circle() + .frame(width: size, height: size) + .overlay( + Circle() + .stroke(Color.white, lineWidth: 2) + ) + .offset(x: -size/2, y: -size/2) + } +} + +struct CurveEditorView: View { + @State var offsetPoint0: CGSize = .zero + @State var offsetPoint1: CGSize = .zero + @Binding var controlPoint0: RelativePoint + @Binding var controlPoint1: RelativePoint + @Binding var initialPoint0: CGSize + @Binding var initialPoint1: CGSize + + var curvePoint0: RelativePoint { + return (initialPoint0 + offsetPoint0).toPoint + } + + var curvePoint1: RelativePoint { + return (initialPoint1 + offsetPoint1).toPoint + } + + var body: some View { + + let primaryColor = Color.blue + let secondaryColor = primaryColor.opacity(0.7) + + return GeometryReader { reader in + + CurveShape(cp0: self.curvePoint0, cp1: self.curvePoint1) + .stroke(primaryColor, lineWidth: 4) + .foregroundColor(.teal) + + Path { p in + p.move(to: CGPoint(x: 0, y: 1 * reader.size.height)) + p.addLine(to: self.curvePoint0 * reader.size) + }.stroke(secondaryColor, lineWidth: 2) + + Path { p in + p.move(to: CGPoint(x: 1 * reader.size.width, y: 0)) + p.addLine(to: self.curvePoint1 * reader.size) + }.stroke(secondaryColor, lineWidth: 2) + + ControlPointHandle() + .offset(self.initialPoint0 * reader.size) + .foregroundColor(primaryColor) + .draggable(onChanged: { (size) in + self.offsetPoint0 = size / reader.size + self.controlPoint0 = self.curvePoint0 + }) + + ControlPointHandle() + .offset(self.initialPoint1 * reader.size) + .foregroundColor(primaryColor) + .draggable(onChanged: { (size) in + self.offsetPoint1 = size / reader.size + self.controlPoint1 = self.curvePoint1 + }) + } + .aspectRatio(contentMode: .fit) + } +} + +struct CurveEditorView_Previews: PreviewProvider { + static var previews: some View { + CurveEditorView(controlPoint0: .constant(.zero), controlPoint1: .constant(.zero), initialPoint0: .constant(.init(width: 0.8, height: 0.9)), initialPoint1: .constant(.init(width: 0.2, height: 0.1))) + } +} diff --git a/DMXEditor/DMXData.swift b/DMXEditor/DMXData.swift index 42d0d16..0314c57 100644 --- a/DMXEditor/DMXData.swift +++ b/DMXEditor/DMXData.swift @@ -9,8 +9,20 @@ import Foundation struct DMXData: Identifiable, Codable, Hashable { var id = UUID() - var address: Int - var value: Int + var address: Int { didSet { + if(address > 511){ + address = 511 + } else if (address < 1){ + address = 1 + } + }} + var value: Int { didSet { + if(value > 255){ + value = 255 + } else if (value < 0){ + value = 0 + } + }} static func getDefault() -> [DMXData]{ var result: [DMXData] = [] diff --git a/DMXEditor/DMXEditorApp.swift b/DMXEditor/DMXEditorApp.swift index d0795dd..cc1dc88 100644 --- a/DMXEditor/DMXEditorApp.swift +++ b/DMXEditor/DMXEditorApp.swift @@ -10,21 +10,20 @@ import SwiftUI @main struct DMXEditorApp: App { @State private var showSettings = false - @State private var initialPopup = false + @State private var showEditor = false; + @State var controlPoint0: RelativePoint = .init(x: 0.1, y: 0.2) + @State var controlPoint1: RelativePoint = .init(x: 0.3, y: 0.4) + @State var initialPoint0: CGSize = .init(width: 0.1, height: 0.2) + @State var initialPoint1: CGSize = .init(width: 0.3, height: 0.4) var body: some Scene { DocumentGroup(newDocument: DMXEditorDocument()) { file in if(showSettings == false){ EditView(showSettings: $showSettings, data: file.$document.documentData) - .frame(minWidth: 700) - .popover(isPresented: $initialPopup, content: { - VStack{ - Text("HI") - } - }) + .frame(minWidth: 1000) } else { SettingsView(showSettings: $showSettings, data: file.$document.documentData) - .frame(minWidth: 700) + .frame(minWidth: 900) } } } diff --git a/DMXEditor/DMXTransition.swift b/DMXEditor/DMXTransition.swift new file mode 100644 index 0000000..5b4d83f --- /dev/null +++ b/DMXEditor/DMXTransition.swift @@ -0,0 +1,122 @@ +// +// DMXAnimation.swift +// DMXEditor +// +// Created by Maximilian Inckmann on 16.11.22. +// + +import Foundation +import SwiftUI + +struct DMXTransition: Codable, Identifiable, Hashable { + + enum AnimationMode: String, Codable { + case none + case linear + case bezier + case fadeInFadeOut + } + + var id: UUID = UUID() + var mode: AnimationMode + var steps: Int + + func animate(from: [DMXData], to: [DMXData]) -> [[DMXData]]{ + if(steps > 0) { + switch mode{ + case .none: return [to] + case .linear: return animateLinear(from: from, to: to) + case .bezier: return animateBezier(from: from, to: to) + case .fadeInFadeOut: return animateBezier(from: from, to: to) + } + } else { + return [to] + } + } + + // MARK: Variables only necessary for Bezier animation + var bezierPoint0: CGPoint = .zero + var bezierPoint1: CGPoint = .zero +} + +// MARK: Linear animation +extension DMXTransition { + func animateLinear(from: [DMXData], to: [DMXData]) -> [[DMXData]]{ + var valueMatrix : [[DMXData]] = Array(repeating: DMXData.getDefault(), count: steps) + if steps > 0 { + for i in 0...steps-1{ + for j in 0...511 { + var value = from[j].value + if to[j].value > from[j].value { + value = Int(Double(from[j].value) + (Double(i)/Double(steps)) * Double(to[j].value - from[j].value)) + } else if to[j].value < from[j].value { + value = Int(Double(from[j].value) - (Double(i)/Double(steps)) * Double(from[j].value - to[j].value)) + } + + if value > 255 { + value = 255 + } else if value < 0 { + value = 0 + } + + valueMatrix[i][j] = DMXData(address: from[j].address, value: value) + } + } + } + valueMatrix[valueMatrix.count-1] = to + return valueMatrix + } +} + +extension CGPoint: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(x) + hasher.combine(y) + } +} + +// MARK: Animation with Bezier curve +extension DMXTransition { + func animateBezier(from: [DMXData], to: [DMXData]) -> [[DMXData]]{ + var result : [[DMXData]] = Array(repeating: DMXData.getDefault(), count: steps + 1) + + if (bezierPoint0 != .zero && bezierPoint1 != .zero){ + for i in 0...from.count-1 { + let size:Double = Double(from[i].value - to[i].value) + + let start = CGPoint(x: 0.0, y: size) + let end = CGPoint(x: size, y: 0.0) + + let p1 = CGPoint(x: bezierPoint0.x*size, y: bezierPoint0.y*size) + let p2 = CGPoint(x: bezierPoint1.x*size, y: bezierPoint1.y*size) + + let bezierCurve: [CGPoint] = bPolyCubicBezier(firstPoint: start, secondPoint: p1, thirdPoint: p2, lastPoint: end, steps: steps) + for j in 0...bezierCurve.count-1 { + let value = from[i].value - Int(bezierCurve[j].x) + result[j][i] = DMXData(address: from[i].address, value: value) + } + } + } + + result[result.count-1] = to + return result + } + + private func bPolyCubicBezier(firstPoint: CGPoint, secondPoint: CGPoint, thirdPoint: CGPoint, lastPoint: CGPoint, steps: Int) -> [CGPoint] { + var m = [CGPoint]() + for t in stride(from: 0, to: 1.01, by: 1.01 / Double(steps)) { + let s:Double = 1.0 - Double(t) + let t2:Double = pow(t,2) + let t3:Double = pow(t,3) + let s2:Double = pow(s,2) + let s3:Double = pow(s,3) + + let x1 = (Double(firstPoint.x) * s3) + Double(3 * secondPoint.x) * (s2 * Double(t)) + Double(3 * thirdPoint.x) * (s * t2) + (Double(lastPoint.x) * t3) + let y1 = (Double(firstPoint.y) * s3) + Double(3 * secondPoint.y) * (s2 * Double(t)) + Double(3 * thirdPoint.y) * (s * t2) + (Double(lastPoint.y) * t3) + + let np = CGPoint(x: x1, y: y1) + m.append(np) + } + return m + } +} diff --git a/DMXEditor/Dragging.swift b/DMXEditor/Dragging.swift new file mode 100644 index 0000000..4524de3 --- /dev/null +++ b/DMXEditor/Dragging.swift @@ -0,0 +1,55 @@ +// +// Dragging.swift +// MicroMove +// +// Created by Vasilis Akoinoglou on 26/2/20. +// Copyright © 2020 Vasilis Akoinoglou. All rights reserved. +// + +import SwiftUI + +// MARK: - Modifier Implementation +struct Draggable: ViewModifier { + @State var isDragging: Bool = false + + @State var offset: CGSize = .zero + @State var dragOffset: CGSize = .zero + + var onChanged: ((CGSize) -> Void)? + var onEnded: ((CGSize) -> Void)? + + func body(content: Content) -> some View { + let drag = DragGesture() + .onChanged { (value) in + self.offset = self.dragOffset + value.translation + self.isDragging = true + self.onChanged?(self.offset) + }.onEnded { (value) in + self.isDragging = false + self.offset = self.dragOffset + value.translation + self.dragOffset = self.offset + self.onEnded?(self.offset) + } + return content.offset(offset).gesture(drag) + } +} + +// MARK: - ViewBuilder Implementation +//struct DraggableView: View where Content: View { +// let content: () -> Content +// +// init(@ViewBuilder content: @escaping () -> Content) { +// self.content = content +// } +// +// var body: some View { +// return content().modifier(Draggable(updating: updating)) +// } +// +//} + +extension View { + func draggable(onChanged: ((CGSize) -> Void)? = nil, onEnded: ((CGSize) -> Void)? = nil) -> some View { + return self.modifier(Draggable(onChanged: onChanged, onEnded: onEnded)) + } +} diff --git a/DMXEditor/Frame.swift b/DMXEditor/Frame.swift new file mode 100644 index 0000000..1136147 --- /dev/null +++ b/DMXEditor/Frame.swift @@ -0,0 +1,50 @@ +// +// Delay.swift +// DMXEditor +// +// Created by Maximilian Inckmann on 11.11.22. +// + +import Foundation + +struct Frame: Identifiable, Codable, Comparable, Hashable { + static func == (lhs: Frame, rhs: Frame) -> Bool { + return lhs.id == rhs.id + } + + static func < (lhs: Frame, rhs: Frame) -> Bool { + return lhs.relativeTimeInSeconds < rhs.relativeTimeInSeconds + } + + enum CodingKeys: String, CodingKey{ + case id + case relativeTimeInSeconds + case dmxData + case transition + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + id = try values.decode(UUID.self, forKey: .id) + relativeTimeInSeconds = try values.decode(Double.self, forKey: .relativeTimeInSeconds) + dmxData = try values.decode([DMXData].self, forKey: .dmxData) + transition = try values.decodeIfPresent(DMXTransition.self, forKey: .transition) ?? DMXTransition(mode: .fadeInFadeOut, steps: 32) + } + + init(relativeTimeInSeconds: Double, dmxData: [DMXData]){ + self.relativeTimeInSeconds = relativeTimeInSeconds + self.dmxData = dmxData + transition = DMXTransition(mode: .fadeInFadeOut, steps: 32) + } + + init(relativeTimeInSeconds: Double, dmxData: [DMXData], transition: DMXTransition){ + self.relativeTimeInSeconds = relativeTimeInSeconds + self.dmxData = dmxData + self.transition = transition + } + + var id = UUID() + var relativeTimeInSeconds: Double + var dmxData: [DMXData] + var transition: DMXTransition +} diff --git a/DMXEditor/GeometryExtensions.swift b/DMXEditor/GeometryExtensions.swift new file mode 100644 index 0000000..3c25c26 --- /dev/null +++ b/DMXEditor/GeometryExtensions.swift @@ -0,0 +1,41 @@ +// +// GeometryExtensions.swift +// MicroMove +// +// Created by Vasilis Akoinoglou on 26/2/20. +// Copyright © 2020 Vasilis Akoinoglou. All rights reserved. +// + +import Foundation + +typealias AbsolutePoint = CGPoint +typealias RelativePoint = CGPoint + +func * (lhs: CGSize, rhs: CGSize) -> CGSize { + .init(width: lhs.width * rhs.width, height: lhs.height * rhs.height) +} + +func * (lhs: CGPoint, rhs: CGSize) -> CGPoint { + .init(x: lhs.x * rhs.width, y: lhs.y * rhs.height) +} + +func - (lhs: CGPoint, rhs: CGFloat) -> CGPoint { + .init(x: lhs.x - rhs, y: lhs.y - rhs) +} + +func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { + .init(x: lhs.x + rhs.x, y: lhs.y + rhs.y) +} + +func + (lhs: CGSize, rhs: CGSize) -> CGSize { + .init(width: lhs.width + rhs.width, height: lhs.height + rhs.height) +} + +func / (lhs: CGSize, rhs: CGSize) -> CGSize { + .init(width: lhs.width / rhs.width, height: lhs.height / rhs.height) +} + +extension CGSize { + var toPoint: CGPoint { .init(x: width, y: height) } + var half: CGSize { .init(width: width/2, height: height/2) } +} diff --git a/DMXEditor/MultiSliderEditable.swift b/DMXEditor/MultiSliderEditable.swift index f8b9535..134e1c6 100644 --- a/DMXEditor/MultiSliderEditable.swift +++ b/DMXEditor/MultiSliderEditable.swift @@ -22,17 +22,17 @@ struct MultiSliderEditable: View { HStack { HStack { Text("Address: ") - NumberField(value: $device.address[0], min: 0, max: 511) + NumberField(value: $device.address[0], min: 1, max: 511) } HStack { Text("Address: ") - NumberField(value: $device.address[1], min: 0, max: 511) + NumberField(value: $device.address[1], min: 1, max: 511) } HStack { Text("Address: ") - NumberField(value: $device.address[2], min: 0, max: 511) + NumberField(value: $device.address[2], min: 1, max: 511) } } } diff --git a/DMXEditor/NumberField.swift b/DMXEditor/NumberField.swift index 77750cf..4228427 100644 --- a/DMXEditor/NumberField.swift +++ b/DMXEditor/NumberField.swift @@ -15,7 +15,7 @@ struct NumberField: View { var body: some View { TextField("", value: $value, format: .number) .textFieldStyle(.roundedBorder) - .frame(width: 45) + .frame(width: 65) .disableAutocorrection(true) .onSubmit { print(value) diff --git a/DMXEditor/OLAHandler.swift b/DMXEditor/OLAHandler.swift index 660c060..360e18a 100644 --- a/DMXEditor/OLAHandler.swift +++ b/DMXEditor/OLAHandler.swift @@ -7,6 +7,20 @@ import Foundation +func sendAnimatedValues(serverAddress: String, universe: Int = 0, previousData: [DMXData], goalData:[DMXData], transition: DMXTransition) { + let valueMatrix = transition.animate(from: previousData, to: goalData) + for i in valueMatrix { + var values: [Int] = [] + for j in i { + values.append(j.value) + } + print(values) + sendDataToServer(universe: universe, data: values, serverAddress: serverAddress) + } + print("Finished sending data") + // print(valueMatrix) +} + func sendValues(serverAddress: String, universe: Int = 0, previousData: [DMXData], goalData:[DMXData], amountSteps: Int) { var valueMatrix : [[Int]] = Array(repeating: Array(repeating: 0, count: 512), count: amountSteps+1) if amountSteps > 0 { diff --git a/DMXEditor/RGBPicker.swift b/DMXEditor/RGBPicker.swift index 9442ac3..936d433 100644 --- a/DMXEditor/RGBPicker.swift +++ b/DMXEditor/RGBPicker.swift @@ -12,37 +12,12 @@ struct RGBPicker: View { @Binding var red: Int @Binding var green: Int @Binding var blue: Int - @State private var showPicker = false var body: some View { VStack{ - if(showPicker){ - ColorPicker("", selection: $color) - .controlSize(.large) - .onChange(of: color) { [color] newState in - let rCol = color.cgColor?.components?[0] - let gCol = color.cgColor?.components?[1] - let bCol = color.cgColor?.components?[2] - - let r = Int((rCol ?? 0) * 255) - let g = Int((gCol ?? 0) * 255) - let b = Int((bCol ?? 0) * 255) - - red = r - green = g - blue = b - } - - Button(action: { - showPicker = false - }, label: { - Spacer() - Text("Submit") - .foregroundColor(.teal) - Spacer() - }) - .buttonStyle(.borderless) - .onSubmit { + ColorPicker("", selection: $color) + .controlSize(.large) + .onChange(of: color) { [color] newState in let rCol = color.cgColor?.components?[0] let gCol = color.cgColor?.components?[1] let bCol = color.cgColor?.components?[2] @@ -55,25 +30,10 @@ struct RGBPicker: View { green = g blue = b } - - } else { - Button(action: { - showPicker = true - }, label: { - Spacer() - Text("Show Picker") - .foregroundColor(.teal) - // .padding() - Spacer() - }) - .buttonStyle(.borderless) - } - - Text("R \(red)") - .foregroundColor(.teal) - Text("G \(green)") + + Text("RGB \(red) \(green) \(blue)") .foregroundColor(.teal) - Text("B \(blue)") + Text("Hex \(String(format:"%02X", red))\(String(format:"%02X", green))\(String(format:"%02X", blue))") .foregroundColor(.teal) } .padding() diff --git a/DMXEditor/Settings.swift b/DMXEditor/Settings.swift index 7d28987..6a3f7b4 100644 --- a/DMXEditor/Settings.swift +++ b/DMXEditor/Settings.swift @@ -9,14 +9,13 @@ import Foundation struct Settings: Identifiable, Codable, Equatable { static func == (lhs: Settings, rhs: Settings) -> Bool { - return lhs.host == rhs.host && lhs.universe == rhs.universe && lhs.devices == rhs.devices && lhs.transitionSteps == rhs.transitionSteps + return lhs.host == rhs.host && lhs.universe == rhs.universe && lhs.devices == rhs.devices } var id = UUID() var host: String var universe: Int = 0 var devices: [Device] - var transitionSteps: Int = 0 func getHighestAddress() -> Int { var maxAddress = 0 diff --git a/DMXEditor/SingleSlider.swift b/DMXEditor/SingleSlider.swift index 252cb59..98d6a4d 100644 --- a/DMXEditor/SingleSlider.swift +++ b/DMXEditor/SingleSlider.swift @@ -28,7 +28,7 @@ struct SingleSlider: View { Stepper(value: $data.value, in: 0...255){ NumberField(value: $data.value, min: 0, max: 255) - } + }.frame(minWidth:50) } .frame(minWidth: 400) } diff --git a/DMXEditor/SingleSliderEditable.swift b/DMXEditor/SingleSliderEditable.swift index 298b6c9..b6c53fa 100644 --- a/DMXEditor/SingleSliderEditable.swift +++ b/DMXEditor/SingleSliderEditable.swift @@ -18,7 +18,7 @@ struct SingleSliderEditable: View { .frame(maxWidth: 125) Text("Address: ") - NumberField(value: $device.address[0], min: 0, max: 511) + NumberField(value: $device.address[0], min: 1, max: 511) } } } diff --git a/DMXEditor/Slide.swift b/DMXEditor/Slide.swift index c6707d1..9a35aba 100644 --- a/DMXEditor/Slide.swift +++ b/DMXEditor/Slide.swift @@ -9,12 +9,60 @@ import Foundation import UniformTypeIdentifiers import SwiftUI -struct Slide: Identifiable, Codable, Hashable { +struct Slide: Identifiable, Codable, Equatable, Hashable { static func == (lhs: Slide, rhs: Slide) -> Bool { return lhs.number == rhs.number } + enum DeCodingKeys: String, CodingKey{ + case id + case number + case dmxData + case frames + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: DeCodingKeys.self) + id = try values.decode(UUID.self, forKey: .id) + number = try values.decode(Int.self, forKey: .number) + frames = try values.decodeIfPresent([Frame].self, forKey: .frames) ?? [] + frames.sort() + if let data0 = try values.decodeIfPresent([DMXData].self, forKey: .dmxData){ + frames.append(Frame(relativeTimeInSeconds: 0, dmxData: data0)) + } + } + + init(number: Int, dmxData: [DMXData]?, frames: [Frame]?){ + self.number = number + + if(frames != nil) { + self.frames = frames! + } else { + self.frames = [] + } + + if(dmxData != nil) { + self.frames.append(Frame(relativeTimeInSeconds: 0, dmxData: dmxData!)) + } else { + self.frames.append(Frame(relativeTimeInSeconds: 0, dmxData: DMXData.getDefault())) + } + + self.frames.sort() + } + + init(number: Int, frames: [Frame]?){ + self.number = number + + if(frames != nil) { + self.frames = frames! + } else { + self.frames = [Frame(relativeTimeInSeconds: 0, dmxData: DMXData.getDefault())] + } + + self.frames.sort() + } + var id = UUID() var number: Int - var dmxData: [DMXData] + var frames: [Frame] } diff --git a/DMXEditor/StepPicker.swift b/DMXEditor/StepPicker.swift new file mode 100644 index 0000000..d65abb2 --- /dev/null +++ b/DMXEditor/StepPicker.swift @@ -0,0 +1,25 @@ +// +// StepPicker.swift +// DMXEditor +// +// Created by Maximilian Inckmann on 25.02.23. +// + +import SwiftUI + +struct StepPicker: View { + @Binding var steps: Int + var body: some View { + HStack{ + Text("Steps") + NumberField(value: $steps, min: 0, max: 255) + } + .help("Number of steps for the animation (0-255)") + } +} + +struct StepPicker_Previews: PreviewProvider { + static var previews: some View { + StepPicker(steps: .constant(42)) + } +} diff --git a/DMXEditor/Views/EditView.swift b/DMXEditor/Views/EditView.swift index d7648aa..930f460 100644 --- a/DMXEditor/Views/EditView.swift +++ b/DMXEditor/Views/EditView.swift @@ -12,210 +12,310 @@ struct EditView: View { let utType = UTType.utf8PlainText @Binding var showSettings: Bool @Binding var data: ProjectData + @State private var visibility: NavigationSplitViewVisibility = .all @State private var showAlert: Bool = false + @State private var showTimerAlert: Bool = false @State private var selectedSlide: Int? = nil + @State private var selectedFrame: Frame? = nil + @State private var selectedFrameIndex: Int? = nil @State private var activePresentation: Bool = false @State private var activePreview: Bool = false @State private var undefinedSlides: Bool = false @State private var highestUnavailableSlides: Int = 0 + @State private var lastDMXData:[DMXData] = DMXData.getDefault() + @State private var activeTasks = [Task{try await Task.sleep(nanoseconds:1)}] var body: some View { - NavigationView{ - VStack{ - List(selection: $selectedSlide){ - ForEach($data.slides, id: \.self.number){ slide in - NavigationLink("Slide \(slide.wrappedValue.number)", destination: SlideView(slide: slide, devices: $data.settings.devices)) - } - .onMove { indices, destination in - let startIndex = indices.first! - var destinationIndex:Int = destination - - if startIndex > destinationIndex { - for i in destinationIndex...startIndex{ - data.slides[i].number = i + 2 - } - } else if startIndex < destinationIndex { - if destination > 0 { - destinationIndex = destination - 1 + VStack{ + NavigationSplitView(columnVisibility: $visibility){ + VStack{ + List(selection: $selectedSlide){ + ForEach(data.slides, id: \.self.number){ slide in + NavigationLink(value: slide){ + HStack{ + Image(systemName: "display") + Text("Slide \(slide.number)") + } } - for i in startIndex+1...destinationIndex{ - data.slides[i].number = i + } + .onMove { indices, destination in + let startIndex = indices.first! + var destinationIndex:Int = destination + + if startIndex > destinationIndex { + for i in destinationIndex...startIndex{ + data.slides[i].number = i + 2 + } + } else if startIndex < destinationIndex { + if destination > 0 { + destinationIndex = destination - 1 + } + for i in startIndex+1...destinationIndex{ + data.slides[i].number = i + } } + + data.slides[startIndex].number = destinationIndex + 1 + data.slides.move(fromOffsets: indices, toOffset: destination) } - - data.slides[startIndex].number = destinationIndex + 1 - data.slides.move(fromOffsets: indices, toOffset: destination) - } - } - .onCopyCommand{ - let jsonSlide = try! JSONEncoder().encode(data.slides[selectedSlide!-1]) - print(jsonSlide.base64EncodedString()) - return [NSItemProvider(object: NSString(string: jsonSlide.base64EncodedString()))] - } - .onPasteCommand(of: [self.utType]){data in - loadPastedSlide(from: data) - } - .onDeleteCommand(perform: {showAlert = true}) - .onMoveCommand{ i in - if i == MoveCommandDirection.down { - if selectedSlide! > 0{ - selectedSlide! -= 1 - } else { - selectedSlide! = 0 - } - } else if i == MoveCommandDirection.up { - if selectedSlide! < data.slides.count - 1{ - selectedSlide! += 1 - } else { - selectedSlide! = data.slides.count - 1 + .navigationTitle("Slides") + .onCopyCommand{ + let jsonSlide = try! JSONEncoder().encode(data.slides[selectedSlide!-1]) + print(jsonSlide.base64EncodedString()) + return [NSItemProvider(object: NSString(string: jsonSlide.base64EncodedString()))] + } + .onPasteCommand(of: [self.utType]){data in + loadPasted(from: data) + } + .onDeleteCommand(perform: {showAlert = true}) + .onMoveCommand{ i in + if i == MoveCommandDirection.down { + if selectedSlide! > 0{ + selectedSlide! -= 1 + } else { + selectedSlide! = 0 + } + } else if i == MoveCommandDirection.up { + if selectedSlide! < data.slides.count - 1{ + selectedSlide! += 1 + } else { + selectedSlide! = data.slides.count - 1 + } } } - } - .alert(isPresented: $showAlert){ - Alert(title: Text((selectedSlide == data.slides.count) ? "Delete Slide" : "Delete Content"), - message: Text((selectedSlide == data.slides.count) ? "Are you sure you want to delete this slide?" : "Are you sure you want to delete the content of this slide?"), - primaryButton: .destructive( - Text("Delete"), - action: ({ - if(selectedSlide != nil){ - if (selectedSlide == data.slides.count){ + .alert(isPresented: $showAlert){ + Alert(title: Text("Delete Slide" ), + message: Text("Are you sure you want to delete this slide?"), + primaryButton: .destructive( + Text("Delete"), + action: ({ + if(selectedSlide != nil){ + for i in selectedSlide!-1...data.slides.count-1{ + data.slides[i].number -= 1 + } data.slides.remove(at: selectedSlide!-1) - } else { - data.slides[selectedSlide! - 1 ].dmxData = DMXData.getDefault() + selectedSlide = nil + print("Deleted") } + + }) + ), + secondaryButton: .cancel( + Text("Cancel"), + action: ({ + print("Canceled") + }) + ) + ) + } + + Divider() + + Button(action: {addSlide()}, label: { + HStack{ + Image(systemName: "plus") + .foregroundColor(Color.primary) + Text("Add Slide") .foregroundColor(Color.primary) + } + }) + .buttonStyle(.borderless) + + Spacer() + } + } content: { + VStack{ + if let selectedSlide = selectedSlide { + List(data.slides[selectedSlide-1].frames, id:\.id ,selection: $selectedFrame){ frame in + NavigationLink(value: frame) { + HStack{ + Image(systemName: "timer") + Text("+ \(frame.relativeTimeInSeconds.formatted()) s") } - selectedSlide = nil - print("Delete") - }) - ), - secondaryButton: .cancel( - Text("Cancel"), - action: ({ - print("Cancel") - }) - ) - ) + } + } + .navigationTitle("Slide \(selectedSlide)") + .onCopyCommand{ + if let selectedFrame{ + let jsonFrame = try! JSONEncoder().encode(selectedFrame) + print(jsonFrame.base64EncodedString()) + return [NSItemProvider(object: NSString(string: jsonFrame.base64EncodedString()))] + } + return [] + } + .onPasteCommand(of: [self.utType]){data in + loadPasted(from: data) + } + .onDeleteCommand(perform: {showTimerAlert = true}) + .alert(isPresented: $showTimerAlert){ + Alert(title: Text("Delete Timer"), + message: Text("Are you sure you want to delete this timer?"), + primaryButton: .destructive( + Text("Delete"), + action: ({ + if(selectedFrame != nil){ + let frameIndex = data.slides[selectedSlide-1].frames.firstIndex(where: { f in + return f.id == selectedFrame!.id + }) + + data.slides[selectedSlide-1].frames.remove(at: frameIndex!) + selectedFrame = nil + print("Delete") + } + }) + ), + secondaryButton: .cancel( + Text("Cancel") + ) + ) + } + + Divider() + + Button(action: {addTimer()}, label: { + HStack{ + Image(systemName: "plus") + .foregroundColor(Color.primary) + Text("Add Timer") .foregroundColor(Color.primary) + } + }) + .buttonStyle(.borderless) + + Spacer() + } else { + Text("Please select a slide!") + } } - - Divider() - - Button(action: {addSlide()}, label: { - HStack{ - Image(systemName: "plus") - .foregroundColor(Color.primary) - Text("Add Slide") .foregroundColor(Color.primary) + } detail: { + if let selectedSlide, let selectedFrame = selectedFrame, let frameIndex = data.slides[selectedSlide-1].frames.firstIndex(where: { f in + return f.id == selectedFrame.id + }){ + FrameView(slide: $data.slides[selectedSlide-1], frame: $data.slides[selectedSlide-1].frames[frameIndex], devices: $data.settings.devices) + } else { + VStack{ + Text("To start you must first create slides and configure devices.") + + Button(action: {addSlide()}, label: { + HStack{ + Image(systemName: "plus") + .foregroundColor(Color.primary) + Text("Add Slide") + .foregroundColor(Color.primary) + } + .frame(minWidth: 110) + }) + + Button(action: { + showSettings = true + }, label: { + HStack{ + Image(systemName: "gear") + .foregroundColor(Color.primary) + Text("Go to Settings") + .foregroundColor(Color.primary) + } + .frame(minWidth: 110) + }) } - }) - .buttonStyle(.borderless) - - Spacer() + .navigationSplitViewColumnWidth(min: 500, ideal: 1000) + } } - - VStack{ - Text("To start you must first create slides and configure devices.") + .toolbar(){ + ToolbarItem{ + Button(action: { + activePreview.toggle() + activePresentation = false + preview() + }, label: { + VStack{ + Image(systemName: activePreview ? "stop.fill" : "play.fill") + Text(activePreview ? "Stop preview" : "Start preview") + } + .foregroundColor(.primary) + }) + .padding(.trailing) + .buttonStyle(.borderless) + } - Button(action: {addSlide()}, label: { - HStack{ - Image(systemName: "plus") - .foregroundColor(Color.primary) - Text("Add Slide") - .foregroundColor(Color.primary) - } - .frame(minWidth: 110) - }) + ToolbarItem{ + Button(action: { + activePresentation.toggle() + activePreview = false + present() + }, label: { + VStack{ + Image(systemName: activePresentation ? "stop.fill" : "play.fill") + Text(activePresentation ? "Stop presentation" : "Start presentation") + } + .foregroundColor(.primary) + }) + .padding(.trailing) + .buttonStyle(.borderless) + } - Button(action: { - showSettings = true - }, label: { - HStack{ - Image(systemName: "gear") - .foregroundColor(Color.primary) - Text("Go to Settings") - .foregroundColor(Color.primary) - } - .frame(minWidth: 110) - }) - } - } - .toolbar(){ - ToolbarItem{ - Button(action: { - activePreview.toggle() - activePresentation = false - preview() - }, label: { - VStack{ - Image(systemName: activePreview ? "stop.fill" : "play.fill") - Text(activePreview ? "Stop preview" : "Start preview") - } - .foregroundColor(.primary) - }) - .padding(.trailing) - .buttonStyle(.borderless) - } - - ToolbarItem{ - Button(action: { - activePresentation.toggle() - activePreview = false - present() - }, label: { - VStack{ - Image(systemName: activePresentation ? "stop.fill" : "play.fill") - Text(activePresentation ? "Stop presentation" : "Start presentation") - } - .foregroundColor(.primary) - }) - .padding(.trailing) - .buttonStyle(.borderless) - } - - ToolbarItem{ - Button(action: { - showSettings = true - }, label: { - VStack{ - Image(systemName: "gear") - Text("Settings") - } - .foregroundColor(.primary) - }) - .padding(.trailing) - .buttonStyle(.borderless) + ToolbarItem{ + Button(action: { + showSettings = true + }, label: { + VStack{ + Image(systemName: "gear") + Text("Settings") + } + .foregroundColor(.primary) + }) + .padding(.trailing) + .buttonStyle(.borderless) + } } - } - .alert(isPresented: $undefinedSlides){ - Alert(title: Text("No data available from slide \(data.slides.count + 1) to \(highestUnavailableSlides)"), - message: Text("As there are no entries for the slides so far, it is recommended to add entries for these slides to the editor. Otherwise the last defined value will be lasting until the end but is not controllable by the presentation."), - primaryButton: .cancel( - Text("+ Add slides to editor"), - action: { - for i in data.slides.count...highestUnavailableSlides-1 { - print("Adding Slide \(i)") - if data.slides.count >= 1 { - addSlide(valueOfSlide: data.slides.count) - } else { - addSlide() + .alert(isPresented: $undefinedSlides){ + Alert(title: Text("No data available from slide \(data.slides.count + 1) to \(highestUnavailableSlides)"), + message: Text("As there are no entries for the slides so far, it is recommended to add entries for these slides to the editor. Otherwise the last defined value will be lasting until the end but is not controllable by the presentation."), + primaryButton: .cancel( + Text("+ Add slides to editor"), + action: { + for i in data.slides.count...highestUnavailableSlides-1 { + print("Adding Slide \(i)") + if data.slides.count >= 1 { + addSlide(valueOfSlide: data.slides.count) + } else { + addSlide() + } } - } - }), - secondaryButton: .cancel()) + }), + secondaryButton: .cancel()) + } } } func present() { - DispatchQueue.global(qos: .background).async { - var last: Int = 1 + Task(priority:.background){ + var last: Int = 0 while activePresentation { let actual = getSlide() - if actual != nil && actual != last && actual! <= data.slides.count{ - sendValues( - serverAddress: data.settings.host, - universe: data.settings.universe, - previousData: data.slides[last-1].dmxData, - goalData: data.slides[actual!-1].dmxData, - amountSteps: data.settings.transitionSteps) + if actual != nil && actual != last && actual! <= data.slides.count { + for i in activeTasks{ + i.cancel() + } + activeTasks = [] + print("Cancelled all Tasks") + for i in data.slides[actual!-1].frames{ + activeTasks.append(Task(priority:.background){ + let preSleepSlide: Int = getSlide()! + let delayInNs: UInt64 = UInt64(i.relativeTimeInSeconds*1_000_000_000) + if (delayInNs > 0) { + print("Initiating sleep for \(delayInNs) ns - Slide \(preSleepSlide)") + try await Task.sleep(nanoseconds: delayInNs) + } + print("Sending data with delay of \(delayInNs) ns - Slide \(preSleepSlide)") + sendAnimatedValues( + serverAddress: data.settings.host, + universe: data.settings.universe, + previousData: lastDMXData, + goalData: i.dmxData, + transition: i.transition) + lastDMXData = i.dmxData + }) + } last = actual! } else if actual != nil && actual! > data.slides.count && highestUnavailableSlides != actual! { highestUnavailableSlides = actual! @@ -233,13 +333,13 @@ struct EditView: View { } func preview() { - DispatchQueue.global(qos: .background).async { + Task(priority:.background){ while activePreview { - if selectedSlide != nil { + if let selectedFrame { sendValues( serverAddress: data.settings.host, universe: data.settings.universe, - data: data.slides[selectedSlide!-1].dmxData) + data: selectedFrame.dmxData) } } sendValues( @@ -250,38 +350,85 @@ struct EditView: View { } func addSlide(valueOfSlide: Int) { - data.slides.append(Slide(number: (data.slides.count+1), dmxData: data.slides[valueOfSlide-1].dmxData)) + data.slides.append(Slide(number: data.slides.count + 1, frames: data.slides[valueOfSlide].frames)) } func addSlide() { if (selectedSlide != nil && data.slides.count > selectedSlide!) { - addSlide(valueOfSlide: selectedSlide!) + addSlide(valueOfSlide: selectedSlide! - 1) } else { - data.slides.append(Slide(number: (data.slides.count+1), dmxData: DMXData.getDefault())) + data.slides.append(Slide(number: (data.slides.count+1), dmxData: DMXData.getDefault(), frames: [])) } } - func loadPastedSlide(from array: [NSItemProvider]) { - guard let lastItem = array.last else { - assertionFailure("Nothing to paste") - return - } - lastItem.loadDataRepresentation(forTypeIdentifier: utType.identifier) { - (pasteData, error) in - guard error == nil else { - assertionFailure("Could not load data: \(error.debugDescription)") - return + func addTimer() { + if let selectedSlide{ + let defaultDMX:[DMXData] + if(data.slides[selectedSlide - 1].frames.count > 0 ){ + defaultDMX = data.slides[selectedSlide - 1].frames[data.slides[selectedSlide - 1].frames.count - 1].dmxData + } else { + defaultDMX = DMXData.getDefault() + } + + let newTime = (data.slides[selectedSlide - 1].frames.max()?.relativeTimeInSeconds ?? 0) + 2.5 + + if let selectedFrame{ + data.slides[selectedSlide - 1].frames.append(Frame(relativeTimeInSeconds: newTime, dmxData: selectedFrame.dmxData, transition: selectedFrame.transition)) + } else { + data.slides[selectedSlide - 1].frames.append(Frame(relativeTimeInSeconds: newTime, dmxData: defaultDMX)) } - guard let pasteData = pasteData else { - assertionFailure("Could not load data") + + data.slides[selectedSlide - 1].frames.sort() + } + } + + func loadPasted(from array: [NSItemProvider]) { + DispatchQueue.main.async{ + guard let lastItem = array.last else { + assertionFailure("Nothing to paste") return } - let parsedData = try! JSONDecoder().decode(Slide.self, from: Data(base64Encoded: pasteData)!) - print(parsedData) - if(parsedData.number == selectedSlide!){ - data.slides.append(Slide(number: data.slides.count + 1, dmxData: parsedData.dmxData)) - } else { - data.slides[selectedSlide!-1].dmxData = parsedData.dmxData + lastItem.loadDataRepresentation(forTypeIdentifier: utType.identifier) { + (pasteData, error) in + guard error == nil else { + assertionFailure("Could not load data: \(error.debugDescription)") + return + } + guard let pasteData = pasteData else { + assertionFailure("Could not load data") + return + } + + // check if Slide can be parsed + if var parsedSlide = try? JSONDecoder().decode(Slide.self, from: Data(base64Encoded: pasteData) ?? Data()){ + + parsedSlide.id = UUID() + + print(parsedSlide) + + if(parsedSlide.number == selectedSlide!){ + data.slides.append(Slide(number: data.slides.count + 1, frames: parsedSlide.frames)) + } else { + data.slides[selectedSlide!-1].frames = parsedSlide.frames + } + } + // check if Frame can be parsed + else if var parsedData = try? JSONDecoder().decode(Frame.self, from: Data(base64Encoded: pasteData)!){ + print(parsedData) + + parsedData.id = UUID() + + if let selectedFrame, let selectedSlide, let frameIndex = data.slides[selectedSlide-1].frames.firstIndex(where: { f in + return f.id == selectedFrame.id + }){ + data.slides[selectedSlide - 1].frames[frameIndex].dmxData = parsedData.dmxData + data.slides[selectedSlide - 1].frames[frameIndex].transition = parsedData.transition + } else { + data.slides[selectedSlide! - 1].frames.append(parsedData) + } + data.slides[selectedSlide!-1].frames.sort() + print("Pasted") + } } } } diff --git a/DMXEditor/Views/FrameView.swift b/DMXEditor/Views/FrameView.swift new file mode 100644 index 0000000..ad2ad94 --- /dev/null +++ b/DMXEditor/Views/FrameView.swift @@ -0,0 +1,143 @@ +// +// TimerView.swift +// DMXEditor +// +// Created by Maximilian Inckmann on 11.11.22. +// + +import SwiftUI + +struct FrameView: View { + @Binding var slide: Slide + @Binding var frame: Frame + @Binding var devices: [Device] + + @State private var showEditor = false; + @State var initialPoint0: CGSize = .init(width: 0.1, height: 0.2) + @State var initialPoint1: CGSize = .init(width: 0.3, height: 0.4) + + var body: some View { + VStack{ + ScrollView { + HStack{ + let bind = Binding(get: { + Double($frame.relativeTimeInSeconds.wrappedValue) + }, set: { + $frame.relativeTimeInSeconds.wrappedValue = Double(String(format:"%.2f", $0))! + slide.frames.sort() + }) + + Slider(value: bind, in: 0...20){ + Text("Delay in seconds") + } + + Stepper(value: bind, in: 0...255){ + TextField("", value: bind, format: .number) + .textFieldStyle(.roundedBorder) + .disableAutocorrection(true) + .frame(width: 65) + } + } + + HStack { + Picker(selection: $frame.transition.mode, label: Text("Animation mode")) { + Text("No Animation") + .tag(DMXTransition.AnimationMode.none) + Text("Linear Animation") + .tag(DMXTransition.AnimationMode.linear) + Text("Fade in - Fade out") + .tag(DMXTransition.AnimationMode.fadeInFadeOut) + Text("Custom Bezier Animation") + .tag(DMXTransition.AnimationMode.bezier) + } + + switch(frame.transition.mode){ + case .linear: StepPicker(steps: $frame.transition.steps) + case .bezier: + HStack{ + StepPicker(steps: $frame.transition.steps) + Button("Adjust Bezier Curve"){ + if(frame.transition.bezierPoint0 != .zero && frame.transition.bezierPoint1 != .zero){ + let controlPoint0 = frame.transition.bezierPoint0 + let controlPoint1 = frame.transition.bezierPoint1 + if (controlPoint0 != .zero && controlPoint1 != .zero){ + initialPoint0 = .init( + width: controlPoint0.x, + height: controlPoint0.y) + initialPoint1 = .init( + width: controlPoint1.x, + height: controlPoint1.y) + } + } else { + initialPoint0 = CGSize(width: 0.4, height: 0.3) + initialPoint1 = CGSize(width: 0.6, height: 0.6) + frame.transition.bezierPoint0 = initialPoint0.toPoint + frame.transition.bezierPoint1 = initialPoint1.toPoint + } + showEditor = true + } + .popover(isPresented: $showEditor, arrowEdge: .trailing){ + VStack{ + CurveEditor(controlPoint0: $frame.transition.bezierPoint0, + controlPoint1: $frame.transition.bezierPoint1, + initialPoint0: $initialPoint0, + initialPoint1: $initialPoint1) + Button("Reset"){ + initialPoint0 = CGSize(width: 0.4, height: 0.3) + initialPoint1 = CGSize(width: 0.6, height: 0.6) + frame.transition.bezierPoint0 = initialPoint0.toPoint + frame.transition.bezierPoint1 = initialPoint1.toPoint + showEditor = false + } + }.padding() + } + } + case .fadeInFadeOut: + StepPicker(steps: $frame.transition.steps) + .onAppear(){ + initialPoint0 = CGSize(width: 0.8, height: 0.9) + initialPoint1 = CGSize(width: 0.2, height: 0.1) + frame.transition.bezierPoint0 = initialPoint0.toPoint + frame.transition.bezierPoint1 = initialPoint1.toPoint + showEditor = false + } + default: Spacer() + } + } + + Divider() + .padding(.bottom) + + ForEach(devices){ device in + VStack { + HStack { + Text(device.name) + .bold() + .font(.title3) + + Spacer() + } + + if (device.multipleSliders){ + MultiSlider( + dataR: $frame.dmxData[device.address[0]-1], + dataG: $frame.dmxData[device.address[1]-1], + dataB: $frame.dmxData[device.address[2]-1]) + .padding(.leading) + } else { + SingleSlider(data: $frame.dmxData[device.address[0]-1]) + .padding(.leading) + } + } .padding(.bottom) + } + } + .padding() + } + } +} + +struct TimerView_Previews: PreviewProvider { + static var previews: some View { + FrameView(slide: .constant(Slide(number: 23, frames: [])), frame: .constant(Frame(relativeTimeInSeconds: 2.4, dmxData: [DMXData(address: 0, value: 1)])), devices: .constant(ProjectData.defaultData.settings.devices)) + } +} diff --git a/DMXEditor/Views/GeneralSettingsView.swift b/DMXEditor/Views/GeneralSettingsView.swift index d3c8d58..b212b9a 100644 --- a/DMXEditor/Views/GeneralSettingsView.swift +++ b/DMXEditor/Views/GeneralSettingsView.swift @@ -41,29 +41,6 @@ struct GeneralSettingsView: View { } .padding(.horizontal) - HStack{ - VStack{ - Text("Duration of the transition in Steps") - .frame(width:250) - Text("(16 Steps = 1s)") - .frame(width:250) - .font(.footnote) - .foregroundColor(.gray) - } - Spacer() - TextField("", value: $settings.transitionSteps, format: .number) - .textFieldStyle(.roundedBorder) - .padding(.horizontal) - .disableAutocorrection(true) - .onSubmit { - print(settings.transitionSteps) - if settings.transitionSteps < 0 { - settings.transitionSteps = 0 - } - } - } - .padding(.horizontal) - Spacer() } } diff --git a/DMXEditor/Views/SlideView.swift b/DMXEditor/Views/SlideView.swift deleted file mode 100644 index 983dfa8..0000000 --- a/DMXEditor/Views/SlideView.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// SlideView.swift -// DMXEditorForKeynote -// -// Created by Maximilian Inckmann on 14.02.22. -// - -import SwiftUI - -struct SlideView: View { - @Binding var slide: Slide - @Binding var devices: [Device] - - var body: some View { - ScrollView { - ForEach(devices){ device in - VStack { - HStack { - Text(device.name) - .bold() - .font(.title3) - - Spacer() - } - - if (device.multipleSliders){ - MultiSlider( - dataR: $slide.dmxData[device.address[0]-1], - dataG: $slide.dmxData[device.address[1]-1], - dataB: $slide.dmxData[device.address[2]-1]) - .padding(.leading) - } else { - SingleSlider(data: $slide.dmxData[device.address[0]-1]) - .padding(.leading) - } - } .padding(.bottom) - } - } - .padding() - } -} - -struct SlideView_Previews: PreviewProvider { - static var previews: some View { - SlideView(slide: .constant(Slide(number: 1, dmxData: [DMXData(address: 0, value: 1)])), devices: .constant(ProjectData.defaultData.settings.devices)) - } -}