source: tools/area_editor.py @ 187:42a10c4f70c8

Last change on this file since 187:42a10c4f70c8 was 187:42a10c4f70c8, checked in by Stefano Rivera <stefano@…>, 7 years ago

Err, in pymunk coordinates

  • Property exe set to *
File size: 12.7 KB
Line 
1#!/usr/bin/env python
2
3# The basic area editor
4#
5# To edit an existing level, use
6# editor levelname
7#
8# To create a new level:
9#
10# editor levelname <xsize> <ysiz>
11# (size specified in pixels
12#
13
14import os
15import sys
16
17import pygame
18import pygame.locals as pgl
19
20sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
21
22import pymunk
23
24from albow.root import RootWidget
25from albow.widget import Widget
26from albow.controls import Button
27from albow.dialogs import alert
28
29from nagslang.constants import SCREEN
30from nagslang.level import Level, POLY_COLORS
31
32
33# layout constants
34MENU_BUTTON_HEIGHT = 35
35MENU_PAD = 6
36MENU_HALF_PAD = MENU_PAD // 2
37MENU_LEFT = SCREEN[0] + MENU_HALF_PAD
38MENU_WIDTH = 200 - MENU_PAD
39
40
41class EditorLevel(Level):
42
43    def __init__(self, name, x=800, y=600):
44        super(EditorLevel, self).__init__(name)
45        self.x = x
46        self.y = y
47
48    def round_point(self, pos):
49        return (10 * (pos[0] // 10), 10 * (pos[1] // 10))
50
51    def point_to_pymunk(self, pos):
52        # inverse of point_to_pygame
53        # (this is also the same as point_to_pygame, but a additional
54        # function for sanity later in pyweek).
55        return (pos[0], self.y - pos[1])
56
57    def add_point(self, poly_index, pos):
58        self.polygons.setdefault(poly_index, [])
59        if not self.polygons[poly_index]:
60            point = self.point_to_pymunk(self.round_point(pos))
61            self.polygons[poly_index].append(point)
62        else:
63            add_pos = self.fix_angle(poly_index, pos)
64            self.polygons[poly_index].append(add_pos)
65
66    def fix_angle(self, index, pos):
67        # Last point
68        point1 = self.point_to_pygame(self.polygons[index][-1])
69        pos = self.round_point(pos)
70        # We want the line (point1 to pos) to be an angle of
71        # 0, 45, 90, 135, 180, 225, 270, 305
72        # However, we only need to consider half the circle
73        # This is a hack to approximate the right thing
74        pos0 = (pos[0], point1[1])
75        pos90 = (point1[0], pos[1])
76        dist = max(abs(point1[0] - pos[0]), abs(point1[1] - pos[1]))
77        pos45 = (point1[0] + dist, point1[1] + dist)
78        pos135 = (point1[0] + dist, point1[1] - dist)
79        pos225 = (point1[0] - dist, point1[1] - dist)
80        pos305 = (point1[0] - dist, point1[1] + dist)
81        min_dist = 9999999
82        new_pos = point1
83        for cand in [pos0, pos90, pos45, pos135, pos225, pos305]:
84            dist = (pos[0] - cand[0]) ** 2 + (pos[1] - cand[1]) ** 2
85            if dist < min_dist:
86                new_pos = cand
87                min_dist = dist
88        return self.point_to_pymunk(new_pos)
89
90    def delete_point(self, index):
91        if index in self.polygons and len(self.polygons[index]) > 0:
92            self.polygons[index].pop()
93
94    def close_poly(self, index):
95        """Attempts to close the current polygon.
96
97           We allow a small additional step to close the polygon, but
98           it's limited as it's a magic point addition"""
99        if len(self.polygons[index]) < 2:
100            # Too small
101            return False
102        first = self.polygons[index][0]
103        if self.fix_angle(index, self.point_to_pygame(first)) == first:
104            self.add_point(index, self.point_to_pygame(first))
105            return True
106        candidates = [(first[0] + 10 * i, first[1]) for
107                      i in (-3, -2, -1, 1, 2, 3)]
108        candidates.extend([(first[0], first[1] + 10 * i) for
109                          i in (-3, -2, -1, 1, 2, 3)])
110        candidates.extend([(first[0] + 10 * i, first[1] + 10 * i) for
111                          i in (-3, -2, -1, 1, 2, 3)])
112        candidates.extend([(first[0] + 10 * i, first[1] - 10 * i) for
113                          i in (-3, -2, -1, 1, 2, 3)])
114        min_dist = 99999
115        poss = None
116        for cand in candidates:
117            if self.fix_angle(index, self.point_to_pygame(cand)) == cand:
118                dist = (first[0] - cand[0]) ** 2 + (first[1] - cand[1]) ** 2
119                if dist < min_dist:
120                    poss = cand
121        if poss is not None:
122            self.add_point(index, self.point_to_pygame(poss))
123            self.add_point(index, self.point_to_pygame(first))
124            return True
125        return False
126
127    def draw(self, surface, topleft, mouse_pos, mouse_poly, filled):
128        self._draw_background(True)
129        # Draw polygons as needed for the editor
130        if filled:
131            self._draw_exterior(True)
132        for index, polygon in self.polygons.items():
133            color = POLY_COLORS[index]
134            if len(polygon) > 1:
135                pointlist = [self.point_to_pygame(p) for p in polygon]
136                pygame.draw.lines(self._surface, color, False, pointlist, 2)
137            if index == mouse_poly and mouse_pos:
138                endpoint = self.fix_angle(index, mouse_pos)
139                pygame.draw.line(self._surface, color,
140                                 self.point_to_pygame(polygon[-1]),
141                                 self.point_to_pygame(endpoint))
142        surface_area = pygame.rect.Rect(topleft, SCREEN)
143        surface.blit(self._surface, (0, 0), surface_area)
144
145
146class LevelWidget(Widget):
147
148    def __init__(self, level):
149        super(LevelWidget, self).__init__(pygame.rect.Rect(0, 0,
150                                          SCREEN[0], SCREEN[1]))
151        self.level = level
152        self.pos = (0, 0)
153        self.filled_mode = False
154        self.mouse_pos = None
155        self.cur_poly = None
156        self._mouse_drag = False
157
158    def _level_coordinates(self, pos):
159        # Move positions to level values
160        if not pos:
161            return (0, 0)
162        return pos[0] + self.pos[0], pos[1] + self.pos[1]
163
164    def _move_view(self, offset):
165        new_pos = [self.pos[0] + offset[0], self.pos[1] + offset[1]]
166        if new_pos[0] < 0:
167            new_pos[0] = self.pos[0]
168        elif new_pos[0] > self.level.x - SCREEN[0]:
169            new_pos[0] = self.pos[0]
170        if new_pos[1] < 0:
171            new_pos[1] = self.pos[1]
172        elif new_pos[1] > self.level.y - SCREEN[1]:
173            new_pos[1] = self.pos[1]
174        self.pos = tuple(new_pos)
175
176    def draw(self, surface):
177        if (self.cur_poly is not None and self.cur_poly in self.level.polygons
178                and len(self.level.polygons[self.cur_poly])):
179            # We have an active polygon
180            mouse_pos = self._level_coordinates(self.mouse_pos)
181        else:
182            mouse_pos = None
183        level.draw(surface, self.pos, mouse_pos, self.cur_poly,
184                   self.filled_mode)
185
186    def change_poly(self, new_poly):
187        self.cur_poly = new_poly
188        if self.cur_poly is not None:
189            self.filled_mode = False
190
191    def key_down(self, ev):
192        if ev.key == pgl.K_LEFT:
193            self._move_view((-10, 0))
194        elif ev.key == pgl.K_RIGHT:
195            self._move_view((10, 0))
196        elif ev.key == pgl.K_UP:
197            self._move_view((0, -10))
198        elif ev.key == pgl.K_DOWN:
199            self._move_view((0, 10))
200        elif ev.key in (pgl.K_1, pgl.K_2, pgl.K_3, pgl.K_4, pgl.K_5, pgl.K_6):
201            self.change_poly(ev.key - pgl.K_0)
202        elif ev.key == pgl.K_0:
203            self.change_poly(None)
204        elif ev.key == pgl.K_d and self.cur_poly:
205            self.level.delete_point(self.cur_poly)
206        elif ev.key == pgl.K_f:
207            self.set_filled()
208        elif ev.key == pgl.K_c:
209            self.close_poly()
210
211    def set_filled(self):
212        closed, _ = self.level.all_closed()
213        if closed:
214            self.cur_poly = None
215            self.filled_mode = True
216        else:
217            alert('Not all polygons closed, so not filling')
218
219    def mouse_move(self, ev):
220        old_pos = self.mouse_pos
221        self.mouse_pos = ev.pos
222        if self.cur_poly and old_pos != self.mouse_pos:
223            self.invalidate()
224
225    def mouse_drag(self, ev):
226        if self._mouse_drag:
227            old_pos = self.mouse_pos
228            self.mouse_pos = ev.pos
229            diff = (-self.mouse_pos[0] + old_pos[0],
230                    -self.mouse_pos[1] + old_pos[1])
231            self._move_view(diff)
232            self.invalidate()
233
234    def mouse_down(self, ev):
235        if ev.button == 1:
236            print "Click: %r" % (
237                self.level.point_to_pymunk(self._level_coordinates(ev.pos)),)
238        if ev.button == 4:  # Scroll up
239            self._move_view((0, -10))
240        elif ev.button == 5:  # Scroll down
241            self._move_view((0, 10))
242        elif ev.button == 6:  # Scroll left
243            self._move_view((-10, 0))
244        elif ev.button == 7:  # Scroll right
245            self._move_view((10, 0))
246        elif self.cur_poly and ev.button == 1:
247            # Add a point
248            self.level.add_point(self.cur_poly,
249                                 self._level_coordinates(ev.pos))
250        elif ev.button == 3:
251            self._mouse_drag = True
252
253    def mouse_up(self, ev):
254        if ev.button == 3:
255            self._mouse_drag = False
256
257    def close_poly(self):
258        if self.cur_poly is None:
259            return
260        if self.level.close_poly(self.cur_poly):
261            alert("Successfully closed the polygon")
262            self.change_poly(None)
263        else:
264            alert("Failed to close the polygon")
265
266
267class PolyButton(Button):
268    """Button for coosing the correct polygon"""
269
270    def __init__(self, index, level_widget):
271        if index is not None:
272            text = "Draw: %s" % index
273        else:
274            text = 'Exit Draw Mode'
275        super(PolyButton, self).__init__(text)
276        self.index = index
277        self.level_widget = level_widget
278
279    def action(self):
280        self.level_widget.change_poly(self.index)
281
282
283class EditorApp(RootWidget):
284
285    def __init__(self, level, surface):
286        super(EditorApp, self).__init__(surface)
287        self.level = level
288        self.level_widget = LevelWidget(self.level)
289        self.add(self.level_widget)
290
291        # Add poly buttons
292        y = 15
293        for poly in range(1, 7):
294            but = PolyButton(poly, self.level_widget)
295            but.rect = pygame.rect.Rect(0, 0, MENU_WIDTH // 2 - MENU_PAD,
296                                        MENU_BUTTON_HEIGHT)
297            if poly % 2:
298                but.rect.move_ip(MENU_LEFT, y)
299            else:
300                but.rect.move_ip(MENU_LEFT + MENU_WIDTH // 2 - MENU_HALF_PAD,
301                                 y)
302                y += MENU_BUTTON_HEIGHT + MENU_PAD
303            self.add(but)
304
305        button_rect = pygame.rect.Rect(0, 0, MENU_WIDTH, MENU_BUTTON_HEIGHT)
306
307        end_poly_but = PolyButton(None, self.level_widget)
308        end_poly_but.rect = button_rect.copy()
309        end_poly_but.rect.move_ip(MENU_LEFT, y)
310        self.add(end_poly_but)
311        y += MENU_BUTTON_HEIGHT + MENU_PAD
312
313        fill_but = Button('Fill exterior', action=self.level_widget.set_filled)
314        fill_but.rect = button_rect.copy()
315        fill_but.rect.move_ip(MENU_LEFT, y)
316        self.add(fill_but)
317        y += MENU_BUTTON_HEIGHT + MENU_PAD
318
319        save_but = Button('Save Level', action=self.save)
320        save_but.rect = button_rect.copy()
321        save_but.rect.move_ip(MENU_LEFT, y)
322        self.add(save_but)
323        y += MENU_BUTTON_HEIGHT + MENU_PAD
324
325        close_poly_but = Button('Close Polygon',
326                                action=self.level_widget.close_poly)
327        close_poly_but.rect = button_rect.copy()
328        close_poly_but.rect.move_ip(MENU_LEFT, y)
329        self.add(close_poly_but)
330        y += MENU_BUTTON_HEIGHT + MENU_PAD
331
332        quit_but = Button('Quit', action=self.quit)
333        quit_but.rect = button_rect.copy()
334        quit_but.rect.move_ip(MENU_LEFT, y)
335        self.add(quit_but)
336
337    def key_down(self, ev):
338        if ev.key == pgl.K_ESCAPE:
339            self.quit()
340        elif ev.key == pgl.K_s:
341            self.save()
342        else:
343            self.level_widget.key_down(ev)
344
345    def save(self):
346        closed, messages = self.level.all_closed()
347        if closed:
348            self.level.save()
349            # display success
350            alert("Level %s saved successfully." % self.level.name)
351        else:
352            # display errors
353            alert("Failed to save level.\n\n%s" % '\n'.join(messages))
354
355    def mouse_move(self, ev):
356        self.level_widget.mouse_move(ev)
357
358
359if __name__ == "__main__":
360    if len(sys.argv) == 2:
361        level = EditorLevel(sys.argv[1])
362        level.load(pymunk.Space())
363    elif len(sys.argv) == 4:
364        level = EditorLevel(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))
365    else:
366        print 'Please supply a levelname or levelname and level size'
367        sys.exit()
368    pygame.display.init()
369    pygame.font.init()
370    pygame.display.set_mode((SCREEN[0] + MENU_WIDTH, SCREEN[1]),
371                            pgl.SWSURFACE)
372    pygame.display.set_caption('Nagslang Area Editor')
373    pygame.key.set_repeat(200, 100)
374    app = EditorApp(level, pygame.display.get_surface())
375    app.run()
Note: See TracBrowser for help on using the repository browser.