Sizing handles again

Just starting out? Need help? Post your questions and find answers here.
wombats
Enthusiast
Enthusiast
Posts: 664
Joined: Thu Dec 29, 2011 5:03 pm

Sizing handles again

Post by wombats »

Hi,

My quest to perfect sizing handles continues. :? I'm getting in such a mess with it. :(

The cursor test isn't accurate, especially when the shape is rotated. What do I need to adjust the mouse or handle position by based on the rotation?

How can I tell what to do with the sizing handle when the shape is rotated? For example, right now the code thinks it's always going to need to adjust the height, even when the handle is on the right. I'd really rather avoid doing something like this:

Code: Select all

    If a => 0 And a < 24
      *b\handle\which = #HANDLE_N
      *b\handle\which = #HANDLE_NE
      *b\handle\which = #HANDLE_E
      *b\handle\which = #HANDLE_SE
      *b\handle\which = #HANDLE_S
      *b\handle\which = #HANDLE_SW
      *b\handle\which = #HANDLE_W
      *b\handle\which = #HANDLE_NW
    ElseIf a => 24 And a < 48
      *b\handle\which = #HANDLE_NE
      *b\handle\which = #HANDLE_E
      ; ...and so on...
    ElseIf a => 48 And a < 72
    ElseIf a => 72 And a < 96
    ElseIf a => 96 And a < 120
    ElseIf a => 120 And a < 144
    ElseIf a => 144 And a < 168
    ElseIf a => 168 And a < 192
    ElseIf a => 192 And a < 216
    ElseIf a => 216 And a < 240
    ElseIf a => 240 And a < 264
    ElseIf a => 264 And a < 288
    ElseIf a => 288 And a < 312
    ElseIf a => 312 And a < 336
    ElseIf a => 336 And a < 360
    EndIf
When the height of the shape is changed when the shape is rotated, the position of the shape also changes. How can I calculate what to add/subtract from the position to keep it in the same space?

Code: Select all

EnableExplicit

CompilerIf #PB_Compiler_OS = #PB_OS_MacOS
  Structure Point : x.i : y.i : EndStructure
CompilerEndIf

Procedure rotatePoint(*point.Point, *rotationCenterPoint.Point, degrees.F, *_return.Point)
  Define radians.f = Radian(degrees)
  Define pointX.f = *point\x - *rotationCenterPoint\x
  Define pointY.f = *point\y - *rotationCenterPoint\y
  Define newPointX.f = pointX * Cos(radians) - pointY * Sin(radians)
  Define newPointY.f = pointX * Sin(radians) + pointY * Cos(radians)
  *_return\x = newPointX + *rotationCenterPoint\x
  *_return\y = newPointY + *rotationCenterPoint\y
EndProcedure

Structure Box
  x.i : y.i
  w.i : h.i
  handleCur.Point
  handleOrig.Point
  angle.f
EndStructure

Global currentPoint.Point, lastPoint.Point, resize

Declare UpdateBoxHandle()

Global *b.Box = AllocateStructure(Box)
With *b
  \x = 200 : \y = 150
  \w = 100 : \h = 125
  \handleCur\x = \x + (\w / 2)
  \handleCur\y = \y
  CopyStructure(@\handleCur, @\handleOrig, Point)
  \angle = 45
EndWith
UpdateBoxHandle()

Procedure UpdateBoxHandle()
  Protected handle.Point, center.Point, newPos.Point
  handle\x = *b\x + (*b\w / 2)
  handle\y = *b\y - 4
  center\x = *b\x + *b\w / 2
  center\y = *b\y + *b\h / 2
  rotatePoint(@handle, @center, *b\angle, *b\handleCur)
  *b\handleCur\x - 4
  *b\handleCur\y - 4
EndProcedure

