Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
M
multitouch
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Taddeüs Kroes
multitouch
Commits
c8706a27
Commit
c8706a27
authored
Jun 17, 2012
by
Taddeüs Kroes
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added various fixes for transformations in the test application.
parent
f3801a8a
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
366 additions
and
30 deletions
+366
-30
src/__init__.py
src/__init__.py
+2
-1
src/areas.py
src/areas.py
+1
-15
src/geometry.py
src/geometry.py
+9
-1
src/trackers/transform.py
src/trackers/transform.py
+43
-7
test.py
test.py
+3
-3
tests/draw.py
tests/draw.py
+2
-2
tests/parse_arguments.py
tests/parse_arguments.py
+2
-1
tests/testapp.py
tests/testapp.py
+269
-0
tests/utils.py
tests/utils.py
+35
-0
No files found.
src/__init__.py
View file @
c8706a27
from
logger
import
Logger
from
logger
import
Logger
from
tracker
import
GestureTracker
,
Gesture
from
drivers
import
create_driver
from
drivers
import
create_driver
from
tracker
import
GestureTracker
,
Gesture
from
geometry
import
Positionable
from
areas
import
*
from
areas
import
*
src/areas.py
View file @
c8706a27
...
@@ -31,21 +31,7 @@ class RectangularArea(Area):
...
@@ -31,21 +31,7 @@ class RectangularArea(Area):
and
self
.
y
<=
y
<=
self
.
y
+
self
.
height
and
self
.
y
<=
y
<=
self
.
y
+
self
.
height
def
contains_event
(
self
,
event
):
def
contains_event
(
self
,
event
):
if
self
.
contains
(
*
event
.
get_position
()):
return
self
.
contains
(
*
event
.
get_position
())
return
True
if
isinstance
(
event
,
PointMoveEvent
):
px
,
py
=
event
.
point
.
get_previous_position
()
if
self
.
parent
:
x
,
y
=
self
.
parent
.
get_screen_offset
()
else
:
x
=
y
=
0
if
self
.
contains
(
px
-
x
,
py
-
y
):
self
.
handle_event
(
PointUpEvent
(
event
.
point
))
return
False
class
CircularArea
(
Area
):
class
CircularArea
(
Area
):
...
...
src/geometry.py
View file @
c8706a27
from
__future__
import
division
from
__future__
import
division
from
math
import
atan2
,
pi
from
math
import
atan2
,
pi
,
sin
,
cos
import
time
import
time
...
@@ -60,6 +60,14 @@ class Positionable(object):
...
@@ -60,6 +60,14 @@ class Positionable(object):
def
translate
(
self
,
x
,
y
):
def
translate
(
self
,
x
,
y
):
self
.
set_position
(
self
.
x
+
x
,
self
.
y
+
y
)
self
.
set_position
(
self
.
x
+
x
,
self
.
y
+
y
)
def
scale
(
self
,
scale
):
self
.
set_position
(
self
.
x
*
scale
,
self
.
y
*
scale
)
def
rotate
(
self
,
angle
):
c
=
cos
(
angle
)
s
=
sin
(
angle
)
self
.
set_position
(
c
*
self
.
x
-
s
*
self
.
y
,
s
*
self
.
x
+
c
*
self
.
y
)
class
MovingPositionable
(
Positionable
):
class
MovingPositionable
(
Positionable
):
"""
"""
...
...
src/trackers/transform.py
View file @
c8706a27
...
@@ -21,7 +21,7 @@ class RotationGesture(Gesture, Positionable):
...
@@ -21,7 +21,7 @@ class RotationGesture(Gesture, Positionable):
%
(
self
.
x
,
self
.
y
,
self
.
angle
)
%
(
self
.
x
,
self
.
y
,
self
.
angle
)
def
get_angle
(
self
):
def
get_angle
(
self
):
return
self
.
angle
return
-
self
.
angle
class
PinchGesture
(
Gesture
,
Positionable
):
class
PinchGesture
(
Gesture
,
Positionable
):
...
@@ -84,6 +84,8 @@ class TransformationTracker(GestureTracker):
...
@@ -84,6 +84,8 @@ class TransformationTracker(GestureTracker):
# Current and previous centroid of all touch points
# Current and previous centroid of all touch points
self
.
centroid
=
None
self
.
centroid
=
None
self
.
deleted
=
[]
def
update_centroid
(
self
):
def
update_centroid
(
self
):
if
not
self
.
points
:
if
not
self
.
points
:
self
.
centroid
=
None
self
.
centroid
=
None
...
@@ -105,18 +107,28 @@ class TransformationTracker(GestureTracker):
...
@@ -105,18 +107,28 @@ class TransformationTracker(GestureTracker):
def
on_point_down
(
self
,
event
):
def
on_point_down
(
self
,
event
):
self
.
points
.
append
(
event
.
point
)
self
.
points
.
append
(
event
.
point
)
self
.
update_centroid
()
self
.
update_centroid
()
event
.
stop_propagation
()
def
on_point_move
(
self
,
event
):
def
on_point_move
(
self
,
event
):
point
=
event
.
point
point
=
event
.
point
if
point
not
in
self
.
points
:
if
point
not
in
self
.
points
:
return
pid
=
point
.
get_id
()
if
pid
not
in
self
.
deleted
:
return
self
.
debug
(
'recovered %s'
%
point
)
self
.
deleted
.
remove
(
pid
)
self
.
points
.
append
(
point
)
self
.
update_centroid
()
event
.
stop_propagation
()
self
.
invalidate_points
()
l
=
len
(
self
.
points
)
l
=
len
(
self
.
points
)
if
l
>
1
:
if
l
>
1
:
offset_centroid
=
self
.
centroid
-
self
.
area
.
get_screen_offset
()
offset_centroid
=
self
.
centroid
-
self
.
area
.
get_screen_offset
()
print
self
.
centroid
,
self
.
area
,
offset_centroid
# Rotation (around the previous centroid)
# Rotation (around the previous centroid)
rotation
=
point
.
rotation_around
(
self
.
centroid
)
/
l
rotation
=
point
.
rotation_around
(
self
.
centroid
)
/
l
...
@@ -137,8 +149,32 @@ class TransformationTracker(GestureTracker):
...
@@ -137,8 +149,32 @@ class TransformationTracker(GestureTracker):
self
.
centroid
.
translation
(),
l
))
self
.
centroid
.
translation
(),
l
))
def
on_point_up
(
self
,
event
):
def
on_point_up
(
self
,
event
):
if
event
.
point
not
in
self
.
points
:
if
event
.
point
in
self
.
points
:
return
self
.
points
.
remove
(
event
.
point
)
self
.
update_centroid
()
event
.
stop_propagation
()
def
invalidate_points
(
self
):
"""
Check if all points are still in the corresponding area, and remove
those which are not.
"""
delete
=
[]
if
self
.
area
.
parent
:
ox
,
oy
=
self
.
area
.
parent
.
get_screen_offset
()
else
:
ox
=
oy
=
0
self
.
points
.
remove
(
event
.
point
)
for
i
,
p
in
enumerate
(
self
.
points
):
self
.
update_centroid
()
x
,
y
=
p
.
get_position
()
if
not
self
.
area
.
contains
(
x
-
ox
,
y
-
oy
):
self
.
debug
(
'deleted %s'
%
p
)
delete
.
append
(
i
)
self
.
deleted
.
append
(
p
.
get_id
())
if
delete
:
self
.
points
=
[
p
for
i
,
p
in
enumerate
(
self
.
points
)
if
i
not
in
delete
]
self
.
update_centroid
()
test.py
View file @
c8706a27
#!/usr/bin/env python
#!/usr/bin/env python
import
os
import
os
src_path
=
os
.
path
.
realpath
(
'src'
)
import
sys
import
sys
sys
.
path
.
insert
(
0
,
src_path
)
sys
.
path
.
insert
(
0
,
os
.
path
.
realpath
(
'src'
))
sys
.
path
.
insert
(
0
,
os
.
path
.
realpath
(
'tests'
))
del
sys
.
argv
[
0
]
del
sys
.
argv
[
0
]
execfile
(
sys
.
argv
[
0
])
execfile
(
sys
.
argv
[
0
])
tests/draw.py
View file @
c8706a27
#!/usr/bin/env python
lambda
g
:
#!/usr/bin/env python
from
__future__
import
division
from
__future__
import
division
import
pygame
import
pygame
from
threading
import
Thread
from
threading
import
Thread
from
math
import
degrees
from
math
import
degrees
from
tests.parse_arguments
import
create_parser
,
parse_args
from
src
import
FullscreenArea
,
create_driver
from
src
import
FullscreenArea
,
create_driver
from
src.screen
import
screen_size
from
src.screen
import
screen_size
from
parse_arguments
import
create_parser
,
parse_args
# Parse arguments
# Parse arguments
parser
=
create_parser
()
parser
=
create_parser
()
...
...
tests/parse_arguments.py
View file @
c8706a27
import
argparse
import
argparse
import
logging
import
logging
from
src
.logger
import
Logger
from
src
import
Logger
# Parse arguments
# Parse arguments
...
@@ -18,6 +18,7 @@ def create_parser():
...
@@ -18,6 +18,7 @@ def create_parser():
def
parse_args
(
parser
):
def
parse_args
(
parser
):
print
'here:'
,
parser
.
format_usage
()
args
=
parser
.
parse_args
()
args
=
parser
.
parse_args
()
# Configure logger
# Configure logger
...
...
tests/
cairotest
.py
→
tests/
testapp
.py
View file @
c8706a27
#!/usr/bin/env python
#!/usr/bin/env python
from
__future__
import
division
from
__future__
import
division
import
gtk
import
gtk
import
cairo
from
math
import
radians
from
threading
import
Thread
from
threading
import
Thread
from
math
import
pi
,
tan
import
src
as
mt
import
src
as
mt
from
utils
import
BoundingBoxArea
RED
=
1
,
0
,
0
GREEN
=
0
,
1
,
0
BLUE
=
0
,
0
,
1
WHITE
=
1
,
1
,
1
BLACK
=
0
,
0
,
0
class
Rectangle
(
mt
.
RectangularArea
):
class
Rectangle
(
mt
.
RectangularArea
):
def
__init__
(
self
,
x
,
y
,
width
,
height
,
color
=
(
1
,
0
,
0
)):
def
__init__
(
self
,
x
,
y
,
width
,
height
,
color
=
(
1
,
0
,
0
)):
super
(
Rectangle
,
self
).
__init__
(
x
,
y
,
width
,
height
)
super
(
Rectangle
,
self
).
__init__
(
x
,
y
,
width
,
height
)
self
.
w
=
width
self
.
h
=
height
self
.
scale
=
1
self
.
angle
=
0
self
.
color
=
color
self
.
color
=
color
self
.
t
=
cairo
.
Matrix
()
self
.
on_drag
(
self
.
handle_drag
)
self
.
t
.
translate
(
x
,
y
)
self
.
on_drag
(
self
.
move
)
def
handle_drag
(
self
,
g
):
self
.
on_pinch
(
self
.
resize
)
tx
,
ty
=
g
.
get_translation
()
#self.on_rotate(self.rotate)
self
.
translate
(
tx
,
ty
)
def
move
(
self
,
g
):
print
'move:'
,
g
self
.
translate
(
*
g
.
get_translation
())
self
.
ttrans
(
*
g
.
get_translation
())
refresh
()
refresh
()
def
resize
(
self
,
g
):
def
draw
(
self
,
cr
):
print
'resize:'
,
g
cr
.
rectangle
(
self
.
x
,
self
.
y
,
self
.
width
,
self
.
height
)
cr
.
set_source_rgb
(
*
self
.
color
)
cr
.
fill
()
x
,
y
=
g
.
get_position
()
scale
=
g
.
get_scale
()
self
.
ttrans
(
x
,
y
)
self
.
tscale
(
scale
)
self
.
ttrans
(
-
x
,
-
y
)
self
.
translate
(
x
-
x
*
scale
,
y
-
y
*
scale
)
class
Polygon
(
BoundingBoxArea
):
def
__init__
(
self
,
x
,
y
,
points
,
color
=
BLUE
,
border_color
=
RED
):
super
(
Polygon
,
self
).
__init__
(
x
,
y
,
points
)
self
.
fill_color
=
color
self
.
border_color
=
border_color
self
.
width
*=
scale
self
.
on_drag
(
self
.
handle_drag
)
self
.
height
*=
scale
self
.
on_pinch
(
self
.
handle_pinch
)
self
.
on_rotate
(
self
.
handle_rotate
)
def
handle_drag
(
self
,
g
):
tx
,
ty
=
g
.
get_translation
()
self
.
translate
(
tx
,
ty
)
refresh
()
refresh
()
def
rotate
(
self
,
g
):
def
handle_pinch
(
self
,
g
):
print
'rotate:'
,
g
cx
,
cy
=
g
.
get_position
()
self
.
scale_points
(
g
.
get_scale
(),
cx
,
cy
)
x
,
y
=
g
.
get_position
()
self
.
update_bounds
()
self
.
ttrans
(
x
,
y
)
refresh
()
self
.
trot
(
-
g
.
get_angle
())
self
.
ttrans
(
-
x
,
-
y
)
def
handle_rotate
(
self
,
g
):
cx
,
cy
=
g
.
get_position
()
self
.
rotate_points
(
g
.
get_angle
(),
cx
,
cy
)
self
.
update_bounds
()
refresh
()
refresh
()
def
ttrans
(
self
,
tx
,
ty
):
def
draw
(
self
,
cr
):
t
=
cairo
.
Matrix
()
# Draw bounding box
t
.
translate
(
tx
,
ty
)
if
draw_bounding_boxes
:
self
.
t
=
t
*
self
.
t
cr
.
rectangle
(
self
.
x
,
self
.
y
,
self
.
width
,
self
.
height
)
cr
.
set_source_rgb
(
*
self
.
border_color
)
cr
.
set_line_width
(
3
)
cr
.
stroke
()
def
tscale
(
self
,
s
):
# Fill polygon
t
=
cairo
.
Matrix
()
cr
.
translate
(
self
.
x
,
self
.
y
)
t
.
scale
(
s
,
s
)
cr
.
new_path
()
self
.
t
=
t
*
self
.
t
def
trot
(
self
,
a
):
for
x
,
y
in
zip
(
*
self
.
points
):
t
=
cairo
.
Matrix
()
cr
.
line_to
(
x
,
y
)
t
.
rotate
(
a
)
self
.
t
=
t
*
self
.
t
def
draw
(
self
,
cr
):
cr
.
set_source_rgb
(
*
self
.
fill_color
)
cr
.
transform
(
self
.
t
)
cr
.
rectangle
(
0
,
0
,
self
.
w
,
self
.
h
)
cr
.
set_source_rgb
(
*
self
.
color
)
cr
.
fill
()
cr
.
fill
()
fullscreen
=
False
fullscreen
=
False
draw_bounding_boxes
=
draw_touch_points
=
True
W
,
H
=
mt
.
screen
.
screen_size
W
,
H
=
mt
.
screen
.
screen_size
...
@@ -92,10 +94,12 @@ def create_context_window(w, h, callback):
...
@@ -92,10 +94,12 @@ def create_context_window(w, h, callback):
"""Synchronize root multi-touch area with GTK window."""
"""Synchronize root multi-touch area with GTK window."""
root
.
set_position
(
*
event
.
get_coords
())
root
.
set_position
(
*
event
.
get_coords
())
root
.
set_size
(
event
.
width
,
event
.
height
)
root
.
set_size
(
event
.
width
,
event
.
height
)
overlay
.
set_size
(
event
.
width
,
event
.
height
)
draw
()
draw
()
def
handle_key
(
win
,
event
):
def
handle_key
(
win
,
event
):
"""Handle key event. 'f' toggles fullscreen, 'q' exits the program."""
"""Handle key event. 'f' toggles fullscreen, 'q' exits the program, 'b'
toggles bounding boxes, 'p' toggles touch points."""
if
event
.
keyval
>=
256
:
if
event
.
keyval
>=
256
:
return
return
...
@@ -107,10 +111,19 @@ def create_context_window(w, h, callback):
...
@@ -107,10 +111,19 @@ def create_context_window(w, h, callback):
fullscreen
=
not
fullscreen
fullscreen
=
not
fullscreen
elif
key
==
'q'
:
elif
key
==
'q'
:
quit
()
quit
()
elif
key
==
'b'
:
global
draw_bounding_boxes
draw_bounding_boxes
=
not
draw_bounding_boxes
refresh
()
elif
key
==
'p'
:
global
draw_touch_points
draw_touch_points
=
not
draw_touch_points
refresh
()
# Root area (will be synchronized with GTK window)
# Root area (will be synchronized with GTK window)
global
root
global
root
,
overlay
root
=
mt
.
RectangularArea
(
0
,
0
,
w
,
h
)
root
=
mt
.
RectangularArea
(
0
,
0
,
w
,
h
)
overlay
=
mt
.
RectangularArea
(
0
,
0
,
w
,
h
)
# GTK window
# GTK window
global
window
global
window
...
@@ -121,6 +134,9 @@ def create_context_window(w, h, callback):
...
@@ -121,6 +134,9 @@ def create_context_window(w, h, callback):
window
.
connect
(
'configure-event'
,
move_window
)
window
.
connect
(
'configure-event'
,
move_window
)
window
.
connect
(
'show'
,
callback
)
window
.
connect
(
'show'
,
callback
)
if
fullscreen
:
window
.
fullscreen
()
# Drawing area, needed by cairo context for drawing
# Drawing area, needed by cairo context for drawing
area
=
gtk
.
DrawingArea
()
area
=
gtk
.
DrawingArea
()
area
.
set_size_request
(
w
,
h
)
area
.
set_size_request
(
w
,
h
)
...
@@ -137,7 +153,7 @@ def draw(*args):
...
@@ -137,7 +153,7 @@ def draw(*args):
# Background
# Background
cr
.
rectangle
(
0
,
0
,
*
root
.
get_size
())
cr
.
rectangle
(
0
,
0
,
*
root
.
get_size
())
cr
.
set_source_rgb
(
0
,
1
,
0
)
cr
.
set_source_rgb
(
*
BLACK
)
cr
.
fill
()
cr
.
fill
()
# Drawable objects (use save and restore to allow transformations)
# Drawable objects (use save and restore to allow transformations)
...
@@ -146,6 +162,24 @@ def draw(*args):
...
@@ -146,6 +162,24 @@ def draw(*args):
obj
.
draw
(
cr
)
obj
.
draw
(
cr
)
cr
.
restore
()
cr
.
restore
()
if
draw_touch_points
:
ox
,
oy
=
root
.
get_position
()
cr
.
set_source_rgb
(
*
WHITE
)
for
x
,
y
in
touch_points
.
itervalues
():
x
-=
ox
y
-=
oy
cr
.
set_line_width
(
3
)
cr
.
arc
(
x
,
y
,
20
,
0
,
2
*
pi
)
cr
.
stroke
()
cr
.
set_line_width
(
1
)
cr
.
move_to
(
x
-
8
,
y
)
cr
.
line_to
(
x
+
8
,
y
)
cr
.
move_to
(
x
,
y
-
8
)
cr
.
line_to
(
x
,
y
+
8
)
cr
.
stroke
()
def
refresh
():
def
refresh
():
window
.
queue_draw
()
window
.
queue_draw
()
...
@@ -156,8 +190,13 @@ def quit(*args):
...
@@ -156,8 +190,13 @@ def quit(*args):
# Initialization
# Initialization
window
=
cr
=
root
=
None
window
=
cr
=
root
=
overlay
=
None
draw_objects
=
[]
draw_objects
=
[]
touch_points
=
{}
def
triangle_height
(
width
):
return
abs
(.
5
*
width
*
tan
(
2
/
3
*
pi
))
def
on_show
(
window
):
def
on_show
(
window
):
...
@@ -165,18 +204,53 @@ def on_show(window):
...
@@ -165,18 +204,53 @@ def on_show(window):
root
.
on_double_tap
(
root_dtap
)
root
.
on_double_tap
(
root_dtap
)
# Create blue rectangle
# Create blue rectangle
rect
=
Rectangle
(
300
,
200
,
250
,
150
,
color
=
(
0
,
0
,
1
))
x
,
y
,
w
,
h
=
0
,
0
,
250
,
150
rect
=
Polygon
(
x
,
y
,
[(
0
,
0
),
(
0
,
h
),
(
w
,
h
),
(
w
,
0
)])
draw_objects
.
append
(
rect
)
draw_objects
.
append
(
rect
)
root
.
add_area
(
rect
)
root
.
add_area
(
rect
)
def
rect_tap
(
g
):
print
'tapped on rectangle'
def
rect_tap
(
g
):
print
'tapped on rectangle'
rect
.
on_tap
(
rect_tap
,
propagate_up_event
=
False
)
rect
.
on_tap
(
rect_tap
,
propagate_up_event
=
False
)
# Create green triangle
x
,
y
,
w
=
400
,
400
,
200
h
=
triangle_height
(
w
)
triangle
=
Polygon
(
x
,
y
,
[(
0
,
h
),
(
w
,
h
),
(
w
/
2
,
0
)],
color
=
GREEN
)
draw_objects
.
append
(
triangle
)
root
.
add_area
(
triangle
)
# Overlay catches basic events
def
handle_down
(
gesture
):
point
=
gesture
.
get_event
().
get_touch_object
()
touch_points
[
point
.
get_id
()]
=
point
.
get_position
()
if
draw_touch_points
:
refresh
()
def
handle_up
(
gesture
):
point
=
gesture
.
get_event
().
get_touch_object
()
del
touch_points
[
point
.
get_id
()]
if
draw_touch_points
:
refresh
()
overlay
.
on_point_down
(
handle_down
)
overlay
.
on_point_move
(
handle_down
)
overlay
.
on_point_up
(
handle_up
)
root
.
add_area
(
overlay
)
if
__name__
==
'__main__'
:
if
__name__
==
'__main__'
:
from
parse_arguments
import
create_parser
,
parse_args
# Parse arguments
# Parse arguments
from
tests.parse_arguments
import
create_parser
,
parse_args
parser
=
create_parser
()
parse_args
(
create_parser
())
parser
.
add_argument
(
'-f'
,
'--fullscreen'
,
action
=
'store_true'
,
default
=
False
,
help
=
'run in fullscreen initially'
)
args
=
parse_args
(
parser
)
fullscreen
=
args
.
fullscreen
# Create a window with a Cairo context in it and a multi-touch area
# Create a window with a Cairo context in it and a multi-touch area
# syncronized with it
# syncronized with it
...
...
tests/utils.py
0 → 100644
View file @
c8706a27
from
numpy
import
array
,
diag
,
dot
,
cos
,
sin
from
src
import
RectangularArea
class
BoundingBoxArea
(
RectangularArea
):
def
__init__
(
self
,
x
,
y
,
points
):
super
(
BoundingBoxArea
,
self
).
__init__
(
x
,
y
,
0
,
0
)
self
.
points
=
array
(
points
).
T
self
.
update_bounds
()
def
translate_points
(
self
,
tx
,
ty
):
self
.
points
+=
[[
tx
],
[
ty
]]
def
scale_points
(
self
,
scale
,
cx
,
cy
):
self
.
translate_points
(
-
cx
,
-
cy
)
self
.
points
=
dot
(
diag
([
scale
,
scale
]),
self
.
points
)
self
.
translate_points
(
cx
,
cy
)
def
rotate_points
(
self
,
angle
,
cx
,
cy
):
cosa
=
cos
(
angle
)
sina
=
sin
(
angle
)
mat
=
array
([[
cosa
,
-
sina
],
[
sina
,
cosa
]])
self
.
translate_points
(
-
cx
,
-
cy
)
self
.
points
=
dot
(
mat
,
self
.
points
)
self
.
translate_points
(
cx
,
cy
)
def
update_bounds
(
self
):
min_x
,
min_y
=
self
.
points
.
min
(
1
)
max_x
,
max_y
=
self
.
points
.
max
(
1
)
self
.
set_size
(
max_x
-
min_x
,
max_y
-
min_y
)
if
min_x
or
min_y
:
self
.
translate
(
min_x
,
min_y
)
self
.
translate_points
(
-
min_x
,
-
min_y
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment