diff --git a/Themis.xcodeproj/project.pbxproj b/Themis.xcodeproj/project.pbxproj index fb312239..eeaafb3b 100644 --- a/Themis.xcodeproj/project.pbxproj +++ b/Themis.xcodeproj/project.pbxproj @@ -15,20 +15,12 @@ 650538B92A1E1EF200C45E18 /* ExerciseMockExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650538B82A1E1EF200C45E18 /* ExerciseMockExtension.swift */; }; 650538BC2A1E226A00C45E18 /* MockAssessmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650538BB2A1E226A00C45E18 /* MockAssessmentViewModel.swift */; }; 650538BE2A1E6E3D00C45E18 /* SubmissionMockExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650538BD2A1E6E3D00C45E18 /* SubmissionMockExtension.swift */; }; - 650D87FE2A7A5B65004E7533 /* UMLUseCaseDiagramRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650D87FD2A7A5B65004E7533 /* UMLUseCaseDiagramRenderer.swift */; }; - 650D88002A7A5B78004E7533 /* UMLDiagramRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650D87FF2A7A5B78004E7533 /* UMLDiagramRenderer.swift */; }; - 650D88022A7A5C33004E7533 /* UMLUseCaseDiagramElementRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650D88012A7A5C33004E7533 /* UMLUseCaseDiagramElementRenderer.swift */; }; - 650D88042A7A5C77004E7533 /* UMLUseCaseDiagramRelationshipRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650D88032A7A5C77004E7533 /* UMLUseCaseDiagramRelationshipRenderer.swift */; }; 65101DD32A33362F007B598A /* ProgrammingAssessmentResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65101DD22A33362F007B598A /* ProgrammingAssessmentResult.swift */; }; 65101DD52A33363A007B598A /* TextAsessmentResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65101DD42A33363A007B598A /* TextAsessmentResult.swift */; }; 65101DD72A35FB98007B598A /* UnknownAssessmentResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65101DD62A35FB98007B598A /* UnknownAssessmentResult.swift */; }; 651530072A8C0E1C00893970 /* FiletreeSkeleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651530062A8C0E1C00893970 /* FiletreeSkeleton.swift */; }; 65153F9F2A0F9B260020B6FE /* LoginVMExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65153F9E2A0F9B260020B6FE /* LoginVMExtension.swift */; }; 65153FA12A0FAD4A0020B6FE /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65153FA02A0FAD4A0020B6FE /* RootViewModel.swift */; }; - 651976182A65A08A004E33C8 /* UMLRelationshipType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651976172A65A08A004E33C8 /* UMLRelationshipType.swift */; }; - 6519761A2A65A09F004E33C8 /* UMLElementType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651976192A65A09F004E33C8 /* UMLElementType.swift */; }; - 6519761C2A65A0DF004E33C8 /* UMLElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6519761B2A65A0DF004E33C8 /* UMLElement.swift */; }; - 6519761E2A65A14E004E33C8 /* UMLRelationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6519761D2A65A14E004E33C8 /* UMLRelationship.swift */; }; 651A44B82A1672DE00DBCD92 /* ExerciseGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651A44B72A1672DE00DBCD92 /* ExerciseGroups.swift */; }; 651A44BB2A167FDF00DBCD92 /* CourseMockExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651A44BA2A167FDF00DBCD92 /* CourseMockExtension.swift */; }; 6523D9C12A8E1E940050EB9A /* ExamDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6523D9C02A8E1E940050EB9A /* ExamDetailViewModel.swift */; }; @@ -47,12 +39,8 @@ 6540AEE62AB0FC0C00643A91 /* UnsupportedFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6540AEE52AB0FC0C00643A91 /* UnsupportedFileView.swift */; }; 654BC9972A6FF7630081CF22 /* UMLRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654BC9932A6FF7630081CF22 /* UMLRenderer.swift */; }; 654BC9982A6FF7630081CF22 /* ModelingAssessmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654BC9942A6FF7630081CF22 /* ModelingAssessmentView.swift */; }; - 654BC9992A6FF7630081CF22 /* UMLClassDiagramRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654BC9962A6FF7630081CF22 /* UMLClassDiagramRenderer.swift */; }; - 654BC99B2A7007D00081CF22 /* SelectableUMLItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654BC99A2A7007D00081CF22 /* SelectableUMLItem.swift */; }; 654F61222A1DEADE0044C747 /* ThemisUndoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654F61212A1DEADE0044C747 /* ThemisUndoManager.swift */; }; 655084632A1B4E2300748A6F /* ProblemStatementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 655084622A1B4E2300748A6F /* ProblemStatementView.swift */; }; - 65593DF72A72B87000B2DEBE /* UMLClassDiagramElementRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65593DF62A72B87000B2DEBE /* UMLClassDiagramElementRenderer.swift */; }; - 65593DF92A72B8A300B2DEBE /* UMLClassDiagramRelationshipRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65593DF82A72B8A300B2DEBE /* UMLClassDiagramRelationshipRenderer.swift */; }; 655EEA2C2ACADD6B00AE6AEB /* FileErrorIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 655EEA2B2ACADD6B00AE6AEB /* FileErrorIcon.swift */; }; 6562B90C2A7FA93000C9C7D5 /* ExampleModelingSolutionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6562B90B2A7FA93000C9C7D5 /* ExampleModelingSolutionView.swift */; }; 6562B90E2A7FA94600C9C7D5 /* ExampleTextSolutionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6562B90D2A7FA94600C9C7D5 /* ExampleTextSolutionView.swift */; }; @@ -106,13 +94,10 @@ 65B49F492A0817DC00C9A45F /* ToolbarSaveButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B49F482A0817DC00C9A45F /* ToolbarSaveButton.swift */; }; 65B49F4C2A08192100C9A45F /* LeftGripView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B49F4B2A08192100C9A45F /* LeftGripView.swift */; }; 65B49F4E2A08220D00C9A45F /* RightGripView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B49F4D2A08220D00C9A45F /* RightGripView.swift */; }; - 65B981AF2A7BDEF800FFC2F4 /* CGPointExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B981AE2A7BDEF800FFC2F4 /* CGPointExtension.swift */; }; - 65B981B12A7C37E000FFC2F4 /* UMLGraphicsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B981B02A7C37E000FFC2F4 /* UMLGraphicsContext.swift */; }; 65B9BD042AAE15110086F9EC /* FileUploadAssessmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B9BD032AAE15110086F9EC /* FileUploadAssessmentView.swift */; }; 65B9BD092AAE18570086F9EC /* FileUploadAssessmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B9BD082AAE18570086F9EC /* FileUploadAssessmentViewModel.swift */; }; 65B9BD102AAE413B0086F9EC /* FileUploadSubmissionServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B9BD0F2AAE413B0086F9EC /* FileUploadSubmissionServiceImpl.swift */; }; 65BA61D72AE02B5C009FE11C /* PathStringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BA61D62AE02B5C009FE11C /* PathStringExtension.swift */; }; - 65BC14532A6FD9F30002D089 /* UMLBadgeSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BC14522A6FD9F30002D089 /* UMLBadgeSymbol.swift */; }; 65C2F7532A03E3D4001EDADC /* FormatterExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2F7522A03E3D4001EDADC /* FormatterExtension.swift */; }; 65C2F7552A03E428001EDADC /* JSONDecoderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2F7542A03E428001EDADC /* JSONDecoderExtension.swift */; }; 65CC29902ADC23760095ABDA /* GradingCriterionMockExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65CC298F2ADC23750095ABDA /* GradingCriterionMockExtension.swift */; }; @@ -136,7 +121,6 @@ 65E6A1092A1400EB0058E5D6 /* RepositoryServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E6A1082A1400EB0058E5D6 /* RepositoryServiceImpl.swift */; }; 65E797282A1A4B9400E17401 /* SectionTitleModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E797272A1A4B9400E17401 /* SectionTitleModifier.swift */; }; 65E7972A2A1A4EF800E17401 /* ExamListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E797292A1A4EF800E17401 /* ExamListItem.swift */; }; - 65E85CC32A57EEAE007E3A06 /* UMLModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E85CC22A57EEAE007E3A06 /* UMLModel.swift */; }; 65EDED372A33103A003BF14B /* UserFacingErrorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65EDED362A33103A003BF14B /* UserFacingErrorExtension.swift */; }; 65F007982A86C837000FD641 /* Shimmer in Frameworks */ = {isa = PBXBuildFile; productRef = 65F007972A86C837000FD641 /* Shimmer */; }; 65F018FF2A1CBD8D00BB1C98 /* TextSubmissionServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F018FE2A1CBD8D00BB1C98 /* TextSubmissionServiceImpl.swift */; }; @@ -200,6 +184,9 @@ E536EAD1297458240078E077 /* SwiftUIReorderableForEach in Frameworks */ = {isa = PBXBuildFile; productRef = E536EAD0297458240078E077 /* SwiftUIReorderableForEach */; }; E536EAD3298C00380078E077 /* DateTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E536EAD2298C00380078E077 /* DateTimelineView.swift */; }; E58C68062935037C008E6FEE /* EditFeedbackViewBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58C68052935037C008E6FEE /* EditFeedbackViewBase.swift */; }; + F72D68F52ACB31EE001A0871 /* UMLBadgeSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72D68F42ACB31EE001A0871 /* UMLBadgeSymbol.swift */; }; + F734D1AC2B18D83200435866 /* ApollonShared in Frameworks */ = {isa = PBXBuildFile; productRef = F734D1AB2B18D83200435866 /* ApollonShared */; }; + F734D1AE2B18D83200435866 /* ApollonView in Frameworks */ = {isa = PBXBuildFile; productRef = F734D1AD2B18D83200435866 /* ApollonView */; }; F922443A297BFD1A008E4374 /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9224439297BFD1A008E4374 /* CircularProgressView.swift */; }; F958C4DE29887DE800E70669 /* ShakeEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = F958C4DD29887DE800E70669 /* ShakeEffect.swift */; }; /* End PBXBuildFile section */ @@ -229,20 +216,12 @@ 650538B82A1E1EF200C45E18 /* ExerciseMockExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseMockExtension.swift; sourceTree = ""; }; 650538BB2A1E226A00C45E18 /* MockAssessmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAssessmentViewModel.swift; sourceTree = ""; }; 650538BD2A1E6E3D00C45E18 /* SubmissionMockExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionMockExtension.swift; sourceTree = ""; }; - 650D87FD2A7A5B65004E7533 /* UMLUseCaseDiagramRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLUseCaseDiagramRenderer.swift; sourceTree = ""; }; - 650D87FF2A7A5B78004E7533 /* UMLDiagramRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLDiagramRenderer.swift; sourceTree = ""; }; - 650D88012A7A5C33004E7533 /* UMLUseCaseDiagramElementRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLUseCaseDiagramElementRenderer.swift; sourceTree = ""; }; - 650D88032A7A5C77004E7533 /* UMLUseCaseDiagramRelationshipRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLUseCaseDiagramRelationshipRenderer.swift; sourceTree = ""; }; 65101DD22A33362F007B598A /* ProgrammingAssessmentResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgrammingAssessmentResult.swift; sourceTree = ""; }; 65101DD42A33363A007B598A /* TextAsessmentResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextAsessmentResult.swift; sourceTree = ""; }; 65101DD62A35FB98007B598A /* UnknownAssessmentResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnknownAssessmentResult.swift; sourceTree = ""; }; 651530062A8C0E1C00893970 /* FiletreeSkeleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiletreeSkeleton.swift; sourceTree = ""; }; 65153F9E2A0F9B260020B6FE /* LoginVMExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginVMExtension.swift; sourceTree = ""; }; 65153FA02A0FAD4A0020B6FE /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = ""; }; - 651976172A65A08A004E33C8 /* UMLRelationshipType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLRelationshipType.swift; sourceTree = ""; }; - 651976192A65A09F004E33C8 /* UMLElementType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLElementType.swift; sourceTree = ""; }; - 6519761B2A65A0DF004E33C8 /* UMLElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLElement.swift; sourceTree = ""; }; - 6519761D2A65A14E004E33C8 /* UMLRelationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLRelationship.swift; sourceTree = ""; }; 651A44B72A1672DE00DBCD92 /* ExerciseGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseGroups.swift; sourceTree = ""; }; 651A44BA2A167FDF00DBCD92 /* CourseMockExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseMockExtension.swift; sourceTree = ""; }; 6523D9C02A8E1E940050EB9A /* ExamDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamDetailViewModel.swift; sourceTree = ""; }; @@ -261,12 +240,8 @@ 6540AEE52AB0FC0C00643A91 /* UnsupportedFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedFileView.swift; sourceTree = ""; }; 654BC9932A6FF7630081CF22 /* UMLRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UMLRenderer.swift; sourceTree = ""; }; 654BC9942A6FF7630081CF22 /* ModelingAssessmentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelingAssessmentView.swift; sourceTree = ""; }; - 654BC9962A6FF7630081CF22 /* UMLClassDiagramRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UMLClassDiagramRenderer.swift; sourceTree = ""; }; - 654BC99A2A7007D00081CF22 /* SelectableUMLItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableUMLItem.swift; sourceTree = ""; }; 654F61212A1DEADE0044C747 /* ThemisUndoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemisUndoManager.swift; sourceTree = ""; }; 655084622A1B4E2300748A6F /* ProblemStatementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemStatementView.swift; sourceTree = ""; }; - 65593DF62A72B87000B2DEBE /* UMLClassDiagramElementRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLClassDiagramElementRenderer.swift; sourceTree = ""; }; - 65593DF82A72B8A300B2DEBE /* UMLClassDiagramRelationshipRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLClassDiagramRelationshipRenderer.swift; sourceTree = ""; }; 655EEA2B2ACADD6B00AE6AEB /* FileErrorIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileErrorIcon.swift; sourceTree = ""; }; 6562B90B2A7FA93000C9C7D5 /* ExampleModelingSolutionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleModelingSolutionView.swift; sourceTree = ""; }; 6562B90D2A7FA94600C9C7D5 /* ExampleTextSolutionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTextSolutionView.swift; sourceTree = ""; }; @@ -308,13 +283,10 @@ 65B49F482A0817DC00C9A45F /* ToolbarSaveButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarSaveButton.swift; sourceTree = ""; }; 65B49F4B2A08192100C9A45F /* LeftGripView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftGripView.swift; sourceTree = ""; }; 65B49F4D2A08220D00C9A45F /* RightGripView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightGripView.swift; sourceTree = ""; }; - 65B981AE2A7BDEF800FFC2F4 /* CGPointExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPointExtension.swift; sourceTree = ""; }; - 65B981B02A7C37E000FFC2F4 /* UMLGraphicsContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLGraphicsContext.swift; sourceTree = ""; }; 65B9BD032AAE15110086F9EC /* FileUploadAssessmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadAssessmentView.swift; sourceTree = ""; }; 65B9BD082AAE18570086F9EC /* FileUploadAssessmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadAssessmentViewModel.swift; sourceTree = ""; }; 65B9BD0F2AAE413B0086F9EC /* FileUploadSubmissionServiceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadSubmissionServiceImpl.swift; sourceTree = ""; }; 65BA61D62AE02B5C009FE11C /* PathStringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathStringExtension.swift; sourceTree = ""; }; - 65BC14522A6FD9F30002D089 /* UMLBadgeSymbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLBadgeSymbol.swift; sourceTree = ""; }; 65C2F7522A03E3D4001EDADC /* FormatterExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatterExtension.swift; sourceTree = ""; }; 65C2F7542A03E428001EDADC /* JSONDecoderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDecoderExtension.swift; sourceTree = ""; }; 65CC298F2ADC23750095ABDA /* GradingCriterionMockExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradingCriterionMockExtension.swift; sourceTree = ""; }; @@ -338,7 +310,6 @@ 65E6A1082A1400EB0058E5D6 /* RepositoryServiceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryServiceImpl.swift; sourceTree = ""; }; 65E797272A1A4B9400E17401 /* SectionTitleModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionTitleModifier.swift; sourceTree = ""; }; 65E797292A1A4EF800E17401 /* ExamListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamListItem.swift; sourceTree = ""; }; - 65E85CC22A57EEAE007E3A06 /* UMLModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLModel.swift; sourceTree = ""; }; 65EDED362A33103A003BF14B /* UserFacingErrorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFacingErrorExtension.swift; sourceTree = ""; }; 65F018FE2A1CBD8D00BB1C98 /* TextSubmissionServiceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextSubmissionServiceImpl.swift; sourceTree = ""; }; 65F019002A1CCC0300BB1C98 /* UnknownSubmissionServiceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnknownSubmissionServiceImpl.swift; sourceTree = ""; }; @@ -403,6 +374,7 @@ E2E6748729203E350081238B /* ExerciseSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ExerciseSection.swift; path = Themis/Views/Exercises/ExerciseSection.swift; sourceTree = SOURCE_ROOT; }; E536EAD2298C00380078E077 /* DateTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimelineView.swift; sourceTree = ""; }; E58C68052935037C008E6FEE /* EditFeedbackViewBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFeedbackViewBase.swift; sourceTree = ""; }; + F72D68F42ACB31EE001A0871 /* UMLBadgeSymbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMLBadgeSymbol.swift; sourceTree = ""; }; F9224439297BFD1A008E4374 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; F958C4DD29887DE800E70669 /* ShakeEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShakeEffect.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -418,10 +390,12 @@ 657534BC2A0F7A52004B0C3F /* UserStore in Frameworks */, 6564DC102A00EFFC00CF2F89 /* UserStore in Frameworks */, 657534AA2A0F7A52004B0C3F /* APIClient in Frameworks */, + F734D1AC2B18D83200435866 /* ApollonShared in Frameworks */, 657534B42A0F7A52004B0C3F /* Login in Frameworks */, 657534B62A0F7A52004B0C3F /* ProfileInfo in Frameworks */, 657534B02A0F7A52004B0C3F /* Common in Frameworks */, 657534BA2A0F7A52004B0C3F /* SharedModels in Frameworks */, + F734D1AE2B18D83200435866 /* ApollonView in Frameworks */, E2192EE1291E6F780092CE58 /* KeychainAccess in Frameworks */, 657534AE2A0F7A52004B0C3F /* ArtemisMarkdown in Frameworks */, E536EAD1297458240078E077 /* SwiftUIReorderableForEach in Frameworks */, @@ -461,7 +435,6 @@ 4065F854292E8729005DAEE8 /* Models */ = { isa = PBXGroup; children = ( - 651976162A65A054004E33C8 /* UML */, 65101DD12A33360A007B598A /* Result */, 6581C67B2A305F1D00E49E24 /* Feedback */, 4065F857292E8A08005DAEE8 /* Appearance */, @@ -559,26 +532,6 @@ path = Mock; sourceTree = ""; }; - 650D87FB2A7A5B3C004E7533 /* Class Diagram */ = { - isa = PBXGroup; - children = ( - 654BC9962A6FF7630081CF22 /* UMLClassDiagramRenderer.swift */, - 65593DF62A72B87000B2DEBE /* UMLClassDiagramElementRenderer.swift */, - 65593DF82A72B8A300B2DEBE /* UMLClassDiagramRelationshipRenderer.swift */, - ); - path = "Class Diagram"; - sourceTree = ""; - }; - 650D87FC2A7A5B4C004E7533 /* Use Case Diagram */ = { - isa = PBXGroup; - children = ( - 650D87FD2A7A5B65004E7533 /* UMLUseCaseDiagramRenderer.swift */, - 650D88012A7A5C33004E7533 /* UMLUseCaseDiagramElementRenderer.swift */, - 650D88032A7A5C77004E7533 /* UMLUseCaseDiagramRelationshipRenderer.swift */, - ); - path = "Use Case Diagram"; - sourceTree = ""; - }; 65101DD12A33360A007B598A /* Result */ = { isa = PBXGroup; children = ( @@ -612,20 +565,6 @@ path = Core; sourceTree = ""; }; - 651976162A65A054004E33C8 /* UML */ = { - isa = PBXGroup; - children = ( - 65E85CC22A57EEAE007E3A06 /* UMLModel.swift */, - 651976172A65A08A004E33C8 /* UMLRelationshipType.swift */, - 651976192A65A09F004E33C8 /* UMLElementType.swift */, - 6519761B2A65A0DF004E33C8 /* UMLElement.swift */, - 6519761D2A65A14E004E33C8 /* UMLRelationship.swift */, - 65BC14522A6FD9F30002D089 /* UMLBadgeSymbol.swift */, - 654BC99A2A7007D00081CF22 /* SelectableUMLItem.swift */, - ); - path = UML; - sourceTree = ""; - }; 6523D9BF2A8E1E890050EB9A /* Exam */ = { isa = PBXGroup; children = ( @@ -655,22 +594,10 @@ children = ( 654BC9932A6FF7630081CF22 /* UMLRenderer.swift */, 654BC9942A6FF7630081CF22 /* ModelingAssessmentView.swift */, - 65B981B02A7C37E000FFC2F4 /* UMLGraphicsContext.swift */, - 654BC9952A6FF7630081CF22 /* Diagram Renderers */, ); path = "Modeling Exercise"; sourceTree = ""; }; - 654BC9952A6FF7630081CF22 /* Diagram Renderers */ = { - isa = PBXGroup; - children = ( - 650D87FC2A7A5B4C004E7533 /* Use Case Diagram */, - 650D87FB2A7A5B3C004E7533 /* Class Diagram */, - 650D87FF2A7A5B78004E7533 /* UMLDiagramRenderer.swift */, - ); - path = "Diagram Renderers"; - sourceTree = ""; - }; 6562B90A2A7FA92200C9C7D5 /* Example Solution */ = { isa = PBXGroup; children = ( @@ -716,6 +643,7 @@ 6589205B29EF0829005EE6FA /* Extensions */ = { isa = PBXGroup; children = ( + F72D68F32ACB31BF001A0871 /* Apollon */, 650538B72A1E1EE500C45E18 /* Mock */, 65153F9D2A0F9B090020B6FE /* Core */, 6589205C29EF0840005EE6FA /* ColorExtension.swift */, @@ -731,7 +659,6 @@ 651A44BA2A167FDF00DBCD92 /* CourseMockExtension.swift */, 658E4A312A61C7D600B43798 /* ComparableExtension.swift */, 657EBAFA2A8927E8005525AA /* SkeletonExtension.swift */, - 65B981AE2A7BDEF800FFC2F4 /* CGPointExtension.swift */, 652EDCBC2AC5CAB1009D9A3C /* GeometryProxyExtension.swift */, 65BA61D62AE02B5C009FE11C /* PathStringExtension.swift */, 652C6D9C2B08F0AF0021B5BD /* CollectionExtension.swift */, @@ -1163,6 +1090,14 @@ path = Themis/Views/Exercises; sourceTree = SOURCE_ROOT; }; + F72D68F32ACB31BF001A0871 /* Apollon */ = { + isa = PBXGroup; + children = ( + F72D68F42ACB31EE001A0871 /* UMLBadgeSymbol.swift */, + ); + path = Apollon; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1196,6 +1131,8 @@ 657534BB2A0F7A52004B0C3F /* UserStore */, 65F007972A86C837000FD641 /* Shimmer */, 06A99D3E2A94D99000E48066 /* Starscream */, + F734D1AB2B18D83200435866 /* ApollonShared */, + F734D1AD2B18D83200435866 /* ApollonView */, ); productName = feedback2go; productReference = 83396D2A29155935003EF727 /* Themis.app */; @@ -1277,6 +1214,7 @@ 65F007962A86C837000FD641 /* XCRemoteSwiftPackageReference "SwiftUI-Shimmer" */, 06A99D3D2A94D99000E48066 /* XCRemoteSwiftPackageReference "Starscream" */, 0697E6F02B0653DD00534F5B /* XCRemoteSwiftPackageReference "SwiftLint" */, + F734D1AA2B18D83200435866 /* XCRemoteSwiftPackageReference "apollon-ios-module" */, ); productRefGroup = 83396D2B29155935003EF727 /* Products */; projectDirPath = ""; @@ -1349,7 +1287,6 @@ 65E6A0FF2A13D1630058E5D6 /* ProgrammingAssessmentServiceImpl.swift in Sources */, 6562B90C2A7FA93000C9C7D5 /* ExampleModelingSolutionView.swift in Sources */, DAFFA3FD297F12A800EA72B8 /* ExerciseListItem.swift in Sources */, - 650D88042A7A5C77004E7533 /* UMLUseCaseDiagramRelationshipRenderer.swift in Sources */, DA98C6612954DED7005727FB /* SubmissionSearchResult.swift in Sources */, 6530B0F92A6BF86700CBCA71 /* ModelingFeedbackDetail.swift in Sources */, 65D2C69C2A011CDC001D5CD1 /* SubmissionExtension.swift in Sources */, @@ -1359,9 +1296,9 @@ E200F785297ECDB700DE269D /* FileView.swift in Sources */, 65E7972A2A1A4EF800E17401 /* ExamListItem.swift in Sources */, 658E4A322A61C7D600B43798 /* ComparableExtension.swift in Sources */, + 658E4A322A61C7D600B43798 /* ComparableExtension.swift in Sources */, 65AB56D52ADEF60D004B5F3F /* CoursePickerTip.swift in Sources */, 658E4A322A61C7D600B43798 /* ComparableExtension.swift in Sources */, - 650D88022A7A5C33004E7533 /* UMLUseCaseDiagramElementRenderer.swift in Sources */, DAFFA3FB297F066200EA72B8 /* ArtemisDateHelpers.swift in Sources */, 65E4C1982A25345100CA173B /* FeedbackDelegate.swift in Sources */, E2CAE63B2935219E00FAD327 /* ExerciseViewModel.swift in Sources */, @@ -1377,7 +1314,6 @@ E168C723296DCCE100A1D1D4 /* ThemisButtonStyle.swift in Sources */, F922443A297BFD1A008E4374 /* CircularProgressView.swift in Sources */, 65DD80BE2A758BE200234BFE /* ProgrammingAssessmentViewModel.swift in Sources */, - 651976182A65A08A004E33C8 /* UMLRelationshipType.swift in Sources */, 652A98022A07D51C00EB1F32 /* ToolbarCancelButton.swift in Sources */, 6567A90329EFF85200A5EFDA /* ScorePicker.swift in Sources */, 65D9AF002AB5E10A00500088 /* UnsupportedFileViewModel.swift in Sources */, @@ -1406,13 +1342,11 @@ 6581C67D2A305F3800E49E24 /* FeedbackDetail.swift in Sources */, 6589205729EE7C18005EE6FA /* LoginButtonStyle.swift in Sources */, 83396D3029155935003EF727 /* ContentView.swift in Sources */, - 654BC99B2A7007D00081CF22 /* SelectableUMLItem.swift in Sources */, 655084632A1B4E2300748A6F /* ProblemStatementView.swift in Sources */, 65A49CE72A558AC200F8B716 /* ExerciseHelperService.swift in Sources */, 0BEC4C35292EA1A400F9F802 /* FiletreeSidebarView.swift in Sources */, 659F879D2A6EF16A005ABAFD /* ExerciseRendererViewModel.swift in Sources */, 65C2F7532A03E3D4001EDADC /* FormatterExtension.swift in Sources */, - 65593DF72A72B87000B2DEBE /* UMLClassDiagramElementRenderer.swift in Sources */, E21AF055292B9D4C0096ACFC /* TabsView.swift in Sources */, 65D9AEFC2AB58C3900500088 /* QuickLookFileRenderer.swift in Sources */, 8384C43129210474008CCB4D /* FeedbackListView.swift in Sources */, @@ -1427,7 +1361,6 @@ E200F787297F10B300DE269D /* SourceCodeLanguage.swift in Sources */, 65D2C6892A011815001D5CD1 /* ExerciseExtension.swift in Sources */, 8384C441292AA1FB008CCB4D /* GradingCriteriaCellView.swift in Sources */, - 6519761A2A65A09F004E33C8 /* UMLElementType.swift in Sources */, 658057182A02C64B00835AD0 /* ExamExtension.swift in Sources */, 8384C42F29210458008CCB4D /* CorrectionGuidelinesCellView.swift in Sources */, 657EBAF92A8907D4005525AA /* ExamMockExtension.swift in Sources */, @@ -1438,19 +1371,14 @@ 650538BE2A1E6E3D00C45E18 /* SubmissionMockExtension.swift in Sources */, 659CDC8129FAD79100E5FA50 /* ViewHideExtension.swift in Sources */, 651A44B82A1672DE00DBCD92 /* ExerciseGroups.swift in Sources */, - 65B981B12A7C37E000FFC2F4 /* UMLGraphicsContext.swift in Sources */, - 65593DF92A72B8A300B2DEBE /* UMLClassDiagramRelationshipRenderer.swift in Sources */, 6536D70B2AB253E500A26E59 /* FileUploadAssessmentResult.swift in Sources */, 65DD80C22A758C1600234BFE /* ModelingAssessmentViewModel.swift in Sources */, 65B49F402A07F29400C9A45F /* PaneViewModel.swift in Sources */, - 6519761E2A65A14E004E33C8 /* UMLRelationship.swift in Sources */, 65A869962AAEF5D200B89199 /* FileRendererViewModel.swift in Sources */, 65F019072A1CDE1200BB1C98 /* TextAssessmentView.swift in Sources */, 65B49F3E2A07ED6100C9A45F /* ToolbarFileTreeToggleButton.swift in Sources */, - 65BC14532A6FD9F30002D089 /* UMLBadgeSymbol.swift in Sources */, 6530B0F72A6BE17E00CBCA71 /* ModelingAssessmentResult.swift in Sources */, E2192EE5291EBFFE0092CE58 /* AuthenticationView.swift in Sources */, - 65E85CC32A57EEAE007E3A06 /* UMLModel.swift in Sources */, 650538B92A1E1EF200C45E18 /* ExerciseMockExtension.swift in Sources */, 65B9BD092AAE18570086F9EC /* FileUploadAssessmentViewModel.swift in Sources */, 65DD80C02A758C0A00234BFE /* TextAssessmentViewModel.swift in Sources */, @@ -1458,11 +1386,10 @@ E1DC9BA5293D28E900674F5B /* SubmissionSearchViewModel.swift in Sources */, 65B49F4E2A08220D00C9A45F /* RightGripView.swift in Sources */, 6530B0F52A6BE00000CBCA71 /* ModelingAssessmentServiceImpl.swift in Sources */, - 65B981AF2A7BDEF800FFC2F4 /* CGPointExtension.swift in Sources */, + E2192EE5291EBFFE0092CE58 /* AuthenticationView.swift in Sources */, 65E6A1022A13E7260058E5D6 /* SubmissionService.swift in Sources */, 658057162A02C4DD00835AD0 /* CourseExtension.swift in Sources */, 65B9BD042AAE15110086F9EC /* FileUploadAssessmentView.swift in Sources */, - 654BC9992A6FF7630081CF22 /* UMLClassDiagramRenderer.swift in Sources */, E171F00E2933B5DD00236BC4 /* CodeEditorViewModel.swift in Sources */, A9752D792992AA8B004441D1 /* ExamSection.swift in Sources */, DA249E9629301E72008DE1D5 /* SubmissionListViewModel.swift in Sources */, @@ -1472,15 +1399,14 @@ E2192ED7291E57810092CE58 /* RESTError.swift in Sources */, 65B49F382A07D5C000C9A45F /* ToolbarPointsLabel.swift in Sources */, 655EEA2C2ACADD6B00AE6AEB /* FileErrorIcon.swift in Sources */, - 650D87FE2A7A5B65004E7533 /* UMLUseCaseDiagramRenderer.swift in Sources */, 83396D2E29155935003EF727 /* ThemisApp.swift in Sources */, 6536D7052AB1912400A26E59 /* FileRendererFactory.swift in Sources */, 6589205D29EF0840005EE6FA /* ColorExtension.swift in Sources */, + F72D68F52ACB31EE001A0871 /* UMLBadgeSymbol.swift in Sources */, E21AF058292B9D4C0096ACFC /* HighlightName.swift in Sources */, 65D2E1FF2A8CA4A8003DB837 /* CodeEditorSkeleton.swift in Sources */, DA249E9829302531008DE1D5 /* PreviewAuthenticationViewModel.swift in Sources */, 65CC29902ADC23760095ABDA /* GradingCriterionMockExtension.swift in Sources */, - 650D88002A7A5B78004E7533 /* UMLDiagramRenderer.swift in Sources */, 65AB56D72ADEF6EB004B5F3F /* FeedbackModeTip.swift in Sources */, E21AF054292B9D4C0096ACFC /* CodeView.swift in Sources */, 65BA61D72AE02B5C009FE11C /* PathStringExtension.swift in Sources */, @@ -1514,7 +1440,6 @@ 0BEC4C37292EA32A00F9F802 /* CodeEditorView.swift in Sources */, 6562B90E2A7FA94600C9C7D5 /* ExampleTextSolutionView.swift in Sources */, E2DC4821294626AD00DAF0CC /* DrawingEngine.swift in Sources */, - 6519761C2A65A0DF004E33C8 /* UMLElement.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1912,6 +1837,14 @@ kind = branch; }; }; + F734D1AA2B18D83200435866 /* XCRemoteSwiftPackageReference "apollon-ios-module" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ls1intum/apollon-ios-module"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -2000,6 +1933,16 @@ package = E536EACF297458240078E077 /* XCRemoteSwiftPackageReference "swiftui-reorderable-foreach" */; productName = SwiftUIReorderableForEach; }; + F734D1AB2B18D83200435866 /* ApollonShared */ = { + isa = XCSwiftPackageProductDependency; + package = F734D1AA2B18D83200435866 /* XCRemoteSwiftPackageReference "apollon-ios-module" */; + productName = ApollonShared; + }; + F734D1AD2B18D83200435866 /* ApollonView */ = { + isa = XCSwiftPackageProductDependency; + package = F734D1AA2B18D83200435866 /* XCRemoteSwiftPackageReference "apollon-ios-module" */; + productName = ApollonView; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 83396D2229155934003EF727 /* Project object */; diff --git a/Themis/Models/UML/UMLBadgeSymbol.swift b/Themis/Extensions/Apollon/UMLBadgeSymbol.swift similarity index 100% rename from Themis/Models/UML/UMLBadgeSymbol.swift rename to Themis/Extensions/Apollon/UMLBadgeSymbol.swift diff --git a/Themis/Extensions/CGPointExtension.swift b/Themis/Extensions/CGPointExtension.swift deleted file mode 100644 index 65d312c2..00000000 --- a/Themis/Extensions/CGPointExtension.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// CGPointExtension.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 03.08.23. -// - -import Foundation - -extension CGPoint { - /// Returns the angle relative to the comparison point - /// Inspired from: https://stackoverflow.com/a/33158630/7074664 - func angle(to comparisonPoint: CGPoint) -> CGFloat { - let originX = comparisonPoint.x - x - let originY = comparisonPoint.y - y - let bearingRadians = atan2f(Float(originY), Float(originX)) - var bearingDegrees = (bearingRadians * 180) / Float.pi - 90 - - while bearingDegrees >= 360 { bearingDegrees -= 360 } - while bearingDegrees < 0 { bearingDegrees += 360 } - - return CGFloat(bearingDegrees * -1) - } - - func rotated(around center: CGPoint, angleInDegrees angle: CGFloat) -> CGPoint { - let angleInRadians = angle * CGFloat.pi / 180.0 - let translatedPoint = CGPoint(x: self.x - center.x, y: self.y - center.y) - let rotatedX = translatedPoint.x * cos(angleInRadians) - translatedPoint.y * sin(angleInRadians) - let rotatedY = translatedPoint.x * sin(angleInRadians) + translatedPoint.y * cos(angleInRadians) - return CGPoint(x: rotatedX + center.x, y: rotatedY + center.y) - } - - func invertY() -> CGPoint { - Self(x: self.x, y: -self.y) - } -} diff --git a/Themis/Extensions/Core/UserFacingErrorExtension.swift b/Themis/Extensions/Core/UserFacingErrorExtension.swift index 4b32c25c..526c81a2 100644 --- a/Themis/Extensions/Core/UserFacingErrorExtension.swift +++ b/Themis/Extensions/Core/UserFacingErrorExtension.swift @@ -15,7 +15,9 @@ extension UserFacingError { // MARK: - Modeling static let diagramNotSupported = UserFacingError(title: "This UML diagram type is not supported") - + static let couldNotParseDiagram = UserFacingError(title: "Could not find UML diagram") + static let unsupportedDiagramVersion = UserFacingError(title: "This UML diagram has an unsupported or invalid version") + // MARK: - File Upload static let couldNotFetchFileDetails = UserFacingError(title: """ Could not fetch details of the uploaded file. The file URL may be invalid. diff --git a/Themis/Models/Feedback/ModelingFeedbackDetail.swift b/Themis/Models/Feedback/ModelingFeedbackDetail.swift index 20131f7e..5f12439a 100644 --- a/Themis/Models/Feedback/ModelingFeedbackDetail.swift +++ b/Themis/Models/Feedback/ModelingFeedbackDetail.swift @@ -7,6 +7,7 @@ import Foundation import SharedModels +import ApollonShared struct ModelingFeedbackDetail: FeedbackDetail { var umlItem: SelectableUMLItem? diff --git a/Themis/Models/UML/SelectableUMLItem.swift b/Themis/Models/UML/SelectableUMLItem.swift deleted file mode 100644 index 0d930491..00000000 --- a/Themis/Models/UML/SelectableUMLItem.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// SelectableUMLItem.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 25.07.23. -// - -import SwiftUI - -/// Represents any UML item (element, relationship, etc.) that can be selected by the user to add a referenced feedback -protocol SelectableUMLItem { - var id: String? { get } - var name: String? { get } - var owner: String? { get } - var bounds: Boundary? { get } - /// The path that should be highlighted when this element is selected - var highlightPath: Path? { get } - /// The path that should be highlighted when the corresponding feedback cell is tapped - var temporaryHighlightPath: Path? { get } - /// The path that should be highlighted if the item has a suggested feedback - var suggestedHighlightPath: Path? { get } - /// The point where this UML item prefers to have it's badge. Badges are used to indicate a feedback referencing this item - var badgeLocation: CGPoint? { get } - var boundsAsCGRect: CGRect? { get } - var typeAsString: String? { get } - - /// Returns true if the given point lies within the boundary of this element - func boundsContains(point: CGPoint) -> Bool -} - -extension SelectableUMLItem { // default implementations for all conforming types - var boundsAsCGRect: CGRect? { - guard let xCoordinate = bounds?.x, - let yCoordinate = bounds?.y, - let width = bounds?.width, - let height = bounds?.height else { - return nil - } - return CGRect(x: xCoordinate, y: yCoordinate, width: width, height: height) - } - - var temporaryHighlightPath: Path? { - highlightPath - } - - var suggestedHighlightPath: Path? { - highlightPath - } -} diff --git a/Themis/Models/UML/UMLElement.swift b/Themis/Models/UML/UMLElement.swift deleted file mode 100644 index 6676e638..00000000 --- a/Themis/Models/UML/UMLElement.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// UMLElement.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 17.07.23. -// - -import SwiftUI - -// Note: this is not a struct because we need references to handle parent-child relationship between elements -class UMLElement: Decodable, SelectableUMLItem { - let id: String? - let name: String? - let type: UMLElementType? - let owner: String? - let bounds: Boundary? - let assessmentNote: String? - - var children: [UMLElement]? = [] // not decoded - - /// Children of this element sorted by their vertical position (top to bottom) - var verticallySortedChildren: [UMLElement]? { - children?.sorted(by: { ($0.bounds?.y ?? 0.0) < ($1.bounds?.y ?? 0.0) }) - } - - var typeAsString: String? { - type?.rawValue - } - - var highlightPath: Path? { - guard let boundsAsCGRect else { - return nil - } - - return Path(boundsAsCGRect.insetBy(dx: -1, dy: -1)) - } - - lazy var badgeLocation: CGPoint? = { - guard let boundsAsCGRect else { - return nil - } - - return CGPoint(x: boundsAsCGRect.maxX, y: boundsAsCGRect.minY) - }() - - /// Returns a rectangular path from the top of the element until the top of the vertically highest child - var suggestedHighlightPath: Path? { - guard let boundsAsCGRect, - let highestChildMinY = verticallySortedChildren?.first?.boundsAsCGRect?.minY else { - return highlightPath - } - - var path = Path() - path.move(to: boundsAsCGRect.origin) - path.addLine(to: .init(x: boundsAsCGRect.maxX, y: boundsAsCGRect.minY)) - path.addLine(to: .init(x: boundsAsCGRect.maxX, y: highestChildMinY)) - path.addLine(to: .init(x: boundsAsCGRect.minX, y: highestChildMinY)) - return path - } - - /// Recursively looks for the child UML element located at the given point - func getChild(at point: CGPoint) -> UMLElement? { - guard let children else { - return nil - } - - for child in children where child.boundsContains(point: point) { - return child.getChild(at: point) ?? child - } - - return nil - } - - func addChild(_ child: UMLElement) { - if children == nil { - self.children = [child] - } else { - self.children?.append(child) - } - } - - func boundsContains(point: CGPoint) -> Bool { - guard let bounds else { - return false - } - - let isXWithinBounds = point.x > bounds.x && point.x < (bounds.x + bounds.width) - let isYWithinBounds = point.y > bounds.y && point.y < (bounds.y + bounds.height) - - return isXWithinBounds && isYWithinBounds - } -} - -struct Boundary: Decodable { - // swiftlint:disable identifier_name - let x: Double - let y: Double - let width: Double - let height: Double - // swiftlint:enable identifier_name -} diff --git a/Themis/Models/UML/UMLElementType.swift b/Themis/Models/UML/UMLElementType.swift deleted file mode 100644 index a00f0dce..00000000 --- a/Themis/Models/UML/UMLElementType.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// UMLElementType.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 17.07.23. -// - -import Foundation - -enum UMLElementType: String, Decodable { - case colorLegend = "ColorLegend" - case flowchartTerminal = "FlowchartTerminal" - case flowchartProcess = "FlowchartProcess" - case flowchartDecision = "FlowchartDecision" - case flowchartInputOutput = "FlowchartInputOutput" - case flowchartFunctionCall = "FlowchartFunctionCall" - case syntaxTreeTerminal = "SyntaxTreeTerminal" - case syntaxTreeNonterminal = "SyntaxTreeNonterminal" - case reachabilityGraphMarking = "ReachabilityGraphMarking" - case petriNetPlace = "PetriNetPlace" - case petriNetTransition = "PetriNetTransition" - case deploymentNode = "DeploymentNode" - case deploymentComponent = "DeploymentComponent" - case deploymentArtifact = "DeploymentArtifact" - case deploymentInterface = "DeploymentInterface" - case component = "Component" - case componentInterface = "ComponentInterface" - case communicationLinkMessage = "CommunicationLinkMessage" - case useCase = "UseCase" - case useCaseActor = "UseCaseActor" - case useCaseSystem = "UseCaseSystem" - case activity = "Activity" - case activityActionNode = "ActivityActionNode" - case activityFinalNode = "ActivityFinalNode" - case activityForkNode = "ActivityForkNode" - case activityForkNodeHorizontal = "ActivityForkNodeHorizontal" - case activityInitialNode = "ActivityInitialNode" - case activityMergeNode = "ActivityMergeNode" - case activityObjectNode = "ActivityObjectNode" - case objectName = "ObjectName" - case objectAttribute = "ObjectAttribute" - case objectMethod = "ObjectMethod" - case package = "Package" - // swiftlint:disable:next identifier_name - case Class = "Class" - case abstractClass = "AbstractClass" - case interface = "Interface" - case enumeration = "Enumeration" - case classAttribute = "ClassAttribute" - case classMethod = "ClassMethod" - - /// The string that should be shown on the UML elements to indicate the type of the element - var annotationTitle: String { - switch self { - case .interface: - return "≪interface≫" - case .enumeration: - return "≪enumeration≫" - case .abstractClass: - return "≪abstract≫" - default: - return "" - } - } -} diff --git a/Themis/Models/UML/UMLModel.swift b/Themis/Models/UML/UMLModel.swift deleted file mode 100644 index 778415e3..00000000 --- a/Themis/Models/UML/UMLModel.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// UMLModel.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 07.07.23. -// -import Foundation -import SharedModels - -struct UMLModel: Decodable { - let version: String? - let type: UMLDiagramType? - let size: Size? - var elements: [UMLElement]? - let relationships: [UMLRelationship]? -} - -struct Size: Decodable { - let width: Double - let height: Double - - var asCGSize: CGSize { - CGSize(width: width, height: height) - } -} diff --git a/Themis/Models/UML/UMLRelationship.swift b/Themis/Models/UML/UMLRelationship.swift deleted file mode 100644 index 4e0a44a4..00000000 --- a/Themis/Models/UML/UMLRelationship.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// UMLRelationship.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 17.07.23. -// -// swiftlint:disable discouraged_optional_boolean - -import Foundation -import SwiftUI - -struct UMLRelationship: Decodable, SelectableUMLItem { - let id: String? - let name: String? - let type: UMLRelationshipType? - let owner: String? - let bounds: Boundary? - let assessmentNote: String? - let path: [PathPoint]? - let source: UMLRelationshipEndPoint? - let target: UMLRelationshipEndPoint? - let isManuallyLayouted: Bool? - - var typeAsString: String? { - type?.rawValue - } - - /// Contains rectangles drawn between PathPoints - private var pathRects: [CGRect] { - guard let path, - let boundsX = bounds?.x, - let boundsY = bounds?.y, - let boundsAsCGRect else { - return [] - } - - var result = [CGRect]() - - // Iterate through points and create CGRects between them - for index in 0..= 2 else { - return nil - } - - let points = selfPath.map { - $0.asCGPoint.applying(.init(translationX: boundsAsCGRect.minX, - y: boundsAsCGRect.minY)) - } - - var path = Path() - path.addLines(Array(points)) - - return path - } - - func boundsContains(point: CGPoint) -> Bool { - if let pathWithCGPoints { - return pathWithCGPoints.strokedPath(.init(lineWidth: 20)).contains(point) - } - return pathRects.contains(where: { $0.contains(point) }) - } -} - -struct PathPoint: Decodable { - // swiftlint:disable identifier_name - let x: Double - let y: Double - - var asCGPoint: CGPoint { - CGPoint(x: x, y: y) - } -} - -struct UMLRelationshipEndPoint: Decodable { - /// The id of the UML element that the endpoint is attached to - let element: String? - /// Indicates the side, from which the endpoint is attached to the UML element - let direction: Direction? - let multiplicity: String? - let role: String? -} - -enum Direction: String, Decodable { - case left = "Left" - case right = "Right" - case up = "Up" - case down = "Down" - case upRight = "Upright" - case upLeft = "Upleft" - case downRight = "Downright" - case downLeft = "Downleft" - case topRight = "Topright" - case topLeft = "Topleft" - case bottomRight = "Bottomright" - case bottomLeft = "Bottomleft" - - var inverted: Self { - switch self { - case .left: - return .right - case .right: - return .left - case .down: - return .up - case .up: - return .down - case .upRight: - return .upLeft - case .upLeft: - return .upRight - case .downRight: - return .downLeft - case .downLeft: - return .downRight - case .topRight: - return .bottomRight - case .topLeft: - return .bottomLeft - case .bottomRight: - return .topRight - case .bottomLeft: - return .topLeft - } - } -} -// swiftlint:enable identifier_name -// swiftlint:enable discouraged_optional_boolean diff --git a/Themis/Models/UML/UMLRelationshipType.swift b/Themis/Models/UML/UMLRelationshipType.swift deleted file mode 100644 index 6ce9a14d..00000000 --- a/Themis/Models/UML/UMLRelationshipType.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// UMLRelationshipType.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 17.07.23. -// - -import Foundation - -enum UMLRelationshipType: String, Decodable { - case flowchartFlowline = "FlowchartFlowline" - case syntaxTreeLink = "SyntaxTreeLink" - case reachabilityGraphArc = "ReachabilityGraphArc" - case petriNetArc = "PetriNetArc" - case deploymentAssociation = "DeploymentAssociation" - case deploymentInterfaceProvided = "DeploymentInterfaceProvided" - case deploymentInterfaceRequired = "DeploymentInterfaceRequired" - case deploymentDependency = "DeploymentDependency" - case componentInterfaceProvided = "ComponentInterfaceProvided" - case componentInterfaceRequired = "ComponentInterfaceRequired" - case componentDependency = "ComponentDependency" - case communicationLink = "CommunicationLink" - case useCaseAssociation = "UseCaseAssociation" - case useCaseGeneralization = "UseCaseGeneralization" - case useCaseInclude = "UseCaseInclude" - case useCaseExtend = "UseCaseExtend" - case activityControlFlow = "ActivityControlFlow" - case objectLink = "ObjectLink" - case classBidirectional = "ClassBidirectional" - case classUnidirectional = "ClassUnidirectional" - case classInheritance = "ClassInheritance" - case classRealization = "ClassRealization" - case classDependency = "ClassDependency" - case classAggregation = "ClassAggregation" - case classComposition = "ClassComposition" - - /// The string that should be shown on the UML relationship to indicate the type of the element - var annotationTitle: String { - switch self { - case .useCaseExtend: - return "≪extend≫" - case .useCaseInclude: - return "≪include≫" - default: - return "" - } - } -} diff --git a/Themis/ViewModels/Assessment/Modeling/UMLRendererViewModel.swift b/Themis/ViewModels/Assessment/Modeling/UMLRendererViewModel.swift index de6ad73a..e9c891f5 100644 --- a/Themis/ViewModels/Assessment/Modeling/UMLRendererViewModel.swift +++ b/Themis/ViewModels/Assessment/Modeling/UMLRendererViewModel.swift @@ -9,12 +9,14 @@ import Foundation import SharedModels import Common import SwiftUI +import ApollonShared class UMLRendererViewModel: ExerciseRendererViewModel { @Published var umlModel: UMLModel? @Published var selectedElement: SelectableUMLItem? @Published var error: Error? @Published var currentDragLocation = CGPoint.zero + @Published var offset = CGPoint(x: 15, y: 15) /// Intended to get user's attention to a particular UML item temporarily @Published var temporaryHighlight: UMLHighlight? { @@ -51,8 +53,6 @@ class UMLRendererViewModel: ExerciseRendererViewModel { /// A Task that sets the value of `temporaryHighlight` to nil after some time private var temporaryHighlightRemovalTask: Task<(), Error>? - private var diagramTypeUnsupported = false - private var symbolSize = 30.0 /// Sets this VM up based on the given submission @@ -60,6 +60,8 @@ class UMLRendererViewModel: ExerciseRendererViewModel { func setup(basedOn submission: BaseSubmission? = nil, _ assessmentResult: AssessmentResult) { guard let modelingSubmission = submission as? ModelingSubmission, let modelData = modelingSubmission.model?.data(using: .utf8) else { + log.error("Could not get model data from submission") + setError(.unsupportedDiagramVersion) return } self.umlModel = nil @@ -69,10 +71,16 @@ class UMLRendererViewModel: ExerciseRendererViewModel { do { umlModel = try JSONDecoder().decode(UMLModel.self, from: modelData) + guard let type = umlModel?.type, !UMLDiagramType.isDiagramTypeUnsupported(diagramType: type) else { + log.error("This diagram type is not supported") + setError(.diagramNotSupported) + return + } determineChildren() - orphanElements = umlModel?.elements?.filter({ $0.owner == nil }) ?? [] + orphanElements = umlModel?.elements?.values.filter { $0.owner == nil } ?? [] } catch { log.error("Could not parse UML string: \(error)") + setError(.couldNotParseDiagram) } let feedbacks = assessmentResult.inlineFeedback + assessmentResult.automaticFeedback @@ -85,6 +93,7 @@ class UMLRendererViewModel: ExerciseRendererViewModel { func setup(basedOn umlModelString: String) { guard let modelData = umlModelString.data(using: .utf8) else { log.error("Invalid UML model string") + setError(.couldNotParseDiagram) return } self.umlModel = nil @@ -94,39 +103,22 @@ class UMLRendererViewModel: ExerciseRendererViewModel { do { umlModel = try JSONDecoder().decode(UMLModel.self, from: modelData) + guard let type = umlModel?.type, !UMLDiagramType.isDiagramTypeUnsupported(diagramType: type) else { + log.error("This diagram type is not yet supported") + setError(.diagramNotSupported) + return + } determineChildren() - orphanElements = umlModel?.elements?.filter({ $0.owner == nil }) ?? [] + orphanElements = umlModel?.elements?.values.filter { $0.owner == nil } ?? [] + } catch { log.error("Could not parse UML string: \(error)") + setError(.couldNotParseDiagram) } undoManager.removeAllActions() } - @MainActor - func render(_ context: inout GraphicsContext, size: CGSize) { - guard let model = umlModel, - let modelType = model.type, - !diagramTypeUnsupported else { - return - } - let umlContext = UMLGraphicsContext(context) - let canvasBounds = CGRect(x: 0, y: 0, width: size.width, height: size.height) - - let renderer = UMLDiagramRendererFactory.renderer(for: modelType, - context: umlContext, - canvasBounds: canvasBounds, - fontSize: fontSize) - - if let renderer { - renderer.render(umlModel: model) - } else { - log.error("Attempted to draw an unknown diagram type") - diagramTypeUnsupported = true - setError(.diagramNotSupported) - } - } - private func setError(_ error: UserFacingError) { if self.error == nil { Task { @MainActor [weak self] in @@ -164,10 +156,10 @@ class UMLRendererViewModel: ExerciseRendererViewModel { } private func getSelectableItem(at point: CGPoint) -> SelectableUMLItem? { - let point = CGPoint(x: point.x - UMLGraphicsContext.defaultOffset, - y: point.y - UMLGraphicsContext.defaultOffset) + let point = CGPoint(x: point.x - offset.x, + y: point.y - offset.y) // Look for relationships - if let foundRelationship = umlModel?.relationships?.first(where: { $0.boundsContains(point: point) }) { + if let foundRelationship = umlModel?.relationships?.values.first(where: { $0.boundsContains(point: point) }) { return foundRelationship } @@ -186,11 +178,11 @@ class UMLRendererViewModel: ExerciseRendererViewModel { log.warning("Could not find elements in the model") return } - var potentialChildren = elements.filter({ $0.owner != nil }) - - for (elementIndex, element) in elements.enumerated().reversed() { - for (index, potentialChild) in potentialChildren.enumerated().reversed() where potentialChild.owner == element.id { - elements[elementIndex].addChild(potentialChild) + var potentialChildren = elements.values.filter({ $0.owner != nil }) + + for element in elements.reversed() { + for (index, potentialChild) in potentialChildren.enumerated().reversed() where potentialChild.owner == element.value.id { + elements[element.key]?.addChild(potentialChild) potentialChildren.remove(at: index) } } @@ -203,10 +195,10 @@ class UMLRendererViewModel: ExerciseRendererViewModel { var selectableItem: SelectableUMLItem? if let elements = umlModel?.elements, - let foundElement = elements.first(where: { $0.id == id }) { + let foundElement = elements.values.first(where: { $0.id == id }) { selectableItem = foundElement } else if let relationships = umlModel?.relationships, - let foundRelationship = relationships.first(where: { $0.id == id }) { + let foundRelationship = relationships.values.first(where: { $0.id == id }) { selectableItem = foundRelationship } @@ -273,7 +265,7 @@ extension UMLRendererViewModel { @MainActor func renderHighlights(_ context: inout GraphicsContext, size: CGSize) { - var context = UMLGraphicsContext(context) + var context = UMLGraphicsContext(context, offset: offset) // Highlight selected element if there is one if !pencilModeDisabled, diff --git a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Class Diagram/UMLClassDiagramElementRenderer.swift b/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Class Diagram/UMLClassDiagramElementRenderer.swift deleted file mode 100644 index c2b84221..00000000 --- a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Class Diagram/UMLClassDiagramElementRenderer.swift +++ /dev/null @@ -1,167 +0,0 @@ -// -// UMLClassDiagramElementRenderer.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 27.07.23. -// - -import SwiftUI -import Common - -struct UMLClassDiagramElementRenderer: UMLDiagramRenderer { - var context: UMLGraphicsContext - let canvasBounds: CGRect - var fontSize: CGFloat - - func render(umlModel: UMLModel) { - guard let elements = umlModel.elements else { - log.error("The UML model contains no elements") - return - } - - for element in elements { - draw(element: element) - } - } - - private func draw(element: UMLElement) { - guard let xCoordinate = element.bounds?.x, - let yCoordinate = element.bounds?.y, - let width = element.bounds?.width, - let height = element.bounds?.height else { - log.warning("Failed to draw a UML element: \(element)") - return - } - - let elementRect = CGRect(x: xCoordinate, y: yCoordinate, width: width, height: height) - - switch element.type { - case .Class, .abstractClass, .classMethod, .classAttribute, .enumeration, .interface: - drawClassLikeElement(element: element, elementRect: elementRect) - case .package: - drawPackage(element: element, elementRect: elementRect) - default: - drawUnknownElement(element: element, elementRect: elementRect) - } - } - - private func drawClassLikeElement(element: UMLElement, elementRect: CGRect) { - switch element.type { - case .classAttribute, .classMethod: - drawAttributeOrMethod(element, in: elementRect) - default: - drawTitle(of: element, in: elementRect) - } - } - - private func drawPackage(element: UMLElement, elementRect: CGRect) { - - let topCornerRect = CGRect(x: elementRect.minX, - y: elementRect.minY, - width: 45, - height: 10) - context.fill(Path(topCornerRect), with: .color(Color(UIColor.systemBackground))) - context.stroke(Path(topCornerRect), with: .color(Color.primary)) - - let newElementRect = CGRect(x: elementRect.minX, - y: elementRect.minY + topCornerRect.height, - width: elementRect.width, - height: elementRect.height - topCornerRect.height) - - context.fill(Path(newElementRect), with: .color(Color(UIColor.systemBackground))) - context.stroke(Path(newElementRect), with: .color(Color.primary)) - drawTitle(of: element, in: newElementRect) - } - - private func drawUnknownElement(element: UMLElement, elementRect: CGRect) { - log.warning("Drawing logic for elements of type \(element.type?.rawValue ?? "nil") is not implemented") - context.stroke(Path(elementRect), with: .color(Color.secondary)) - } - - private func drawTitle(of element: UMLElement, in elementRect: CGRect) { - var titleY: CGFloat - - context.fill(Path(elementRect), with: .color(Color(UIColor.systemBackground))) - context.stroke(Path(elementRect), with: .color(Color.primary)) - - // START: Draw type text - let typeTextString = element.type?.annotationTitle ?? "" - var typeText = Text(typeTextString) - typeText = typeText.font(.system(size: fontSize * 0.7, weight: .bold).monospaced()) - let typeResolved = context.resolve(typeText) - let typeTextSize = typeResolved.measure(in: elementRect.size) - - let typeRect = CGRect(x: elementRect.midX - typeTextSize.width / 2, - y: elementRect.minY + 5, - width: typeTextSize.width, - height: typeTextSize.height) - context.draw(typeResolved, in: typeRect) - // END: Draw type text - - titleY = typeTextString.isEmpty ? elementRect.minY + 5 : typeRect.maxY - - // START: Draw title text - var text = Text(element.name ?? "") - text = text.font(.system(size: fontSize, weight: .bold)) - - if element.type == .abstractClass { - text = text.italic() - } - - let elementTitle = context.resolve(text) - let titleSize = elementTitle.measure(in: elementRect.size) - let titleRect = CGRect(x: elementRect.midX - titleSize.width / 2, - y: titleY, - width: titleSize.width, - height: titleSize.height) - - context.draw(elementTitle, in: titleRect) - // END: Draw title text - - if [UMLElementType.Class, .abstractClass, .enumeration, .interface].contains(element.type) { - drawAttributeAndMethodSeparators(element, in: elementRect) - } - } - - private func drawAttributeAndMethodSeparators(_ element: UMLElement, in elementRect: CGRect) { - // Draw a line above the first attribute of this element - if let firstAttribute = element.verticallySortedChildren?.first(where: { $0.type == .classAttribute }), - let firstAttributeTopLeft = firstAttribute.boundsAsCGRect?.origin, - let firstAttributeSize = firstAttribute.boundsAsCGRect?.size { - let firstAttributeTopRight = firstAttributeTopLeft.applying(.init(translationX: firstAttributeSize.width, y: 0)) - - var attributePath = Path() - attributePath.move(to: firstAttributeTopLeft) - attributePath.addLine(to: firstAttributeTopRight) - - context.stroke(attributePath, with: .color(.primary)) - } - - // Draw a line above the first method of this element - if let firstMethod = element.verticallySortedChildren?.first(where: { $0.type == .classMethod }), - let firstMethodTopLeft = firstMethod.boundsAsCGRect?.origin, - let firstMethodSize = firstMethod.boundsAsCGRect?.size { - let firstMethodTopRight = firstMethodTopLeft.applying(.init(translationX: firstMethodSize.width, y: 0)) - - var methodPath = Path() - methodPath.move(to: firstMethodTopLeft) - methodPath.addLine(to: firstMethodTopRight) - - context.stroke(methodPath, with: .color(.primary)) - } - } - - private func drawAttributeOrMethod(_ element: UMLElement, in elementRect: CGRect) { - var text = Text(element.name ?? "") - text = text.font(.system(size: fontSize)) - - let elementTitle = context.resolve(text) - let titleSize = elementTitle.measure(in: elementRect.size) - let titleRect = CGRect(x: elementRect.minX + 5, - y: elementRect.midY - titleSize.height / 2, - width: titleSize.width, - height: titleSize.height) - - context.draw(elementTitle, in: titleRect) - } -} diff --git a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Class Diagram/UMLClassDiagramRelationshipRenderer.swift b/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Class Diagram/UMLClassDiagramRelationshipRenderer.swift deleted file mode 100644 index ff69a440..00000000 --- a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Class Diagram/UMLClassDiagramRelationshipRenderer.swift +++ /dev/null @@ -1,347 +0,0 @@ -// -// UMLClassDiagramRelationshipRenderer.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 27.07.23. -// - -import SwiftUI -import Common - -// swiftlint:disable:next type_body_length -struct UMLClassDiagramRelationshipRenderer: UMLDiagramRenderer { - var context: UMLGraphicsContext - let canvasBounds: CGRect - var fontSize: CGFloat - - func render(umlModel: UMLModel) { - guard let relationships = umlModel.relationships else { - log.warning("The UML model contains no relationships") - return - } - - for relationship in relationships { - draw(relationship: relationship) - } - } - - private func draw(relationship: UMLRelationship) { - guard let relationshipRect = relationship.boundsAsCGRect else { - log.warning("Failed to draw a UML relationship: \(relationship)") - return - } - - switch relationship.type { - case .classDependency: - drawDependency(relationship, in: relationshipRect) - case .classAggregation, .classComposition: - drawAggregationOrComposition(relationship, in: relationshipRect) - case .classInheritance: - drawInheritance(relationship, in: relationshipRect) - case .classRealization: - drawRealization(relationship, in: relationshipRect) - case .classBidirectional: - drawAssociation(relationship, in: relationshipRect) - case .classUnidirectional: - drawAssociation(relationship, in: relationshipRect) - default: - drawUnknown(relationship, in: relationshipRect) - } - - drawMultiplicityText(relationship, in: relationshipRect) - drawRoleText(relationship, in: relationshipRect) - } - - private func drawDependency(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = relationship.pathWithCGPoints else { - return - } - - context.stroke(path, with: .color(Color.primary), style: .init(dash: [7, 7])) - drawArrowhead(for: relationship, on: path) - } - - private func drawAggregationOrComposition(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = relationship.pathWithCGPoints else { - return - } - - context.stroke(path, with: .color(Color.primary)) - drawArrowhead(for: relationship, on: path) - } - - private func drawInheritance(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = relationship.pathWithCGPoints else { - return - } - - context.stroke(path, with: .color(Color.primary)) - drawArrowhead(for: relationship, on: path) - } - - private func drawRealization(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = relationship.pathWithCGPoints else { - return - } - - context.stroke(path, with: .color(Color.primary), style: .init(dash: [7, 7])) - drawArrowhead(for: relationship, on: path) - } - - private func drawAssociation(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = relationship.pathWithCGPoints else { - return - } - - context.stroke(path, with: .color(Color.primary)) - drawArrowhead(for: relationship, on: path) - } - - private func drawUnknown(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - drawAssociation(relationship, in: relationshipRect) - } - - private func drawArrowhead(for relationship: UMLRelationship, on path: Path) { - guard let endPoint = path.currentPoint, - let arrowTargetDirection = relationship.target?.direction else { - log.warning("Could not draw arrowhead for: \(relationship)") - return - } - - var type: ArrowHeadType - - switch relationship.type { - case .classBidirectional: - return // bidirectional arrows have no heads - case .classDependency, .classUnidirectional: - type = .triangleWithoutBase - case .classInheritance, .classRealization: - type = .triangle - case .classComposition: - type = .rhombusFilled - case .classAggregation: - type = .rhombus - default: - type = .triangle - } - - drawArrowhead(at: endPoint, lookingAt: arrowTargetDirection.inverted, type: type) - } - - private func drawArrowhead(at point: CGPoint, lookingAt direction: Direction, type: ArrowHeadType) { - var path = Path() - let size: CGFloat = 10 - - switch type { - case .triangle: - path.move(to: .init(x: point.x, y: point.y)) - path.addLine(to: .init(x: point.x - size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x + size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x, y: point.y)) - case .triangleWithoutBase: - path.move(to: .init(x: point.x - size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x, y: point.y)) - path.addLine(to: .init(x: point.x + size, y: point.y + size * 1.5)) - case .rhombus, .rhombusFilled: - path.move(to: .init(x: point.x, y: point.y)) - path.addLine(to: .init(x: point.x - size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x, y: point.y + size * 3)) - path.addLine(to: .init(x: point.x + size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x, y: point.y)) - } - - switch direction { - case .up, .topLeft, .topRight: - path = path.offsetBy(dx: 0, dy: size * 0.15) - case .down, .downRight: - path = path.rotation(.degrees(180)).path(in: path.boundingRect) - if type == .rhombus || type == .rhombusFilled { - path = path.offsetBy(dx: 0, dy: size * -3.1) - } else { - path = path.offsetBy(dx: 0, dy: size * -1.6) - } - case .right, .upRight, .bottomLeft: - path = path.rotation(.degrees(90)).path(in: path.boundingRect) - if type == .rhombus || type == .rhombusFilled { - path = path.offsetBy(dx: size * -1.55, dy: size * -1.5) - } else { - path = path.offsetBy(dx: size * -0.8, dy: size * -0.75) - } - case .left, .upLeft, .downLeft, .bottomRight: - path = path.rotation(.degrees(-90)).path(in: path.boundingRect) - if type == .rhombus || type == .rhombusFilled { - path = path.offsetBy(dx: size * 1.55, dy: size * -1.5) - } else { - path = path.offsetBy(dx: size * 0.8, dy: size * -0.75) - } - } - - context.stroke(path, with: .color(Color.primary)) - - // Fill - if type != .triangleWithoutBase { - let fillColor = (type == .rhombusFilled) ? Color(UIColor.label) : Color(UIColor.systemBackground) - context.fill(path, with: .color(fillColor)) - } - } - - private func drawMultiplicityText(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let sourcePoint = relationship.path?.first?.asCGPoint, - let sourceDirection = relationship.source?.direction?.inverted, - let targetPoint = relationship.path?.last?.asCGPoint, - let targetDirection = relationship.target?.direction?.inverted else { - log.warning("Could not draw multiplicity text for: \(relationship)") - return - } - - var targetOffset: CGPoint - - switch relationship.type { - case .classDependency, .classUnidirectional: - targetOffset = .init(x: 10, y: 10) - case .classInheritance, .classRealization: - targetOffset = .init(x: 0, y: fontSize * 1.05) - case .classComposition, .classAggregation: - targetOffset = .init(x: 0, y: fontSize * 1.4) - default: - targetOffset = .zero - } - - if let sourceMultiplicity = relationship.source?.multiplicity { - drawMultiplicityText(sourceMultiplicity, - at: sourcePoint, - inParentRect: relationshipRect, - direction: sourceDirection, - offset: .zero) - } - - if let targetMultiplicity = relationship.target?.multiplicity { - drawMultiplicityText(targetMultiplicity, - at: targetPoint, - inParentRect: relationshipRect, - direction: targetDirection, - offset: targetOffset) - } - } - - /// Draws text representing the multiplicity value of a UML relationship - /// - Parameters: - /// - text: multiplicity - /// - point: where to draw - /// - rect: parent rect of the relationship - /// - direction: where the arrowhead is looking at - /// - offset: to move the text in case the arrow head is too large (this is usually needed for aggregation and composition relationships) - private func drawMultiplicityText(_ text: String, - at point: CGPoint, - inParentRect rect: CGRect, - direction: Direction, - offset: CGPoint) { - let multiplicityText = Text(text).font(.system(size: fontSize)) - let resolvedText = context.resolve(multiplicityText) - let textSize = resolvedText.measure(in: rect.size) - - var xPosition: CGFloat - var yPosition: CGFloat - - switch direction { - case .up, .topLeft, .topRight: - xPosition = rect.minX + point.x + 7 + offset.x - yPosition = rect.minY + point.y + 5 + offset.y - case .down, .downLeft, .downRight: - xPosition = rect.minX + point.x + 7 + offset.x - yPosition = rect.minY + point.y - 20 - offset.y - case .right, .upRight, .bottomRight: - xPosition = rect.minX + point.x - 14 - offset.y - yPosition = rect.minY + point.y + 5 + offset.x - case .left, .upLeft, .bottomLeft: - xPosition = rect.minX + point.x + 7 + offset.y - yPosition = rect.minY + point.y + 5 + offset.x - } - - let textRect = CGRect(x: xPosition, y: yPosition, width: textSize.width, height: textSize.height) - - context.draw(resolvedText, in: textRect) - } - - private func drawRoleText(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let sourcePoint = relationship.path?.first?.asCGPoint, - let sourceDirection = relationship.source?.direction?.inverted, - let targetPoint = relationship.path?.last?.asCGPoint, - let targetDirection = relationship.target?.direction?.inverted else { - log.warning("Could not draw role text for: \(relationship)") - return - } - - var targetOffset: CGPoint - - switch relationship.type { - case .classDependency, .classUnidirectional: - targetOffset = .init(x: 0, y: fontSize * 1.05) - case .classInheritance, .classRealization: - targetOffset = .init(x: 0, y: fontSize * 1.05) - case .classComposition, .classAggregation: - targetOffset = .init(x: 0, y: fontSize * 1.4) - default: - targetOffset = .zero - } - - if let sourceRole = relationship.source?.role { - drawRoleText(sourceRole, - at: sourcePoint, - inParentRect: relationshipRect, - direction: sourceDirection, - offset: .zero) - } - - if let targetRole = relationship.target?.role { - drawRoleText(targetRole, - at: targetPoint, - inParentRect: relationshipRect, - direction: targetDirection, - offset: targetOffset) - } - } - - /// Draws text representing the role of an element in a UML relationship - /// - Parameters: - /// - text: role - /// - point: where to draw - /// - rect: parent rect of the relationship - /// - direction: where the arrowhead is looking at - /// - offset: to move the text in case the arrow head is too large (this is usually needed for aggregation and composition relationships) - private func drawRoleText(_ text: String, - at point: CGPoint, - inParentRect rect: CGRect, - direction: Direction, - offset: CGPoint) { - let roleText = Text(text).font(.system(size: fontSize)) - let resolvedText = context.resolve(roleText) - let textSize = resolvedText.measure(in: canvasBounds.size) - - var xPosition: CGFloat - var yPosition: CGFloat - - switch direction { - case .up, .topLeft, .topRight: - xPosition = rect.minX + point.x - textSize.width - 7 - offset.x - yPosition = rect.minY + point.y + 5 + offset.y - case .down, .downLeft, .downRight: - xPosition = rect.minX + point.x - textSize.width - 7 + offset.x - yPosition = rect.minY + point.y - textSize.height - 5 - offset.y - case .right, .upRight, .bottomRight: - xPosition = rect.minX + point.x - textSize.width - offset.y - yPosition = rect.minY + point.y - textSize.height - 5 + offset.x - case .left, .upLeft, .bottomLeft: - xPosition = rect.minX + point.x + 7 + offset.y - yPosition = rect.minY + point.y - textSize.height - 5 + offset.x - } - - let textRect = CGRect(x: xPosition, y: yPosition, width: textSize.width, height: textSize.height) - - context.draw(resolvedText, in: textRect) - } -} - -enum ArrowHeadType { - case triangle, triangleWithoutBase, rhombus, rhombusFilled -} diff --git a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Class Diagram/UMLClassDiagramRenderer.swift b/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Class Diagram/UMLClassDiagramRenderer.swift deleted file mode 100644 index e68f27f6..00000000 --- a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Class Diagram/UMLClassDiagramRenderer.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// UMLClassDiagramRenderer.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 07.07.23. -// - -import SwiftUI -import Common -import SharedModels - -struct UMLClassDiagramRenderer: UMLDiagramRenderer { - var context: UMLGraphicsContext - let canvasBounds: CGRect - var fontSize: CGFloat = 14 - - func render(umlModel: UMLModel) { - let elementRenderer = UMLClassDiagramElementRenderer(context: context, - canvasBounds: canvasBounds, - fontSize: fontSize) - let relationshipRenderer = UMLClassDiagramRelationshipRenderer(context: context, - canvasBounds: canvasBounds, - fontSize: fontSize) - - elementRenderer.render(umlModel: umlModel) - relationshipRenderer.render(umlModel: umlModel) - } -} diff --git a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/UMLDiagramRenderer.swift b/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/UMLDiagramRenderer.swift deleted file mode 100644 index dca7a081..00000000 --- a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/UMLDiagramRenderer.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// UMLDiagramRenderer.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 02.08.23. -// - -import SwiftUI -import SharedModels - -protocol UMLDiagramRenderer { - /// This instance should be used for all actions that one would normally perform with a `GraphicsContext` - var context: UMLGraphicsContext { get set } - var canvasBounds: CGRect { get } - var fontSize: CGFloat { get set } - - func render(umlModel: UMLModel) -} - -enum UMLDiagramRendererFactory { - static func renderer(for type: UMLDiagramType, context: UMLGraphicsContext, canvasBounds: CGRect, fontSize: CGFloat) -> UMLDiagramRenderer? { - switch type { - case .classDiagram: - return UMLClassDiagramRenderer(context: context, canvasBounds: canvasBounds, fontSize: fontSize) - case .useCaseDiagram: - return UMLUseCaseDiagramRenderer(context: context, canvasBounds: canvasBounds, fontSize: fontSize) - default: - return nil - } - } -} diff --git a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Use Case Diagram/UMLUseCaseDiagramElementRenderer.swift b/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Use Case Diagram/UMLUseCaseDiagramElementRenderer.swift deleted file mode 100644 index 8cacd95f..00000000 --- a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Use Case Diagram/UMLUseCaseDiagramElementRenderer.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// UMLUseCaseDiagramElementRenderer.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 02.08.23. -// - -import SwiftUI -import Common - -struct UMLUseCaseDiagramElementRenderer: UMLDiagramRenderer { - var context: UMLGraphicsContext - let canvasBounds: CGRect - var fontSize: CGFloat - - func render(umlModel: UMLModel) { - guard let elements = umlModel.elements else { - log.error("The UML model contains no elements") - return - } - - for element in elements { - draw(element: element) - } - } - - private func draw(element: UMLElement) { - guard let elementRect = element.boundsAsCGRect else { - log.warning("Failed to draw a UML element: \(element)") - return - } - - switch element.type { - case .useCaseSystem: - drawSystem(element: element, elementRect: elementRect) - case .useCaseActor: - drawActor(element: element, elementRect: elementRect) - case .useCase: - drawUseCase(element: element, elementRect: elementRect) - default: - drawUnknownElement(element: element, elementRect: elementRect) - } - } - - private func drawSystem(element: UMLElement, elementRect: CGRect) { - context.fill(Path(elementRect), with: .color(Color(UIColor.systemBackground))) - context.stroke(Path(elementRect), with: .color(Color.primary)) - drawTitle(of: element, in: elementRect) - } - - private func drawActor(element: UMLElement, elementRect: CGRect) { - let headDiameter = elementRect.width / 2 - let headPath = Path(ellipseIn: .init(x: elementRect.midX - headDiameter / 2, - y: elementRect.minY, - width: headDiameter, - height: headDiameter)) - context.fill(headPath, with: .color(Color(UIColor.systemBackground))) - context.stroke(headPath, with: .color(Color.primary)) - - var bodyPath = Path() - bodyPath.move(to: .init(x: headPath.boundingRect.midX, - y: headPath.boundingRect.maxY)) - bodyPath.addLine(to: .init(x: headPath.boundingRect.midX, - y: headPath.boundingRect.maxY + elementRect.height * 0.45)) - context.stroke(bodyPath, with: .color(Color.primary)) - - var armPath = Path() - armPath.move(to: .init(x: elementRect.minX, - y: headPath.boundingRect.maxY * 1.03)) - armPath.addLine(to: .init(x: elementRect.maxX, - y: headPath.boundingRect.maxY * 1.03)) - context.stroke(armPath, with: .color(Color.primary)) - - var legPath = Path() - legPath.move(to: bodyPath.currentPoint ?? .zero) - legPath.addLine(to: .init(x: elementRect.minX, - y: elementRect.maxY)) - legPath.move(to: bodyPath.currentPoint ?? .zero) - legPath.addLine(to: .init(x: elementRect.maxX, - y: elementRect.maxY)) - context.stroke(legPath, with: .color(Color.primary)) - - drawTitle(of: element, in: elementRect) - } - - private func drawUseCase(element: UMLElement, elementRect: CGRect) { - let circlePath = Path(ellipseIn: elementRect) - context.fill(circlePath, with: .color(Color(UIColor.systemBackground))) - context.stroke(circlePath, with: .color(Color.primary)) - drawTitle(of: element, in: elementRect) - } - - private func drawTitle(of element: UMLElement, in elementRect: CGRect) { - var text = Text(element.name ?? "") - text = text.font(.system(size: fontSize, weight: .bold)) - - let elementTitle = context.resolve(text) - let titleSize = elementTitle.measure(in: elementRect.size) - - var titleRect: CGRect - - switch element.type { - case .useCaseSystem: - titleRect = CGRect(x: elementRect.midX - titleSize.width / 2, - y: elementRect.minY + 5, - width: titleSize.width, - height: titleSize.height) - case .useCase: - titleRect = CGRect(x: elementRect.midX - titleSize.width / 2, - y: elementRect.midY - titleSize.height / 2, - width: titleSize.width, - height: titleSize.height) - case .useCaseActor: - titleRect = CGRect(x: elementRect.midX - titleSize.width / 2, - y: elementRect.maxY, - width: titleSize.width, - height: titleSize.height) - default: - titleRect = CGRect(x: elementRect.midX - titleSize.width / 2, - y: elementRect.minY + 5, - width: titleSize.width, - height: titleSize.height) - } - - context.draw(elementTitle, in: titleRect) - } - - - private func drawUnknownElement(element: UMLElement, elementRect: CGRect) { - log.warning("Drawing logic for elements of type \(element.type?.rawValue ?? "nil") is not implemented") - context.stroke(Path(elementRect), with: .color(Color.secondary)) - } -} diff --git a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Use Case Diagram/UMLUseCaseDiagramRelationshipRenderer.swift b/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Use Case Diagram/UMLUseCaseDiagramRelationshipRenderer.swift deleted file mode 100644 index 07cddb38..00000000 --- a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Use Case Diagram/UMLUseCaseDiagramRelationshipRenderer.swift +++ /dev/null @@ -1,224 +0,0 @@ -// -// UMLUseCaseDiagramRelationshipRenderer.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 02.08.23. -// - -import SwiftUI -import Common - -struct UMLUseCaseDiagramRelationshipRenderer: UMLDiagramRenderer { - var context: UMLGraphicsContext - let canvasBounds: CGRect - var fontSize: CGFloat - - func render(umlModel: UMLModel) { - guard let relationships = umlModel.relationships else { - log.warning("The UML model contains no relationships") - return - } - - for relationship in relationships { - draw(relationship: relationship) - } - } - - private func draw(relationship: UMLRelationship) { - guard let relationshipRect = relationship.boundsAsCGRect else { - log.warning("Failed to draw a UML relationship: \(relationship)") - return - } - - switch relationship.type { - case .useCaseAssociation: - drawAssociation(relationship, in: relationshipRect) - case .useCaseExtend, .useCaseInclude: - drawExtendOrInclude(relationship, in: relationshipRect) - case .useCaseGeneralization: - drawGeneralization(relationship, in: relationshipRect) - default: - drawUnknown(relationship, in: relationshipRect) - } - } - - private func drawAssociation(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = relationship.pathWithCGPoints else { - return - } - - context.stroke(path, with: .color(Color.primary)) - drawTitleText(for: relationship, on: path) - } - - private func drawExtendOrInclude(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = relationship.pathWithCGPoints else { - return - } - - context.stroke(path, with: .color(Color.primary), style: .init(dash: [7, 7])) - drawArrowhead(for: relationship, on: path) - drawTypeText(for: relationship, on: path) - } - - private func drawGeneralization(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = relationship.pathWithCGPoints else { - return - } - - context.stroke(path, with: .color(Color.primary)) - drawArrowhead(for: relationship, on: path) - } - - private func drawUnknown(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - drawAssociation(relationship, in: relationshipRect) - } - - private func drawTypeText(for relationship: UMLRelationship, on path: Path) { - guard let relationshipRect = relationship.boundsAsCGRect, - let relationshipTypeString = relationship.type?.annotationTitle, - let endPointYInverted = path.currentPoint?.invertY(), - let pointCount = relationship.path?.count, - let previousPoint = relationship.path?[pointCount - 2].asCGPoint else { - log.warning("Could not draw type text for: \(relationship)") - return - } - - let text = Text(relationshipTypeString).font(.system(size: fontSize)) - let resolvedText = context.resolve(text) - let textSize = resolvedText.measure(in: canvasBounds.size) - let textRect = CGRect(x: relationshipRect.midX - textSize.width / 2, - y: relationshipRect.midY - textSize.height / 2, - width: textSize.width, - height: textSize.height) - - let previousPointYInverted = previousPoint - .applying(.init(translationX: relationshipRect.minX, y: relationshipRect.minY)) - .invertY() - - var rotationDegrees = previousPointYInverted.angle(to: endPointYInverted) + 90 - - if abs(rotationDegrees) > 90 { // prevents upside-down text - rotationDegrees += 180 - } - - context.baseGraphicsContext.drawLayer { layerContext in - // Perform rotation - layerContext.translateBy(x: context.xOffset, y: context.yOffset) - layerContext.translateBy(x: textSize.width / 2, y: textSize.height * 0.4) - layerContext.rotate(by: Angle(degrees: rotationDegrees)) - let rotatedTextOrigin = textRect.origin.rotated(around: .zero, - angleInDegrees: -rotationDegrees) - - // Generate a background for the text (to prevent overlap with the arrow line) - let backgroundRect = CGRect(x: rotatedTextOrigin.x - textSize.width * 0.35, - y: rotatedTextOrigin.y - textSize.height * 0.125, - width: textRect.width * 0.7, - height: textRect.height * 0.5) - layerContext.fill(Path(backgroundRect), with: .color(Color(UIColor.systemBackground))) - - // Draw text - layerContext.draw(resolvedText, at: rotatedTextOrigin, anchor: .center) - } - } - - private func drawTitleText(for relationship: UMLRelationship, on path: Path) { - guard let relationshipRect = relationship.boundsAsCGRect, - let relationshipName = relationship.name, - let endPointYInverted = path.currentPoint?.invertY(), - let pointCount = relationship.path?.count, - let previousPoint = relationship.path?[pointCount - 2].asCGPoint else { - log.warning("Could not draw type text for: \(relationship)") - return - } - - let text = Text(relationshipName).font(.system(size: fontSize)) - let resolvedText = context.resolve(text) - let textSize = resolvedText.measure(in: canvasBounds.size) - let textRect = CGRect(x: relationshipRect.midX - textSize.width / 2, - y: relationshipRect.midY - textSize.height / 2, - width: textSize.width, - height: textSize.height) - - let previousPointYInverted = previousPoint - .applying(.init(translationX: relationshipRect.minX, y: relationshipRect.minY)) - .invertY() - - var rotationDegrees = previousPointYInverted.angle(to: endPointYInverted) + 90 - - if abs(rotationDegrees) > 90 { // prevents upside-down text - rotationDegrees += 180 - } - - context.baseGraphicsContext.drawLayer { layerContext in - layerContext.translateBy(x: context.xOffset, y: context.yOffset) - layerContext.translateBy(x: textSize.width / 2, y: textSize.height) - layerContext.rotate(by: Angle(degrees: rotationDegrees)) - let rotatedTextOrigin = textRect.origin.rotated(around: .init(x: 0, y: textSize.height / 2), - angleInDegrees: -rotationDegrees) - layerContext.draw(resolvedText, at: rotatedTextOrigin, anchor: .center) - } - } - - private func drawArrowhead(for relationship: UMLRelationship, on path: Path) { - guard let relationshipRect = relationship.boundsAsCGRect, - let endPoint = path.currentPoint, - let pointCount = relationship.path?.count, - let previousPoint = relationship.path?[pointCount - 2].asCGPoint else { - log.warning("Could not draw arrowhead for: \(relationship)") - return - } - - var type: ArrowHeadType - - switch relationship.type { - case .useCaseInclude, .useCaseExtend: - type = .triangleWithoutBase - case .useCaseGeneralization: - type = .triangle - default: - return - } - - // The inversion below is necessary for the angle calculation to work correctly. - // Unlike a regular coordinate system, `Canvas` has an inverted y axis. - var previousPointInverted = previousPoint.applying(.init(translationX: relationshipRect.minX, - y: relationshipRect.minY)) - previousPointInverted.y *= -1 - - var endPointInverted = endPoint - endPointInverted.y *= -1 - - let rotationDegrees = previousPointInverted.angle(to: endPointInverted) - drawArrowhead(at: endPoint, rotatedBy: .degrees(rotationDegrees), type: type) - } - - private func drawArrowhead(at point: CGPoint, rotatedBy angle: Angle, type: ArrowHeadType) { - var path = Path() - let size: CGFloat = 10 - - switch type { - case .triangle: - path.move(to: .init(x: point.x, y: point.y)) - path.addLine(to: .init(x: point.x - size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x + size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x, y: point.y)) - case .triangleWithoutBase: - path.move(to: .init(x: point.x - size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x, y: point.y)) - path.addLine(to: .init(x: point.x + size, y: point.y + size * 1.5)) - default: - path.move(to: .init(x: point.x, y: point.y)) - } - - path = path.rotation(angle, anchor: .top).path(in: path.boundingRect) - - context.stroke(path, with: .color(Color.primary)) - - // Fill - if type != .triangleWithoutBase { - let fillColor = (type == .rhombusFilled) ? Color(UIColor.label) : Color(UIColor.systemBackground) - context.fill(path, with: .color(fillColor)) - } - } -} diff --git a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Use Case Diagram/UMLUseCaseDiagramRenderer.swift b/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Use Case Diagram/UMLUseCaseDiagramRenderer.swift deleted file mode 100644 index 763bc72b..00000000 --- a/Themis/Views/Assessment/Modeling Exercise/Diagram Renderers/Use Case Diagram/UMLUseCaseDiagramRenderer.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// UMLUseCaseDiagramRenderer.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 02.08.23. -// - -import SwiftUI - -struct UMLUseCaseDiagramRenderer: UMLDiagramRenderer { - var context: UMLGraphicsContext - let canvasBounds: CGRect - var fontSize: CGFloat = 14 - - func render(umlModel: UMLModel) { - let elementRenderer = UMLUseCaseDiagramElementRenderer(context: context, canvasBounds: canvasBounds, fontSize: fontSize) - let relationshipRenderer = UMLUseCaseDiagramRelationshipRenderer(context: context, canvasBounds: canvasBounds, fontSize: fontSize) - - elementRenderer.render(umlModel: umlModel) - relationshipRenderer.render(umlModel: umlModel) - } -} diff --git a/Themis/Views/Assessment/Modeling Exercise/Element Renderers/UMLClassDiagramElementRenderer.swift b/Themis/Views/Assessment/Modeling Exercise/Element Renderers/UMLClassDiagramElementRenderer.swift deleted file mode 100644 index 194a1db0..00000000 --- a/Themis/Views/Assessment/Modeling Exercise/Element Renderers/UMLClassDiagramElementRenderer.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// UMLClassDiagramElementRenderer.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 27.07.23. -// - -import SwiftUI -import Common - -struct UMLClassDiagramElementRenderer: UMLDiagramRenderer { - var context: GraphicsContext - let canvasBounds: CGRect - - private let fontSize: CGFloat = 14 - - func render(umlModel: UMLModel) { - guard let elements = umlModel.elements else { - log.error("The UML model contains no elements") - return - } - - for element in elements { - draw(element: element) - } - } - - private func draw(element: UMLElement) { - guard let xCoordinate = element.bounds?.x, - let yCoordinate = element.bounds?.y, - let width = element.bounds?.width, - let height = element.bounds?.height else { - log.warning("Failed to draw a UML element: \(element)") - return - } - - let elementRect = CGRect(x: xCoordinate, y: yCoordinate, width: width, height: height) - - switch element.type { - case .Class, .abstractClass, .classMethod, .classAttribute, .enumeration, .interface: - drawClassLikeElement(element: element, elementRect: elementRect) - case .package: - drawPackage(element: element, elementRect: elementRect) - default: - drawUnknownElement(element: element, elementRect: elementRect) - } - } - - private func drawClassLikeElement(element: UMLElement, elementRect: CGRect) { - switch element.type { - case .classAttribute, .classMethod: - drawAttributeOrMethod(element, in: elementRect) - default: - drawTitle(of: element, in: elementRect) - } - } - - private func drawPackage(element: UMLElement, elementRect: CGRect) { - - let topCornerRect = CGRect(x: elementRect.minX, - y: elementRect.minY, - width: 45, - height: 10) - context.fill(Path(topCornerRect), with: .color(Color(UIColor.systemBackground))) - context.stroke(Path(topCornerRect), with: .color(Color.primary)) - - let newElementRect = CGRect(x: elementRect.minX, - y: elementRect.minY + topCornerRect.height, - width: elementRect.width, - height: elementRect.height - topCornerRect.height) - - context.fill(Path(newElementRect), with: .color(Color(UIColor.systemBackground))) - context.stroke(Path(newElementRect), with: .color(Color.primary)) - drawTitle(of: element, in: newElementRect) - } - - private func drawUnknownElement(element: UMLElement, elementRect: CGRect) { - log.warning("Drawing logic for elements of type \(element.type?.rawValue ?? "nil") is not implemented") - context.stroke(Path(elementRect), with: .color(Color.secondary)) - } - - private func drawTitle(of element: UMLElement, in elementRect: CGRect) { - var titleY: CGFloat - - context.fill(Path(elementRect), with: .color(Color(UIColor.systemBackground))) - context.stroke(Path(elementRect), with: .color(Color.primary)) - - // START: Draw type text - let typeTextString = element.type?.annotationTitle ?? "" - var typeText = Text(typeTextString) - typeText = typeText.font(.system(size: fontSize * 0.7, weight: .bold).monospaced()) - let typeResolved = context.resolve(typeText) - let typeTextSize = typeResolved.measure(in: elementRect.size) - - let typeRect = CGRect(x: elementRect.midX - typeTextSize.width / 2, - y: elementRect.minY + 5, - width: typeTextSize.width, - height: typeTextSize.height) - context.draw(typeResolved, in: typeRect) - // END: Draw type text - - titleY = typeTextString.isEmpty ? elementRect.minY + 5 : typeRect.maxY - - // START: Draw title text - var text = Text(element.name ?? "") - text = text.font(.system(size: fontSize, weight: .bold)) - - let elementTitle = context.resolve(text) - let titleSize = elementTitle.measure(in: elementRect.size) - let titleRect = CGRect(x: elementRect.midX - titleSize.width / 2, - y: titleY, - width: titleSize.width, - height: titleSize.height) - - context.draw(elementTitle, in: titleRect) - // END: Draw title text - - if [UMLElementType.Class, .abstractClass, .enumeration, .interface].contains(element.type) { - drawAttributeAndMethodSeparators(element, in: elementRect) - } - } - - private func drawAttributeAndMethodSeparators(_ element: UMLElement, in elementRect: CGRect) { - // Draw a line above the first attribute of this element - if let firstAttribute = element.verticallySortedChildren?.first(where: { $0.type == .classAttribute }), - let firstAttributeTopLeft = firstAttribute.boundsAsCGRect?.origin, - let firstAttributeSize = firstAttribute.boundsAsCGRect?.size { - let firstAttributeTopRight = firstAttributeTopLeft.applying(.init(translationX: firstAttributeSize.width, y: 0)) - - var attributePath = Path() - attributePath.move(to: firstAttributeTopLeft) - attributePath.addLine(to: firstAttributeTopRight) - - context.stroke(attributePath, with: .color(.primary)) - } - - // Draw a line above the first method of this element - if let firstMethod = element.verticallySortedChildren?.first(where: { $0.type == .classMethod }), - let firstMethodTopLeft = firstMethod.boundsAsCGRect?.origin, - let firstMethodSize = firstMethod.boundsAsCGRect?.size { - let firstMethodTopRight = firstMethodTopLeft.applying(.init(translationX: firstMethodSize.width, y: 0)) - - var methodPath = Path() - methodPath.move(to: firstMethodTopLeft) - methodPath.addLine(to: firstMethodTopRight) - - context.stroke(methodPath, with: .color(.primary)) - } - } - - private func drawAttributeOrMethod(_ element: UMLElement, in elementRect: CGRect) { - var text = Text(element.name ?? "") - text = text.font(.system(size: fontSize)) - - let elementTitle = context.resolve(text) - let titleSize = elementTitle.measure(in: elementRect.size) - let titleRect = CGRect(x: elementRect.minX + 5, - y: elementRect.midY - titleSize.height / 2, - width: titleSize.width, - height: titleSize.height) - - context.draw(elementTitle, in: titleRect) - } -} diff --git a/Themis/Views/Assessment/Modeling Exercise/Element Renderers/UMLClassDiagramRelationshipRenderer.swift b/Themis/Views/Assessment/Modeling Exercise/Element Renderers/UMLClassDiagramRelationshipRenderer.swift deleted file mode 100644 index f5b8ada4..00000000 --- a/Themis/Views/Assessment/Modeling Exercise/Element Renderers/UMLClassDiagramRelationshipRenderer.swift +++ /dev/null @@ -1,370 +0,0 @@ -// -// UMLClassDiagramRelationshipRenderer.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 27.07.23. -// - -import SwiftUI -import Common - -// swiftlint:disable type_body_length -struct UMLClassDiagramRelationshipRenderer: UMLDiagramRenderer { - var context: GraphicsContext - let canvasBounds: CGRect - - private let fontSize: CGFloat = 14 - - func render(umlModel: UMLModel) { - guard let relationships = umlModel.relationships else { - log.warning("The UML model contains no relationships") - return - } - - for relationship in relationships { - draw(relationship: relationship) - } - } - - private func draw(relationship: UMLRelationship) { - guard let xCoordinate = relationship.bounds?.x, - let yCoordinate = relationship.bounds?.y, - let width = relationship.bounds?.width, - let height = relationship.bounds?.height else { - log.warning("Failed to draw a UML relationship: \(relationship)") - return - } - - let relationshipRect = CGRect(x: xCoordinate, y: yCoordinate, width: width, height: height) - - switch relationship.type { - case .classDependency: - drawDependency(relationship, in: relationshipRect) - case .classAggregation, .classComposition: - drawAggregationOrComposition(relationship, in: relationshipRect) - case .classInheritance: - drawInheritance(relationship, in: relationshipRect) - case .classRealization: - drawRealization(relationship, in: relationshipRect) - case .classBidirectional: - drawAssociation(relationship, in: relationshipRect) - case .classUnidirectional: - drawAssociation(relationship, in: relationshipRect) - default: - drawUnknown(relationship, in: relationshipRect) - } - - drawMultiplicityText(relationship, in: relationshipRect) - drawRoleText(relationship, in: relationshipRect) - } - - private func drawDependency(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = getLinePath(for: relationship, in: relationshipRect) else { - return - } - - context.stroke(path, with: .color(Color.primary), style: .init(dash: [7, 7])) - drawArrowhead(for: relationship, on: path) - } - - private func drawAggregationOrComposition(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = getLinePath(for: relationship, in: relationshipRect) else { - return - } - - context.stroke(path, with: .color(Color.primary)) - drawArrowhead(for: relationship, on: path) - } - - private func drawInheritance(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = getLinePath(for: relationship, in: relationshipRect) else { - return - } - - context.stroke(path, with: .color(Color.primary)) - drawArrowhead(for: relationship, on: path) - } - - private func drawRealization(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = getLinePath(for: relationship, in: relationshipRect) else { - return - } - - context.stroke(path, with: .color(Color.primary), style: .init(dash: [7, 7])) - drawArrowhead(for: relationship, on: path) - } - - private func drawAssociation(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let path = getLinePath(for: relationship, in: relationshipRect) else { - return - } - - context.stroke(path, with: .color(Color.primary)) - drawArrowhead(for: relationship, on: path) - } - - private func drawUnknown(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - drawAssociation(relationship, in: relationshipRect) - } - - private func getLinePath(for relationship: UMLRelationship, in relationshipRect: CGRect) -> Path? { - guard let relationshipPath = relationship.path, - relationshipPath.count >= 2 else { - return nil - } - - let points = relationshipPath.map { $0.asCGPoint.applying(.init(translationX: relationshipRect.minX, y: relationshipRect.minY)) } - - var path = Path() - path.addLines(Array(points)) - - return path - } - - private func drawArrowhead(for relationship: UMLRelationship, on path: Path) { - guard let endPoint = path.currentPoint, - let arrowTargetDirection = relationship.target?.direction else { - log.warning("Could not draw arrowhead for: \(relationship)") - return - } - - var type: ArrowHeadType - - switch relationship.type { - case .classBidirectional: - return // bidirectional arrows have no heads - case .classDependency, .classUnidirectional: - type = .triangleWithoutBase - case .classInheritance, .classRealization: - type = .triangle - case .classComposition: - type = .rhombusFilled - case .classAggregation: - type = .rhombus - default: - type = .triangle - } - - drawArrowhead(at: endPoint, lookingAt: arrowTargetDirection.inverted, type: type) - } - - private func drawArrowhead(at point: CGPoint, lookingAt direction: Direction, type: ArrowHeadType) { - var path = Path() - let size: CGFloat = (fontSize * 0.7).rounded() - - switch type { - case .triangle: - path.move(to: .init(x: point.x, y: point.y)) - path.addLine(to: .init(x: point.x - size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x + size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x, y: point.y)) - case .triangleWithoutBase: - path.move(to: .init(x: point.x - size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x, y: point.y)) - path.addLine(to: .init(x: point.x + size, y: point.y + size * 1.5)) - case .rhombus, .rhombusFilled: - path.move(to: .init(x: point.x, y: point.y)) - path.addLine(to: .init(x: point.x - size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x, y: point.y + size * 3)) - path.addLine(to: .init(x: point.x + size, y: point.y + size * 1.5)) - path.addLine(to: .init(x: point.x, y: point.y)) - } - - switch direction { - case .up, .topLeft, .topRight: - path = path.offsetBy(dx: 0, dy: size * 0.15) - case .down, .downRight: - path = path.rotation(.degrees(180)).path(in: path.boundingRect) - if type == .rhombus || type == .rhombusFilled { - path = path.offsetBy(dx: 0, dy: size * -3.1) - } else { - path = path.offsetBy(dx: 0, dy: size * -1.6) - } - case .right, .upRight, .bottomLeft: - path = path.rotation(.degrees(90)).path(in: path.boundingRect) - if type == .rhombus || type == .rhombusFilled { - path = path.offsetBy(dx: size * -1.55, dy: size * -1.5) - } else { - path = path.offsetBy(dx: size * -0.8, dy: size * -0.75) - } - case .left, .upLeft, .downLeft, .bottomRight: - path = path.rotation(.degrees(-90)).path(in: path.boundingRect) - if type == .rhombus || type == .rhombusFilled { - path = path.offsetBy(dx: size * 1.55, dy: size * -1.5) - } else { - path = path.offsetBy(dx: size * 0.8, dy: size * -0.75) - } - } - - context.stroke(path, with: .color(Color.primary)) - - // Fill - if type != .triangleWithoutBase { - let fillColor = (type == .rhombusFilled) ? Color(UIColor.label) : Color(UIColor.systemBackground) - context.fill(path, with: .color(fillColor)) - } - } - - private func drawMultiplicityText(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let sourcePoint = relationship.path?.first?.asCGPoint, - let sourceDirection = relationship.source?.direction?.inverted, - let targetPoint = relationship.path?.last?.asCGPoint, - let targetDirection = relationship.target?.direction?.inverted else { - log.warning("Could not draw multiplicity text for: \(relationship)") - return - } - - var targetOffset: CGPoint - let relativeSize = (fontSize * 0.7).rounded() - - switch relationship.type { - case .classDependency, .classUnidirectional: - targetOffset = .init(x: 10, y: 10) - case .classInheritance, .classRealization: - targetOffset = .init(x: 0, y: relativeSize * 1.5) - case .classComposition, .classAggregation: - targetOffset = .init(x: 0, y: relativeSize * 2) - default: - targetOffset = .zero - } - - if let sourceMultiplicity = relationship.source?.multiplicity { - drawMultiplicityText(sourceMultiplicity, - at: sourcePoint, - inParentRect: relationshipRect, - direction: sourceDirection, - offset: .zero) - } - - if let targetMultiplicity = relationship.target?.multiplicity { - drawMultiplicityText(targetMultiplicity, - at: targetPoint, - inParentRect: relationshipRect, - direction: targetDirection, - offset: targetOffset) - } - } - - /// Draws text representing the multiplicity value of a UML relationship - /// - Parameters: - /// - text: multiplicity - /// - point: where to draw - /// - rect: parent rect of the relationship - /// - direction: where the arrowhead is looking at - /// - offset: to move the text in case the arrow head is too large (this is usually needed for aggregation and composition relationships) - private func drawMultiplicityText(_ text: String, - at point: CGPoint, - inParentRect rect: CGRect, - direction: Direction, - offset: CGPoint) { - let multiplicityText = Text(text).font(.system(size: fontSize)) - let resolvedText = context.resolve(multiplicityText) - let textSize = resolvedText.measure(in: rect.size) - - var xPosition: CGFloat - var yPosition: CGFloat - - switch direction { - case .up, .topLeft, .topRight: - xPosition = rect.minX + point.x + 7 + offset.x - yPosition = rect.minY + point.y + 5 + offset.y - case .down, .downLeft, .downRight: - xPosition = rect.minX + point.x + 7 + offset.x - yPosition = rect.minY + point.y - 20 - offset.y - case .right, .upRight, .bottomRight: - xPosition = rect.minX + point.x - 14 - offset.y - yPosition = rect.minY + point.y + 5 + offset.x - case .left, .upLeft, .bottomLeft: - xPosition = rect.minX + point.x + 7 + offset.y - yPosition = rect.minY + point.y + 5 + offset.x - } - - let textRect = CGRect(x: xPosition, y: yPosition, width: textSize.width, height: textSize.height) - - context.draw(resolvedText, in: textRect) - } - - private func drawRoleText(_ relationship: UMLRelationship, in relationshipRect: CGRect) { - guard let sourcePoint = relationship.path?.first?.asCGPoint, - let sourceDirection = relationship.source?.direction?.inverted, - let targetPoint = relationship.path?.last?.asCGPoint, - let targetDirection = relationship.target?.direction?.inverted else { - log.warning("Could not draw role text for: \(relationship)") - return - } - - var targetOffset: CGPoint - let relativeSize = (fontSize * 0.7).rounded() - - switch relationship.type { - case .classDependency, .classUnidirectional: - targetOffset = .init(x: 0, y: relativeSize * 1.5) - case .classInheritance, .classRealization: - targetOffset = .init(x: 0, y: relativeSize * 1.5) - case .classComposition, .classAggregation: - targetOffset = .init(x: 0, y: relativeSize * 2) - default: - targetOffset = .zero - } - - if let sourceRole = relationship.source?.role { - drawRoleText(sourceRole, - at: sourcePoint, - inParentRect: relationshipRect, - direction: sourceDirection, - offset: .zero) - } - - if let targetRole = relationship.target?.role { - drawRoleText(targetRole, - at: targetPoint, - inParentRect: relationshipRect, - direction: targetDirection, - offset: targetOffset) - } - } - - /// Draws text representing the role of an element in a UML relationship - /// - Parameters: - /// - text: role - /// - point: where to draw - /// - rect: parent rect of the relationship - /// - direction: where the arrowhead is looking at - /// - offset: to move the text in case the arrow head is too large (this is usually needed for aggregation and composition relationships) - private func drawRoleText(_ text: String, - at point: CGPoint, - inParentRect rect: CGRect, - direction: Direction, - offset: CGPoint) { - let roleText = Text(text).font(.system(size: fontSize)) - let resolvedText = context.resolve(roleText) - let textSize = resolvedText.measure(in: canvasBounds.size) - - var xPosition: CGFloat - var yPosition: CGFloat - - switch direction { - case .up, .topLeft, .topRight: - xPosition = rect.minX + point.x - textSize.width - 7 - offset.x - yPosition = rect.minY + point.y + 5 + offset.y - case .down, .downLeft, .downRight: - xPosition = rect.minX + point.x - textSize.width - 7 + offset.x - yPosition = rect.minY + point.y - textSize.height - 5 - offset.y - case .right, .upRight, .bottomRight: - xPosition = rect.minX + point.x - textSize.width - offset.y - yPosition = rect.minY + point.y - textSize.height - 5 + offset.x - case .left, .upLeft, .bottomLeft: - xPosition = rect.minX + point.x + 7 + offset.y - yPosition = rect.minY + point.y - textSize.height - 5 + offset.x - } - - let textRect = CGRect(x: xPosition, y: yPosition, width: textSize.width, height: textSize.height) - - context.draw(resolvedText, in: textRect) - } -} -// swiftlint:enable type_body_length - -enum ArrowHeadType { - case triangle, triangleWithoutBase, rhombus, rhombusFilled -} diff --git a/Themis/Views/Assessment/Modeling Exercise/Element Renderers/UMLClassDiagramRenderer.swift b/Themis/Views/Assessment/Modeling Exercise/Element Renderers/UMLClassDiagramRenderer.swift deleted file mode 100644 index 989eb92f..00000000 --- a/Themis/Views/Assessment/Modeling Exercise/Element Renderers/UMLClassDiagramRenderer.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// UMLClassDiagramRenderer.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 07.07.23. -// - -import SwiftUI -import Common -import SharedModels - -protocol UMLDiagramRenderer { - func render(umlModel: UMLModel) -} - -struct UMLClassDiagramRenderer: UMLDiagramRenderer { - var context: GraphicsContext - let canvasBounds: CGRect - - private let fontSize: CGFloat = 14 - - func render(umlModel: UMLModel) { - let elementRenderer = UMLClassDiagramElementRenderer(context: context, canvasBounds: canvasBounds) - let relationshipRenderer = UMLClassDiagramRelationshipRenderer(context: context, canvasBounds: canvasBounds) - - elementRenderer.render(umlModel: umlModel) - relationshipRenderer.render(umlModel: umlModel) - } -} diff --git a/Themis/Views/Assessment/Modeling Exercise/UMLGraphicsContext.swift b/Themis/Views/Assessment/Modeling Exercise/UMLGraphicsContext.swift deleted file mode 100644 index d4d9f704..00000000 --- a/Themis/Views/Assessment/Modeling Exercise/UMLGraphicsContext.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// UMLGraphicsContext.swift -// Themis -// -// Created by Tarlan Ismayilsoy on 03.08.23. -// - -import SwiftUI - -/// A wrapper for `GraphicsContext` to customize some of its behavior -struct UMLGraphicsContext { - var baseGraphicsContext: GraphicsContext - var xOffset: CGFloat - var yOffset: CGFloat - - static var defaultOffset: CGFloat = 15 - - init(_ context: GraphicsContext, - xOffset: CGFloat = defaultOffset, - yOffset: CGFloat = defaultOffset) { - self.baseGraphicsContext = context - self.xOffset = xOffset - self.yOffset = yOffset - } - - // MARK: - Fill - func fill(_ path: Path, with shading: GraphicsContext.Shading, style: FillStyle = FillStyle()) { - let pathWithOffset = path.offsetBy(dx: xOffset, dy: yOffset) - baseGraphicsContext.fill(pathWithOffset, with: shading, style: style) - } - - // MARK: - Stroke - func stroke(_ path: Path, with shading: GraphicsContext.Shading, lineWidth: CGFloat = 1) { - let pathWithOffset = path.offsetBy(dx: xOffset, dy: yOffset) - baseGraphicsContext.stroke(pathWithOffset, with: shading, lineWidth: lineWidth) - } - - func stroke(_ path: Path, with shading: GraphicsContext.Shading, style: StrokeStyle) { - let pathWithOffset = path.offsetBy(dx: xOffset, dy: yOffset) - baseGraphicsContext.stroke(pathWithOffset, with: shading, style: style) - } - - - // MARK: - Draw - func draw(_ text: GraphicsContext.ResolvedText, in rect: CGRect) { - let rectWithOffset = rect.offsetBy(dx: xOffset, dy: yOffset) - baseGraphicsContext.draw(text, in: rectWithOffset) - } - - func draw(_ symbol: GraphicsContext.ResolvedSymbol, in rect: CGRect) { - let rectWithOffset = rect.offsetBy(dx: xOffset, dy: yOffset) - baseGraphicsContext.draw(symbol, in: rectWithOffset) - } - - func draw(_ image: GraphicsContext.ResolvedImage, in rect: CGRect, style: FillStyle = FillStyle()) { - let rectWithOffset = rect.offsetBy(dx: xOffset, dy: yOffset) - baseGraphicsContext.draw(image, in: rectWithOffset, style: style) - } - - func draw(_ image: GraphicsContext.ResolvedImage, at point: CGPoint, anchor: UnitPoint = .center) { - let pointWithOffset = CGPoint(x: point.x + xOffset, y: point.y + yOffset) - baseGraphicsContext.draw(image, at: pointWithOffset, anchor: anchor) - } - - // MARK: - Resolve - func resolve(_ text: Text) -> GraphicsContext.ResolvedText { - baseGraphicsContext.resolve(text) - } - - func resolveSymbol(id: ID) -> GraphicsContext.ResolvedSymbol? where ID: Hashable { - baseGraphicsContext.resolveSymbol(id: id) - } -} diff --git a/Themis/Views/Assessment/Modeling Exercise/UMLRenderer.swift b/Themis/Views/Assessment/Modeling Exercise/UMLRenderer.swift index 3a05dda5..33f1389b 100644 --- a/Themis/Views/Assessment/Modeling Exercise/UMLRenderer.swift +++ b/Themis/Views/Assessment/Modeling Exercise/UMLRenderer.swift @@ -8,30 +8,36 @@ import SwiftUI import SharedModels import Common +import ApollonView +import ApollonShared struct UMLRenderer: View { @ObservedObject var umlRendererVM: UMLRendererViewModel - + @State var showResetButton = true @State var scale: CGFloat = 1 @State private var progressingScale: CGFloat = 1 @State private var startDragLocation = CGPoint.zero @State private var dragStarted = true - + /// The minimum scale value that the UML model can be scaled down to private let minScale = 0.1 var body: some View { ZStack(alignment: .topLeading) { - Image("umlRendererBackground") - .resizable(resizingMode: .tile) - + /// Render the Grid Background from the Apollon Package + GridBackgroundView(gridBackgroundViewModel: GridBackgroundViewModel()) Group { - Canvas(rendersAsynchronously: true) { context, size in - umlRendererVM.render(&context, size: size) + /// If the UML Model is set, create an ApollonView View + if let model = umlRendererVM.umlModel, let type = model.type { + ApollonView(umlModel: model, + diagramType: type, + fontSize: umlRendererVM.fontSize, + themeColor: Color.Artemis.artemisBlue, + diagramOffset: umlRendererVM.offset, + isGridBackground: false) {} } - Canvas(rendersAsynchronously: true) { context, size in umlRendererVM.renderHighlights(&context, size: size) } symbols: {