From 17107dbf14939028e7b4fd834ff431ddb92822b8 Mon Sep 17 00:00:00 2001 From: Lars Brubaker Date: Thu, 2 Nov 2023 18:26:31 -0700 Subject: [PATCH] Making the path editor able to zoom and pan --- .../DesignTools/Operations/PathEditor.cs | 170 ------------ .../Operations/PathEditorFactory.cs | 73 ++++++ .../Operations/PathEditorWidget.cs | 245 ++++++++++++++++++ .../Operations/RadialPinchObject3D.cs | 6 +- .../PartPreviewWindow/GCode2DWidget.cs | 28 +- 5 files changed, 335 insertions(+), 187 deletions(-) delete mode 100644 MatterControlLib/DesignTools/Operations/PathEditor.cs create mode 100644 MatterControlLib/DesignTools/Operations/PathEditorFactory.cs create mode 100644 MatterControlLib/DesignTools/Operations/PathEditorWidget.cs diff --git a/MatterControlLib/DesignTools/Operations/PathEditor.cs b/MatterControlLib/DesignTools/Operations/PathEditor.cs deleted file mode 100644 index ca639b032..000000000 --- a/MatterControlLib/DesignTools/Operations/PathEditor.cs +++ /dev/null @@ -1,170 +0,0 @@ -/* -Copyright (c) 2018, Lars Brubaker, John Lewin -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and documentation are those -of the authors and should not be interpreted as representing official policies, -either expressed or implied, of the FreeBSD Project. -*/ - -using System; -using MatterHackers.Agg; -using MatterHackers.Agg.Image; -using MatterHackers.Agg.Platform; -using MatterHackers.Agg.Transform; -using MatterHackers.ImageProcessing; -using MatterHackers.Agg.UI; -using MatterHackers.Agg.VertexSource; -using MatterHackers.DataConverters3D; -using MatterHackers.Localizations; - -namespace MatterHackers.MatterControl.DesignTools -{ - public class PathEditor : IPropertyEditorFactory - { - [AttributeUsage(AttributeTargets.Property)] - public class TopAndBottomMoveXOnlyAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Property)] - public class XMustBeGreaterThan0Attribute : Attribute - { - } - - private Action vertexChanged; - private ThemeConfig theme; - private VertexStorage vertexStorage; - private ImageWidget imageWidget; - private Object3D object3D; - - public GuiWidget CreateEditor(PropertyEditor propertyEditor, EditableProperty property, EditorContext context, ref int tabIndex) - { - if (property.Source is Object3D object3D) - { - this.object3D = object3D; - object3D.Invalidated += RebuildImage; - } - - if (property.Value is VertexStorage vertexStorage) - { - var wdiget = CreateEditor(vertexStorage, propertyEditor.UndoBuffer, propertyEditor.Theme, VertexBufferChanged); - imageWidget.Closed += ImageWidget_Closed; - - return wdiget; - } - - return null; - } - - private void VertexBufferChanged() - { - object3D.Invalidate(InvalidateType.Path); - } - - private void ImageWidget_Closed(object sender, EventArgs e) - { - imageWidget.Closed -= ImageWidget_Closed; - object3D.Invalidated -= RebuildImage; - } - - bool rebuildingImage = false; - - void RebuildImage(object item, EventArgs e) - { - if (!rebuildingImage - && imageWidget.Image.Width != imageWidget.Width) - { - rebuildingImage = true; - - imageWidget.Height = imageWidget.Width / 2; - imageWidget.Image.Allocate((int)imageWidget.Width, (int)imageWidget.Height, 32, new BlenderBGRA()); - - var graphics2D = imageWidget.Image.NewGraphics2D(); - graphics2D.Clear(theme.BackgroundColor); - - var bounds = imageWidget.Image.GetBounds(); - graphics2D.Rectangle(bounds, theme.PrimaryAccentColor); - - var pathBounds = vertexStorage.GetBounds(); - - new VertexSourceApplyTransform(vertexStorage, Affine.NewScaling(1 / pathBounds.Height * bounds.Height)).RenderCurve(graphics2D, theme.TextColor, 2, true, theme.PrimaryAccentColor.Blend(theme.TextColor, .5), theme.PrimaryAccentColor); - - rebuildingImage = false; - } - } - - public GuiWidget CreateEditor(VertexStorage vertexStorage, UndoBuffer undoBuffer, ThemeConfig theme, Action vertexChanged) - { - rebuildingImage = false; - - this.vertexChanged = vertexChanged; - this.theme = theme; - this.vertexStorage = vertexStorage; - - var topToBottom = new FlowLayoutWidget(FlowDirection.TopToBottom) - { - HAnchor = HAnchor.Stretch, - BackgroundOutlineWidth = 1, - BackgroundColor = theme.BackgroundColor, - BorderColor = theme.TextColor, - Margin = 1, - }; - - imageWidget = new ImageWidget(100, 300) - { - HAnchor = HAnchor.Stretch, - }; - imageWidget.SizeChanged += RebuildImage; - - topToBottom.AddChild(imageWidget); - - var toolBar = new FlowLayoutWidget() - { - HAnchor = HAnchor.Stretch, - }; - topToBottom.AddChild(new HorizontalLine(theme.TextColor)); - topToBottom.AddChild(toolBar); - - var menuTheme = ApplicationController.Instance.MenuTheme; - var homeButton = new ThemedTextIconButton("Home".Localize(), StaticData.Instance.LoadIcon("fa-home_16.png", 16, 16).GrayToColor(menuTheme.TextColor), theme) - { - BackgroundColor = theme.SlightShade, - HoverColor = theme.SlightShade.WithAlpha(75), - Margin = new BorderDouble(3, 3, 6, 3), - ToolTipText = "Reset Zoom".Localize() - }; - toolBar.AddChild(homeButton); - - homeButton.Click += (s, e) => - { - UiThread.RunOnIdle(() => - { - ApplicationController.LaunchBrowser("https://www.matterhackers.com/store/c/3d-printer-filament"); - }); - }; - - return topToBottom; - } - } -} \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Operations/PathEditorFactory.cs b/MatterControlLib/DesignTools/Operations/PathEditorFactory.cs new file mode 100644 index 000000000..4124ddba0 --- /dev/null +++ b/MatterControlLib/DesignTools/Operations/PathEditorFactory.cs @@ -0,0 +1,73 @@ +/* +Copyright (c) 2018, Lars Brubaker, John Lewin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. +*/ + +using MatterHackers.Agg.UI; +using MatterHackers.Agg.VertexSource; +using MatterHackers.DataConverters3D; +using System; + +namespace MatterHackers.MatterControl.DesignTools +{ + public class PathEditorFactory : IPropertyEditorFactory + { + private Object3D object3D; + + public GuiWidget CreateEditor(PropertyEditor propertyEditor, EditableProperty property, EditorContext context, ref int tabIndex) + { + if (property.Value is VertexStorage vertexStorage) + { + var pathEditorWidget = new PathEditorWidget(vertexStorage, propertyEditor.UndoBuffer, propertyEditor.Theme, VertexBufferChanged); + + if (property.Source is Object3D object3D) + { + this.object3D = object3D; + } + + return pathEditorWidget; + } + + return null; + } + + private void VertexBufferChanged() + { + object3D.Invalidate(InvalidateType.Path); + } + + [AttributeUsage(AttributeTargets.Property)] + public class TopAndBottomMoveXOnlyAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Property)] + public class XMustBeGreaterThan0Attribute : Attribute + { + } + } +} \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Operations/PathEditorWidget.cs b/MatterControlLib/DesignTools/Operations/PathEditorWidget.cs new file mode 100644 index 000000000..bab0edffb --- /dev/null +++ b/MatterControlLib/DesignTools/Operations/PathEditorWidget.cs @@ -0,0 +1,245 @@ +/* +Copyright (c) 2018, Lars Brubaker, John Lewin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. +*/ + +using MatterHackers.Agg; +using MatterHackers.Agg.Platform; +using MatterHackers.Agg.Transform; +using MatterHackers.Agg.UI; +using MatterHackers.Agg.VertexSource; +using MatterHackers.ImageProcessing; +using MatterHackers.Localizations; +using MatterHackers.VectorMath; +using System; +using static MatterHackers.MatterControl.PartPreviewWindow.GCode2DWidget; + +namespace MatterHackers.MatterControl.DesignTools +{ + public class PathEditorWidget : GuiWidget + { + private Vector2 lastMousePosition = new Vector2(0, 0); + private Vector2 mouseDownPosition = new Vector2(0, 0); + private double pinchStartScale = 1; + private double startDistanceBetweenPoints = 1; + private ThemeConfig theme; + + private Vector2 unscaledRenderOffset = new Vector2(0, 0); + private Action vertexChanged; + + private VertexStorage vertexStorage; + + public PathEditorWidget(VertexStorage vertexStorage, UndoBuffer undoBuffer, ThemeConfig theme, Action vertexChanged) + { + HAnchor = HAnchor.Stretch; + BackgroundOutlineWidth = 1; + BackgroundColor = theme.BackgroundColor; + BorderColor = theme.TextColor; + Margin = 1; + + var topToBottom = this; + + SizeChanged += (s, e) => + { + Height = Width / 2; + }; + + this.vertexChanged = vertexChanged; + this.theme = theme; + this.vertexStorage = vertexStorage; + + var toolBar = new FlowLayoutWidget() + { + HAnchor = HAnchor.Stretch, + VAnchor = VAnchor.Bottom + }; + + this.AddChild(toolBar); + + var menuTheme = ApplicationController.Instance.MenuTheme; + var homeButton = new ThemedTextIconButton("Home".Localize(), StaticData.Instance.LoadIcon("fa-home_16.png", 16, 16).GrayToColor(menuTheme.TextColor), theme) + { + BackgroundColor = theme.SlightShade, + HoverColor = theme.SlightShade.WithAlpha(75), + Margin = new BorderDouble(3, 3, 6, 3), + ToolTipText = "Reset Zoom".Localize() + }; + toolBar.AddChild(homeButton); + + homeButton.Click += (s, e) => + { + UiThread.RunOnIdle(() => + { + ApplicationController.LaunchBrowser("https://www.matterhackers.com/store/c/3d-printer-filament"); + }); + }; + } + + public override void OnSizeChanged(EventArgs e) + { + base.OnSizeChanged(e); + } + + public ETransformState TransformState { get; set; } + private double layerScale { get; set; } = 1; + + private Affine ScalingTransform => Affine.NewScaling(layerScale, layerScale); + private Affine TotalTransform => Affine.NewTranslation(unscaledRenderOffset) * ScalingTransform * Affine.NewTranslation(Width / 2, Height / 2); + + public override void OnMouseDown(MouseEventArgs mouseEvent) + { + base.OnMouseDown(mouseEvent); + if (MouseCaptured) + { + if (mouseEvent.NumPositions == 1) + { + mouseDownPosition.X = mouseEvent.X; + mouseDownPosition.Y = mouseEvent.Y; + } + else + { + Vector2 centerPosition = (mouseEvent.GetPosition(1) + mouseEvent.GetPosition(0)) / 2; + mouseDownPosition = centerPosition; + } + + lastMousePosition = mouseDownPosition; + + if (mouseEvent.NumPositions > 1) + { + startDistanceBetweenPoints = (mouseEvent.GetPosition(1) - mouseEvent.GetPosition(0)).Length; + pinchStartScale = layerScale; + } + } + } + + public override void OnMouseMove(MouseEventArgs mouseEvent) + { + base.OnMouseMove(mouseEvent); + Vector2 mousePos = new Vector2(); + if (mouseEvent.NumPositions == 1) + { + mousePos = new Vector2(mouseEvent.X, mouseEvent.Y); + } + else + { + Vector2 centerPosition = (mouseEvent.GetPosition(1) + mouseEvent.GetPosition(0)) / 2; + mousePos = centerPosition; + } + if (MouseCaptured) + { + Vector2 mouseDelta = mousePos - lastMousePosition; + switch (TransformState) + { + case ETransformState.Scale: + double zoomDelta = 1; + if (mouseDelta.Y < 0) + { + zoomDelta = 1 - (-1 * mouseDelta.Y / 100); + } + else if (mouseDelta.Y > 0) + { + zoomDelta = 1 + (1 * mouseDelta.Y / 100); + } + + Vector2 mousePreScale = mouseDownPosition; + TotalTransform.inverse_transform(ref mousePreScale); + + layerScale *= zoomDelta; + + Vector2 mousePostScale = mouseDownPosition; + TotalTransform.inverse_transform(ref mousePostScale); + + unscaledRenderOffset += (mousePostScale - mousePreScale); + break; + + case ETransformState.Move: + default: // also treat everything else like a move + ScalingTransform.inverse_transform(ref mouseDelta); + + unscaledRenderOffset += mouseDelta; + break; + } + + Invalidate(); + } + lastMousePosition = mousePos; + + // check if we should do some scaling + if (TransformState == ETransformState.Move + && mouseEvent.NumPositions > 1 + && startDistanceBetweenPoints > 0) + { + double curDistanceBetweenPoints = (mouseEvent.GetPosition(1) - mouseEvent.GetPosition(0)).Length; + + double scaleAmount = pinchStartScale * curDistanceBetweenPoints / startDistanceBetweenPoints; + ScalePartAndFixPosition(mouseEvent, scaleAmount); + } + } + + public override void OnMouseWheel(MouseEventArgs mouseEvent) + { + base.OnMouseWheel(mouseEvent); + if (FirstWidgetUnderMouse) // TODO: find a good way to decide if you are what the wheel is trying to do + { + const double deltaFor1Click = 120; + double scaleAmount = (mouseEvent.WheelDelta / deltaFor1Click) * .1; + + ScalePartAndFixPosition(mouseEvent, layerScale + layerScale * scaleAmount); + + mouseEvent.Handled = true; + + Invalidate(); + } + } + + public override void OnDraw(Graphics2D graphics2D) + { + new VertexSourceApplyTransform(vertexStorage, TotalTransform).RenderCurve(graphics2D, theme.TextColor, 2, true, theme.PrimaryAccentColor.Blend(theme.TextColor, .5), theme.PrimaryAccentColor); + + base.OnDraw(graphics2D); + } + + public void Zoom(double scaleAmount) + { + ScalePartAndFixPosition(new MouseEventArgs(MouseButtons.None, 0, Width / 2, Height / 2, 0), layerScale * scaleAmount); + Invalidate(); + } + + private void ScalePartAndFixPosition(MouseEventArgs mouseEvent, double scaleAmount) + { + Vector2 mousePreScale = new Vector2(mouseEvent.X, mouseEvent.Y); + TotalTransform.inverse_transform(ref mousePreScale); + + layerScale = scaleAmount; + + Vector2 mousePostScale = new Vector2(mouseEvent.X, mouseEvent.Y); + TotalTransform.inverse_transform(ref mousePostScale); + + unscaledRenderOffset += (mousePostScale - mousePreScale); + } + } +} \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Operations/RadialPinchObject3D.cs b/MatterControlLib/DesignTools/Operations/RadialPinchObject3D.cs index 606c353c6..b90e105d8 100644 --- a/MatterControlLib/DesignTools/Operations/RadialPinchObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/RadialPinchObject3D.cs @@ -59,13 +59,13 @@ namespace MatterHackers.MatterControl.DesignTools public RadialPinchObject3D() { // make sure the path editor is registered - PropertyEditor.RegisterEditor(typeof(EditableVertexStorage), new PathEditor()); + PropertyEditor.RegisterEditor(typeof(EditableVertexStorage), new PathEditorFactory()); Name = "Radial Pinch".Localize(); } - [PathEditor.TopAndBottomMoveXOnly] - [PathEditor.XMustBeGreaterThan0] + [PathEditorFactory.TopAndBottomMoveXOnly] + [PathEditorFactory.XMustBeGreaterThan0] public EditableVertexStorage PathForHorizontalOffsets { get; set; } = new EditableVertexStorage(); [Description("Specifies the number of vertical cuts required to ensure the part can be pinched well.")] diff --git a/MatterControlLib/PartPreviewWindow/GCode2DWidget.cs b/MatterControlLib/PartPreviewWindow/GCode2DWidget.cs index 9af97c03c..66dc1bfbc 100644 --- a/MatterControlLib/PartPreviewWindow/GCode2DWidget.cs +++ b/MatterControlLib/PartPreviewWindow/GCode2DWidget.cs @@ -127,18 +127,18 @@ namespace MatterHackers.MatterControl.PartPreviewWindow private double layerScale { get; set; } = 1; - private GCodeFile loadedGCode => printer.Bed.LoadedGCode; + private GCodeFile LoadedGCode => printer.Bed.LoadedGCode; - private Affine scalingTransform => Affine.NewScaling(layerScale, layerScale); + private Affine ScalingTransform => Affine.NewScaling(layerScale, layerScale); - private Affine totalTransform => Affine.NewTranslation(unscaledRenderOffset) * scalingTransform * Affine.NewTranslation(Width / 2, Height / 2); + private Affine TotalTransform => Affine.NewTranslation(unscaledRenderOffset) * ScalingTransform * Affine.NewTranslation(Width / 2, Height / 2); public void CenterPartInView() { - if (loadedGCode != null) + if (LoadedGCode != null) { - RectangleDouble partBounds = loadedGCode.GetBounds(); - Vector2 weightedCenter = loadedGCode.GetWeightedCenter(); + RectangleDouble partBounds = LoadedGCode.GetBounds(); + Vector2 weightedCenter = LoadedGCode.GetWeightedCenter(); unscaledRenderOffset = -weightedCenter; layerScale = Math.Min(Height / partBounds.Height, Width / partBounds.Width); @@ -170,7 +170,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow public override void OnDraw(Graphics2D graphics2D) { - if (loadedGCode != null) + if (LoadedGCode != null) { if (layerScale == 0) { @@ -178,7 +178,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } //using (new PerformanceTimer("GCode Timer", "Total")) { - Affine transform = totalTransform; + Affine transform = TotalTransform; if (this.options.RenderBed) { @@ -266,19 +266,19 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } Vector2 mousePreScale = mouseDownPosition; - totalTransform.inverse_transform(ref mousePreScale); + TotalTransform.inverse_transform(ref mousePreScale); layerScale *= zoomDelta; Vector2 mousePostScale = mouseDownPosition; - totalTransform.inverse_transform(ref mousePostScale); + TotalTransform.inverse_transform(ref mousePostScale); unscaledRenderOffset += (mousePostScale - mousePreScale); break; case ETransformState.Move: default: // also treat everything else like a move - scalingTransform.inverse_transform(ref mouseDelta); + ScalingTransform.inverse_transform(ref mouseDelta); unscaledRenderOffset += mouseDelta; break; @@ -322,7 +322,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow private void LoadedGCodeChanged(object sender, EventArgs e) { - if (loadedGCode == null) + if (LoadedGCode == null) { // TODO: Display an overlay for invalid GCode } @@ -351,12 +351,12 @@ namespace MatterHackers.MatterControl.PartPreviewWindow private void ScalePartAndFixPosition(MouseEventArgs mouseEvent, double scaleAmount) { Vector2 mousePreScale = new Vector2(mouseEvent.X, mouseEvent.Y); - totalTransform.inverse_transform(ref mousePreScale); + TotalTransform.inverse_transform(ref mousePreScale); layerScale = scaleAmount; Vector2 mousePostScale = new Vector2(mouseEvent.X, mouseEvent.Y); - totalTransform.inverse_transform(ref mousePostScale); + TotalTransform.inverse_transform(ref mousePostScale); unscaledRenderOffset += (mousePostScale - mousePreScale); }