Procedure DrawCanvas()
  Protected txt$
  txt$ = ~"Left/right to rotate\n\n0 to reset\n\nEscape to end\n\n\nAngle: " + Str(*b\angle) + Chr(176)
  If StartVectorDrawing(CanvasVectorOutput(0))
    VectorSourceColor(RGBA(0, 0, 0, 255))
    FillVectorOutput()
    VectorSourceColor(RGBA(255, 255, 255, 255))
    MovePathCursor(20, 20)
    DrawVectorParagraph(txt$, 200, 200)
    RotateCoordinates(*b\x + *b\w / 2, *b\y + *b\h / 2, *b\angle)
    AddPathBox(*b\x, *b\y, *b\w, *b\h)  
    VectorSourceColor(RGBA(0, 0, 255, 255))
    StrokePath(1)
    StopVectorDrawing()
  EndIf
  If StartDrawing(CanvasOutput(0))
    Box(*b\handleCur\x, *b\handleCur\y, 8, 8, RGB(255, 255, 255))
    Box(*b\handleOrig\x, *b\handleOrig\y, 8, 8, RGB(255, 165, 0))
    DrawingMode(#PB_2DDrawing_Transparent)
    StopDrawing()
  EndIf
EndProcedure

Procedure OnCanvasEvents()
  Protected offset.Point, newY, newH, mousePos.Point, center.Point
  Select EventType()
    Case #PB_EventType_MouseMove
      currentPoint\x = GetGadgetAttribute(0, #PB_Canvas_MouseX)
      currentPoint\y = GetGadgetAttribute(0, #PB_Canvas_MouseY)
      If resize & GetGadgetAttribute(0, #PB_Canvas_Buttons) & #PB_Canvas_LeftButton
        offset\y = currentPoint\y - lastPoint\y
        *b\y + offset\y
        *b\h - offset\y
        UpdateBoxHandle()
        DrawCanvas()
      Else
        center\x = *b\x + *b\w / 2
        center\y = *b\y + *b\h / 2
        rotatePoint(@currentPoint, @center, -*b\angle, @mousePos)
        If mousePos\x => *b\handleOrig\x And mousePos\x < *b\handleOrig\x + 8 And
           mousePos\y => *b\handleOrig\y And mousePos\y < *b\handleOrig\y + 8
          resize = 1
          SetGadgetAttribute(0, #PB_Canvas_Cursor, #PB_Cursor_UpDown)
        Else
          SetGadgetAttribute(0, #PB_Canvas_Cursor, #PB_Cursor_Default)
        EndIf
      EndIf
      lastPoint\x = GetGadgetAttribute(0, #PB_Canvas_MouseX)
      lastPoint\y = GetGadgetAttribute(0, #PB_Canvas_MouseY)
    Case #PB_EventType_LeftButtonUp
      resize = 0
      SetGadgetAttribute(0, #PB_Canvas_Cursor, #PB_Cursor_Default)
    Case #PB_EventType_KeyDown
      Select GetGadgetAttribute(0, #PB_Canvas_Key)
        Case #PB_Shortcut_Left
          *b\angle - 5
          If *b\angle < 0
            *b\angle = 359
          EndIf
          If *b\angle > 359
            *b\angle = 0
          EndIf
          UpdateBoxHandle()
          DrawCanvas()
        Case #PB_Shortcut_Right
          *b\angle + 5
          If *b\angle < 0
            *b\angle = 359
          EndIf
          If *b\angle > 359
            *b\angle = 0
          EndIf
          UpdateBoxHandle()
          DrawCanvas()
        Case #PB_Shortcut_0
          *b\angle = 0
          UpdateBoxHandle()
          DrawCanvas()
        Case #PB_Shortcut_Escape : End
          DrawCanvas()
      EndSelect
  EndSelect
EndProcedure

If OpenWindow(0, 0, 0, 640, 480, "", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  CanvasGadget(0, 0, 0, 640, 480, #PB_Canvas_Keyboard)
  BindGadgetEvent(0, @OnCanvasEvents(), #PB_All)
  DrawCanvas()
  SetActiveGadget(0)
  Repeat : Until WaitWindowEvent() = #PB_Event_CloseWindow
EndIf
I would really appreciate any help. Thank you.
srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Re: Sizing handles again

Post by srod »

I enjoyed this one! :)

Must admit that I would have probably done this with some good old fashioned belt and braces style trig and vector geometry (I have used a little), but I followed your lead somewhat. I haven't really tried to debug your code, though I do suspect that you were moving the centre of rotation during the resize which is a massive no-no when resizing just one dimension of the rectangle. Not sure why your hit-testing wasn't working, but like I said, I didn't want to debug and get myself in a tizzy!

If you run the following you will see the original rectangle laid out with a dashed red line so you can compare and contrast. I've also displayed the centre of rotation.

Use the left cursor key to rotate anti-clockwise etc.

You will note that following a resize I move the original rectangle to match the new centre point. This seemed logical to me, but you can easily change that. If you do, and do not move the centre point following a resize, then it will be possible to subsequently rotate the rectangle about an exterior point which will cause the said rectangle to dance about the screen like fangbeast during the high point of a summer solstice! You would need to add some validation code to limit the original drag/resize etc.

Hope it helps.

Code: Select all

;This code shows one way of allowing the user to resize one side of a possibly rotated rectangle. This is not a scaling of the rectangle
;in question, but a one-way stretch.
;We basically record info on the original (vertical) rectangle together with an appropriate angle. When resizing the rotated rectangle,
;we simply resize the vertical one in an appropriate manner and rotate when rendering to a canvas etc. careful to keep the centre of rotation
;intact during the resize. MUST NOT move the centre of rotation during the resize!
;At the conclusion of the resize we then move the centre (and thus the matching vertical rectangle) to match the newly rotated/resized object.
;This allows for seemless repeated resizing/rotating etc. Without this there will be a lot of jumping about and, in some cases, the rectangle
;will rotate about an external point! Can easily be changed mind.
;We draw the original rectangle (outlined red dash) for reference purposes.

;Left cursor key to rotate anti-clockwise. Right cursor key to rotate clockwise.

CompilerIf #PB_Compiler_OS = #PB_OS_MacOS
  Structure POINT
    x.i
    y.i
  EndStructure
CompilerEndIf

;The following structure wraps up our rectangle object. We store information only on the underlying vertical rectangle
;whose position depends on the rotation in effect etc. That is, these fields record the original un-rotated rectangle.
Structure BOX
  ;(x, y) refer to the top-left point which can change during resizing.
    x.i
    y.i
    width.i
    height.i
  ;The following record the centre of the original rectangle and will change ONLY after each resize (not during!)
    cenX.i
    cenY.i
  ;The following field gives the rotation in degrees from the positive x-axis. A positive value represents a clockwise rotation.
    angle.i
EndStructure

;We use the following structure to wrap up some of our drag fields.
  Structure RESIZEDRAG
    blnIsResizing.i
    originalY.i
    originalHeight.i
    originalCursorPos.i
    delta.i             ;Tells us how much the cursor has moved (in the relevant direction) since the resize was instigated.
  EndStructure

;Create a box to play around with.
  Global gBox.BOX
  With gBox
    \x = 280
    \y = 180
    \width = 80
    \height = 120
    ;We have to set the coordinates of the centre manually at this stage. We don't do this dynamically since the centre coordinates change
    ;ONLY at the conclusion of a resize - not during!
      \cenX = \x + \width/2
      \cenY = \y + \height/2
    \angle = 0
  EndWith

Global gResize.RESIZEDRAG

;The following procedure rotates the given coordinate
;Angle must be specified in degrees.
;A positive angle rotates clockwise.
Procedure.i RotatePoint(*pt.POINT, cenX, cenY, angle)
  Protected newX.d, newY.d, rAngle.d
  rAngle = Radian(angle)
  newX = cenX + Cos(rAngle) * (*pt\x - cenX) - Sin(rAngle) * (*pt\y - cenY)
  newY = cenY + Sin(rAngle) * (*pt\x - cenX) + Cos(rAngle) * (*pt\y - cenY)
  *pt\x = newX
  *pt\y = newY
EndProcedure

Procedure DrawRotatedRectangle()
  Protected cenX, cenY
  If StartVectorDrawing(CanvasVectorOutput(0))
    ;Erase.
      VectorSourceColor($FFFFFFFF)
      FillVectorOutput()
    ;Draw a dashed outline of the 'original' box for reference.
      VectorSourceColor(RGBA(255, 0, 0, 255))
      AddPathBox(gBox\x, gBox\y, gBox\width, gBox\height)  
      DashPath(1, 4)
      ;Visually mark the centre of rotation.
        AddPathBox(gBox\cenX - 2, gBox\cenY - 2, 4, 4)  
        FillPath()
    ;Draw the rotated box.
      ;Set rotation.
        RotateCoordinates(gBox\cenX, gBox\cenY, gBox\angle)
      VectorSourceColor(RGBA(0, 0, 255, 255))
      AddPathBox(gBox\x, gBox\y, gBox\width, gBox\height)  
      StrokePath(1)
    ;Draw a single sizing box.
      VectorSourceColor(RGBA(0, 0, 0, 255))
      AddPathBox(gBox\x + gBox\width/2 - 4, gBox\y - 4, 8, 8)  
      FillPath()
    StopVectorDrawing()
  EndIf
EndProcedure

Procedure OnCanvasEvents()
  Protected pt.POINT, movedY
  Select EventType()
    Case #PB_EventType_LeftButtonDown ;Check the cursor position and, if over the resize handle, instigate a resize.
      pt\x = GetGadgetAttribute(0, #PB_Canvas_MouseX)
      pt\y = GetGadgetAttribute(0, #PB_Canvas_MouseY)
      ;Counter rotate the cursor position.
        RotatePoint(pt, gBox\cenX, gBox\cenY, -gBox\angle)
      ;Are we atop the unrotated handle?
        If pt\x >= gBox\x + gBox\width/2 -4 And pt\x <= gBox\x + gBox\width/2 + 4 And pt\y >= gBox\y - 4 And pt\y <= gBox\y + 4 
          ;Instigate a resize action.
            With gResize
              \blnIsResizing = #True
              \originalY = gBox\y
              \originalHeight = gBox\height
              \originalCursorPos = pt\y
              \delta = 0
            EndWith
        EndIf

    Case #PB_EventType_MouseMove
      pt\x = GetGadgetAttribute(0, #PB_Canvas_MouseX)
      pt\y = GetGadgetAttribute(0, #PB_Canvas_MouseY)
      If gResize\blnIsResizing
        ;Calculate how far the user has moved the cursor since starting the resize.
          ;Counter rotate the cursor position.
            RotatePoint(pt, gBox\cenX, gBox\cenY, -gBox\angle)
          movedY = pt\y - gResize\originalCursorPos    
        ;Adjust vertical rectangle height.
          gBox\y = gResize\originalY + movedY
          gBox\height = gResize\originalHeight - movedY
        ;Redraw.
          DrawRotatedRectangle()
      Else  ;We set the cursor appropriately if the cursor is over the handle.
            ;For simplicity we just use a cross-hair cursor.
        ;Counter rotate the cursor position.
          RotatePoint(pt, gBox\cenX, gBox\cenY, -gBox\angle)
        ;Are we atop the unrotated handle?
          If pt\x >= gBox\x + gBox\width/2 -4 And pt\x <= gBox\x + gBox\width/2 + 4 And pt\y >= gBox\y - 4 And pt\y <= gBox\y + 4 
            SetGadgetAttribute(0, #PB_Canvas_Cursor, #PB_Cursor_Cross)
          Else
            SetGadgetAttribute(0, #PB_Canvas_Cursor, #PB_Cursor_Default)
          EndIf
      EndIf

    Case #PB_EventType_LeftButtonUp
      If gResize\blnIsResizing
        ;Reset the rectangle structure ready for the next rotation etc.
        ;We move the vertical rectangle so that it's centre matches that of the newly resized (and possible rotated) rectangle.
          ;First rotate the centre of the newly sized rectangle about the original centre point. This will give us the centre point
          ;of the rotated and resized rectangle.
            pt\x = gBox\x + gBox\width/2
            pt\y = gBox\y + gBox\height/2
            RotatePoint(pt, gBox\cenX, gBox\cenY, gBox\angle)
          ;Can now set the new centre point.
            gBox\cenX = pt\x
            gBox\cenY = pt\y
          ;Now we can reset the (x, y) of the top-left of the matching vertical rectangle.
            gBox\x = gBox\cenX - gBox\width/2
            gBox\y = gBox\ceny - gBox\height/2
        ;Clear the resizing flag.
          gResize\blnIsResizing = #False
        ;Redraw the canvas to show the revamped original rectangle (in red dash lines).
          DrawRotatedRectangle()
      EndIf

    Case #PB_EventType_KeyDown
      Select GetGadgetAttribute(0, #PB_Canvas_Key)
        Case #PB_Shortcut_Left
          gBox\angle - 5
          DrawRotatedRectangle()
        Case #PB_Shortcut_Right
          gBox\angle + 5
          DrawRotatedRectangle()
      EndSelect
  EndSelect
EndProcedure

If OpenWindow(0, 0, 0, 640, 480, "", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  CanvasGadget(0, 0, 0, 640, 480, #PB_Canvas_Keyboard)
  BindGadgetEvent(0, @OnCanvasEvents(), #PB_All)
  DrawRotatedRectangle()
  SetActiveGadget(0)
  Repeat : Until WaitWindowEvent() = #PB_Event_CloseWindow
EndIf
I may look like a mule, but I'm not a complete ass.
Post Reply