Compass - Measure angles and scale distances with your mouse

Post your working scripts, libraries and tools for AHK v1.1 and older
kon
Posts: 1756
Joined: 29 Sep 2013, 17:11

Compass - Measure angles and scale distances with your mouse

24 Apr 2015, 11:25

Compass
Measure angles and scale distances with your mouse.

Code: Select all

#Requires AutoHotkey v1.1.33+
#SingleInstance Force
#NoEnv
SetBatchLines, -1
MsgBox, 64, Screen Scale Instructions,
(
Hold down the keys while measuring.

Ctrl + Alt + RightMouse
    Measure a known distance. Sets the scale factor for measuring subsequent distances.

Ctrl + Alt + LeftMouse
    Measure a distance or angle.

Ctrl + Win + RightMouse
    Clear the scale factor. Distance will not be calculated for subsequent measurements.

Ctrl + F12
    Toggle snap to horizontal/vertical when within +/-3 degrees.

Escape
    Delete all lines on screen.
)
Compass.ToggleSnap() ; Turn Snap on at startup (snaps to right angles when within 3 degrees)
return

^!LButton::Compass.Measure()    ; Ctrl+Alt+LButton
 
; Measure a known distance. Sets the scale factor for measuring subsequent distances.
^!RButton::Compass.SetScale()   ; Ctrl+Alt+RButton
 
; Clear the scale factor. Distance will not be calculated for subsequent measurements.
^#RButton::Compass.ClearScale() ; Ctrl+Win+RButton
 
; Toggle snap to horizontal/vertical when within +/-3 degrees.
^F12::Compass.ToggleSnap()      ; Ctrl+F12

; Draws a crosshair when Ctrl and Alt are held down before the mouse is clicked
^Alt::
!Ctrl::
    Compass.Crosshair()
return

#If Compass.Lines.MaxIndex() > 0
; Deletes all lines on screen.
Esc::Compass.DeleteLines()      ; Escape
#If

; To change the orientation of the angle:
;   1   Up=90, Down=-90, Left=180, Right=0
;   2   Up=0,  Down=180, Left=270, Right=90
;Compass.SetOrientation(2) ; Default is 1


/*  Compass
 *  By kon - ahkscript.org/boards/viewtopic.php?t=7225
 *  Translation to stand-alone Gdip thanks to jNizM - ahkscript.org/boards/viewtopic.php?&t=7225&p=45589#p45570
 *  Updated 2020-08-11
 *      Measure angles and scale distances with your mouse. Press the hotkeys and drag your mouse between two points.
 *  Note:
 *      *If you change the Measure/SetScale hotkeys, also change the keys GetKeyState monitors in the while-loop.
 *  Methods:
 *      Measure             Measure angles and scale distances with your mouse.
 *      SetScale            Sets the scale factor to use for subsequent measurements.
 *      ToggleSnap          Toggle snap to horizontal/vertical when within +/-3 degrees.
 *  Methods (Internal):
 *      SetScaleGui         Displays a GUI which prompts the user for the distance measured.
 *      pxDist              Returns the distance between two points in pixels.
 *      DecFtToArch         Converts decimal feet to architectural.
 *      gcd                 Euclidean GCD.
 *  Added methods 2020-08-05:
 *      ClearScale          Clear the scale factor. Distance will not be calculated for subsequent measurements.
 *      AdjustSnap          Adjusts points as required for snap.
 *      GDIP_Start          Start GDIP.
 *      GDIP_Close          Close GDIP.
 *      GDIP_Setup          Setup window, brush, graphics, etc for GDIP.
 *      GDIP_Cleanup        Delete brush, graphics, etc.
 *      GDIP_CreatePen      Create pen.
 *      GDIP_DeletePen      Delete pen.
 *      GDIP_CreateBrush    Create brush.
 *      GDIP_DeleteBrush    Delete brush.
 *      GDIP_DrawLine       Draw a line.
 *      GDIP_UpdateWindow   Refresh the window.
 *      DeleteLines         Delete all lines.
 *      SetOrientation      Set the orientation of the angle
 *      UpdateLines         Redraw the lines in Convert.Lines and update the window.
 *      GetWinArea          Get the total resolution of all the monitors.
 *  Added methods and adjustments 2020-08-11 thanks to gwarble 
 *  https://www.autohotkey.com/boards/viewtopic.php?p=346071#p346071
 *  https://www.autohotkey.com/boards/viewtopic.php?p=346072#p346072
 *      GDIP_DrawCrosshair  Draw a crosshair (before the mouse is clicked)
 */
class Compass {
    static Color1 := 0xffff0000, Color2 := 0xff0066ff, Lineweight1 := 1
    , ScaleFactor := "", Units := "ft", Cross := 20
    , WinW := A_ScreenWidth ; this will be updated by Compass.GetWinArea() in case there are multiple monitors
    , WinH := A_ScreenHeight
    , Orientation := 1
    , Lines := []
    
