Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modeling Assessment: Make Grid Background adhear to ApollonView #260

Merged
merged 1 commit into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 1 addition & 15 deletions Themis/ViewModels/Assessment/Modeling/UMLRendererViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ 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? {
willSet {
Expand Down Expand Up @@ -110,7 +109,6 @@ class UMLRendererViewModel: ExerciseRendererViewModel {
}
determineChildren()
orphanElements = umlModel?.elements?.values.filter { $0.owner == nil } ?? []

} catch {
log.error("Could not parse UML string: \(error)")
setError(.couldNotParseDiagram)
Expand All @@ -127,18 +125,6 @@ class UMLRendererViewModel: ExerciseRendererViewModel {
}
}

@MainActor
/// Sets the drag location to the specified point
/// - Parameter point: when nil, the drag location is set in such a way that centers the diagram
func setDragLocation(at point: CGPoint? = nil) {
if let point {
currentDragLocation = point
} else {
currentDragLocation = .init(x: diagramSize.height - 50, // 50 – default padding added by Apollon
y: diagramSize.width - 50)
}
}

@MainActor
func selectItem(at point: CGPoint) {
selectedElement = getSelectableItem(at: point)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,43 +13,20 @@ struct ExampleModelingSolutionView: View {

@StateObject private var umlRendererVM = UMLRendererViewModel()

private let scale = 0.5

var body: some View {
VStack {
UMLRenderer(umlRendererVM: umlRendererVM, showResetButton: false, scale: scale)
.onChange(of: umlRendererVM.diagramSize) {
setDragLocationWithScale()
}
.onAppear {
umlRendererVM.setup(basedOn: exercise.exampleSolutionModel ?? "")
}
.clipped()

// The default reset button of UMLRenderer is replaced by the button below
// because the default one is not shown properly when put inside a List
centerButton
}
}

@ViewBuilder
private var centerButton: some View {
Button {
setDragLocationWithScale()
} label: {
HStack {
Image(systemName: "scope")
Text("Center")
VStack(alignment: .leading) {
if let elements = umlRendererVM.umlModel?.elements, !elements.isEmpty {
UMLRenderer(umlRendererVM: umlRendererVM)
.scaledToFit()
.clipped()
} else {
Text("Not available for this exercise.")
.font(.body)
}
.foregroundColor(.white)
}
.buttonStyle(ThemisButtonStyle())
}

private func setDragLocationWithScale() {
umlRendererVM.setDragLocation() // center ignoring the scale first
umlRendererVM.setDragLocation(at: .init(x: umlRendererVM.currentDragLocation.x * 0.75,
y: umlRendererVM.currentDragLocation.y * scale * 0.75))
.onAppear {
umlRendererVM.setup(basedOn: exercise.exampleSolutionModel ?? "")
}
}
}

Expand Down
120 changes: 17 additions & 103 deletions Themis/Views/Assessment/Modeling Exercise/UMLRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,122 +14,36 @@ 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) {
/// Render the Grid Background from the Apollon Package
GridBackgroundView(gridBackgroundViewModel: GridBackgroundViewModel())
Group {
/// 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: {
umlRendererVM.generatePossibleSymbols()
}
.onTapGesture { tapLocation in
umlRendererVM.selectItem(at: tapLocation)
}
}
.frame(minWidth: umlRendererVM.diagramSize.width,
minHeight: umlRendererVM.diagramSize.height)
.padding()
.scaleEffect(scale * progressingScale)
.position(umlRendererVM.currentDragLocation)

resetZoomAndLocationButton
}
.onChange(of: umlRendererVM.diagramSize) {
umlRendererVM.setDragLocation()
}
.gesture(
DragGesture()
.onChanged(handleDrag)
.onEnded { _ in
dragStarted = true
}
)
.simultaneousGesture(
MagnificationGesture()
.onChanged(handleMagnification)
.onEnded(handleMagnificationEnd)
)
}

@ViewBuilder
private var resetZoomAndLocationButton: some View {
if showResetButton {
Button {
umlRendererVM.setDragLocation()
scale = 1
progressingScale = 1
} label: {
Image(systemName: "scope")
.frame(alignment: .topLeading)
.foregroundColor(.white)
.padding(5)
.background {
RoundedRectangle(cornerRadius: 5)
.foregroundColor(.themisSecondary)
ZStack {
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: true) {
Canvas(rendersAsynchronously: true) { context, size in
umlRendererVM.renderHighlights(&context, size: size)
} symbols: {
umlRendererVM.generatePossibleSymbols()
}
.onTapGesture { tapLocation in
umlRendererVM.selectItem(at: tapLocation)
}
}
}
.padding(12)
}
}

private func handleDrag(_ gesture: DragGesture.Value) {
if dragStarted {
dragStarted = false
startDragLocation = umlRendererVM.currentDragLocation
}
umlRendererVM.setDragLocation(at: CGPoint(x: startDragLocation.x + gesture.translation.width,
y: startDragLocation.y + gesture.translation.height))
}

private func handleMagnification(_ newScale: MagnificationGesture.Value) {
progressingScale = newScale

// Enforce zoom out limit
if progressingScale * scale < minScale {
progressingScale = minScale / scale
}
}

private func handleMagnificationEnd(_ finalScale: MagnificationGesture.Value) {
scale *= finalScale
progressingScale = 1

// Enforce zoom out limit
if scale < minScale {
scale = minScale
}
}
}

struct UMLRenderer_Previews: PreviewProvider {
static var umlRendererVM = UMLRendererViewModel()

static var previews: some View {
UMLRenderer(umlRendererVM: umlRendererVM)
.onAppear {
umlRendererVM.setup(basedOn: Submission.mockModeling.baseSubmission, AssessmentResult())
}
.padding()
}
}
Loading