From 05136ffe3809434886281bb8acc57ef37771eb85 Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Mon, 14 Nov 2022 17:57:03 +0100 Subject: [PATCH 1/4] First draft --- DMXEditor.xcodeproj/project.pbxproj | 14 + DMXEditor/DMXData.swift | 16 +- DMXEditor/DMXEditorApp.swift | 10 +- DMXEditor/Frame.swift | 21 ++ DMXEditor/MultiSliderEditable.swift | 6 +- DMXEditor/NumberField.swift | 2 +- DMXEditor/SingleSlider.swift | 2 +- DMXEditor/SingleSliderEditable.swift | 2 +- DMXEditor/Slide.swift | 54 ++- DMXEditor/Views/DMXDataEditor.swift | 48 +++ DMXEditor/Views/EditView.swift | 510 +++++++++++++++++---------- DMXEditor/Views/SlideView.swift | 88 ++--- DMXEditor/Views/TimerView.swift | 75 ++++ 13 files changed, 601 insertions(+), 247 deletions(-) create mode 100644 DMXEditor/Frame.swift create mode 100644 DMXEditor/Views/DMXDataEditor.swift create mode 100644 DMXEditor/Views/TimerView.swift diff --git a/DMXEditor.xcodeproj/project.pbxproj b/DMXEditor.xcodeproj/project.pbxproj index 5fc74ec..ad4de53 100644 --- a/DMXEditor.xcodeproj/project.pbxproj +++ b/DMXEditor.xcodeproj/project.pbxproj @@ -11,6 +11,9 @@ 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 */; }; + A74CC46F291E8C1F0069F387 /* DMXDataEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74CC46E291E8C1F0069F387 /* DMXDataEditor.swift */; }; + A74CC471291E8E290069F387 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74CC470291E8E290069F387 /* TimerView.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 */; }; @@ -40,6 +43,9 @@ 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 = ""; }; + A74CC46E291E8C1F0069F387 /* DMXDataEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMXDataEditor.swift; sourceTree = ""; }; + A74CC470291E8E290069F387 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.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 = ""; }; @@ -121,6 +127,7 @@ A7E7BC1B27BEEACE000A83BB /* DMXData.swift */, A7E7BC1F27BEEACE000A83BB /* Settings.swift */, A7E7BC1E27BEEACE000A83BB /* Slide.swift */, + A74CC46C291E74D70069F387 /* Frame.swift */, ); name = Models; sourceTree = ""; @@ -146,6 +153,8 @@ A7E7BC2227BEEACE000A83BB /* SlideView.swift */, A7E7BC3927C2C48C000A83BB /* SlideEditView.swift */, A7AAEE8827CD36EB00B7BBA7 /* GeneralSettingsView.swift */, + A74CC46E291E8C1F0069F387 /* DMXDataEditor.swift */, + A74CC470291E8E290069F387 /* TimerView.swift */, ); path = Views; sourceTree = ""; @@ -240,11 +249,14 @@ A7E7BC2827BEEACE000A83BB /* DMXData.swift in Sources */, A7E7BC2627BEEACE000A83BB /* SettingsView.swift in Sources */, A7E7BC3227BEEACE000A83BB /* RGBPicker.swift in Sources */, + A74CC471291E8E290069F387 /* TimerView.swift in Sources */, + A74CC46F291E8C1F0069F387 /* DMXDataEditor.swift in Sources */, A7E7BC3A27C2C48C000A83BB /* SlideEditView.swift in Sources */, A7E7BC3827C171E6000A83BB /* SingleSliderEditable.swift in Sources */, A7E7BC2727BEEACE000A83BB /* MultiSlider.swift in Sources */, A7E7BC2C27BEEACE000A83BB /* Settings.swift in Sources */, A7E7BC3027BEEACE000A83BB /* Device.swift in Sources */, + A74CC46D291E74D70069F387 /* Frame.swift in Sources */, A7E7BC2927BEEACE000A83BB /* EditView.swift in Sources */, A7E7BC2E27BEEACE000A83BB /* SingleSlider.swift in Sources */, A711E41527BEE9CA009E1205 /* DMXEditorDocument.swift in Sources */, @@ -404,6 +416,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 +451,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/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..9067226 100644 --- a/DMXEditor/DMXEditorApp.swift +++ b/DMXEditor/DMXEditorApp.swift @@ -10,21 +10,15 @@ import SwiftUI @main struct DMXEditorApp: App { @State private var showSettings = false - @State private var initialPopup = false 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/Frame.swift b/DMXEditor/Frame.swift new file mode 100644 index 0000000..e787059 --- /dev/null +++ b/DMXEditor/Frame.swift @@ -0,0 +1,21 @@ +// +// Delay.swift +// DMXEditor +// +// Created by Maximilian Inckmann on 11.11.22. +// + +import Foundation + +struct Frame: Identifiable, Codable, Hashable, Comparable { + static func < (lhs: Frame, rhs: Frame) -> Bool { + return lhs.relativeTimeInSeconds < rhs.relativeTimeInSeconds + } +// static func == (lhs: Frame, rhs: Frame) -> Bool { +// return lhs.relativeTimeInSeconds == rhs.relativeTimeInSeconds +// } + + var id = UUID() + var relativeTimeInSeconds: Double + var dmxData: [DMXData] +} 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/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..a2539e8 100644 --- a/DMXEditor/Slide.swift +++ b/DMXEditor/Slide.swift @@ -14,7 +14,59 @@ struct Slide: Identifiable, Codable, Hashable { 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.dmxData = dmxData! + self.frames.append(Frame(relativeTimeInSeconds: 0, dmxData: dmxData!)) + } else { + self.frames.append(Frame(relativeTimeInSeconds: 0, dmxData: DMXData.getDefault())) +// self.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 dmxData: [DMXData] + var frames: [Frame] } diff --git a/DMXEditor/Views/DMXDataEditor.swift b/DMXEditor/Views/DMXDataEditor.swift new file mode 100644 index 0000000..24dc746 --- /dev/null +++ b/DMXEditor/Views/DMXDataEditor.swift @@ -0,0 +1,48 @@ +//// +//// DMXDataEditor.swift +//// DMXEditor +//// +//// Created by Maximilian Inckmann on 11.11.22. +//// +// +//import SwiftUI +// +//struct DMXDataEditor: View { +// @Binding var dmxData: [DMXData] +// @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: $dmxData[device.address[0]-1], +// dataG: $dmxData[device.address[1]-1], +// dataB: $dmxData[device.address[2]-1]) +// .padding(.leading) +// } else { +// SingleSlider(data: $dmxData[device.address[0]-1]) +// .padding(.leading) +// } +// } .padding(.bottom) +// } +// } +// .padding() +// } +//} +// +//struct DMXDataEditor_Previews: PreviewProvider { +// static var previews: some View { +// DMXDataEditor(dmxData: .constant([DMXData(address: 0, value: 1)]), devices: .constant(ProjectData.defaultData.settings.devices)) +// +// } +//} diff --git a/DMXEditor/Views/EditView.swift b/DMXEditor/Views/EditView.swift index d7648aa..54ab091 100644 --- a/DMXEditor/Views/EditView.swift +++ b/DMXEditor/Views/EditView.swift @@ -12,210 +12,312 @@ 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") + ) + ) + } + + Spacer() + 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 + }){ + TimerView(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 { +// if(actual != last){ + 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)") + sendValues(serverAddress: data.settings.host, + universe: data.settings.universe, + previousData: lastDMXData, + goalData: i.dmxData, + amountSteps: data.settings.transitionSteps) + lastDMXData = i.dmxData + }) + } last = actual! } else if actual != nil && actual! > data.slides.count && highestUnavailableSlides != actual! { highestUnavailableSlides = actual! @@ -233,13 +335,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,18 +352,31 @@ 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: [])) } func addSlide() { if (selectedSlide != nil && data.slides.count > selectedSlide!) { addSlide(valueOfSlide: selectedSlide!) } 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]) { + func addTimer() { + if (selectedSlide != nil){ + 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() + } + data.slides[selectedSlide! - 1].frames.append(Frame(relativeTimeInSeconds: 2.5, dmxData:defaultDMX)) + data.slides[selectedSlide!-1].frames.sort() + } + } + + func loadPasted(from array: [NSItemProvider]) { guard let lastItem = array.last else { assertionFailure("Nothing to paste") return @@ -276,12 +391,35 @@ struct EditView: View { assertionFailure("Could not load data") 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 + + // 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 + } else { + data.slides[selectedSlide! - 1].frames.append(parsedData) + } + data.slides[selectedSlide!-1].frames.sort() + print("Pasted") } } } diff --git a/DMXEditor/Views/SlideView.swift b/DMXEditor/Views/SlideView.swift index 983dfa8..927812e 100644 --- a/DMXEditor/Views/SlideView.swift +++ b/DMXEditor/Views/SlideView.swift @@ -1,47 +1,47 @@ +//// +//// SlideView.swift +//// DMXEditorForKeynote +//// +//// Created by Maximilian Inckmann on 14.02.22. +//// // -// SlideView.swift -// DMXEditorForKeynote +//import SwiftUI // -// Created by Maximilian Inckmann on 14.02.22. +//struct SlideView: View { +// @Binding var slide: Slide +// @Binding var devices: [Device] // - -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)) - } -} +// 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)], frames: [])), devices: .constant(ProjectData.defaultData.settings.devices)) +// } +//} diff --git a/DMXEditor/Views/TimerView.swift b/DMXEditor/Views/TimerView.swift new file mode 100644 index 0000000..04d937c --- /dev/null +++ b/DMXEditor/Views/TimerView.swift @@ -0,0 +1,75 @@ +// +// TimerView.swift +// DMXEditor +// +// Created by Maximilian Inckmann on 11.11.22. +// + +import SwiftUI + +struct TimerView: View { + @Binding var slide: Slide + @Binding var frame: Frame + @Binding var devices: [Device] + + 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) + } + } + + 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() + +// DMXDataEditor(dmxData: $delay.dmxData, devices: $devices) + } + } +} + +struct TimerView_Previews: PreviewProvider { + static var previews: some View { + TimerView(slide: .constant(Slide(number: 23, frames: [])), frame: .constant(Frame(relativeTimeInSeconds: 2.4, dmxData: [DMXData(address: 0, value: 1)])), devices: .constant(ProjectData.defaultData.settings.devices)) + } +} From 8bb689c7cb5f424eb51c0cf80013931e90ccea5e Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Mon, 14 Nov 2022 18:00:58 +0100 Subject: [PATCH 2/4] some cleanup --- DMXEditor.xcodeproj/project.pbxproj | 8 ----- DMXEditor/Frame.swift | 3 -- DMXEditor/RGBPicker.swift | 1 - DMXEditor/Slide.swift | 7 ++--- DMXEditor/Views/DMXDataEditor.swift | 48 ----------------------------- DMXEditor/Views/EditView.swift | 16 +++++----- DMXEditor/Views/SlideView.swift | 47 ---------------------------- DMXEditor/Views/TimerView.swift | 2 -- 8 files changed, 9 insertions(+), 123 deletions(-) delete mode 100644 DMXEditor/Views/DMXDataEditor.swift delete mode 100644 DMXEditor/Views/SlideView.swift diff --git a/DMXEditor.xcodeproj/project.pbxproj b/DMXEditor.xcodeproj/project.pbxproj index ad4de53..4e5b9ef 100644 --- a/DMXEditor.xcodeproj/project.pbxproj +++ b/DMXEditor.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ 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 */; }; - A74CC46F291E8C1F0069F387 /* DMXDataEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74CC46E291E8C1F0069F387 /* DMXDataEditor.swift */; }; A74CC471291E8E290069F387 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74CC470291E8E290069F387 /* TimerView.swift */; }; A770FBAD285CF06800BBA4AF /* AppleScriptRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = A770FBAC285CF06800BBA4AF /* AppleScriptRunner.swift */; }; A7AAEE8727CD2AC400B7BBA7 /* OLAHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7AAEE8627CD2AC400B7BBA7 /* OLAHandler.swift */; }; @@ -26,7 +25,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 */; }; @@ -44,7 +42,6 @@ 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 = ""; }; - A74CC46E291E8C1F0069F387 /* DMXDataEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMXDataEditor.swift; sourceTree = ""; }; A74CC470291E8E290069F387 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.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 = ""; }; @@ -58,7 +55,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 = ""; }; @@ -150,10 +146,8 @@ children = ( A7E7BC1C27BEEACE000A83BB /* EditView.swift */, A7E7BC1927BEEACE000A83BB /* SettingsView.swift */, - A7E7BC2227BEEACE000A83BB /* SlideView.swift */, A7E7BC3927C2C48C000A83BB /* SlideEditView.swift */, A7AAEE8827CD36EB00B7BBA7 /* GeneralSettingsView.swift */, - A74CC46E291E8C1F0069F387 /* DMXDataEditor.swift */, A74CC470291E8E290069F387 /* TimerView.swift */, ); path = Views; @@ -250,7 +244,6 @@ A7E7BC2627BEEACE000A83BB /* SettingsView.swift in Sources */, A7E7BC3227BEEACE000A83BB /* RGBPicker.swift in Sources */, A74CC471291E8E290069F387 /* TimerView.swift in Sources */, - A74CC46F291E8C1F0069F387 /* DMXDataEditor.swift in Sources */, A7E7BC3A27C2C48C000A83BB /* SlideEditView.swift in Sources */, A7E7BC3827C171E6000A83BB /* SingleSliderEditable.swift in Sources */, A7E7BC2727BEEACE000A83BB /* MultiSlider.swift in Sources */, @@ -265,7 +258,6 @@ 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; diff --git a/DMXEditor/Frame.swift b/DMXEditor/Frame.swift index e787059..b85b8cd 100644 --- a/DMXEditor/Frame.swift +++ b/DMXEditor/Frame.swift @@ -11,9 +11,6 @@ struct Frame: Identifiable, Codable, Hashable, Comparable { static func < (lhs: Frame, rhs: Frame) -> Bool { return lhs.relativeTimeInSeconds < rhs.relativeTimeInSeconds } -// static func == (lhs: Frame, rhs: Frame) -> Bool { -// return lhs.relativeTimeInSeconds == rhs.relativeTimeInSeconds -// } var id = UUID() var relativeTimeInSeconds: Double diff --git a/DMXEditor/RGBPicker.swift b/DMXEditor/RGBPicker.swift index 9442ac3..ea70329 100644 --- a/DMXEditor/RGBPicker.swift +++ b/DMXEditor/RGBPicker.swift @@ -63,7 +63,6 @@ struct RGBPicker: View { Spacer() Text("Show Picker") .foregroundColor(.teal) - // .padding() Spacer() }) .buttonStyle(.borderless) diff --git a/DMXEditor/Slide.swift b/DMXEditor/Slide.swift index a2539e8..ed2a7e4 100644 --- a/DMXEditor/Slide.swift +++ b/DMXEditor/Slide.swift @@ -35,7 +35,7 @@ struct Slide: Identifiable, Codable, Hashable { init(number: Int, dmxData: [DMXData]?, frames: [Frame]?){ self.number = number - + if(frames != nil) { self.frames = frames! } else { @@ -43,11 +43,9 @@ struct Slide: Identifiable, Codable, Hashable { } if(dmxData != nil) { -// self.dmxData = dmxData! self.frames.append(Frame(relativeTimeInSeconds: 0, dmxData: dmxData!)) } else { self.frames.append(Frame(relativeTimeInSeconds: 0, dmxData: DMXData.getDefault())) -// self.dmxData = DMXData.getDefault() } self.frames.sort() @@ -55,7 +53,7 @@ struct Slide: Identifiable, Codable, Hashable { init(number: Int, frames: [Frame]?){ self.number = number - + if(frames != nil) { self.frames = frames! } else { @@ -67,6 +65,5 @@ struct Slide: Identifiable, Codable, Hashable { var id = UUID() var number: Int -// var dmxData: [DMXData] var frames: [Frame] } diff --git a/DMXEditor/Views/DMXDataEditor.swift b/DMXEditor/Views/DMXDataEditor.swift deleted file mode 100644 index 24dc746..0000000 --- a/DMXEditor/Views/DMXDataEditor.swift +++ /dev/null @@ -1,48 +0,0 @@ -//// -//// DMXDataEditor.swift -//// DMXEditor -//// -//// Created by Maximilian Inckmann on 11.11.22. -//// -// -//import SwiftUI -// -//struct DMXDataEditor: View { -// @Binding var dmxData: [DMXData] -// @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: $dmxData[device.address[0]-1], -// dataG: $dmxData[device.address[1]-1], -// dataB: $dmxData[device.address[2]-1]) -// .padding(.leading) -// } else { -// SingleSlider(data: $dmxData[device.address[0]-1]) -// .padding(.leading) -// } -// } .padding(.bottom) -// } -// } -// .padding() -// } -//} -// -//struct DMXDataEditor_Previews: PreviewProvider { -// static var previews: some View { -// DMXDataEditor(dmxData: .constant([DMXData(address: 0, value: 1)]), devices: .constant(ProjectData.defaultData.settings.devices)) -// -// } -//} diff --git a/DMXEditor/Views/EditView.swift b/DMXEditor/Views/EditView.swift index 54ab091..67c86b3 100644 --- a/DMXEditor/Views/EditView.swift +++ b/DMXEditor/Views/EditView.swift @@ -41,7 +41,7 @@ struct EditView: View { .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 @@ -54,7 +54,7 @@ struct EditView: View { data.slides[i].number = i } } - + data.slides[startIndex].number = destinationIndex + 1 data.slides.move(fromOffsets: indices, toOffset: destination) } @@ -294,13 +294,11 @@ struct EditView: View { while activePresentation { let actual = getSlide() if actual != nil && actual != last && actual! <= data.slides.count { -// if(actual != last){ - for i in activeTasks{ - i.cancel() - } - activeTasks = [] - print("Cancelled all Tasks") -// } + 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()! diff --git a/DMXEditor/Views/SlideView.swift b/DMXEditor/Views/SlideView.swift deleted file mode 100644 index 927812e..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)], frames: [])), devices: .constant(ProjectData.defaultData.settings.devices)) -// } -//} diff --git a/DMXEditor/Views/TimerView.swift b/DMXEditor/Views/TimerView.swift index 04d937c..1e05588 100644 --- a/DMXEditor/Views/TimerView.swift +++ b/DMXEditor/Views/TimerView.swift @@ -62,8 +62,6 @@ struct TimerView: View { } } .padding() - -// DMXDataEditor(dmxData: $delay.dmxData, devices: $devices) } } } From fb68b389ba6416fd97cbd64b08c04dbf710fa265 Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann Date: Fri, 9 Dec 2022 22:30:32 +0100 Subject: [PATCH 3/4] Added Animations --- DMXEditor.xcodeproj/project.pbxproj | 24 +++++- DMXEditor/CurveEditor.swift | 125 +++++++++++++++++++++++++++ DMXEditor/DMXEditorApp.swift | 5 ++ DMXEditor/DMXTransition.swift | 120 ++++++++++++++++++++++++++ DMXEditor/Dragging.swift | 55 ++++++++++++ DMXEditor/Frame.swift | 34 +++++++- DMXEditor/GeometryExtensions.swift | 41 +++++++++ DMXEditor/OLAHandler.swift | 14 +++ DMXEditor/Slide.swift | 3 +- DMXEditor/Views/EditView.swift | 13 +-- DMXEditor/Views/FrameView.swift | 127 ++++++++++++++++++++++++++++ DMXEditor/Views/TimerView.swift | 73 ---------------- 12 files changed, 548 insertions(+), 86 deletions(-) create mode 100644 DMXEditor/CurveEditor.swift create mode 100644 DMXEditor/DMXTransition.swift create mode 100644 DMXEditor/Dragging.swift create mode 100644 DMXEditor/GeometryExtensions.swift create mode 100644 DMXEditor/Views/FrameView.swift delete mode 100644 DMXEditor/Views/TimerView.swift diff --git a/DMXEditor.xcodeproj/project.pbxproj b/DMXEditor.xcodeproj/project.pbxproj index 4e5b9ef..88224cb 100644 --- a/DMXEditor.xcodeproj/project.pbxproj +++ b/DMXEditor.xcodeproj/project.pbxproj @@ -12,10 +12,14 @@ 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 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74CC470291E8E290069F387 /* TimerView.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 */; }; + 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 */; }; @@ -42,10 +46,14 @@ 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 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.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 = ""; }; + 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 = ""; }; @@ -117,6 +125,8 @@ A711E42427BEEA01009E1205 /* Models */ = { isa = PBXGroup; children = ( + A7E20049292E2F1200676956 /* GeometryExtensions.swift */, + A7E2004B292E2F1800676956 /* Dragging.swift */, A711E41427BEE9CA009E1205 /* DMXEditorDocument.swift */, A7E7BC1D27BEEACE000A83BB /* ProjectData.swift */, A7E7BC2327BEEACE000A83BB /* Device.swift */, @@ -124,6 +134,7 @@ A7E7BC1F27BEEACE000A83BB /* Settings.swift */, A7E7BC1E27BEEACE000A83BB /* Slide.swift */, A74CC46C291E74D70069F387 /* Frame.swift */, + A74CC4762924F7E90069F387 /* DMXTransition.swift */, ); name = Models; sourceTree = ""; @@ -137,6 +148,7 @@ A7E7BC2127BEEACE000A83BB /* SingleSlider.swift */, A7E7BC3527C13406000A83BB /* MultiSliderEditable.swift */, A7E7BC3727C171E6000A83BB /* SingleSliderEditable.swift */, + A7E20047292E2DB700676956 /* CurveEditor.swift */, ); name = Components; sourceTree = ""; @@ -148,7 +160,7 @@ A7E7BC1927BEEACE000A83BB /* SettingsView.swift */, A7E7BC3927C2C48C000A83BB /* SlideEditView.swift */, A7AAEE8827CD36EB00B7BBA7 /* GeneralSettingsView.swift */, - A74CC470291E8E290069F387 /* TimerView.swift */, + A74CC470291E8E290069F387 /* FrameView.swift */, ); path = Views; sourceTree = ""; @@ -237,21 +249,25 @@ 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 */, A7E7BC3227BEEACE000A83BB /* RGBPicker.swift in Sources */, - A74CC471291E8E290069F387 /* TimerView.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 */, 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 */, diff --git a/DMXEditor/CurveEditor.swift b/DMXEditor/CurveEditor.swift new file mode 100644 index 0000000..90c392d --- /dev/null +++ b/DMXEditor/CurveEditor.swift @@ -0,0 +1,125 @@ +// +// CurveEditor.swift +// DMXEditor +// +// Created by Maximilian Inckmann on 23.11.22. +// +import Foundation +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 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/DMXEditorApp.swift b/DMXEditor/DMXEditorApp.swift index 9067226..cc1dc88 100644 --- a/DMXEditor/DMXEditorApp.swift +++ b/DMXEditor/DMXEditorApp.swift @@ -10,6 +10,11 @@ import SwiftUI @main struct DMXEditorApp: App { @State private var showSettings = 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 diff --git a/DMXEditor/DMXTransition.swift b/DMXEditor/DMXTransition.swift new file mode 100644 index 0000000..1197b32 --- /dev/null +++ b/DMXEditor/DMXTransition.swift @@ -0,0 +1,120 @@ +// +// 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 + } + + 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) + } + } 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 index b85b8cd..e4b0f52 100644 --- a/DMXEditor/Frame.swift +++ b/DMXEditor/Frame.swift @@ -7,12 +7,44 @@ import Foundation -struct Frame: Identifiable, Codable, Hashable, Comparable { +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: .none, steps: 0) + } + + init(relativeTimeInSeconds: Double, dmxData: [DMXData]){ + self.relativeTimeInSeconds = relativeTimeInSeconds + self.dmxData = dmxData + transition = DMXTransition(mode: .none, steps: 0) + } + + 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/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/Slide.swift b/DMXEditor/Slide.swift index ed2a7e4..9a35aba 100644 --- a/DMXEditor/Slide.swift +++ b/DMXEditor/Slide.swift @@ -9,7 +9,7 @@ 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 } @@ -30,7 +30,6 @@ struct Slide: Identifiable, Codable, Hashable { if let data0 = try values.decodeIfPresent([DMXData].self, forKey: .dmxData){ frames.append(Frame(relativeTimeInSeconds: 0, dmxData: data0)) } - } init(number: Int, dmxData: [DMXData]?, frames: [Frame]?){ diff --git a/DMXEditor/Views/EditView.swift b/DMXEditor/Views/EditView.swift index 67c86b3..b101673 100644 --- a/DMXEditor/Views/EditView.swift +++ b/DMXEditor/Views/EditView.swift @@ -191,7 +191,7 @@ struct EditView: View { if let selectedSlide, let selectedFrame = selectedFrame, let frameIndex = data.slides[selectedSlide-1].frames.firstIndex(where: { f in return f.id == selectedFrame.id }){ - TimerView(slide: $data.slides[selectedSlide-1], frame: $data.slides[selectedSlide-1].frames[frameIndex], devices: $data.settings.devices) + 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.") @@ -308,11 +308,12 @@ struct EditView: View { try await Task.sleep(nanoseconds: delayInNs) } print("Sending data with delay of \(delayInNs) ns - Slide \(preSleepSlide)") - sendValues(serverAddress: data.settings.host, - universe: data.settings.universe, - previousData: lastDMXData, - goalData: i.dmxData, - amountSteps: data.settings.transitionSteps) + sendAnimatedValues( + serverAddress: data.settings.host, + universe: data.settings.universe, + previousData: lastDMXData, + goalData: i.dmxData, + transition: i.transition) lastDMXData = i.dmxData }) } diff --git a/DMXEditor/Views/FrameView.swift b/DMXEditor/Views/FrameView.swift new file mode 100644 index 0000000..7a51a16 --- /dev/null +++ b/DMXEditor/Views/FrameView.swift @@ -0,0 +1,127 @@ +// +// 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("Bezier Animation").tag(DMXTransition.AnimationMode.bezier) + } + + switch(frame.transition.mode){ + case .linear: + HStack{ + Text("Steps") + NumberField(value: $frame.transition.steps) + } + case .bezier: + HStack{ + Text("Steps") + NumberField(value: $frame.transition.steps) + Button("Adjust Bezier Curve"){ + 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) + } + 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() + } + } + 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/TimerView.swift b/DMXEditor/Views/TimerView.swift deleted file mode 100644 index 1e05588..0000000 --- a/DMXEditor/Views/TimerView.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// TimerView.swift -// DMXEditor -// -// Created by Maximilian Inckmann on 11.11.22. -// - -import SwiftUI - -struct TimerView: View { - @Binding var slide: Slide - @Binding var frame: Frame - @Binding var devices: [Device] - - 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) - } - } - - 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 { - TimerView(slide: .constant(Slide(number: 23, frames: [])), frame: .constant(Frame(relativeTimeInSeconds: 2.4, dmxData: [DMXData(address: 0, value: 1)])), devices: .constant(ProjectData.defaultData.settings.devices)) - } -} From a28a1bbdff1d41833280806fad85bb99c5cf6ea8 Mon Sep 17 00:00:00 2001 From: Maximilian Inckmann - KIT Date: Sat, 25 Feb 2023 19:20:15 +0100 Subject: [PATCH 4/4] minor optimizations --- DMXEditor.xcodeproj/project.pbxproj | 8 ++ DMXEditor/CurveEditor.swift | 83 ------------------ DMXEditor/CurveEditorView.swift | 96 ++++++++++++++++++++ DMXEditor/DMXTransition.swift | 2 + DMXEditor/Frame.swift | 4 +- DMXEditor/RGBPicker.swift | 51 ++--------- DMXEditor/Settings.swift | 3 +- DMXEditor/StepPicker.swift | 25 ++++++ DMXEditor/Views/EditView.swift | 102 ++++++++++++---------- DMXEditor/Views/FrameView.swift | 54 ++++++++---- DMXEditor/Views/GeneralSettingsView.swift | 23 ----- 11 files changed, 231 insertions(+), 220 deletions(-) create mode 100644 DMXEditor/CurveEditorView.swift create mode 100644 DMXEditor/StepPicker.swift diff --git a/DMXEditor.xcodeproj/project.pbxproj b/DMXEditor.xcodeproj/project.pbxproj index 88224cb..0193ce0 100644 --- a/DMXEditor.xcodeproj/project.pbxproj +++ b/DMXEditor.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 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 */; }; @@ -51,6 +53,8 @@ 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 = ""; }; @@ -149,6 +153,8 @@ A7E7BC3527C13406000A83BB /* MultiSliderEditable.swift */, A7E7BC3727C171E6000A83BB /* SingleSliderEditable.swift */, A7E20047292E2DB700676956 /* CurveEditor.swift */, + A7B0345629AA71C900CB9426 /* CurveEditorView.swift */, + A7B0345829AA7A1C00CB9426 /* StepPicker.swift */, ); name = Components; sourceTree = ""; @@ -255,6 +261,7 @@ 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 */, @@ -265,6 +272,7 @@ 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 */, diff --git a/DMXEditor/CurveEditor.swift b/DMXEditor/CurveEditor.swift index 90c392d..88e8c6b 100644 --- a/DMXEditor/CurveEditor.swift +++ b/DMXEditor/CurveEditor.swift @@ -7,89 +7,6 @@ import Foundation 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 CurveEditor: View { @Binding var controlPoint0: RelativePoint @Binding var controlPoint1: RelativePoint 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/DMXTransition.swift b/DMXEditor/DMXTransition.swift index 1197b32..5b4d83f 100644 --- a/DMXEditor/DMXTransition.swift +++ b/DMXEditor/DMXTransition.swift @@ -14,6 +14,7 @@ struct DMXTransition: Codable, Identifiable, Hashable { case none case linear case bezier + case fadeInFadeOut } var id: UUID = UUID() @@ -26,6 +27,7 @@ struct DMXTransition: Codable, Identifiable, Hashable { 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] diff --git a/DMXEditor/Frame.swift b/DMXEditor/Frame.swift index e4b0f52..1136147 100644 --- a/DMXEditor/Frame.swift +++ b/DMXEditor/Frame.swift @@ -28,13 +28,13 @@ struct Frame: Identifiable, Codable, Comparable, Hashable { 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: .none, steps: 0) + 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: .none, steps: 0) + transition = DMXTransition(mode: .fadeInFadeOut, steps: 32) } init(relativeTimeInSeconds: Double, dmxData: [DMXData], transition: DMXTransition){ diff --git a/DMXEditor/RGBPicker.swift b/DMXEditor/RGBPicker.swift index ea70329..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,24 +30,10 @@ struct RGBPicker: View { green = g blue = b } - - } else { - Button(action: { - showPicker = true - }, label: { - Spacer() - Text("Show Picker") - .foregroundColor(.teal) - 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/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 b101673..930f460 100644 --- a/DMXEditor/Views/EditView.swift +++ b/DMXEditor/Views/EditView.swift @@ -170,7 +170,6 @@ struct EditView: View { ) } - Spacer() Divider() Button(action: {addTimer()}, label: { @@ -351,74 +350,85 @@ struct EditView: View { } func addSlide(valueOfSlide: Int) { - data.slides.append(Slide(number: (data.slides.count+1), frames: [])) + 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(), frames: [])) } } func addTimer() { - if (selectedSlide != nil){ + 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 + 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() } - data.slides[selectedSlide! - 1].frames.append(Frame(relativeTimeInSeconds: 2.5, dmxData:defaultDMX)) - data.slides[selectedSlide!-1].frames.sort() + + 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)) + } + + data.slides[selectedSlide - 1].frames.sort() } } func loadPasted(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)") + DispatchQueue.main.async{ + guard let lastItem = array.last else { + assertionFailure("Nothing to paste") 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 + 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 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 - } else { - data.slides[selectedSlide! - 1].frames.append(parsedData) + // 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") } - data.slides[selectedSlide!-1].frames.sort() - print("Pasted") } } } diff --git a/DMXEditor/Views/FrameView.swift b/DMXEditor/Views/FrameView.swift index 7a51a16..ad2ad94 100644 --- a/DMXEditor/Views/FrameView.swift +++ b/DMXEditor/Views/FrameView.swift @@ -41,31 +41,38 @@ struct FrameView: View { 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("Bezier Animation").tag(DMXTransition.AnimationMode.bezier) + 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: - HStack{ - Text("Steps") - NumberField(value: $frame.transition.steps) - } + case .linear: StepPicker(steps: $frame.transition.steps) case .bezier: HStack{ - Text("Steps") - NumberField(value: $frame.transition.steps) + StepPicker(steps: $frame.transition.steps) Button("Adjust Bezier Curve"){ - 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) + 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 } @@ -85,6 +92,15 @@ struct FrameView: View { }.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() } } 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() } }