    ; Measure angles and scale distances with your mouse. Returns a line object. The return value is used internally by
    ; the SetScale method.
    Measure() {
        Conv := 180 / (4 * ATan(1)) * (this.Orientation = 1 ? -1 : 1)
        if (this.pToken = "") {
            this.GetWinArea()
            this.GDIP_Start()
        }
        TTNum := this.Lines.MaxIndex() ? this.Lines.MaxIndex() + 1 : 1
        CoordMode, Mouse, Screen
        MouseGetPos, StartX, StartY
        MouseKey := SubStr(A_ThisHotkey, 3), PrevX := "", PrevY := ""
        , hMSVCRT := DllCall("kernel32.dll\LoadLibrary", "Str", "msvcrt.dll", "Ptr")
        while (GetKeyState("Ctrl", "P") && GetKeyState(MouseKey, "P")) {    ; *Loop until Ctrl or L/RButton is released.
            MouseGetPos, CurrentX, CurrentY
            if (PrevX != CurrentX) || (PrevY != CurrentY) {
                PrevX := CurrentX, PrevY := CurrentY
                if (this.Snap)
                    this.AdjustSnap(StartX, CurrentX, StartY, CurrentY)
                this.GDIP_DrawLine(StartX, CurrentX, StartY, CurrentY)
                , Angle := DllCall("msvcrt.dll\atan2", "Double", CurrentY - StartY, "Double", CurrentX - StartX, "CDECL Double") * Conv
                if (this.Orientation = 2)
                    Angle += Angle < -90 ? 450 : 90
                if (this.ScaleFactor) {
                    Dist := Format("{1:0.3f}", this.pxDist(CurrentX, StartX, CurrentY, StartY) * this.ScaleFactor)
                    if (this.Units = "ft")
                        Dist .= " ft (" . this.DecFtToArch(Dist) . ")"
                    else 
                        Dist .= " " . this.Units
                }
                ToolTip, % Format("{1:0.3f}", Angle) . "°" . (Dist ? "`n" Dist : ""),,, % TTNum
                this.UpdateLines()
            }
        }
        DllCall("kernel32.dll\FreeLibrary", "Ptr", hMSVCRT)
        if (this.Snap)
            this.AdjustSnap(StartX, CurrentX, StartY, CurrentY)
        Line := {"StartX": StartX, "EndX": CurrentX, "StartY": StartY, "EndY": CurrentY
            , "Angle": Format("{1:0.3f}", Angle) . "°", "Len": Dist, "pxDist": this.pxDist(StartX, CurrentX, StartY, CurrentY)}
        this.Lines.Push(Line)
        return Line
    }

    ; Enter the known distance measured to set the scale factor to be used with all subsequent calls to Measure. The
    ; user will be prompted to enter the real world dimension corresponding to the current measurement. Distance will be
    ; calculated for any subsequent measurements.
    SetScale() {
        this.SetScaleGui(this.Measure().pxDist)
        ; Pop the most recent line from this.Lines and redraw the remaining lines. (We don't want this line to remain
        ; on screen, or count towards the measurement total in this.Lines.MaxIndex().)
        ToolTip,,,, % this.Lines.MaxIndex()
        this.Lines.Pop()
        this.UpdateLines()
        if (this.Lines.MaxIndex() < 1)
            this.GDIP_Close()
    }

    ; Clear the scale factor. Distance will not be calculated for subsequent measurements.
    ClearScale() {
        this.ScaleFactor := 1
        this.Units := "pixels"
    }
    
    SetScaleGui(pxLen) {
        static Text1, Text2, Text3, Edit1, Edit2, Button1
        Gui, SetScale: -Caption +Owner +LastFound +AlwaysOnTop
        WinID := WinExist()
        Gui, SetScale: Add, Text, vText1 x10 y10, Length
        Gui, SetScale: Add, Text, vText2 x110 y10, Units
        Gui, SetScale: Add, Edit, vEdit1 x10 y+10 w90
        GuiControlGet, Edit1, SetScale:pos, Edit1
        Gui, SetScale: Add, Edit, vEdit2 x+10 w90 y%Edit1Y%, % this.Units
        Gui, SetScale: Add, Text, vText3 x10 y+10, Enter the length of this measurement.
        Gui, SetScale: Add, Button, vButton1 x10 y+10 w190 gSetScaleOK Default, Ok
        Gui, SetScale: Show,, Set Scale
        WinSet AlwaysOnTop
        GuiControl, Focus, Edit1
        WinWaitNotActive, ahk_id %WinID%
        SetScaleGuiEscape:
        Gui, SetScale: Destroy
        return

        SetScaleOK:
        Gui, SetScale: Submit
        if Edit1 is number
            this.ScaleFactor := Edit1 / pxLen
        if (Edit2)
            this.Units := Edit2
        return
    }
    
    AdjustSnap(StartX, ByRef CurrentX, StartY, ByRef CurrentY) {
        if ((ADiff := Abs(CurrentY - StartY) / Abs(CurrentX - StartX))  < 0.052407 && ADiff)
            CurrentY := StartY
        else if (ADiff > 19.081136)
            CurrentX := StartX
    }
    
    ; Toggle snap to horizontal/vertical when within +/-3 degrees.
    ToggleSnap() {
        this.Snap := !this.Snap
        TrayTip, Compass, % "Snap: " (this.Snap ? "On" : "Off")
    }

    pxDist(x1, x2, y1, y2) {
        return Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
    }

    DecFtToArch(DecFt, Precision:=16) {
        Ft := Floor(DecFt), In := 12 * Mod(DecFt, 1)
        , UpperLimit := 1 - (HalfPrecision := 0.5 / Precision)
        , Fraction := Mod(In, 1), In := Floor(In)
        if (Fraction >= UpperLimit) {
            In++, Fraction := ""
            if (In = 12)
                In := 0, Ft++
        }
        else if (Fraction < HalfPrecision)
            Fraction := ""
        else {
            Step := 1 / Precision
            loop % Precision - 1 {
                if (Fraction >= UpperLimit - (A_Index * Step)) {
                    gcd := this.gcd(Precision, n := Precision - A_Index)
                    , Fraction := " " n // gcd "/" Precision // gcd
                    break
                }
            }
        }
        return LTrim((Ft ? Ft "'-" : "") In Fraction """", " 0")
    }

    gcd(a, b) {
        while (b)
            b := Mod(a | 0x0, a := b)
        return a
    }
    
    GDIP_Start() {
        if !(DllCall("kernel32.dll\GetModuleHandle", "Str", "gdiplus", "Ptr"))
            this.hGDIPLUS := DllCall("kernel32.dll\LoadLibrary", "Str", "gdiplus.dll", "Ptr")
        VarSetCapacity(SI, 24, 0), NumPut(1, SI, 0, "UInt")
        DllCall("gdiplus.dll\GdiplusStartup", "UPtrP", pToken, "Ptr", &SI, "Ptr", 0)
        if !(this.pToken := pToken) {
            MsgBox, GDIPlus could not be started.`nCheck the availability of GDIPlus on your system.
            return
        }
        this.GDIP_Setup()
    }
    
    GDIP_Close() {
        this.GDIP_Cleanup()
        DllCall("gdiplus.dll\GdiplusShutdown", "UPtr", this.pToken)
        DllCall("kernel32.dll\FreeLibrary", "Ptr", this.hGDIPLUS)
        this.pToken := "", this.hGDIPLUS := "" ; Clear these values to indicate that GDIP is not started
    }
    
    GDIP_Setup() {
        Gui, Compass: -Caption +E0x80000 +LastFound +AlwaysOnTop +ToolWindow +OwnDialogs
        Gui, Compass: Show, NA
        this.hWnd := WinExist()
        VarSetCapacity(BI, 40, 0), NumPut(this.WinW, BI, 4, "UInt"), NumPut(this.WinH, BI, 8, "UInt"), NumPut(40, BI, 0, "UInt")
        NumPut(1, BI, 12, "UShort"), NumPut(0, BI, 16, "UInt"), NumPut(32, BI, 14, "UShort")
        this.hbm := DllCall("gdi32.dll\CreateDIBSection", "Ptr", this.hdc := DllCall("user32.dll\GetDC", "Ptr", 0, "Ptr"), "UPtr", &BI, "UInt", 0, "UPtr*", 0, "Ptr", 0, "UInt", 0, "Ptr")
        DllCall("user32.dll\ReleaseDC", "Ptr", this.hWnd, "Ptr", this.hdc)
        this.hdc := DllCall("gdi32.dll\CreateCompatibleDC", "Ptr", 0, "Ptr")
        this.obm := DllCall("gdi32.dll\SelectObject", "Ptr", this.hdc, "Ptr", this.hbm)
        DllCall("gdiplus.dll\GdipCreateFromHDC", "Ptr", this.hdc, "PtrP", Graphics)
        this.Graphics := Graphics
        DllCall("gdiplus.dll\GdipSetSmoothingMode", "Ptr", this.Graphics, "Int", 4)
        this.GDIP_CreatePen()
        this.GDIP_CreateBrush()
    }
    
    GDIP_Cleanup() {
        this.GDIP_DeletePen()
        this.GDIP_DeleteBrush()
        DllCall("gdi32.dll\SelectObject", "Ptr", this.hdc, "Ptr", this.obm)
        DllCall("gdi32.dll\DeleteObject", "Ptr", this.hbm)
        DllCall("gdi32.dll\DeleteDC", "Ptr", this.hdc)
        DllCall("gdiplus.dll\GdipDeleteGraphics", "Ptr", this.Graphics)
        this.hdc := "", this.obm := "", this.hdc := "", this.Graphics := ""
        Gui, Compass: Destroy
    }
    
    GDIP_CreatePen() {
        DllCall("gdiplus.dll\GdipCreatePen1", "UInt", this.Color1, "Float", this.Lineweight1, "Int", 2, "PtrP", pPen)
        this.pPen := pPen
    }
    
    GDIP_DeletePen() {
        DllCall("gdiplus.dll\GdipDeletePen", "Ptr", this.pPen)
        this.pPen := ""
    }
    
    GDIP_CreateBrush() {
        DllCall("gdiplus.dll\GdipCreateSolidFill", "Int", this.Color2, "PtrP", pBrush)
        this.pBrush := pBrush
    }
    
    GDIP_DeleteBrush() {
        DllCall("gdiplus.dll\GdipDeleteBrush", "Ptr", this.pBrush)
        this.pBrush := ""
    }
    
    GDIP_DrawLine(StartX, CurrentX, StartY, CurrentY) {
        DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", StartX, "Float", StartY, "Float", CurrentX, "Float", CurrentY)
        , DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", StartX+this.Cross, "Float", StartY, "Float", StartX-this.Cross, "Float", StartY)
        , DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", CurrentX, "Float", CurrentY+this.Cross, "Float", CurrentX, "Float", CurrentY-this.Cross)
        , DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", StartX, "Float", StartY+this.Cross, "Float", StartX, "Float", StartY-this.Cross)
        , DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", CurrentX+this.Cross, "Float", CurrentY, "Float", CurrentX-this.Cross, "Float", CurrentY)
        , DllCall("gdiplus.dll\GdipFillEllipse", "Ptr", this.Graphics, "Ptr", this.pBrush, "Float", StartX - 4, "Float", StartY - 4, "Float", 8, "Float", 8) ;fixed uncentered circles...
        , DllCall("gdiplus.dll\GdipFillEllipse", "Ptr", this.Graphics, "Ptr", this.pBrush, "Float", CurrentX - 4, "Float", CurrentY - 4, "Float", 8, "Float", 8)
    }
    
    GDIP_UpdateWindow() {
        VarSetCapacity(PT, 8), NumPut(0, PT, 0, "UInt"), NumPut(0, PT, 4, "UInt")
        , DllCall("user32.dll\UpdateLayeredWindow", "Ptr", this.hWnd, "Ptr", 0, "UPtr", &PT, "Int64*", this.WinW | this.WinH << 32, "Ptr", this.hdc, "Int64*", 0, "UInt", 0, "UInt*", 0x1FF0000, "UInt", 2)
        , DllCall("gdiplus.dll\GdipGraphicsClear", "Ptr", this.Graphics, "UInt", 0x00FFFFFF)
    }
    
    GDIP_DrawCrosshair(X, Y) {
        DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", X+this.Cross, "Float", Y, "Float", X-this.Cross, "Float", Y)
        , DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", X, "Float", Y+this.Cross, "Float", X, "Float", Y-this.Cross)
        , DllCall("gdiplus.dll\GdipFillEllipse", "Ptr", this.Graphics, "Ptr", this.pBrush, "Float", X - 4, "Float", Y - 4, "Float", 8, "Float", 8)
    }
    
    ; Clears all the visible lines
    DeleteLines() {
        for i, v in this.Lines
            ToolTip,,,, % i
        this.Lines := []
        this.GDIP_Close()
    }
    
    ; Orientation   Angle
    ; 1             Up=90, Down=-90, Left=180, Right=0
    ; 2             Up=0,  Down=180, Left=270, Right=90
    SetOrientation(Orientation:=1) {
        this.Orientation := Orientation
    }
    
    ; Redraw the lines in this.Lines and update the window
    UpdateLines() {
        for i, Line in this.Lines
            this.GDIP_DrawLine(Line.StartX, Line.EndX, Line.StartY, Line.EndY)
        this.GDIP_UpdateWindow()
    }
    
    ; Gets the max coordinates of all the monitors.
    GetWinArea() {
        SysGet, MonCnt, MonitorCount
        Loop, % MonCnt
        {
            SysGet, Mon, Monitor, % A_Index
            if (MonRight > this.WinW)
                this.WinW := MonRight
            if (MonBottom > this.WinH)
                this.WinH := MonBottom
        }
    }
    
    Crosshair() {
        if (this.pToken = "") {
            this.GetWinArea()
            this.GDIP_Start()
        }
        CoordMode, Mouse, Screen
        MouseGetPos, StartX, StartY
        while (GetKeyState("Ctrl", "P")) {    ; *Loop until Ctrl or L/RButton is released.
            MouseGetPos, CurrentX, CurrentY
            if (PrevX != CurrentX) || (PrevY != CurrentY) { ; only redraw when the mouse if moved
                PrevX := CurrentX, PrevY := CurrentY
                this.GDIP_DrawCrosshair(CurrentX, CurrentY)
                this.UpdateLines()
            }
        }
        this.UpdateLines()
    }
}
Demo Gif
Old Version
Last edited by kon on 13 Jan 2023, 00:38, edited 15 times in total.
User avatar
joedf
Posts: 8959
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: [Function] Compass - Measure angles with your mouse

25 Apr 2015, 09:46

Interesting concept! :0
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]
toralf
Posts: 868
Joined: 27 Apr 2014, 21:08
Location: Germany

Re: [Function] Compass - Measure angles with your mouse

25 Apr 2015, 10:39

I'm curious, kon.
Why did you script this?
Besides measuring the angle.
What do you do with the angle?
ciao
toralf
kon
Posts: 1756
Joined: 29 Sep 2013, 17:11

Re: [Function] Compass - Measure angles with your mouse

25 Apr 2015, 11:13

It was originally an "Ask for help" request. [Link] I thought it was interesting.
Also, I sometimes need to calculate angles from incomplete information. ie plans in .pdf format. I will use this as a quick double-check in those situations. It's already come in handy :)
toralf
Posts: 868
Joined: 27 Apr 2014, 21:08
Location: Germany

Re: [Function] Compass - Measure angles with your mouse

25 Apr 2015, 11:51

Thanks for satisfying my curiosity.
ciao
toralf
kon
Posts: 1756
Joined: 29 Sep 2013, 17:11

Re: Compass - Measure angles and scale distances with your m

28 Apr 2015, 12:46

Update: Added the ability to scale distances.
User avatar
joedf
Posts: 8959
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: Compass - Measure angles and scale distances with your m

28 Apr 2015, 18:25

Oh oh Oohh!!!! Add an awesome Gif!! :D
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]
KevShaw
Posts: 11
Joined: 15 May 2015, 08:17

Re: Compass - Measure angles and scale distances with your mouse

15 May 2015, 08:24

Great Idea. Wish I'd have thought of it!! This is something I can use for sure. Getting an error on line 046, call to nonexistent function.
User avatar
jNizM
Posts: 3183
Joined: 30 Sep 2013, 01:33
Contact:

Re: Compass - Measure angles and scale distances with your mouse

15 May 2015, 09:49

@KevShaw:
A: You need to include Gdip.ahk (Lib)
or
B: You use the version without extra lib Compass.ahk
[AHK] v2.0.5 | [WIN] 11 Pro (Version 22H2) | [GitHub] Profile
KevShaw
Posts: 11
Joined: 15 May 2015, 08:17

Re: Compass - Measure angles and scale distances with your mouse

15 May 2015, 11:40

Alright, where could a copy of the Gdip.ahk (Lib) found?
kon
Posts: 1756
Joined: 29 Sep 2013, 17:11

Re: Compass - Measure angles and scale distances with your mouse

15 May 2015, 13:38

KevShaw, Gdip is available here. Currently I recommend getting the "Gdip All" version, rename it to gdip.ahk, and place it in your Lib folder.

Good idea about the Gif joedf. You may have seen that I added one shortly after you suggested it.

Nice job with the alternate version jNizM. (Edit: DllCall("gdiplus.dll\GdipSetSmoothingMode", "Ptr", pGraphics, "Int", 4) should be -> DllCall("gdiplus.dll\GdipSetSmoothingMode", "Ptr", G, "Int", 4) though.)
vasili111
Posts: 747
Joined: 21 Jan 2014, 02:04
Location: Georgia

Re: Compass - Measure angles and scale distances with your mouse

16 May 2015, 01:20

Thanks for sharing! I think it will be useful for making mouse gestures.
DRAKON-AutoHotkey: Visual programming for AutoHotkey.
User avatar
jNizM
Posts: 3183
Joined: 30 Sep 2013, 01:33
Contact:

Re: Compass - Measure angles and scale distances with your mouse

18 May 2015, 07:45

Thx for the hint. Updated. I changed 1-2 small things since last time.
[AHK] v2.0.5 | [WIN] 11 Pro (Version 22H2) | [GitHub] Profile
kon
Posts: 1756
Joined: 29 Sep 2013, 17:11

Re: Compass - Measure angles and scale distances with your mouse

18 May 2015, 12:38

Update:
  • Incorporated translation to stand-alone Gdip thanks to jNizM - http://ahkscript.org/boards/viewtopic.p ... 860#p45570
  • Added new method, ToggleSnap, and adjusted existing method Measure. When Snap is on, the measurement will snap to horizontal/vertical when within +/-3 degrees. New default hotkey, Ctrl+F12, will toggle snap on/off.
(Quick Edit: fixed a divide-by-zero bug introduced by the new snap feature.)
User avatar
FanaticGuru
Posts: 1906
Joined: 30 Sep 2013, 22:25

Re: Compass - Measure angles and scale distances with your mouse

19 May 2015, 17:54

Nice script. Useful for my job.

I added hotkeys to paste the last results from a measurement. Since you were already using F12 for toggle I went with Win+F12 paste decimal and Win+Alt+F12 paste architectural.

Not that it matters much but here is an example of how I do decimal feet to architectural measurements.

Code: Select all

DecFtToArch(Decimal,P:=16)
{
	Feet := Floor(Decimal), Decimal -= Feet, Inches := Floor(Decimal * 12), Decimal := Decimal * 12 - Inches
	RegExMatch(Decimal,"\.(\d+)0*", Match), N := Match1
	D := 10 ** StrLen(N)
	N := Round(N / D * P), D := P
	while X != 1
		X := GCD(N,D), N := N / X, D := D / X
	if (N = D)
		Inches ++, N := 0
	if (Inches = 12)
		Feet++, Inches := 0
	return Feet "'-" Inches (N and D ? " " Floor(N) "/" Floor(D):"")"""" 
}

GCD(A, B) 
{
    while B 
	   B := Mod(A|0x0, A:=B)
    return A
}
Here is a post that offer more options along the same vein:
Decimal2Fraction and Fraction2Decimal
The Highlighted to Fraction example is useful to convert measurements back and forth between decimal and architectural in other programs through the clipboard.

Also there are these functions about reducing terms in fractions:
Reduce Fraction Terms

Great job. If you ever decide to add to the script any more that would be great.

An option to leave the measurements displayed until erased might be nice. Multiply measurements would be nice but even if it was just one it would still be useful.

An option to measure areas.

An option to change the precision.

A GUI to choose options.

You get the idea. :bravo:

FG
Hotkey Help - Help Dialog for Currently Running AHK Scripts
AHK Startup - Consolidate Multiply AHK Scripts with one Tray Icon
Hotstring Manager - Create and Manage Hotstrings
[Class] WinHook - Create Window Shell Hooks and Window Event Hooks
User avatar
iilabs
Posts: 296
Joined: 07 Jun 2020, 16:57

Re: Compass - Measure angles and scale distances with your mouse

05 Aug 2020, 11:40

Very interesting tool. How do you get the lines to show up on the screen. Ive added GDIP to my folder but does it need to be in a specific library folder in the program files section where AHK program file is? Also, is there a way to keep the lines persist for a time or a way to delete it off the screen later. This will allow me to take a snapshot of all the measurements.

Thank you!
awel20
Posts: 211
Joined: 19 Mar 2018, 14:09

Re: Compass - Measure angles and scale distances with your mouse

05 Aug 2020, 16:23

Edit: I've changed this post to include the changes from later in this thread. This should make it easier for people to just copy and paste the following script for a simple working example. I added the crosshairs created by @gwarble and also fixed the memory leak he mentioned later in this thread. Also, I changed the line color to red so that it shows up better on a variety of backgrounds.

Code: Select all

#SingleInstance Force
#NoEnv
SetBatchLines, -1
MsgBox, 64, Screen Scale Instructions,
(
Hold down the keys while measuring.

Ctrl + Alt + RightMouse
    Measure a known distance. Sets the scale factor for measuring subsequent distances.

Ctrl + Alt + LeftMouse
    Measure a distance or angle.

Ctrl + Win + RightMouse
    Clear the scale factor. Distance will not be calculated for subsequent measurements.

Ctrl + F12
    Toggle snap to horizontal/vertical when within +/-3 degrees.

Escape
    Delete all lines on screen.
)
Compass.ToggleSnap() ; Turn Snap on at startup (snaps to right angles when within 3 degrees)
return

^!LButton::Compass.Measure()    ; Ctrl+Alt+LButton
 
; Measure a known distance. Sets the scale factor for measuring subsequent distances.
^!RButton::Compass.SetScale()   ; Ctrl+Alt+RButton
 
; Clear the scale factor. Distance will not be calculated for subsequent measurements.
^#RButton::Compass.ClearScale() ; Ctrl+Win+RButton
 
; Toggle snap to horizontal/vertical when within +/-3 degrees.
^F12::Compass.ToggleSnap()      ; Ctrl+F12

; Draws a crosshair when Ctrl and Alt are held down before the mouse is clicked
^Alt::
!Ctrl::
    Compass.Crosshair()
return

#If Compass.Lines.MaxIndex() > 0
; Deletes all lines on screen.
Esc::Compass.DeleteLines()      ; Escape
#If

; To change the orientation of the angle:
;   1   Up=90, Down=-90, Left=180, Right=0
;   2   Up=0,  Down=180, Left=270, Right=90
;Compass.SetOrientation(2) ; Default is 1


/*  Compass
 *  By kon - ahkscript.org/boards/viewtopic.php?t=7225
 *  Translation to stand-alone Gdip thanks to jNizM - ahkscript.org/boards/viewtopic.php?&t=7225&p=45589#p45570
 *  Updated May 18, 2015
 *      Measure angles and scale distances with your mouse. Press the hotkeys and drag your mouse between two points.
 *  Note:
 *      *If you change the Measure/SetScale hotkeys, also change the keys GetKeyState monitors in the while-loop.
 *  Methods:
 *      Measure             Measure angles and scale distances with your mouse.
 *      SetScale            Sets the scale factor to use for subsequent measurements.
 *      ToggleSnap          Toggle snap to horizontal/vertical when within +/-3 degrees.
 *  Methods (Internal):
 *      SetScaleGui         Displays a GUI which prompts the user for the distance measured.
 *      pxDist              Returns the distance between two points in pixels.
 *      DecFtToArch         Converts decimal feet to architectural.
 *      gcd                 Euclidean GCD.
 *  Added methods 2020-08-05:
 *      ClearScale          Clear the scale factor. Distance will not be calculated for subsequent measurements.
 *      AdjustSnap          Adjusts points as required for snap.
 *      GDIP_Start          Start GDIP.
 *      GDIP_Close          Close GDIP.
 *      GDIP_Setup          Setup window, brush, graphics, etc for GDIP.
 *      GDIP_Cleanup        Delete brush, graphics, etc.
 *      GDIP_CreatePen      Create pen.
 *      GDIP_DeletePen      Delete pen.
 *      GDIP_CreateBrush    Create brush.
 *      GDIP_DeleteBrush    Delete brush.
 *      GDIP_DrawLine       Draw a line.
 *      GDIP_UpdateWindow   Refresh the window.
 *      DeleteLines         Delete all lines.
 *      SetOrientation      Set the orientation of the angle
 *      UpdateLines         Redraw the lines in Convert.Lines and update the window.
 *      GetWinArea          Get the total resolution of all the monitors.
 *  Added methods and adjustments 2020-08-11 thanks to gwarble 
 *  https://www.autohotkey.com/boards/viewtopic.php?p=346071#p346071
 *  https://www.autohotkey.com/boards/viewtopic.php?p=346072#p346072
 *      GDIP_DrawCrosshair  Draw a crosshair (before the mouse is clicked)
 */
class Compass {
    static Color1 := 0xffff0000, Color2 := 0xff0066ff, Lineweight1 := 1
    , ScaleFactor := "", Units := "ft", Cross := 20
    , WinW := A_ScreenWidth ; this will be updated by Compass.GetWinArea() in case there are multiple monitors
    , WinH := A_ScreenHeight
    , Orientation := 1
    , Lines := []
    
    ; Measure angles and scale distances with your mouse. Returns a line object. The return value is used internally by
    ; the SetScale method.
    Measure() {
        Conv := 180 / (4 * ATan(1)) * (this.Orientation = 1 ? -1 : 1)
        if (this.pToken = "") {
            this.GetWinArea()
            this.GDIP_Start()
        }
        TTNum := this.Lines.MaxIndex() ? this.Lines.MaxIndex() + 1 : 1
        CoordMode, Mouse, Screen
        MouseGetPos, StartX, StartY
        MouseKey := SubStr(A_ThisHotkey, 3), PrevX := "", PrevY := ""
        , hMSVCRT := DllCall("kernel32.dll\LoadLibrary", "Str", "msvcrt.dll", "Ptr")
        while (GetKeyState("Ctrl", "P") && GetKeyState(MouseKey, "P")) {    ; *Loop until Ctrl or L/RButton is released.
            MouseGetPos, CurrentX, CurrentY
            if (PrevX != CurrentX) || (PrevY != CurrentY) {
                PrevX := CurrentX, PrevY := CurrentY
                if (this.Snap)
                    this.AdjustSnap(StartX, CurrentX, StartY, CurrentY)
                this.GDIP_DrawLine(StartX, CurrentX, StartY, CurrentY)
                , Angle := DllCall("msvcrt.dll\atan2", "Double", CurrentY - StartY, "Double", CurrentX - StartX, "CDECL Double") * Conv
                if (this.Orientation = 2)
                    Angle += Angle < -90 ? 450 : 90
                if (this.ScaleFactor) {
                    Dist := Format("{1:0.3f}", this.pxDist(CurrentX, StartX, CurrentY, StartY) * this.ScaleFactor)
                    if (this.Units = "ft")
                        Dist .= " ft (" . this.DecFtToArch(Dist) . ")"
                    else 
                        Dist .= " " . this.Units
                }
                ToolTip, % Format("{1:0.3f}", Angle) . "°" . (Dist ? "`n" Dist : ""),,, % TTNum
                this.UpdateLines()
            }
        }
        DllCall("kernel32.dll\FreeLibrary", "Ptr", hMSVCRT)
        if (this.Snap)
            this.AdjustSnap(StartX, CurrentX, StartY, CurrentY)
        Line := {"StartX": StartX, "EndX": CurrentX, "StartY": StartY, "EndY": CurrentY
            , "Angle": Format("{1:0.3f}", Angle) . "°", "Len": Dist, "pxDist": this.pxDist(StartX, CurrentX, StartY, CurrentY)}
        this.Lines.Push(Line)
        return Line
    }

    ; Enter the known distance measured to set the scale factor to be used with all subsequent calls to Measure. The
    ; user will be prompted to enter the real world dimension corresponding to the current measurement. Distance will be
    ; calculated for any subsequent measurements.
    SetScale() {
        this.SetScaleGui(this.Measure().pxDist)
        ; Pop the most recent line from this.Lines and redraw the remaining lines. (We don't want this line to remain
        ; on screen, or count towards the measurement total in this.Lines.MaxIndex().)
        ToolTip,,,, % this.Lines.MaxIndex()
        this.Lines.Pop()
        this.UpdateLines()
        if (this.Lines.MaxIndex() < 1)
            this.GDIP_Close()
    }

    ; Clear the scale factor. Distance will not be calculated for subsequent measurements.
    ClearScale() {
        this.ScaleFactor := 1
        this.Units := "pixels"
    }
    
    SetScaleGui(pxLen) {
        static Text1, Text2, Text3, Edit1, Edit2, Button1
        Gui, SetScale: -Caption +Owner +LastFound +AlwaysOnTop
        WinID := WinExist()
        Gui, SetScale: Add, Text, vText1 x10 y10, Length
        Gui, SetScale: Add, Text, vText2 x110 y10, Units
        Gui, SetScale: Add, Edit, vEdit1 x10 y+10 w90
        GuiControlGet, Edit1, SetScale:pos, Edit1
        Gui, SetScale: Add, Edit, vEdit2 x+10 w90 y%Edit1Y%, % this.Units
        Gui, SetScale: Add, Text, vText3 x10 y+10, Enter the length of this measurement.
        Gui, SetScale: Add, Button, vButton1 x10 y+10 w190 gSetScaleOK Default, Ok
        Gui, SetScale: Show,, Set Scale
        WinSet AlwaysOnTop
        GuiControl, Focus, Edit1
        WinWaitNotActive, ahk_id %WinID%
        SetScaleGuiEscape:
        Gui, SetScale: Destroy
        return

        SetScaleOK:
        Gui, SetScale: Submit
        if Edit1 is number
            this.ScaleFactor := Edit1 / pxLen
        if (Edit2)
            this.Units := Edit2
        return
    }
    
    AdjustSnap(StartX, ByRef CurrentX, StartY, ByRef CurrentY) {
        if ((ADiff := Abs(CurrentY - StartY) / Abs(CurrentX - StartX))  < 0.052407 && ADiff)
            CurrentY := StartY
        else if (ADiff > 19.081136)
            CurrentX := StartX
    }
    
    ; Toggle snap to horizontal/vertical when within +/-3 degrees.
    ToggleSnap() {
        this.Snap := !this.Snap
        TrayTip, Compass, % "Snap: " (this.Snap ? "On" : "Off")
    }

    pxDist(x1, x2, y1, y2) {
        return Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
    }

    DecFtToArch(DecFt, Precision:=16) {
        Ft := Floor(DecFt), In := 12 * Mod(DecFt, 1)
        , UpperLimit := 1 - (HalfPrecision := 0.5 / Precision)
        , Fraction := Mod(In, 1), In := Floor(In)
        if (Fraction >= UpperLimit) {
            In++, Fraction := ""
            if (In = 12)
                In := 0, Ft++
        }
        else if (Fraction < HalfPrecision)
            Fraction := ""
        else {
            Step := 1 / Precision
            loop % Precision - 1 {
                if (Fraction >= UpperLimit - (A_Index * Step)) {
                    gcd := this.gcd(Precision, n := Precision - A_Index)
                    , Fraction := " " n // gcd "/" Precision // gcd
                    break
                }
            }
        }
        return LTrim((Ft ? Ft "'-" : "") In Fraction """", " 0")
    }

    gcd(a, b) {
        while (b)
            b := Mod(a | 0x0, a := b)
        return a
    }
    
    GDIP_Start() {
        if !(DllCall("kernel32.dll\GetModuleHandle", "Str", "gdiplus", "Ptr"))
            this.hGDIPLUS := DllCall("kernel32.dll\LoadLibrary", "Str", "gdiplus.dll", "Ptr")
        VarSetCapacity(SI, 24, 0), NumPut(1, SI, 0, "UInt")
        DllCall("gdiplus.dll\GdiplusStartup", "UPtrP", pToken, "Ptr", &SI, "Ptr", 0)
        if !(this.pToken := pToken) {
            MsgBox, GDIPlus could not be started.`nCheck the availability of GDIPlus on your system.
            return
        }
        this.GDIP_Setup()
    }
    
    GDIP_Close() {
        this.GDIP_Cleanup()
        DllCall("gdiplus.dll\GdiplusShutdown", "UPtr", this.pToken)
        DllCall("kernel32.dll\FreeLibrary", "Ptr", this.hGDIPLUS)
        this.pToken := "", this.hGDIPLUS := "" ; Clear these values to indicate that GDIP is not started
    }
    
    GDIP_Setup() {
        Gui, Compass: -Caption +E0x80000 +LastFound +AlwaysOnTop +ToolWindow +OwnDialogs
        Gui, Compass: Show, NA
        this.hWnd := WinExist()
        VarSetCapacity(BI, 40, 0), NumPut(this.WinW, BI, 4, "UInt"), NumPut(this.WinH, BI, 8, "UInt"), NumPut(40, BI, 0, "UInt")
        NumPut(1, BI, 12, "UShort"), NumPut(0, BI, 16, "UInt"), NumPut(32, BI, 14, "UShort")
        this.hbm := DllCall("gdi32.dll\CreateDIBSection", "Ptr", this.hdc := DllCall("user32.dll\GetDC", "Ptr", 0, "Ptr"), "UPtr", &BI, "UInt", 0, "UPtr*", 0, "Ptr", 0, "UInt", 0, "Ptr")
        DllCall("user32.dll\ReleaseDC", "Ptr", this.hWnd, "Ptr", this.hdc)
        this.hdc := DllCall("gdi32.dll\CreateCompatibleDC", "Ptr", 0, "Ptr")
        this.obm := DllCall("gdi32.dll\SelectObject", "Ptr", this.hdc, "Ptr", this.hbm)
        DllCall("gdiplus.dll\GdipCreateFromHDC", "Ptr", this.hdc, "PtrP", Graphics)
        this.Graphics := Graphics
        DllCall("gdiplus.dll\GdipSetSmoothingMode", "Ptr", this.Graphics, "Int", 4)
        this.GDIP_CreatePen()
        this.GDIP_CreateBrush()
    }
    
    GDIP_Cleanup() {
        this.GDIP_DeletePen()
        this.GDIP_DeleteBrush()
        DllCall("gdi32.dll\SelectObject", "Ptr", this.hdc, "Ptr", this.obm)
        DllCall("gdi32.dll\DeleteObject", "Ptr", this.hbm)
        DllCall("gdi32.dll\DeleteDC", "Ptr", this.hdc)
        DllCall("gdiplus.dll\GdipDeleteGraphics", "Ptr", this.Graphics)
        this.hdc := "", this.obm := "", this.hdc := "", this.Graphics := ""
        Gui, Compass: Destroy
    }
    
    GDIP_CreatePen() {
        DllCall("gdiplus.dll\GdipCreatePen1", "UInt", this.Color1, "Float", this.Lineweight1, "Int", 2, "PtrP", pPen)
        this.pPen := pPen
    }
    
    GDIP_DeletePen() {
        DllCall("gdiplus.dll\GdipDeletePen", "Ptr", this.pPen)
        this.pPen := ""
    }
    
    GDIP_CreateBrush() {
        DllCall("gdiplus.dll\GdipCreateSolidFill", "Int", this.Color2, "PtrP", pBrush)
        this.pBrush := pBrush
    }
    
    GDIP_DeleteBrush() {
        DllCall("gdiplus.dll\GdipDeleteBrush", "Ptr", this.pBrush)
        this.pBrush := ""
    }
    
    GDIP_DrawLine(StartX, CurrentX, StartY, CurrentY) {
        DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", StartX, "Float", StartY, "Float", CurrentX, "Float", CurrentY)
        , DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", StartX+this.Cross, "Float", StartY, "Float", StartX-this.Cross, "Float", StartY)
        , DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", CurrentX, "Float", CurrentY+this.Cross, "Float", CurrentX, "Float", CurrentY-this.Cross)
        , DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", StartX, "Float", StartY+this.Cross, "Float", StartX, "Float", StartY-this.Cross)
        , DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", CurrentX+this.Cross, "Float", CurrentY, "Float", CurrentX-this.Cross, "Float", CurrentY)
        , DllCall("gdiplus.dll\GdipFillEllipse", "Ptr", this.Graphics, "Ptr", this.pBrush, "Float", StartX - 4, "Float", StartY - 4, "Float", 8, "Float", 8) ;fixed uncentered circles...
        , DllCall("gdiplus.dll\GdipFillEllipse", "Ptr", this.Graphics, "Ptr", this.pBrush, "Float", CurrentX - 4, "Float", CurrentY - 4, "Float", 8, "Float", 8)
    }
    
    GDIP_UpdateWindow() {
        VarSetCapacity(PT, 8), NumPut(0, PT, 0, "UInt"), NumPut(0, PT, 4, "UInt")
        , DllCall("user32.dll\UpdateLayeredWindow", "Ptr", this.hWnd, "Ptr", 0, "UPtr", &PT, "Int64*", this.WinW | this.WinH << 32, "Ptr", this.hdc, "Int64*", 0, "UInt", 0, "UInt*", 0x1FF0000, "UInt", 2)
        , DllCall("gdiplus.dll\GdipGraphicsClear", "Ptr", this.Graphics, "UInt", 0x00FFFFFF)
    }
    
    GDIP_DrawCrosshair(X, Y) {
        DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", X+this.Cross, "Float", Y, "Float", X-this.Cross, "Float", Y)
        , DllCall("gdiplus.dll\GdipDrawLine", "Ptr", this.Graphics, "Ptr", this.pPen, "Float", X, "Float", Y+this.Cross, "Float", X, "Float", Y-this.Cross)
        , DllCall("gdiplus.dll\GdipFillEllipse", "Ptr", this.Graphics, "Ptr", this.pBrush, "Float", X - 4, "Float", Y - 4, "Float", 8, "Float", 8)
    }
    
    ; Clears all the visible lines
    DeleteLines() {
        for i, v in this.Lines
            ToolTip,,,, % i
        this.Lines := []
        this.GDIP_Close()
    }
    
    ; Orientation   Angle
    ; 1             Up=90, Down=-90, Left=180, Right=0
    ; 2             Up=0,  Down=180, Left=270, Right=90
    SetOrientation(Orientation:=1) {
        this.Orientation := Orientation
    }
    
    ; Redraw the lines in this.Lines and update the window
    UpdateLines() {
        for i, Line in this.Lines
            this.GDIP_DrawLine(Line.StartX, Line.EndX, Line.StartY, Line.EndY)
        this.GDIP_UpdateWindow()
    }
    
    ; Gets the max coordinates of all the monitors.
    GetWinArea() {
        SysGet, MonCnt, MonitorCount
        Loop, % MonCnt
        {
            SysGet, Mon, Monitor, % A_Index
            if (MonRight > this.WinW)
                this.WinW := MonRight
            if (MonBottom > this.WinH)
                this.WinH := MonBottom
        }
    }
    
    Crosshair() {
        if (this.pToken = "") {
            this.GetWinArea()
            this.GDIP_Start()
        }
        CoordMode, Mouse, Screen
        MouseGetPos, StartX, StartY
        while (GetKeyState("Ctrl", "P")) {    ; *Loop until Ctrl or L/RButton is released.
            MouseGetPos, CurrentX, CurrentY
            if (PrevX != CurrentX) || (PrevY != CurrentY) { ; only redraw when the mouse if moved
                PrevX := CurrentX, PrevY := CurrentY
                this.GDIP_DrawCrosshair(CurrentX, CurrentY)
                this.UpdateLines()
            }
        }
        this.UpdateLines()
    }
}


Hi iilabs. :)
iilabs wrote:
05 Aug 2020, 11:40
How do you get the lines to show up on the screen. Ive added GDIP to my folder but does it need to be in a specific library folder in the program files section where AHK program file is?
You don't need gdip.ahk for this since the script calls all the gdip functions directly with DllCall. I think you were not seeing the lines because you are using multiple monitors; I adjusted the script to detect that.
iilabs wrote:
05 Aug 2020, 11:40
Also, is there a way to keep the lines persist for a time or a way to delete it off the screen later. This will allow me to take a snapshot of all the measurements.
I went ahead and made some adjustments to this script based on the script in your Ask For Help topic (https://www.autohotkey.com/boards/viewtopic.php?f=76&t=78930). So now it will keep the lines on screen until you call Compass.DeleteLines().

Old example for @iilabs' question
Last edited by awel20 on 03 Sep 2020, 14:21, edited 7 times in total.
User avatar
gwarble
Posts: 524
Joined: 30 Sep 2013, 15:01

Re: Compass - Measure angles and scale distances with your mouse

05 Aug 2020, 17:16

nice update, small bug:
when snap is on, the second line created moves the endpoint of the previous line to its unsnapped position

for me some trivial changes, i removed the msgbox and let it add up to 10 lines to the screen before deleting, or deleting on Esc::, this is very cool and useful, thanks kon and awel
EitherMouse - Multiple mice, individual settings . . . . www.EitherMouse.com . . . . forum . . . .
User avatar
iilabs
Posts: 296
Joined: 07 Jun 2020, 16:57

Re: Compass - Measure angles and scale distances with your mouse

05 Aug 2020, 18:47

Thank you all for the improvement. Appreciate it.

I reloaded the script but still dont see any lines being drawn. I have a 4 monitor setup. Thanks Awel20 for picking that up. I do see the measurements so at least I know its working like a champ. Any way to get it to draw lines and get angle as well?

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: hiahkforum, shiori4 and 143 guests