Beispiel #1
0
class LineEffects():
    def __init__(self):
        self.linesegs = LineSegs("lines")
        self.bullet = None

    def remove_bullet(self):
        if self.bullet:
            self.bullet.detach_node()

    def draw_bullet(self, a, b, color):
        if color == 1:
            color = (1,0,1,1)
        elif color == 2:
            color = (0,1,1,1)
        else:
            color = (1,1,1,1)
        self.linesegs.set_color(color)
        a = a.get_pos(render)
        self.linesegs.move_to(a)
        self.linesegs.draw_to(b)
        lines = self.linesegs.create()
        self.bullet = render.attach_new_node(lines)
        impact = base.icons["impact"]
        impact = impact.copy_to(self.bullet)
        impact.set_pos(b)
Beispiel #2
0
 def move_map_avatar(self, move, stop):
     # print move
     # avatar is mapped assuming c_range of 0.5. What do I need to
     # change to use a different c_range? c_range of one is twice
     # the
     if move:
         avt = LineSegs()
         avt.setThickness(1)
         avt.setColor(1, 1, 1)
         # print 'last', self.last_avt
         avt.move_to(self.last_avt[0], -5, self.last_avt[1])
         # print 'move', move
         new_move = [i + (j * self.avt_factor) for i, j in zip(self.last_avt, move)]
         # new_move = [i + j for i, j in zip(self.last_avt, move)]
         # would it be better to have a local stop condition?
         if stop[0]:
             new_move[0] = self.last_avt[0]
             # print 'stop x', self.last_avt[0]
         if stop[1]:
             new_move[1] = self.last_avt[1]
             # print 'stop y', self.last_avt[1]
         # print 'new', new_move
         self.last_avt = [new_move[0], new_move[1]]
         avt.draw_to(new_move[0], -5, new_move[1])
         self.map_avt_node.append(self.render2d.attach_new_node(avt.create()))
         # print self.map_avt_node[-1]
         # can't let too many nodes pile up
         if len(self.map_avt_node) > 299:
             # removing the node does not remove the object from the list
             for i, j in enumerate(self.map_avt_node):
                 j.removeNode()
                 if i > 49:
                     break
             del self.map_avt_node[0:50]
def drawHilbertCurve(pts):  # 2n + c
    for p in range(len(pts) - 1):  # n
        seg = LineSegs()  # c
        seg.setThickness(3)  # c
        seg.draw_to(pts[p][0], 0, pts[p][2])  # c
        seg.draw_to(pts[p + 1][0], 0, pts[p + 1][2])  # c
        node = seg.create()  # c
        nodes.append(node)  # c

    for node in nodes:  # n
        base.aspect2d.attach_new_node(node)  # c
    def __init__(self,
                 name: str = 'cube_mesh',
                 wireframe_thickness: float = 5) -> None:
        self.name = name

        self.__vertex_data_format = GeomVertexFormat.getV3n3()
        self.__vertex_data = GeomVertexData(name, self.__vertex_data_format,
                                            Geom.UHStatic)

        self.geom = Geom(self.__vertex_data)
        self.__triangles = GeomTriangles(Geom.UHStatic)
        self.__triangle_data = self.__triangles.modifyVertices()

        self.__vertex = GeomVertexWriter(self.__vertex_data, 'vertex')
        self.__normal = GeomVertexWriter(self.__vertex_data, 'normal')

        self.__face_count = 0

        def add_face(face: Face) -> None:
            self.__make_face(face)

        self.__make_face(Face.LEFT)
        self.__make_face(Face.RIGHT)
        self.__make_face(Face.BACK)
        self.__make_face(Face.FRONT)
        self.__make_face(Face.BOTTOM)
        self.__make_face(Face.TOP)

        self.__triangles.close_primitive()
        self.geom.add_primitive(self.__triangles)

        def is_connected(x, y, z, x1, y1, z1):
            return (abs(x - x1) == 1 and abs(y - y1) != 1 and abs(z - z1) != 1) or \
                   (abs(x - x1) != 1 and abs(y - y1) == 1 and abs(z - z1) != 1) or \
                   (abs(x - x1) != 1 and abs(y - y1) != 1 and abs(z - z1) == 1)

        ls = LineSegs()
        ls.set_thickness(wireframe_thickness)
        arr_x = [0, 0, 0, 0, 1, 1, 1, 1]
        arr_y = [0, 0, 1, 1, 1, 1, 0, 0]
        arr_z = [0, -1, -1, 0, 0, -1, -1, 0]
        for pos1 in range(len(arr_x) - 1):
            for pos2 in range(pos1, len(arr_x)):
                x = arr_x[pos1]
                y = arr_y[pos1]
                z = arr_z[pos1]
                x1 = arr_x[pos2]
                y1 = arr_y[pos2]
                z1 = arr_z[pos2]
                if (is_connected(x, y, z, x1, y1, z1)):
                    ls.move_to(x, y, z)
                    ls.draw_to(x1, y1, z1)
        self.__wireframe_node = ls.create()
Beispiel #5
0
 def plot_match_square(self, corners):
     print 'plot match square'
     print corners
     match = LineSegs()
     match.setThickness(1.5)
     match.setColor(0, 0, 0)
     match.move_to(corners[0][0], -5, corners[1][0])
     match.draw_to(corners[0][1], -5, corners[1][0])
     match.draw_to(corners[0][1], -5, corners[1][1])
     match.draw_to(corners[0][0], -5, corners[1][1])
     match.draw_to(corners[0][0], -5, corners[1][0])
     # print self.render2d
     self.match_square = self.render2d.attach_new_node(match.create())
Beispiel #6
0
 def plot_match_square(self, corners):
     print 'plot match square'
     print corners
     match = LineSegs()
     match.setThickness(1.5)
     match.setColor(0, 0, 0)
     match.move_to(corners[0][0], -5, corners[1][0])
     match.draw_to(corners[0][1], -5, corners[1][0])
     match.draw_to(corners[0][1], -5, corners[1][1])
     match.draw_to(corners[0][0], -5, corners[1][1])
     match.draw_to(corners[0][0], -5, corners[1][0])
     # print self.render2d
     self.match_square = self.render2d.attach_new_node(match.create())
Beispiel #7
0
def procedural_sight(line_seg, lower_level, engaged):
    sight_width = 0.25
    sight_tick = 0.1
    x0 = -sight_width / 2
    z0 = sight_tick
    sight_lower = -0.23
    sight_upper = 0.23

    if line_seg is None:
        line_seg = LineSegs()

    if lower_level:
        sight_level = sight_lower
        m = 1
    else:
        sight_level = sight_upper
        m = -1
    if engaged:
        x_eng = 0.07
        z_eng = 0.07
    else:
        x_eng = 0
        z_eng = 0

    line_seg.moveTo(x0 + x_eng, 0, m * z0 + sight_level + m * z_eng)
    line_seg.draw_to(x0, 0, sight_level)
    line_seg.draw_to(x0 + sight_width, 0, 0 + sight_level)
    line_seg.draw_to(x0 + sight_width - x_eng, 0,
                     m * z0 + sight_level + m * z_eng)
    # outer - central lines
    line_seg.moveTo(0, 0, 0 + sight_level)
    line_seg.draw_to(0, 0, 0 + sight_level - m * 0.25)

    return line_seg
Beispiel #8
0
 def draw_multiselect_box(self, task):
     if base.mouseWatcherNode.isButtonDown(MouseButton.one()):
         self.multi_select = True
         self.select_box.remove()
         ls = LineSegs()
         ls.move_to(self.box_x, self.box_y, 1)
         ls.draw_to(self.model.getX(), self.box_y, 1)
         ls.draw_to(self.model.getX(), self.model.getY(), 1)
         ls.draw_to(self.box_x, self.model.getY(), 1)
         ls.draw_to(self.box_x, self.box_y, 1)
         node = ls.create()
         #text = TextNode('text')
         #text.setText(str(self.box_x)+","+str(self.box_y)+"\n"+str(self.model.getX())+","+str(self.model.getY()))
         #textnp = NodePath(text)
         #textnp.setPos(self.box_x,self.box_y,1)
         #textnp.setHpr(0,-90,0)
         #textnp.setScale(20.0)
         self.select_box = NodePath(node)
         #textnp.reparentTo(self.select_box)
         self.select_box.reparentTo(render)
         return task.cont
     else:
         self.select_box.hide()
         taskMgr.add(self.task_select_check, "updatePicker")
         return task.done
def draw_grid(x_size, y_size, s):
    lines = LineSegs()
    lines.set_color((0, 0, 0, 1))
    offset = s / 2
    for x in range(x_size):
        x = (x * s) - offset
        lines.move_to(x, -offset, 0)
        lines.draw_to(x, (y_size * s) - offset, 0)
    for y in range(y_size):
        y = (y * s) - offset
        lines.move_to(-offset, y, 0)
        lines.draw_to((x_size * s) - offset, y, 0)
    grid = NodePath(lines.create())
    return grid
def linesegs_sample(x_segs, y_segs, wrap_x=False, wrap_y=False, **columns):
    segs = LineSegs()
    for x in range(x_segs + 1):
        for y in range(y_segs + 1):
            x_e, y_e = x, y
            if wrap_x and x == x_segs:
                x_e = 0
            if wrap_y and y == y_segs:
                y_e = 0
            values = {
                name: f(
                    float(x_e) / float(x_segs),
                    float(y_e) / float(y_segs),
                )
                for name, (dtype, f) in columns.items()
            }
            if y == 0:
                segs.set_color(values['color'])
                segs.move_to(values['vertex'])
            else:
                segs.set_color(values['color'])
                segs.draw_to(values['vertex'])

    for y in range(y_segs + 1):
        for x in range(x_segs + 1):
            x_e, y_e = x, y
            if wrap_x and x == x_segs:
                x_e = 0
            if wrap_y and y == y_segs:
                y_e = 0
            values = {
                name: f(
                    float(x_e) / float(x_segs),
                    float(y_e) / float(y_segs),
                )
                for name, (dtype, f) in columns.items()
            }
            if x == 0:
                segs.set_color(values['color'])
                segs.move_to(values['vertex'])
            else:
                segs.set_color(values['color'])
                segs.draw_to(values['vertex'])

    return segs.create()
Beispiel #11
0
 def plot_border(self):
     border = LineSegs()
     border.setThickness(2.0)
     corner = self.win_size/100 * 5/6
     #print('corner', corner)
     border.move_to(corner, 25, corner)
     border.draw_to(corner, 25, -corner)
     border.draw_to(-corner, 25, -corner)
     border.draw_to(-corner, 25, corner)
     border.draw_to(corner, 25, corner)
     self.base.render.attach_new_node(border.create(True))
Beispiel #12
0
    def draw_path(self, current_position, path):
        from panda3d.core import LineSegs, Vec4, Vec3
        path = [Vec3(*v) for v in path]

        segments = LineSegs()
        segments.set_thickness(2.0)
        segments.set_color((1, 1, 0, 1))
        segments.move_to(current_position)

        for point in path:
            segments.draw_to(point)

        if self._path_node:
            self._path_node.remove_node()

        node = segments.create()
        self._path_node = render.attach_new_node(node)
        self._replan_timer = Timer(1.5)
        self._replan_timer.on_target = self._replan
Beispiel #13
0
 def task_mouse_place(self,task):
     if base.mouseWatcherNode.isButtonDown(MouseButton.one()):
         self.placing_object = True
         self.place_pos = (self.anchor_x,self.anchor_y)
         self.line_dir.remove()
         ls = LineSegs()
         ls.move_to(self.anchor_x,self.anchor_y,1)
         ls.draw_to(self.model.getX(),self.model.getY(),1)
         node = ls.create()
         angle1 = math.atan2(self.anchor_y - self.anchor_y,self.anchor_x - self.anchor_x+50)
         angle2 = math.atan2(self.anchor_y - self.model.getY(),self.anchor_x - self.model.getY());
         final_angle = angle1-angle2;
         self.model.setHpr(final_angle,0,0)
         self.line_dir = NodePath(node)
         self.line_dir.reparentTo(render)
         return task.again
     else:
         self.line_dir.hide()
         taskMgr.add(self.task_mouse_press_check, "checkMousePress")
         return task.done
def draw_triangle(triangle, color):
    point1 = triangle[0]
    point2 = triangle[1]
    point3 = triangle[2]

    seg1 = LineSegs()
    seg1.setColor(color[0], color[1], color[2], 1)
    seg1.setThickness(3)
    seg1.draw_to(point1[0], 0, point1[1])  # x, z, y
    seg1.draw_to(point2[0], 0, point2[1])  # x, z, y
    node1 = seg1.create()
    nodes.append(node1)

    seg2 = LineSegs()
    seg2.setColor(color[0], color[1], color[2], 1)
    seg2.setThickness(3)
    seg2.draw_to(point2[0], 0, point2[1])  # x, z, y
    seg2.draw_to(point3[0], 0, point3[1])  # x, z, y
    node2 = seg2.create()
    nodes.append(node2)

    seg3 = LineSegs()
    seg3.setColor(color[0], color[1], color[2], 1)
    seg3.setThickness(3)
    seg3.draw_to(point3[0], 0, point3[1])  # x, z, y
    seg3.draw_to(point1[0], 0, point1[1])  # x, z, y
    node3 = seg3.create()
    nodes.append(node3)
Beispiel #15
0
 def task_mouse_place(self, task):
     if base.mouseWatcherNode.isButtonDown(MouseButton.one()):
         self.placing_object = True
         self.place_pos = (self.anchor_x, self.anchor_y)
         self.line_dir.remove()
         ls = LineSegs()
         ls.move_to(self.anchor_x, self.anchor_y, 1)
         ls.draw_to(self.model.getX(), self.model.getY(), 1)
         node = ls.create()
         angle1 = math.atan2(self.anchor_y - self.anchor_y,
                             self.anchor_x - self.anchor_x + 50)
         angle2 = math.atan2(self.anchor_y - self.model.getY(),
                             self.anchor_x - self.model.getY())
         final_angle = angle1 - angle2
         self.model.setHpr(final_angle, 0, 0)
         self.line_dir = NodePath(node)
         self.line_dir.reparentTo(render)
         return task.again
     else:
         self.line_dir.hide()
         taskMgr.add(self.task_mouse_press_check, "checkMousePress")
         return task.done
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args,
                         **exclude_from_dict(kwargs, ["wireframe_thickness"]))
        wireframe_thickness: float = kwargs[
            "wireframe_thickness"] if "wireframe_thickness" in kwargs else 5

        def is_connected(x, y, z, x1, y1, z1):
            return (abs(x - x1) == 1 and abs(y - y1) != 1 and abs(z - z1) != 1) or \
                   (abs(x - x1) != 1 and abs(y - y1) == 1 and abs(z - z1) != 1) or \
                   (abs(x - x1) != 1 and abs(y - y1) != 1 and abs(z - z1) == 1)

        ls = LineSegs()
        ls.set_thickness(wireframe_thickness)
        for i, j, k in np.ndindex(self.structure.shape):
            if bool(self.structure[i, j, k] & self.mask):
                self.arr_x = [0, 0, 0, 0, 1, 1, 1, 1]
                self.arr_y = [0, 0, 1, 1, 1, 1, 0, 0]
                self.arr_z = [0, -1, -1, 0, 0, -1, -1, 0]
                for pos1 in range(len(self.arr_x) - 1):
                    for pos2 in range(pos1, len(self.arr_x)):
                        x = self.arr_x[pos1] + i
                        y = self.arr_y[pos1] + j
                        z = self.arr_z[pos1] + k
                        x1 = self.arr_x[pos2] + i
                        y1 = self.arr_y[pos2] + j
                        z1 = self.arr_z[pos2] + k
                        if (is_connected(x, y, z, x1, y1, z1)):
                            ls.move_to(x, y, z)
                            ls.draw_to(x1, y1, z1)
        self.wireframe.attach_new_node(ls.create())

        for idx in np.ndindex(self.structure.shape):
            if bool(self.structure[idx] & self.mask):
                self._add_cube_faces(idx)

        self._triangles.close_primitive()
        self.mesh.add_primitive(self._triangles)
Beispiel #17
0
 def move_map_avatar(self, move, stop):
     # print move
     # avatar is mapped assuming c_range of 0.5. What do I need to
     # change to use a different c_range? c_range of one is twice
     # the
     if move:
         avt = LineSegs()
         avt.setThickness(1)
         avt.setColor(1, 1, 1)
         # print 'last', self.last_avt
         avt.move_to(self.last_avt[0], -5, self.last_avt[1])
         # print 'move', move
         new_move = [
             i + (j * self.avt_factor) for i, j in zip(self.last_avt, move)
         ]
         # new_move = [i + j for i, j in zip(self.last_avt, move)]
         # would it be better to have a local stop condition?
         if stop[0]:
             new_move[0] = self.last_avt[0]
             # print 'stop x', self.last_avt[0]
         if stop[1]:
             new_move[1] = self.last_avt[1]
             # print 'stop y', self.last_avt[1]
         # print 'new', new_move
         self.last_avt = [new_move[0], new_move[1]]
         avt.draw_to(new_move[0], -5, new_move[1])
         self.map_avt_node.append(
             self.render2d.attach_new_node(avt.create()))
         # print self.map_avt_node[-1]
         # can't let too many nodes pile up
         if len(self.map_avt_node) > 299:
             # removing the node does not remove the object from the list
             for i, j in enumerate(self.map_avt_node):
                 j.removeNode()
                 if i > 49:
                     break
             del self.map_avt_node[0:50]
Beispiel #18
0
def procedural_grid(x_min, x_max, y_min, y_max, n):
    del_x = (x_max - x_min) / n
    del_y = (y_max - y_min) / n

    lines = LineSegs()
    # constant y lines
    x0 = x_min
    x1 = x_max
    y0 = y_min
    for i in range(0, n + 1):
        lines.moveTo(x0, y0, 0.1)
        lines.draw_to(x1, y0, 0.1)
        y0 += del_y

    # constant x lines
    y0 = y_min
    y1 = y_max
    x0 = x_min
    for i in range(0, n + 1):
        lines.moveTo(x0, y0, 0.1)
        lines.draw_to(x0, y1, 0.1)
        x0 += del_x

    return lines
Beispiel #19
0
class WartsApp(ShowBase):
    """
    The application running all the graphics.
    """

    def __init__(self, graphicsInterface):
        ShowBase.__init__(self)

        # This is available as a global, but pylint gives an undefined-variable
        # warning if we use it that way. Looking at
        #     https://www.panda3d.org/manual/index.php/ShowBase
        # I would have thought we could reference it as either
        # self.globalClock, direct.showbase.ShowBase.globalClock, or possibly
        # direct.showbase.globalClock, but none of those seems to work. To
        # avoid the pylint warnings, create self.globalClock manually.
        self.globalClock = ClockObject.getGlobalClock()

        self.graphicsInterface = graphicsInterface

        # Mapping from gids to entities.
        self.entities = {}

        # Set up event handling.
        self.mouseState = {}
        self.keys = {}
        self.setupEventHandlers()

        # Set up camera control.
        self.cameraHolder = self.render.attachNewNode('CameraHolder')
        self.cameraHolder.setPos(0, 0, 100)
        self.prevCameraHpr = (0, -80, 0)
        self.usingCustomCamera = True
        self.setCameraCustom()

        self.prevMousePos = None
        self.selectionBox = None
        self.selectionBoxNode = None
        self.selectionBoxOrigin = None
        # TODO[#3]: Magic numbers bad.
        self.resourceDisplay = OnscreenText(pos=(-0.98,.9),
                                            align=TextNode.ALeft,
                                            mayChange=True)

        # Define the ground plane by a normal (+z) and a point (the origin).
        self.groundPlane = core.Plane(core.Vec3(0, 0, 1), core.Point3(0, 0, 0))

        self.graphicsInterface.graphicsReady(self)

    def cleanup(self):
        pass

    def interfaceMessage(self, data):
        # Messages from GraphicsInterface to Graphics are always internal
        # client messages, so no need to catch InvalidMessageError.
        message = deserializeMessage(data)
        if isinstance(message, messages.Tick):
            pass
        elif isinstance(message, cmessages.AddEntity):
            self.addEntity(message.gid, message.pos, message.modelPath,
                           message.isExample, message.isUnit, message.goalSize)
        elif isinstance(message, cmessages.RemoveEntity):
            self.removeEntity(message.gid)
        elif isinstance(message, cmessages.MoveEntity):
            self.moveEntity(message.gid, message.pos)
        elif isinstance(message, cmessages.MarkEntitySelected):
            self.markSelected(message.gid, message.isSelected)
        elif isinstance(message, cmessages.DisplayResources):
            self.displayResources(message.resourceAmt)
        else:
            badIMessageCommand(message, log)

    def addEntity(self, gid, pos, modelPath, isExample, isUnit, goalSize):
        """
        pos is given in graphics coordinates.

        goalSize, if specified, is a pair (width, height) -- the model will be
        scaled in the xy plane so that it's as large as possible while still
        fitting within that width and height. Don't pass 0 as the width or the
        height, because that's just not nice.
        """

        if gid in self.entities:
            raise RuntimeError("Already have entity with gid {gid}."
                               .format(gid=gid))

        log.debug("Adding graphical entity %s at %s", gid, pos)
        x, y = pos

        if isExample:
            # The example panda from the Panda3D "Hello world" tutorial.
            # TODO[#9]: Figure out a more general way of specifying animations.
            model = Actor(modelPath,
                          {"walk": "models/panda-walk4"})
        else:
            model = self.loader.loadModel(getModelPath(modelPath))

        # Put the model in the scene, but don't position it yet.
        rootNode = self.render.attachNewNode("")
        model.reparentTo(rootNode)

        # Rescale the model about its origin. The x and y coordinates of the
        # model's origin should be chosen as wherever it looks like the model's
        # center of mass is, so that rotation about the origin (in the xy
        # plane) feels natural.

        goalWidthX, goalWidthY = goalSize

        bound1, bound2 = model.getTightBounds()
        modelWidthX = abs(bound2[0] - bound1[0])
        modelWidthY = abs(bound2[1] - bound1[1])

        # Scale it to the largest it can be while still fitting within the goal
        # rect. If the aspect ratio of the goal rect is different from that of
        # the model, then it'll only fill that rect in one dimension.
        # altScaleFactor is used for sanity checks below.
        scaleFactor, altScaleFactor = minmax(goalWidthX / modelWidthX,
                                             goalWidthY / modelWidthY)

        # Sanity check the scale factor.
        if scaleFactor <= 0.0:
            if scaleFactor == 0.0:
                log.warn("Graphical entity %s will be scaled negatively!", gid)
            else:
                log.warn("Graphical entity %s will be scaled to zero size.",
                         gid)
        else:
            # TODO[#9]: Currently the example panda triggers this warning.
            # TODO[#3]: Magic numbers bad.
            if altScaleFactor / scaleFactor > 1.001:
                log.warn("Graphical entity %s has different aspect ratio than "
                         "its model: model of size %.3g x %.3g being scaled "
                         "into %.3g x %.3g.",
                         gid, modelWidthX, modelWidthY, goalWidthX, goalWidthY)

        model.setScale(scaleFactor)

        # Place the model at z=0. The model's origin should be placed so that
        # this looks natural -- for most units this means it should be right at
        # the bottom of the model, but if we add any units that are intended to
        # float above the ground, then this can be accomplished by just
        # positioning the model above its origin.
        rootNode.setPos(x, y, 0.0)

        entity = Entity(gid, model, rootNode, isExample)
        self.entities[gid] = entity

        if isUnit:
            # TODO[#52]: Sigh. This is a terrible hack. I guess we could pipe
            # through yet another bool for "is this my unit", but I don't want
            # to have a growing collection of bools that need to be passed into
            # the graphics for each unit. For now, "is this an example model?"
            # and "is this my unit" are equivalent, so I guess we'll just
            # piggyback off of isExample....
            if isExample:
                entity.setIndicator(self.loader.loadModel(
                    getModelPath("unit-indicator-mine.egg")
                ))
            else:
                entity.setIndicator(self.loader.loadModel(
                    getModelPath("unit-indicator-notmine.egg")
                ))

    def removeEntity(self, gid):
        log.debug("Removing graphical entity %s", gid)
        entity = self.entities.pop(gid)
        entity.cleanup()

    def moveEntity(self, gid, newPos):
        log.debug("Moving graphical entity %s to %s", gid, newPos)
        entity = self.entities[gid]

        x, y = newPos
        oldX, oldY, oldZ = entity.rootNode.getPos()
        z = oldZ

        # Ensure the entity is facing the right direction.
        heading = math.atan2(y - oldY, x - oldX)
        heading *= 180.0 / math.pi
        # Magic angle adjustment needed to stop the panda always facing
        # sideways.
        # TODO[#9]: Establish a convention about which way _our_ models face;
        # figure out whether we need something like this. (Hopefully not?)
        heading += 90.0
        entity.rootNode.setHpr(heading, 0, 0)

        moveInterval = entity.rootNode.posInterval(config.TICK_LENGTH,
                                                   (x, y, z))
        moveInterval.start()

        if entity.isActor and "walk" in entity.model.getAnimNames():
            currFrame = entity.model.getCurrentFrame("walk")
            if currFrame is None:
                currFrame = 0
            # Supposedly, it's possible to pass a startFrame and a duration to
            # actorInterval, instead of calculating the endFrame ourself. But
            # for some reason, that doesn't seem to work; if I do that, then
            # the animation just keeps jumping around the early frames and
            # never gets past frame 5 or so. I'm not sure why. For now at
            # least, just calculate the endFrame ourselves to work around this.
            log.debug("Animating entity %s from frame %s/%s",
                      gid, currFrame, entity.model.getNumFrames("walk"))
            frameRate = entity.model.getAnimControl("walk").getFrameRate()
            endFrame = currFrame + int(math.ceil(frameRate *
                                                 config.TICK_LENGTH))
            animInterval = entity.model.actorInterval(
                "walk", loop=1, startFrame=currFrame, endFrame=endFrame
            )
            animInterval.start()

    def markSelected(self, gid, isSelected):
        log.debug("Marking graphical entity %s as %sselected",
                  gid, "" if isSelected else "not ")
        entity = self.entities[gid]

        if isSelected:
            entity.setIndicator(self.loader.loadModel(
                getModelPath("unit-indicator-selected.egg")
            ))
        else:
            # You can't currently select others' units, so if a unit is being
            # deselected it must be mine.
            entity.setIndicator(self.loader.loadModel(
                getModelPath("unit-indicator-mine.egg")
            ))

    def displayResources(self, resourceAmt):
        self.resourceDisplay.setText("Resource: {}".format(resourceAmt))

    def createSelectionBox(self, corner1, corner2):
        """
        Create a selection "box" given the coordinates of two opposite corners.
        The corners are given in world coordinates (well, 3d graphics
        coordinates).
        """

        assert self.selectionBox is None

        p1, p2, p3, p4 = self.convert3dBoxToScreen(corner1, corner2)
        x1, y1 = p1
        x2, y2 = p2
        x3, y3 = p3
        x4, y4 = p4

        # TODO[#3]: Magic numbers bad.
        self.selectionBox = LineSegs("SelectionBox")
        self.selectionBox.setThickness(3.0)
        self.selectionBox.setColor(0.0, 1.0, 0.25, 1.0)
        self.selectionBox.move_to(x1, 0, y1)
        self.selectionBox.draw_to(x2, 0, y2)
        self.selectionBox.draw_to(x3, 0, y3)
        self.selectionBox.draw_to(x4, 0, y4)
        self.selectionBox.draw_to(x1, 0, y1)

        self.selectionBoxNode = self.render2d.attachNewNode(
            self.selectionBox.create())

    def moveSelectionBox(self, corner1, corner2):
        assert self.selectionBox is not None

        p1, p2, p3, p4 = self.convert3dBoxToScreen(corner1, corner2)
        x1, y1 = p1
        x2, y2 = p2
        x3, y3 = p3
        x4, y4 = p4

        self.selectionBox.setVertex(0, x1, 0, y1)
        self.selectionBox.setVertex(1, x2, 0, y2)
        self.selectionBox.setVertex(2, x3, 0, y3)
        self.selectionBox.setVertex(3, x4, 0, y4)
        self.selectionBox.setVertex(4, x1, 0, y1)

    def removeSelectionBox(self):
        self.selectionBoxNode.removeNode()
        self.selectionBox     = None
        self.selectionBoxNode = None

    def convert3dBoxToScreen(self, corner1, corner3):
        """
        Return screen coordinates of the 4 corners of a box, given in 3d
        coordinates. The box is specified using 2 opposite corners.
        """

        wx1, wy1, wz1 = corner1
        wx3, wy3, wz3 = corner3

        wx2, wy2 = (wx1, wy3)
        wx4, wy4 = (wx3, wy1)

        # Note: corner1 and corner2 could have nonzero z because floating-point
        # calculations, but they should at least be close. We'll just average
        # their z and not worry about it.
        wz2 = wz4 = 0.5 * (wz1 + wz3)

        p1 = self.coord3dToScreen((wx1, wy1, wz1))
        p2 = self.coord3dToScreen((wx2, wy2, wz2))
        p3 = self.coord3dToScreen((wx3, wy3, wz3))
        p4 = self.coord3dToScreen((wx4, wy4, wz4))

        return (p1, p2, p3, p4)

    def setCameraCustom(self):
        """
        Change to using our custom task to control the camera.
        """

        # Disable the default mouse-based camera control task, so we don't have
        # to fight with it for control of the camera.
        self.disableMouse()

        # Face the camera in the appropriate angle.
        self.camera.setHpr(self.prevCameraHpr)

        # Put it in the same location as the cameraHolder, and make it stay
        # put relative to the cameraHolder (so we can move the camera around by
        # changing the cameraHolder's position).
        self.camera.reparentTo(self.cameraHolder)
        self.camera.setPos(0, 0, 0)

        # Substitute our own camera control task.
        self.taskMgr.add(self.updateCameraTask, "UpdateCameraTask")

        self.usingCustomCamera = True

        # Need a task to handle mouse-dragging because there doesn't seem to be
        # a built-in mouseMove event.
        self.taskMgr.add(self.mouseMoveTask, "MouseMoveTask")

    def setCameraDefault(self):
        """
        Change to using the default mouse-based camera controls.
        """

        self.taskMgr.remove("UpdateCameraTask")

        # Save current location for when this control style is restored.
        self.prevCameraHpr = self.camera.getHpr()

        # Use the existing camera location, rather than jumping back to the one
        # from last time the default camera controller was active.
        # Copied from https://www.panda3d.org/manual/index.php/Mouse_Support
        mat = Mat4(self.camera.getMat())
        mat.invertInPlace()
        self.mouseInterfaceNode.setMat(mat)
        self.enableMouse()

        self.usingCustomCamera = False

    def toggleCameraStyle(self):
        """
        Switch to whichever style of camera control isn't currently active.
        """

        if self.usingCustomCamera:
            self.setCameraDefault()
        else:
            self.setCameraCustom()

    # We don't use task, but we can't remove it because the function signature
    # is from Panda3D.
    def updateCameraTask(self, task):  # pylint: disable=unused-argument
        """
        Move the camera sensibly.
        """

        dt = self.globalClock.getDt()
        translateSpeed = 30 * dt
        rotateSpeed    = 50 * dt

        # Separately track whether the camera should translate in each of the 4
        # directions. These 4 are initialized based on the various inputs that
        # might tell us to scroll, and different inputs saying the same thing
        # don't stack. That way if we get inputs saying both "left" and
        # "right", they can cancel and the camera just won't move along that
        # axis -- even if, say, there are two inputs saying "left" and only one
        # saying "right'.
        moveLeft  = self.keys["arrow_left"]
        moveRight = self.keys["arrow_right"]
        moveUp    = self.keys["arrow_up"]
        moveDown  = self.keys["arrow_down"]

        # Check if the mouse is over the window.
        if self.mouseWatcherNode.hasMouse():
            # Get the position.
            # Each coordinate is normalized to the interval [-1, 1].
            mousePos = self.mouseWatcherNode.getMouse()
            xPos, yPos = mousePos.getX(), mousePos.getY()
            # Only move if the mouse is close to the edge, and actually within
            # the window.
            if  (1.0 - EDGE_SCROLL_WIDTH) < xPos <=  1.0:
                moveRight = 1
            if -(1.0 - EDGE_SCROLL_WIDTH) > xPos >= -1.0:
                moveLeft  = 1
            if  (1.0 - EDGE_SCROLL_WIDTH) < yPos <=  1.0:
                moveUp    = 1
            if -(1.0 - EDGE_SCROLL_WIDTH) > yPos >= -1.0:
                moveDown  = 1

        forward  = translateSpeed * (moveUp    - moveDown)
        sideways = translateSpeed * (moveRight - moveLeft)
        self.cameraHolder.setPos(self.cameraHolder, sideways, forward, 0)

        if sideways != 0 or forward != 0:
            self.updateSelectionBox()

        rotate = rotateSpeed * (self.keys["a"] - self.keys["d"])
        self.cameraHolder.setHpr(self.cameraHolder, rotate, 0, 0)

        return Task.cont

    def zoomCamera(self, inward):
        """
        Zoom in or out.
        """

        dt = self.globalClock.getDt()
        zoomSpeed = 100 * dt

        zoom = -zoomSpeed if inward else zoomSpeed
        self.cameraHolder.setPos(self.cameraHolder, 0, 0, zoom)

    def centerView(self):
        """
        Center the view sensibly.
        """

        message = cmessages.RequestCenter()
        self.graphicsInterface.graphicsMessage(message.serialize())

    # We don't use task, but we can't remove it because the function signature
    # is from Panda3D.
    def mouseMoveTask(self, task):  # pylint: disable=unused-argument
        """
        Handle mouse movement.
        """

        mousePos = self.getMousePos()

        # NOTE: We don't handle clicking and dragging at the same time.
        if mousePos is not None and mousePos != self.prevMousePos:
            for (buttonId, state) in self.mouseState.iteritems():
                state.lastPos = mousePos
                if state.hasMoved:
                    self.handleMouseDragMove(buttonId, state.modifiers,
                                             state.startPos, mousePos)
                else:
                    startX, startY = state.startPos
                    mouseX, mouseY = mousePos
                    distance = math.hypot(mouseX - startX, mouseY - startY)
                    # TODO[#3]: Magic numbers bad.
                    # Check if the mouse has moved outside the dead zone.
                    if distance > 0.0314:
                        self.handleMouseDragStart(buttonId, state.modifiers,
                                                  state.startPos, mousePos)
                        state.hasMoved = True

        if mousePos != self.prevMousePos:
            self.prevMousePos = mousePos

        return Task.cont

    def pandaEventMouseDown(self, buttonId, modifiers):
        if buttonId in self.mouseState:
            # Call pandaEventMouseUp just to clear any state related to the
            # button being down, so we can handle this buttonDown event as if
            # it were a fresh press of the button.
            log.warn("Mouse button %s is already down.", buttonId)
            self.pandaEventMouseUp(buttonId)

        assert buttonId not in self.mouseState

        state = MouseButtonState(modifiers[:], self.getMousePos())
        self.mouseState[buttonId] = state

    def pandaEventMouseUp(self, buttonId):
        if buttonId not in self.mouseState:
            # Drop the event, since there's nothing to do.
            log.warn("Mouse button %s is already up.", buttonId)
            return

        state = self.mouseState[buttonId]

        if state.hasMoved:
            endPos = self.getMousePos()
            if endPos is None:
                endPos = state.lastPos
            self.handleMouseDragEnd(buttonId, state.modifiers,
                                    state.startPos, endPos)
        else:
            self.handleMouseClick(buttonId, state.modifiers, state.startPos)

        del self.mouseState[buttonId]

    def handleMouseClick(self, button, modifiers, pos):
        # Make sure the mouse is inside the screen
        # TODO: Move this check to pandaEventMouseUp?
        if self.mouseWatcherNode.hasMouse() and self.usingCustomCamera:
            x, y, _z = self.coordScreenTo3d(pos)

            if modifiers == []:
                # TODO: This component should take care of decoding the
                # click as far as "left" or "right"; we shouldn't send a
                # numerical button id to the graphicsInterface.
                message = cmessages.Click(button, (x, y))
            elif button == 1 and modifiers == ["shift"]:
                message = cmessages.ShiftLClick((x, y))
            elif button == 1 and modifiers == ["control"]:
                message = cmessages.ControlLClick((x, y))
            elif button == 3 and modifiers == ["shift"]:
                message = cmessages.ShiftRClick((x, y))
            elif button == 3 and modifiers == ["control"]:
                message = cmessages.ControlRClick((x, y))
            else:
                thisShouldNeverHappen(
                    "Unhandled modifiers for click: {}".format(modifiers))

            self.graphicsInterface.graphicsMessage(message.serialize())

    def handleMouseDragStart(self, buttonId, modifiers, startPos, endPos):
        log.debug("Start dragging from %s to %s", startPos, endPos)

        if buttonId == 1 and modifiers == []:
            assert self.selectionBoxOrigin is None
            self.selectionBoxOrigin = self.coordScreenTo3d(startPos)
            endPos = self.coordScreenTo3d(endPos)
            self.createSelectionBox(self.selectionBoxOrigin, endPos)

    def handleMouseDragMove(self, buttonId, modifiers, startPos, endPos):
        log.debug("Continue dragging from %s to %s", startPos, endPos)

        if buttonId == 1 and modifiers == []:
            assert self.selectionBoxOrigin is not None
            endPos = self.coordScreenTo3d(endPos)
            self.moveSelectionBox(self.selectionBoxOrigin, endPos)

    def handleMouseDragEnd(self, buttonId, modifiers, startPos, endPos):
        log.debug("End dragging from %s to %s", startPos, endPos)

        if buttonId == 1 and modifiers == []:
            # Actually select the units.
            endPos = self.coordScreenTo3d(endPos)
            # TODO[#55]: Use 3d graphics coords in messages so we don't have to
            # remove the z coordinates everywhere.
            message = cmessages.DragBox(self.selectionBoxOrigin[:2],
                                        endPos[:2])
            self.graphicsInterface.graphicsMessage(message.serialize())
            # Clear the selection box; we're done dragging.
            self.selectionBoxOrigin = None
            self.removeSelectionBox()

    def updateSelectionBox(self):
        if self.selectionBoxOrigin is not None:
            mousePos = self.getMousePos()
            if mousePos is not None:
                endPos = self.coordScreenTo3d(mousePos)
                self.moveSelectionBox(self.selectionBoxOrigin, endPos)

    def getMousePos(self):
        # Check if the mouse is over the window.
        if self.mouseWatcherNode.hasMouse():
            # Get the position.
            # Each coordinate is normalized to the interval [-1, 1].
            mousePoint = self.mouseWatcherNode.getMouse()
            # Create a copy of mousePoint rather than returning a reference to
            # it, because mousePoint will be modified in place by Panda.
            return (mousePoint.getX(), mousePoint.getY())
        else:
            return None

    def handleWindowClose(self):
        log.info("Window close requested -- shutting down client.")
        message = cmessages.RequestQuit()
        self.graphicsInterface.graphicsMessage(message.serialize())

    def setupEventHandlers(self):
        def pushKey(key, value):
            self.keys[key] = value

        for key in ["arrow_up", "arrow_left", "arrow_right", "arrow_down",
                    "w", "a", "d", "s"]:
            self.keys[key] = False
            self.accept(key, pushKey, [key, True])
            self.accept("shift-%s" % key, pushKey, [key, True])
            self.accept("%s-up" % key, pushKey, [key, False])

        # Camera toggle.
        self.accept("f3",       self.toggleCameraStyle, [])
        self.accept("shift-f3", self.toggleCameraStyle, [])

        # Center view.
        self.accept("space", self.centerView, [])

        # Handle mouse wheel.
        self.accept("wheel_up", self.zoomCamera, [True])
        self.accept("wheel_down", self.zoomCamera, [False])

        # Handle clicking.
        self.accept("mouse1",    self.pandaEventMouseDown, [1, []])
        self.accept("mouse1-up", self.pandaEventMouseUp,   [1])
        # TODO: Make sure this is always the right mouse button.
        self.accept("mouse3",    self.pandaEventMouseDown, [3, []])
        self.accept("mouse3-up", self.pandaEventMouseUp,   [3])

        # Handle clicking with modifier keys.
        self.accept("shift-mouse1",   self.pandaEventMouseDown,
                    [1, ["shift"]])
        self.accept("control-mouse1", self.pandaEventMouseDown,
                    [1, ["control"]])
        self.accept("shift-mouse3",   self.pandaEventMouseDown,
                    [3, ["shift"]])
        self.accept("control-mouse3", self.pandaEventMouseDown,
                    [3, ["control"]])

        # Handle window close request (clicking the X, Alt-F4, etc.)
        self.win.set_close_request_event("window-close")
        self.accept("window-close", self.handleWindowClose)

    def coord3dToScreen(self, coord3d):
        # Empirically, Lens.project takes coordinates in the *camera*'s
        # coordinate system, not its parent or the render. This was not very
        # clear from the documentation, and you'd be surprised how long it took
        # us to figure this out. Anyway, we need to convert the point to be
        # relative to self.camera here; otherwise we'll get bizarre,
        # nonsensical, and hard-to-debug results.
        coord3d = self.camera.getRelativePoint(self.render, coord3d)
        screenCoord = Point2()
        if not self.camLens.project(coord3d, screenCoord):
            log.debug("Attempting 3d-to-screen conversion on point outside of "
                      "camera's viewing frustum.")

        # Convert to a tuple to ensure no one else is keeping a reference
        # around.
        x, y = screenCoord
        return (x, y)

    def coordScreenTo3d(self, screenCoord):
        x, y = screenCoord
        screenPoint = Point2(x, y)

        # Do this calculation using simple geometry, rather than the absurd
        # collision-traversal nonsense we used to use. Thanks to
        #     https://www.panda3d.org/forums/viewtopic.php?t=5409
        # for pointing us at the right methods to make this work.

        # Get two points along the ray extending from the camera, in the
        # direction of the mouse click.
        nearPoint = Point3()
        farPoint = Point3()
        self.camLens.extrude(screenPoint, nearPoint, farPoint)

        # These points are relative to the camera, so need to be converted to
        # be relative to the render. Thanks to the example code (see link
        # above) for saving us probably some hours of debugging figuring that
        # one out again :)
        nearPoint = self.render.getRelativePoint(self.camera, nearPoint)
        farPoint  = self.render.getRelativePoint(self.camera, farPoint)

        intersection = Point3()
        if self.groundPlane.intersectsLine(intersection, nearPoint, farPoint):
            # Convert to a tuple to ensure no one else is keeping a reference
            # around.
            x, y, z = intersection
            return (x, y, z)

        # The ray didn't intersect the ground. This is almost certainly going
        # to happen at some point; all you have to do is find a way to aim the
        # camera (or manipulate the screen coordinate) so that the ray points
        # horizontally. But we don't have code to handle it, so for now just
        # abort.
        thisIsNotHandled()
Beispiel #20
0
class WartsApp(ShowBase):
    """
    The application running all the graphics.
    """
    def __init__(self, graphicsInterface):
        ShowBase.__init__(self)

        # This is available as a global, but pylint gives an undefined-variable
        # warning if we use it that way. Looking at
        #     https://www.panda3d.org/manual/index.php/ShowBase
        # I would have thought we could reference it as either
        # self.globalClock, direct.showbase.ShowBase.globalClock, or possibly
        # direct.showbase.globalClock, but none of those seems to work. To
        # avoid the pylint warnings, create self.globalClock manually.
        self.globalClock = ClockObject.getGlobalClock()

        self.graphicsInterface = graphicsInterface

        # Mapping from gids to entities.
        self.entities = {}

        # Set up event handling.
        self.mouseState = {}
        self.keys = {}
        self.setupEventHandlers()

        # Set up camera control.
        self.cameraHolder = self.render.attachNewNode('CameraHolder')
        self.cameraHolder.setPos(0, 0, 100)
        self.prevCameraHpr = (0, -80, 0)
        self.usingCustomCamera = True
        self.setCameraCustom()

        self.prevMousePos = None
        self.selectionBox = None
        self.selectionBoxNode = None
        self.selectionBoxOrigin = None
        # TODO[#3]: Magic numbers bad.
        self.resourceDisplay = OnscreenText(pos=(-0.98, .9),
                                            align=TextNode.ALeft,
                                            mayChange=True)

        # Define the ground plane by a normal (+z) and a point (the origin).
        self.groundPlane = core.Plane(core.Vec3(0, 0, 1), core.Point3(0, 0, 0))

        self.graphicsInterface.graphicsReady(self)

    def cleanup(self):
        pass

    def interfaceMessage(self, data):
        # Messages from GraphicsInterface to Graphics are always internal
        # client messages, so no need to catch InvalidMessageError.
        message = deserializeMessage(data)
        if isinstance(message, messages.Tick):
            pass
        elif isinstance(message, cmessages.AddEntity):
            self.addEntity(message.gid, message.pos, message.modelPath,
                           message.isExample, message.isUnit, message.goalSize)
        elif isinstance(message, cmessages.RemoveEntity):
            self.removeEntity(message.gid)
        elif isinstance(message, cmessages.MoveEntity):
            self.moveEntity(message.gid, message.pos)
        elif isinstance(message, cmessages.MarkEntitySelected):
            self.markSelected(message.gid, message.isSelected)
        elif isinstance(message, cmessages.DisplayResources):
            self.displayResources(message.resourceAmt)
        else:
            badIMessageCommand(message, log)

    def addEntity(self, gid, pos, modelPath, isExample, isUnit, goalSize):
        """
        pos is given in graphics coordinates.

        goalSize, if specified, is a pair (width, height) -- the model will be
        scaled in the xy plane so that it's as large as possible while still
        fitting within that width and height. Don't pass 0 as the width or the
        height, because that's just not nice.
        """

        if gid in self.entities:
            raise RuntimeError(
                "Already have entity with gid {gid}.".format(gid=gid))

        log.debug("Adding graphical entity %s at %s", gid, pos)
        x, y = pos

        if isExample:
            # The example panda from the Panda3D "Hello world" tutorial.
            # TODO[#9]: Figure out a more general way of specifying animations.
            model = Actor(modelPath, {"walk": "models/panda-walk4"})
        else:
            model = self.loader.loadModel(getModelPath(modelPath))

        # Put the model in the scene, but don't position it yet.
        rootNode = self.render.attachNewNode("")
        model.reparentTo(rootNode)

        # Rescale the model about its origin. The x and y coordinates of the
        # model's origin should be chosen as wherever it looks like the model's
        # center of mass is, so that rotation about the origin (in the xy
        # plane) feels natural.

        goalWidthX, goalWidthY = goalSize

        bound1, bound2 = model.getTightBounds()
        modelWidthX = abs(bound2[0] - bound1[0])
        modelWidthY = abs(bound2[1] - bound1[1])

        # Scale it to the largest it can be while still fitting within the goal
        # rect. If the aspect ratio of the goal rect is different from that of
        # the model, then it'll only fill that rect in one dimension.
        # altScaleFactor is used for sanity checks below.
        scaleFactor, altScaleFactor = minmax(goalWidthX / modelWidthX,
                                             goalWidthY / modelWidthY)

        # Sanity check the scale factor.
        if scaleFactor <= 0.0:
            if scaleFactor == 0.0:
                log.warn("Graphical entity %s will be scaled negatively!", gid)
            else:
                log.warn("Graphical entity %s will be scaled to zero size.",
                         gid)
        else:
            # TODO[#9]: Currently the example panda triggers this warning.
            # TODO[#3]: Magic numbers bad.
            if altScaleFactor / scaleFactor > 1.001:
                log.warn(
                    "Graphical entity %s has different aspect ratio than "
                    "its model: model of size %.3g x %.3g being scaled "
                    "into %.3g x %.3g.", gid, modelWidthX, modelWidthY,
                    goalWidthX, goalWidthY)

        model.setScale(scaleFactor)

        # Place the model at z=0. The model's origin should be placed so that
        # this looks natural -- for most units this means it should be right at
        # the bottom of the model, but if we add any units that are intended to
        # float above the ground, then this can be accomplished by just
        # positioning the model above its origin.
        rootNode.setPos(x, y, 0.0)

        entity = Entity(gid, model, rootNode, isExample)
        self.entities[gid] = entity

        if isUnit:
            # TODO[#52]: Sigh. This is a terrible hack. I guess we could pipe
            # through yet another bool for "is this my unit", but I don't want
            # to have a growing collection of bools that need to be passed into
            # the graphics for each unit. For now, "is this an example model?"
            # and "is this my unit" are equivalent, so I guess we'll just
            # piggyback off of isExample....
            if isExample:
                entity.setIndicator(
                    self.loader.loadModel(
                        getModelPath("unit-indicator-mine.egg")))
            else:
                entity.setIndicator(
                    self.loader.loadModel(
                        getModelPath("unit-indicator-notmine.egg")))

    def removeEntity(self, gid):
        log.debug("Removing graphical entity %s", gid)
        entity = self.entities.pop(gid)
        entity.cleanup()

    def moveEntity(self, gid, newPos):
        log.debug("Moving graphical entity %s to %s", gid, newPos)
        entity = self.entities[gid]

        x, y = newPos
        oldX, oldY, oldZ = entity.rootNode.getPos()
        z = oldZ

        # Ensure the entity is facing the right direction.
        heading = math.atan2(y - oldY, x - oldX)
        heading *= 180.0 / math.pi
        # Magic angle adjustment needed to stop the panda always facing
        # sideways.
        # TODO[#9]: Establish a convention about which way _our_ models face;
        # figure out whether we need something like this. (Hopefully not?)
        heading += 90.0
        entity.rootNode.setHpr(heading, 0, 0)

        moveInterval = entity.rootNode.posInterval(config.TICK_LENGTH,
                                                   (x, y, z))
        moveInterval.start()

        if entity.isActor and "walk" in entity.model.getAnimNames():
            currFrame = entity.model.getCurrentFrame("walk")
            if currFrame is None:
                currFrame = 0
            # Supposedly, it's possible to pass a startFrame and a duration to
            # actorInterval, instead of calculating the endFrame ourself. But
            # for some reason, that doesn't seem to work; if I do that, then
            # the animation just keeps jumping around the early frames and
            # never gets past frame 5 or so. I'm not sure why. For now at
            # least, just calculate the endFrame ourselves to work around this.
            log.debug("Animating entity %s from frame %s/%s", gid, currFrame,
                      entity.model.getNumFrames("walk"))
            frameRate = entity.model.getAnimControl("walk").getFrameRate()
            endFrame = currFrame + int(
                math.ceil(frameRate * config.TICK_LENGTH))
            animInterval = entity.model.actorInterval("walk",
                                                      loop=1,
                                                      startFrame=currFrame,
                                                      endFrame=endFrame)
            animInterval.start()

    def markSelected(self, gid, isSelected):
        log.debug("Marking graphical entity %s as %sselected", gid,
                  "" if isSelected else "not ")
        entity = self.entities[gid]

        if isSelected:
            entity.setIndicator(
                self.loader.loadModel(
                    getModelPath("unit-indicator-selected.egg")))
        else:
            # You can't currently select others' units, so if a unit is being
            # deselected it must be mine.
            entity.setIndicator(
                self.loader.loadModel(getModelPath("unit-indicator-mine.egg")))

    def displayResources(self, resourceAmt):
        self.resourceDisplay.setText("Resource: {}".format(resourceAmt))

    def createSelectionBox(self, corner1, corner2):
        """
        Create a selection "box" given the coordinates of two opposite corners.
        The corners are given in world coordinates (well, 3d graphics
        coordinates).
        """

        assert self.selectionBox is None

        p1, p2, p3, p4 = self.convert3dBoxToScreen(corner1, corner2)
        x1, y1 = p1
        x2, y2 = p2
        x3, y3 = p3
        x4, y4 = p4

        # TODO[#3]: Magic numbers bad.
        self.selectionBox = LineSegs("SelectionBox")
        self.selectionBox.setThickness(3.0)
        self.selectionBox.setColor(0.0, 1.0, 0.25, 1.0)
        self.selectionBox.move_to(x1, 0, y1)
        self.selectionBox.draw_to(x2, 0, y2)
        self.selectionBox.draw_to(x3, 0, y3)
        self.selectionBox.draw_to(x4, 0, y4)
        self.selectionBox.draw_to(x1, 0, y1)

        self.selectionBoxNode = self.render2d.attachNewNode(
            self.selectionBox.create())

    def moveSelectionBox(self, corner1, corner2):
        assert self.selectionBox is not None

        p1, p2, p3, p4 = self.convert3dBoxToScreen(corner1, corner2)
        x1, y1 = p1
        x2, y2 = p2
        x3, y3 = p3
        x4, y4 = p4

        self.selectionBox.setVertex(0, x1, 0, y1)
        self.selectionBox.setVertex(1, x2, 0, y2)
        self.selectionBox.setVertex(2, x3, 0, y3)
        self.selectionBox.setVertex(3, x4, 0, y4)
        self.selectionBox.setVertex(4, x1, 0, y1)

    def removeSelectionBox(self):
        self.selectionBoxNode.removeNode()
        self.selectionBox = None
        self.selectionBoxNode = None

    def convert3dBoxToScreen(self, corner1, corner3):
        """
        Return screen coordinates of the 4 corners of a box, given in 3d
        coordinates. The box is specified using 2 opposite corners.
        """

        wx1, wy1, wz1 = corner1
        wx3, wy3, wz3 = corner3

        wx2, wy2 = (wx1, wy3)
        wx4, wy4 = (wx3, wy1)

        # Note: corner1 and corner2 could have nonzero z because floating-point
        # calculations, but they should at least be close. We'll just average
        # their z and not worry about it.
        wz2 = wz4 = 0.5 * (wz1 + wz3)

        p1 = self.coord3dToScreen((wx1, wy1, wz1))
        p2 = self.coord3dToScreen((wx2, wy2, wz2))
        p3 = self.coord3dToScreen((wx3, wy3, wz3))
        p4 = self.coord3dToScreen((wx4, wy4, wz4))

        return (p1, p2, p3, p4)

    def setCameraCustom(self):
        """
        Change to using our custom task to control the camera.
        """

        # Disable the default mouse-based camera control task, so we don't have
        # to fight with it for control of the camera.
        self.disableMouse()

        # Face the camera in the appropriate angle.
        self.camera.setHpr(self.prevCameraHpr)

        # Put it in the same location as the cameraHolder, and make it stay
        # put relative to the cameraHolder (so we can move the camera around by
        # changing the cameraHolder's position).
        self.camera.reparentTo(self.cameraHolder)
        self.camera.setPos(0, 0, 0)

        # Substitute our own camera control task.
        self.taskMgr.add(self.updateCameraTask, "UpdateCameraTask")

        self.usingCustomCamera = True

        # Need a task to handle mouse-dragging because there doesn't seem to be
        # a built-in mouseMove event.
        self.taskMgr.add(self.mouseMoveTask, "MouseMoveTask")

    def setCameraDefault(self):
        """
        Change to using the default mouse-based camera controls.
        """

        self.taskMgr.remove("UpdateCameraTask")

        # Save current location for when this control style is restored.
        self.prevCameraHpr = self.camera.getHpr()

        # Use the existing camera location, rather than jumping back to the one
        # from last time the default camera controller was active.
        # Copied from https://www.panda3d.org/manual/index.php/Mouse_Support
        mat = Mat4(self.camera.getMat())
        mat.invertInPlace()
        self.mouseInterfaceNode.setMat(mat)
        self.enableMouse()

        self.usingCustomCamera = False

    def toggleCameraStyle(self):
        """
        Switch to whichever style of camera control isn't currently active.
        """

        if self.usingCustomCamera:
            self.setCameraDefault()
        else:
            self.setCameraCustom()

    # We don't use task, but we can't remove it because the function signature
    # is from Panda3D.
    def updateCameraTask(self, task):  # pylint: disable=unused-argument
        """
        Move the camera sensibly.
        """

        dt = self.globalClock.getDt()
        translateSpeed = 30 * dt
        rotateSpeed = 50 * dt

        # Separately track whether the camera should translate in each of the 4
        # directions. These 4 are initialized based on the various inputs that
        # might tell us to scroll, and different inputs saying the same thing
        # don't stack. That way if we get inputs saying both "left" and
        # "right", they can cancel and the camera just won't move along that
        # axis -- even if, say, there are two inputs saying "left" and only one
        # saying "right'.
        moveLeft = self.keys["arrow_left"]
        moveRight = self.keys["arrow_right"]
        moveUp = self.keys["arrow_up"]
        moveDown = self.keys["arrow_down"]

        # Check if the mouse is over the window.
        if self.mouseWatcherNode.hasMouse():
            # Get the position.
            # Each coordinate is normalized to the interval [-1, 1].
            mousePos = self.mouseWatcherNode.getMouse()
            xPos, yPos = mousePos.getX(), mousePos.getY()
            # Only move if the mouse is close to the edge, and actually within
            # the window.
            if (1.0 - EDGE_SCROLL_WIDTH) < xPos <= 1.0:
                moveRight = 1
            if -(1.0 - EDGE_SCROLL_WIDTH) > xPos >= -1.0:
                moveLeft = 1
            if (1.0 - EDGE_SCROLL_WIDTH) < yPos <= 1.0:
                moveUp = 1
            if -(1.0 - EDGE_SCROLL_WIDTH) > yPos >= -1.0:
                moveDown = 1

        forward = translateSpeed * (moveUp - moveDown)
        sideways = translateSpeed * (moveRight - moveLeft)
        self.cameraHolder.setPos(self.cameraHolder, sideways, forward, 0)

        if sideways != 0 or forward != 0:
            self.updateSelectionBox()

        rotate = rotateSpeed * (self.keys["a"] - self.keys["d"])
        self.cameraHolder.setHpr(self.cameraHolder, rotate, 0, 0)

        return Task.cont

    def zoomCamera(self, inward):
        """
        Zoom in or out.
        """

        dt = self.globalClock.getDt()
        zoomSpeed = 100 * dt

        zoom = -zoomSpeed if inward else zoomSpeed
        self.cameraHolder.setPos(self.cameraHolder, 0, 0, zoom)

    def centerView(self):
        """
        Center the view sensibly.
        """

        message = cmessages.RequestCenter()
        self.graphicsInterface.graphicsMessage(message.serialize())

    # We don't use task, but we can't remove it because the function signature
    # is from Panda3D.
    def mouseMoveTask(self, task):  # pylint: disable=unused-argument
        """
        Handle mouse movement.
        """

        mousePos = self.getMousePos()

        # NOTE: We don't handle clicking and dragging at the same time.
        if mousePos is not None and mousePos != self.prevMousePos:
            for (buttonId, state) in self.mouseState.iteritems():
                state.lastPos = mousePos
                if state.hasMoved:
                    self.handleMouseDragMove(buttonId, state.modifiers,
                                             state.startPos, mousePos)
                else:
                    startX, startY = state.startPos
                    mouseX, mouseY = mousePos
                    distance = math.hypot(mouseX - startX, mouseY - startY)
                    # TODO[#3]: Magic numbers bad.
                    # Check if the mouse has moved outside the dead zone.
                    if distance > 0.0314:
                        self.handleMouseDragStart(buttonId, state.modifiers,
                                                  state.startPos, mousePos)
                        state.hasMoved = True

        if mousePos != self.prevMousePos:
            self.prevMousePos = mousePos

        return Task.cont

    def pandaEventMouseDown(self, buttonId, modifiers):
        if buttonId in self.mouseState:
            # Call pandaEventMouseUp just to clear any state related to the
            # button being down, so we can handle this buttonDown event as if
            # it were a fresh press of the button.
            log.warn("Mouse button %s is already down.", buttonId)
            self.pandaEventMouseUp(buttonId)

        assert buttonId not in self.mouseState

        state = MouseButtonState(modifiers[:], self.getMousePos())
        self.mouseState[buttonId] = state

    def pandaEventMouseUp(self, buttonId):
        if buttonId not in self.mouseState:
            # Drop the event, since there's nothing to do.
            log.warn("Mouse button %s is already up.", buttonId)
            return

        state = self.mouseState[buttonId]

        if state.hasMoved:
            endPos = self.getMousePos()
            if endPos is None:
                endPos = state.lastPos
            self.handleMouseDragEnd(buttonId, state.modifiers, state.startPos,
                                    endPos)
        else:
            self.handleMouseClick(buttonId, state.modifiers, state.startPos)

        del self.mouseState[buttonId]

    def handleMouseClick(self, button, modifiers, pos):
        # Make sure the mouse is inside the screen
        # TODO: Move this check to pandaEventMouseUp?
        if self.mouseWatcherNode.hasMouse() and self.usingCustomCamera:
            x, y, _z = self.coordScreenTo3d(pos)

            if modifiers == []:
                # TODO: This component should take care of decoding the
                # click as far as "left" or "right"; we shouldn't send a
                # numerical button id to the graphicsInterface.
                message = cmessages.Click(button, (x, y))
            elif button == 1 and modifiers == ["shift"]:
                message = cmessages.ShiftLClick((x, y))
            elif button == 1 and modifiers == ["control"]:
                message = cmessages.ControlLClick((x, y))
            elif button == 3 and modifiers == ["shift"]:
                message = cmessages.ShiftRClick((x, y))
            elif button == 3 and modifiers == ["control"]:
                message = cmessages.ControlRClick((x, y))
            else:
                thisShouldNeverHappen(
                    "Unhandled modifiers for click: {}".format(modifiers))

            self.graphicsInterface.graphicsMessage(message.serialize())

    def handleMouseDragStart(self, buttonId, modifiers, startPos, endPos):
        log.debug("Start dragging from %s to %s", startPos, endPos)

        if buttonId == 1 and modifiers == []:
            assert self.selectionBoxOrigin is None
            self.selectionBoxOrigin = self.coordScreenTo3d(startPos)
            endPos = self.coordScreenTo3d(endPos)
            self.createSelectionBox(self.selectionBoxOrigin, endPos)

    def handleMouseDragMove(self, buttonId, modifiers, startPos, endPos):
        log.debug("Continue dragging from %s to %s", startPos, endPos)

        if buttonId == 1 and modifiers == []:
            assert self.selectionBoxOrigin is not None
            endPos = self.coordScreenTo3d(endPos)
            self.moveSelectionBox(self.selectionBoxOrigin, endPos)

    def handleMouseDragEnd(self, buttonId, modifiers, startPos, endPos):
        log.debug("End dragging from %s to %s", startPos, endPos)

        if buttonId == 1 and modifiers == []:
            # Actually select the units.
            endPos = self.coordScreenTo3d(endPos)
            # TODO[#55]: Use 3d graphics coords in messages so we don't have to
            # remove the z coordinates everywhere.
            message = cmessages.DragBox(self.selectionBoxOrigin[:2],
                                        endPos[:2])
            self.graphicsInterface.graphicsMessage(message.serialize())
            # Clear the selection box; we're done dragging.
            self.selectionBoxOrigin = None
            self.removeSelectionBox()

    def updateSelectionBox(self):
        if self.selectionBoxOrigin is not None:
            mousePos = self.getMousePos()
            if mousePos is not None:
                endPos = self.coordScreenTo3d(mousePos)
                self.moveSelectionBox(self.selectionBoxOrigin, endPos)

    def getMousePos(self):
        # Check if the mouse is over the window.
        if self.mouseWatcherNode.hasMouse():
            # Get the position.
            # Each coordinate is normalized to the interval [-1, 1].
            mousePoint = self.mouseWatcherNode.getMouse()
            # Create a copy of mousePoint rather than returning a reference to
            # it, because mousePoint will be modified in place by Panda.
            return (mousePoint.getX(), mousePoint.getY())
        else:
            return None

    def handleWindowClose(self):
        log.info("Window close requested -- shutting down client.")
        message = cmessages.RequestQuit()
        self.graphicsInterface.graphicsMessage(message.serialize())

    def setupEventHandlers(self):
        def pushKey(key, value):
            self.keys[key] = value

        for key in [
                "arrow_up", "arrow_left", "arrow_right", "arrow_down", "w",
                "a", "d", "s"
        ]:
            self.keys[key] = False
            self.accept(key, pushKey, [key, True])
            self.accept("shift-%s" % key, pushKey, [key, True])
            self.accept("%s-up" % key, pushKey, [key, False])

        # Camera toggle.
        self.accept("f3", self.toggleCameraStyle, [])
        self.accept("shift-f3", self.toggleCameraStyle, [])

        # Center view.
        self.accept("space", self.centerView, [])

        # Handle mouse wheel.
        self.accept("wheel_up", self.zoomCamera, [True])
        self.accept("wheel_down", self.zoomCamera, [False])

        # Handle clicking.
        self.accept("mouse1", self.pandaEventMouseDown, [1, []])
        self.accept("mouse1-up", self.pandaEventMouseUp, [1])
        # TODO: Make sure this is always the right mouse button.
        self.accept("mouse3", self.pandaEventMouseDown, [3, []])
        self.accept("mouse3-up", self.pandaEventMouseUp, [3])

        # Handle clicking with modifier keys.
        self.accept("shift-mouse1", self.pandaEventMouseDown, [1, ["shift"]])
        self.accept("control-mouse1", self.pandaEventMouseDown,
                    [1, ["control"]])
        self.accept("shift-mouse3", self.pandaEventMouseDown, [3, ["shift"]])
        self.accept("control-mouse3", self.pandaEventMouseDown,
                    [3, ["control"]])

        # Handle window close request (clicking the X, Alt-F4, etc.)
        self.win.set_close_request_event("window-close")
        self.accept("window-close", self.handleWindowClose)

    def coord3dToScreen(self, coord3d):
        # Empirically, Lens.project takes coordinates in the *camera*'s
        # coordinate system, not its parent or the render. This was not very
        # clear from the documentation, and you'd be surprised how long it took
        # us to figure this out. Anyway, we need to convert the point to be
        # relative to self.camera here; otherwise we'll get bizarre,
        # nonsensical, and hard-to-debug results.
        coord3d = self.camera.getRelativePoint(self.render, coord3d)
        screenCoord = Point2()
        if not self.camLens.project(coord3d, screenCoord):
            log.debug("Attempting 3d-to-screen conversion on point outside of "
                      "camera's viewing frustum.")

        # Convert to a tuple to ensure no one else is keeping a reference
        # around.
        x, y = screenCoord
        return (x, y)

    def coordScreenTo3d(self, screenCoord):
        x, y = screenCoord
        screenPoint = Point2(x, y)

        # Do this calculation using simple geometry, rather than the absurd
        # collision-traversal nonsense we used to use. Thanks to
        #     https://www.panda3d.org/forums/viewtopic.php?t=5409
        # for pointing us at the right methods to make this work.

        # Get two points along the ray extending from the camera, in the
        # direction of the mouse click.
        nearPoint = Point3()
        farPoint = Point3()
        self.camLens.extrude(screenPoint, nearPoint, farPoint)

        # These points are relative to the camera, so need to be converted to
        # be relative to the render. Thanks to the example code (see link
        # above) for saving us probably some hours of debugging figuring that
        # one out again :)
        nearPoint = self.render.getRelativePoint(self.camera, nearPoint)
        farPoint = self.render.getRelativePoint(self.camera, farPoint)

        intersection = Point3()
        if self.groundPlane.intersectsLine(intersection, nearPoint, farPoint):
            # Convert to a tuple to ensure no one else is keeping a reference
            # around.
            x, y, z = intersection
            return (x, y, z)

        # The ray didn't intersect the ground. This is almost certainly going
        # to happen at some point; all you have to do is find a way to aim the
        # camera (or manipulate the screen coordinate) so that the ray points
        # horizontally. But we don't have code to handle it, so for now just
        # abort.
        thisIsNotHandled()
Beispiel #21
0
def draw_lines(base):
    linesegs = LineSegs("lines")

    # Border
    xs, ys = base.map_size
    sequence = SequenceNode("border")
    for color in ((0, 0, 0.4, 1), (0, 0, 0.5, 1), (0, 0, 0.6, 1), (0, 0, 0.5,
                                                                   1)):
        linesegs.set_color(color)
        linesegs.set_thickness(3)
        linesegs.move_to((-xs, 0, 0))
        linesegs.draw_to((-xs, ys, 0))
        linesegs.draw_to((xs, ys, 0))
        linesegs.draw_to((xs, 0, 0))
        linesegs.draw_to((-xs, 0, 0))
        lines = linesegs.create()
        sequence.add_child(lines)
    sequence.loop(True)
    sequence.set_frame_rate(30)
    base.border = render.attach_new_node(sequence)
    for i in range(2):
        n = NodePath("border")
        base.border.instance_to(n)
        n.reparent_to(render)
        n.set_z(-(i * 5))

    # Mine cross
    base.models["lines"] = {}
    base.models["lines"]["cross"] = NodePath("cross")
    sequence = SequenceNode("cross")
    for color in ((1, 0, 0, 1), (1, 0, 1, 1), (0, 1, 0, 1), (1, 1, 0, 1)):
        linesegs.set_thickness(3)
        linesegs.move_to((1, 0, 0))
        linesegs.draw_to((-1, 0, 0))
        linesegs.move_to((0, 1, 0))
        linesegs.draw_to((0, -1, 0))
        linesegs.set_color(color)
        lines = linesegs.create()
        sequence.add_child(lines)
    sequence.loop(True)
    sequence.set_frame_rate(60)
    base.models["lines"]["cross"].attach_new_node(sequence)
    base.linesegs = linesegs
Beispiel #22
0
def line_art(sd, stemlet_length, stemlet_diameter, rest_segments, style):
    segs = LineSegs()
    segs.set_thickness(2.0)
    if style.stem:
        segs.set_color(style.stem)
        segs.move_to(0, 0, 0)
        segs.draw_to(0, 0, stemlet_length)

    # Ring around base
    if style.ring:
        # Segment base ring
        segs.set_color(style.ring)
        for r in range(style.ring_segs):
            from_v = r / style.ring_segs * 2 * math.pi
            to_v = (r + 1) / style.ring_segs * 2 * math.pi
            segs.move_to(
                math.sin(from_v) * stemlet_diameter,
                math.cos(from_v) * stemlet_diameter,
                0,
            )
            segs.draw_to(
                math.sin(to_v) * stemlet_diameter,
                math.cos(to_v) * stemlet_diameter,
                0,
            )

        # Endcap ring
        if rest_segments == 1:
            for r in range(style.ring_segs):
                from_v = r / style.ring_segs * 2 * math.pi
                to_v = (r + 1) / style.ring_segs * 2 * math.pi
                segs.move_to(
                    math.sin(from_v) * stemlet_diameter,
                    math.cos(from_v) * stemlet_diameter,
                    stemlet_length,
                )
                segs.draw_to(
                    math.sin(to_v) * stemlet_diameter,
                    math.cos(to_v) * stemlet_diameter,
                    stemlet_length,
                )

    # Bark
    if style.bark:
        segs.set_color(style.bark)
        for r in range(style.ring_segs):
            lobing = 1 + math.sin(2 * math.pi * sd.lobes * r / style.ring_segs)
            v = r / style.ring_segs * 2 * math.pi
            segs.move_to(
                math.sin(v) * stemlet_diameter * lobing,
                math.cos(v) * stemlet_diameter * lobing,
                0,
            )
            segs.draw_to(
                math.sin(v) * stemlet_diameter * lobing,
                math.cos(v) * stemlet_diameter * lobing,
                stemlet_length,
            )

    # x/y indicators
    if style.xyz_at_top:
        indicator_z = stemlet_length
    else:
        indicator_z = 0.0
    if style.x:
        segs.set_color(style.x)
        segs.move_to(0, 0, indicator_z)
        segs.draw_to(stemlet_diameter, 0, indicator_z)
    if style.y:
        segs.set_color(style.y)
        segs.move_to(0, 0, indicator_z)
        segs.draw_to(0, stemlet_diameter, indicator_z)

    return segs.create()
Beispiel #23
0
    def SetupModel(self, VUTProject):
        source = osr.SpatialReference()
        source.ImportFromEPSG(4326)
        target = osr.SpatialReference()
        target.ImportFromEPSG(int(self.OutEPSG))
        transform = osr.CoordinateTransformation(source, target)

        BBxMin = float(self.VRTBoundingBox.split(':')[0].split(',')[0])
        BByMin = float(self.VRTBoundingBox.split(':')[0].split(',')[1])
        BBxMax = float(self.VRTBoundingBox.split(':')[1].split(',')[0])
        BByMax = float(self.VRTBoundingBox.split(':')[1].split(',')[1])

        XLenght = BBxMax - BBxMin
        YLenght = BByMax - BByMin
        NewBBxMax = BBxMax + XLenght / 2
        NewBBxMin = BBxMin - XLenght / 2
        NewBByMax = BByMax + YLenght / 2
        NewBByMin = BByMin - YLenght / 2

        pointMax = ogr.Geometry(ogr.wkbPoint)
        pointMax.AddPoint(NewBBxMax, NewBByMax)
        pointMax.Transform(transform)

        pointMin = ogr.Geometry(ogr.wkbPoint)
        pointMin.AddPoint(NewBBxMin, NewBByMin)
        pointMin.Transform(transform)

        self.BoundingBoxStr = '-te ' + str(pointMin.GetX()) + ' ' + str(
            pointMin.GetY()) + ' ' + str(pointMax.GetX()) + ' ' + str(
                pointMax.GetY()) + ' '

        self.Moves = Sequence()
        Line = LineSegs('Path')
        with open(VUTProject, 'r') as File:
            Counter = 0
            i = 0
            PrevCourse = None
            PrevPos = None
            PrevHPr = None
            for line in File:
                if Counter < 6:
                    pass
                else:
                    line = line.split()
                    lat = float(line[0])
                    lon = float(line[1])
                    ele = float(line[2])
                    course = float(line[4])
                    pitch = float(line[5])
                    roll = float(line[6])
                    if course < 180:
                        course = -course
                    elif course > 180:
                        course = abs(course - 360)

                    point = ogr.Geometry(ogr.wkbPoint)
                    point.AddPoint(lon, lat)
                    point.Transform(transform)
                    if i == 0:
                        FirstPos = (point.GetX() - self.Origin[0],
                                    point.GetY() - self.Origin[1], ele)
                        FirstHpr = (course, pitch, roll)
                        self.cam.setPos(FirstPos)
                        self.cam.setHpr(FirstHpr)
                        Line.move_to(point.GetX() - self.Origin[0],
                                     point.GetY() - self.Origin[1], ele)
                    elif i == 1:
                        self.Moves.append(
                            LerpPosHprInterval(
                                self.cam,
                                1, (point.GetX() - self.Origin[0],
                                    point.GetY() - self.Origin[1], ele),
                                (fitDestAngle2Src(PrevCourse,
                                                  course), pitch, roll),
                                startPos=FirstPos,
                                startHpr=FirstHpr,
                                name='Interval',
                                other=self.render))
                        Line.draw_to(point.GetX() - self.Origin[0],
                                     point.GetY() - self.Origin[1], ele)
                    else:
                        self.Moves.append(
                            LerpPosHprInterval(
                                self.cam,
                                1, (point.GetX() - self.Origin[0],
                                    point.GetY() - self.Origin[1], ele),
                                (fitDestAngle2Src(PrevCourse,
                                                  course), pitch, roll),
                                startPos=PrevPos,
                                startHpr=PrevHPr,
                                name='Interval',
                                other=self.render))
                        Line.draw_to(point.GetX() - self.Origin[0],
                                     point.GetY() - self.Origin[1], ele)
                    i = i + 1
                    PrevCourse = course
                    PrevPos = (point.GetX() - self.Origin[0],
                               point.GetY() - self.Origin[1], ele)
                    PrevHPr = (course, pitch, roll)
                Counter = Counter + 1
                Line.setColor(1, 0.5, 0.5, 1)
                Line.setThickness(3)
                node = Line.create(False)
                nodePath = self.render.attachNewNode(node)
Beispiel #24
0
class WartsApp(ShowBase):
    def __init__(self, graphicsInterface, backend, gameState):
        ShowBase.__init__(self)

        self.graphicsInterface = graphicsInterface
        self.backend           = backend
        self.gameState         = gameState

        self.groundNodes = None
        self.firstTick = True


        # This is available as a global, but pylint gives an undefined-variable
        # warning if we use it that way. Looking at
        #     https://www.panda3d.org/manual/index.php/ShowBase
        # I would have thought we could reference it as either
        # self.globalClock, direct.showbase.ShowBase.globalClock, or possibly
        # direct.showbase.globalClock, but none of those seems to work. To
        # avoid the pylint warnings, create self.globalClock manually.
        self.globalClock = ClockObject.getGlobalClock()

        # Set up event handling.
        self.mouseState = {}
        self.keys = {}
        self.setupEventHandlers()

        # Set up camera control.
        self.cameraHolder = self.render.attachNewNode('CameraHolder')
        self.cameraHolder.setPos(0, 0, 100)
        self.prevCameraHpr = (0, -80, 0)
        self.usingCustomCamera = True
        self.setCameraCustom()

        self.prevMousePos = None
        self.selectionBox = None
        self.selectionBoxNode = None
        self.selectionBoxOrigin = None

        # Define the ground plane by a normal (+z) and a point (the origin).
        self.groundPlane = core.Plane(core.Vec3(0, 0, 1), core.Point3(0, 0, 0))


        graphicsInterface.graphicsReady(self)

    def cleanup(self):
        pass

    # For backward compatibility.
    # TODO[#84]: Remove when old graphics goes away; have backend just call
    # tick() directly.
    def interfaceMessage(self, data):
        message = deserializeMessage(data)
        if isinstance(message, messages.Tick):
            self.tick()

        # Ignore everything else.

    def tick(self):
        # TODO: Multiple levels of log.debug. For now, this is too spammy, so
        # skip it.
        # log.debug("Graphics: tick()")

        if self.firstTick:
            if not self.gameState.hasSize:
                log.error("GameState must be assigned a size before first "
                          "tick().")
                return
            width, height = self.gameState.sizeInChunks
            self.groundNodes = [[None for _x in range(height)]
                                for _y in range(width)]
            for cx in range(width):
                for cy in range(height):
                    self.addGround((cx, cy),
                                   self.gameState.groundTypes[cx][cy])

            self.firstTick = False

        # For now, just call this every tick. Optimize later.
        self.rescanUnits()

    def addGround(self, chunkIndex, terrainType):
        cx, cy = chunkIndex
        wPos = Coord.fromCBU(chunk=(chunkIndex))

        if terrainType == 0:
            modelName = "green-ground.egg"
        else:
            modelName = "red-ground.egg"
            if terrainType != 1:
                log.warn("Unrecognized terrain type %d", terrainType)

        gPos1 = worldToGraphicsPos(wPos)
        gPos2 = worldToGraphicsPos(wPos +
                                   Distance.fromCBU(chunk=(1,1)))

        # Figure out where we want the tile.
        goalCenterX = 0.5 * (gPos2[0] + gPos1[0])
        goalCenterY = 0.5 * (gPos2[1] + gPos1[1])
        goalWidthX  =    abs(gPos2[0] - gPos1[0])
        goalWidthY  =    abs(gPos2[1] - gPos1[1])

        model = self.loader.loadModel(getModelPath(modelName))

        # Put the model in the scene, but don't position it yet.
        rootNode = self.render.attachNewNode("")
        model.reparentTo(rootNode)

        # Rescale the model about its origin. The x and y coordinates of the
        # model's origin should be chosen as wherever it looks like the model's
        # center of mass is, so that rotation about the origin (in the xy
        # plane) feels natural.
        # TODO[#9]: Set a convention for model bounds so we don't have to do a
        # getTightBounds every time. This is dumb.
        # TODO[#3]: Or, as an alternative shorter-term solution, just define a
        # scale in the config files for the few models that aren't ours.
        bound1, bound2 = model.getTightBounds()
        modelWidthX = abs(bound2[0] - bound1[0])
        modelWidthY = abs(bound2[1] - bound1[1])

        # Scale it to the largest it can be while still fitting within the goal
        # rect. If the aspect ratio of the goal rect is different from that of
        # the model, then it'll only fill that rect in one dimension.
        # altScaleFactor is used for sanity checks below.
        scaleFactor, altScaleFactor = minmax(goalWidthX / modelWidthX,
                                             goalWidthY / modelWidthY)

        # Sanity check the scale factor.
        if scaleFactor <= 0.0:
            if scaleFactor == 0.0:
                log.warn("Ground %s will be scaled negatively!", chunkIndex)
            else:
                log.warn("Ground %s will be scaled to zero size.", chunkIndex)
        else:
            # TODO[#9]: Currently the example panda triggers this warning.
            # TODO[#3]: Magic numbers bad.
            if altScaleFactor / scaleFactor > 1.001:
                log.warn("Ground %s has different aspect ratio than "
                         "its model: model of size %.3g x %.3g being scaled "
                         "into %.3g x %.3g.",
                         chunkIndex, modelWidthX, modelWidthY,
                         goalWidthX, goalWidthY)

        model.setScale(scaleFactor)

        # Place the model at z=0. The model's origin should be placed so that
        # this looks natural -- for most units this means it should be right at
        # the bottom of the model, but if we add any units that are intended to
        # float above the ground, then this can be accomplished by just
        # positioning the model above its origin.
        rootNode.setPos(goalCenterX, goalCenterY, 0.0)

        self.groundNodes[cx][cy] = rootNode

    def rescanUnits(self):
        """
        Check for units that have moved or been added/removed since the last
        scan. Update the display accordingly.
        """

        # TODO: Actually write this.
        pass

        # Divide all unit ids from my own list of units and the gamestate's
        # current list into three parts:
        #   - ids in both lists (potential moves)
        #   - ids only in the gamestate's list (additions)
        #   - ids only in my list (removals)
        #
        # Call:
        #     moveUnit()   for each potential move
        #     addUnit()    for each addition
        #     removeUnit() for each removal

    def moveUnit(self, uid, newPos):

        # TODO: Actually write this.
        pass

        # Set unit's node to move to new position.
        # Play "walk" animation
        # ...whatever else we used to do?

    def addUnit(self, uid, pos):

        # TODO: Actually write this.
        pass

        # Create unit/model/node as we used to.
        # Also add it to our uid->unit mapping.

    def removeUnit(self, uid, pos):

        # TODO: Actually write this.
        pass

        # Remove+cleanup unit/model/node as we used to.
        # Also remove it from our uid->unit mapping.

    def createSelectionBox(self, corner1, corner2):
        """
        Create a selection "box" given the coordinates of two opposite corners.
        The corners are given in world coordinates (well, 3d graphics
        coordinates).
        """

        assert self.selectionBox is None

        p1, p2, p3, p4 = self.convert3dBoxToScreen(corner1, corner2)
        x1, y1 = p1
        x2, y2 = p2
        x3, y3 = p3
        x4, y4 = p4

        # TODO[#3]: Magic numbers bad.
        self.selectionBox = LineSegs("SelectionBox")
        self.selectionBox.setThickness(3.0)
        self.selectionBox.setColor(0.0, 1.0, 0.25, 1.0)
        self.selectionBox.move_to(x1, 0, y1)
        self.selectionBox.draw_to(x2, 0, y2)
        self.selectionBox.draw_to(x3, 0, y3)
        self.selectionBox.draw_to(x4, 0, y4)
        self.selectionBox.draw_to(x1, 0, y1)

        self.selectionBoxNode = self.render2d.attachNewNode(
            self.selectionBox.create())

    def moveSelectionBox(self, corner1, corner2):
        assert self.selectionBox is not None

        p1, p2, p3, p4 = self.convert3dBoxToScreen(corner1, corner2)
        x1, y1 = p1
        x2, y2 = p2
        x3, y3 = p3
        x4, y4 = p4

        self.selectionBox.setVertex(0, x1, 0, y1)
        self.selectionBox.setVertex(1, x2, 0, y2)
        self.selectionBox.setVertex(2, x3, 0, y3)
        self.selectionBox.setVertex(3, x4, 0, y4)
        self.selectionBox.setVertex(4, x1, 0, y1)

    def removeSelectionBox(self):
        self.selectionBoxNode.removeNode()
        self.selectionBox     = None
        self.selectionBoxNode = None

    def convert3dBoxToScreen(self, corner1, corner3):
        """
        Return screen coordinates of the 4 corners of a box, given in 3d
        coordinates. The box is specified using 2 opposite corners.
        """

        wx1, wy1, wz1 = corner1
        wx3, wy3, wz3 = corner3

        wx2, wy2 = (wx1, wy3)
        wx4, wy4 = (wx3, wy1)

        # Note: corner1 and corner2 could have nonzero z because floating-point
        # calculations, but they should at least be close. We'll just average
        # their z and not worry about it.
        wz2 = wz4 = 0.5 * (wz1 + wz3)

        p1 = self.coord3dToScreen((wx1, wy1, wz1))
        p2 = self.coord3dToScreen((wx2, wy2, wz2))
        p3 = self.coord3dToScreen((wx3, wy3, wz3))
        p4 = self.coord3dToScreen((wx4, wy4, wz4))

        return (p1, p2, p3, p4)

    def setCameraCustom(self):
        """
        Change to using our custom task to control the camera.
        """

        # Disable the default mouse-based camera control task, so we don't have
        # to fight with it for control of the camera.
        self.disableMouse()

        # Face the camera in the appropriate angle.
        self.camera.setHpr(self.prevCameraHpr)

        # Put it in the same location as the cameraHolder, and make it stay
        # put relative to the cameraHolder (so we can move the camera around by
        # changing the cameraHolder's position).
        self.camera.reparentTo(self.cameraHolder)
        self.camera.setPos(0, 0, 0)

        # Substitute our own camera control task.
        self.taskMgr.add(self.updateCameraTask, "UpdateCameraTask")

        self.usingCustomCamera = True

        # Need a task to handle mouse-dragging because there doesn't seem to be
        # a built-in mouseMove event.
        self.taskMgr.add(self.mouseMoveTask, "MouseMoveTask")

    def setCameraDefault(self):
        """
        Change to using the default mouse-based camera controls.
        """

        self.taskMgr.remove("UpdateCameraTask")

        # Save current location for when this control style is restored.
        self.prevCameraHpr = self.camera.getHpr()

        # Use the existing camera location, rather than jumping back to the one
        # from last time the default camera controller was active.
        # Copied from https://www.panda3d.org/manual/index.php/Mouse_Support
        mat = Mat4(self.camera.getMat())
        mat.invertInPlace()
        self.mouseInterfaceNode.setMat(mat)
        self.enableMouse()

        self.usingCustomCamera = False

    def toggleCameraStyle(self):
        """
        Switch to whichever style of camera control isn't currently active.
        """

        if self.usingCustomCamera:
            self.setCameraDefault()
        else:
            self.setCameraCustom()

    # We don't use task, but we can't remove it because the function signature
    # is from Panda3D.
    def updateCameraTask(self, task):  # pylint: disable=unused-argument
        """
        Move the camera sensibly.
        """

        dt = self.globalClock.getDt()
        translateSpeed = 30 * dt
        rotateSpeed    = 50 * dt

        # Separately track whether the camera should translate in each of the 4
        # directions. These 4 are initialized based on the various inputs that
        # might tell us to scroll, and different inputs saying the same thing
        # don't stack. That way if we get inputs saying both "left" and
        # "right", they can cancel and the camera just won't move along that
        # axis -- even if, say, there are two inputs saying "left" and only one
        # saying "right'.
        moveLeft  = self.keys["arrow_left"]
        moveRight = self.keys["arrow_right"]
        moveUp    = self.keys["arrow_up"]
        moveDown  = self.keys["arrow_down"]

        # Check if the mouse is over the window.
        if self.mouseWatcherNode.hasMouse():
            # Get the position.
            # Each coordinate is normalized to the interval [-1, 1].
            mousePos = self.mouseWatcherNode.getMouse()
            xPos, yPos = mousePos.getX(), mousePos.getY()
            # Only move if the mouse is close to the edge, and actually within
            # the window.
            if  (1.0 - EDGE_SCROLL_WIDTH) < xPos <=  1.0:
                moveRight = 1
            if -(1.0 - EDGE_SCROLL_WIDTH) > xPos >= -1.0:
                moveLeft  = 1
            if  (1.0 - EDGE_SCROLL_WIDTH) < yPos <=  1.0:
                moveUp    = 1
            if -(1.0 - EDGE_SCROLL_WIDTH) > yPos >= -1.0:
                moveDown  = 1

        forward  = translateSpeed * (moveUp    - moveDown)
        sideways = translateSpeed * (moveRight - moveLeft)
        self.cameraHolder.setPos(self.cameraHolder, sideways, forward, 0)

        # Selection box logic
        if sideways != 0 or forward != 0:
            self.updateSelectionBox()

        rotate = rotateSpeed * (self.keys["a"] - self.keys["d"])
        self.cameraHolder.setHpr(self.cameraHolder, rotate, 0, 0)

        return Task.cont

    def zoomCamera(self, inward):
        """
        Zoom in or out.
        """

        dt = self.globalClock.getDt()
        zoomSpeed = 100 * dt

        zoom = -zoomSpeed if inward else zoomSpeed
        self.cameraHolder.setPos(self.cameraHolder, 0, 0, zoom)

    # We don't use task, but we can't remove it because the function signature
    # is from Panda3D.
    def mouseMoveTask(self, task):  # pylint: disable=unused-argument
        """
        Handle mouse movement.
        """

        mousePos = self.getMousePos()

        # NOTE: We don't handle clicking and dragging at the same time.
        if mousePos is not None and mousePos != self.prevMousePos:
            for (buttonId, state) in self.mouseState.iteritems():
                state.lastPos = mousePos
                if state.hasMoved:
                    self.handleMouseDragMove(buttonId, state.modifiers,
                                             state.startPos, mousePos)
                else:
                    startX, startY = state.startPos
                    mouseX, mouseY = mousePos
                    distance = math.hypot(mouseX - startX, mouseY - startY)
                    # TODO[#3]: Magic numbers bad.
                    # Check if the mouse has moved outside the dead zone.
                    if distance > 0.0314:
                        self.handleMouseDragStart(buttonId, state.modifiers,
                                                  state.startPos, mousePos)
                        state.hasMoved = True

        if mousePos != self.prevMousePos:
            self.prevMousePos = mousePos

        return Task.cont

    def pandaEventMouseDown(self, buttonId, modifiers):
        log.debug("Mouse down: button %s w/ mod %s", buttonId, modifiers)
        if buttonId in self.mouseState:
            # Call pandaEventMouseUp just to clear any state related to the
            # button being down, so we can handle this buttonDown event as if
            # it were a fresh press of the button.
            log.warn("Mouse button %s is already down.", buttonId)
            self.pandaEventMouseUp(buttonId)

        assert buttonId not in self.mouseState

        state = MouseButtonState(modifiers[:], self.getMousePos())
        self.mouseState[buttonId] = state

    def pandaEventMouseUp(self, buttonId):
        log.debug("Mouse up: button %s", buttonId)
        if buttonId not in self.mouseState:
            # Drop the event, since there's nothing to do.
            log.warn("Mouse button %s is already up.", buttonId)
            return

        state = self.mouseState[buttonId]

        if state.hasMoved:
            endPos = self.getMousePos()
            if endPos is None:
                endPos = state.lastPos
            self.handleMouseDragEnd(buttonId, state.modifiers,
                                    state.startPos, endPos)
        else:
            self.handleMouseClick(buttonId, state.modifiers, state.startPos)

        del self.mouseState[buttonId]

    def handleMouseClick(self, button, modifiers, pos):
        # Make sure the mouse is inside the screen
        # TODO: Move this check to pandaEventMouseUp?
        if self.mouseWatcherNode.hasMouse() and self.usingCustomCamera:
            x, y, _z = self.coordScreenTo3d(pos)
            uPos = graphicsToWorldPos((x, y))
            self.backend.worldClick(uPos, button, modifiers)

    def handleMouseDragStart(self, buttonId, modifiers, startPos, endPos):
        log.debug("Start dragging from %s to %s", startPos, endPos)

        if buttonId == 1 and modifiers == []:
            assert self.selectionBoxOrigin is None
            self.selectionBoxOrigin = self.coordScreenTo3d(startPos)
            endPos = self.coordScreenTo3d(endPos)
            self.createSelectionBox(self.selectionBoxOrigin, endPos)

    def handleMouseDragMove(self, buttonId, modifiers, startPos, endPos):
        log.debug("Continue dragging from %s to %s", startPos, endPos)

        if buttonId == 1 and modifiers == []:
            assert self.selectionBoxOrigin is not None
            endPos = self.coordScreenTo3d(endPos)
            self.moveSelectionBox(self.selectionBoxOrigin, endPos)

    def handleMouseDragEnd(self, buttonId, modifiers, startPos, endPos):
        log.debug("End dragging from %s to %s", startPos, endPos)

        # TODO: Do we need this check? What is the effect of only calling
        # removeSelectionBox() under this check?
        if buttonId == 1 and modifiers == []:
            # Actually select the units.
            startGPos = self.selectionBoxOrigin[:2]
            endGPos   = self.coordScreenTo3d(endPos)[:2]
            startUPos = graphicsToWorldPos(startGPos)
            endUPos   = graphicsToWorldPos(endGPos)
            self.backend.worldDrag(startUPos, endUPos, buttonId, modifiers)

            # Clear the selection box; we're done dragging.
            self.selectionBoxOrigin = None
            self.removeSelectionBox()

    def updateSelectionBox(self):
        if self.selectionBoxOrigin is not None:
            mousePos = self.getMousePos()
            if mousePos is not None:
                endPos = self.coordScreenTo3d(mousePos)
                self.moveSelectionBox(self.selectionBoxOrigin, endPos)

    def getMousePos(self):
        # Check if the mouse is over the window.
        if self.mouseWatcherNode.hasMouse():
            # Get the position.
            # Each coordinate is normalized to the interval [-1, 1].
            mousePoint = self.mouseWatcherNode.getMouse()
            # Create a copy of mousePoint rather than returning a reference to
            # it, because mousePoint will be modified in place by Panda.
            return (mousePoint.getX(), mousePoint.getY())
        else:
            return None

    def handleWindowClose(self):
        log.info("Window close requested -- shutting down client.")
        # When in Rome, send messages like the Romans do, I guess.
        # TODO: Get rid of messages, I think.
        message = cmessages.RequestQuit()
        self.graphicsInterface.graphicsMessage(message.serialize())

    def setupEventHandlers(self):
        def pushKey(key, value):
            self.keys[key] = value

        for key in ["arrow_up", "arrow_left", "arrow_right", "arrow_down",
                    "w", "a", "d", "s"]:
            self.keys[key] = False
            self.accept(key, pushKey, [key, True])
            self.accept("shift-%s" % key, pushKey, [key, True])
            self.accept("%s-up" % key, pushKey, [key, False])

        # Camera toggle.
        self.accept("f3",       self.toggleCameraStyle, [])
        self.accept("shift-f3", self.toggleCameraStyle, [])

        # Center view.
        # self.accept("space", self.centerView, []) -- TODO

        # Handle mouse wheel.
        self.accept("wheel_up", self.zoomCamera, [True])
        self.accept("wheel_down", self.zoomCamera, [False])

        # Handle clicking.
        self.accept("mouse1",    self.pandaEventMouseDown, [1, []])
        self.accept("mouse1-up", self.pandaEventMouseUp,   [1])
        # TODO: Make sure this is always the right mouse button.
        self.accept("mouse3",    self.pandaEventMouseDown, [3, []])
        self.accept("mouse3-up", self.pandaEventMouseUp,   [3])

        # Handle clicking with modifier keys.
        self.accept("shift-mouse1",   self.pandaEventMouseDown,
                    [1, ["shift"]])
        self.accept("control-mouse1", self.pandaEventMouseDown,
                    [1, ["control"]])
        self.accept("shift-mouse3",   self.pandaEventMouseDown,
                    [3, ["shift"]])
        self.accept("control-mouse3", self.pandaEventMouseDown,
                    [3, ["control"]])

        # Handle window close request (clicking the X, Alt-F4, etc.)
        self.win.set_close_request_event("window-close")
        self.accept("window-close", self.handleWindowClose)

    def coord3dToScreen(self, coord3d):
        # Empirically, Lens.project takes coordinates in the *camera*'s
        # coordinate system, not its parent or the render. This was not very
        # clear from the documentation, and you'd be surprised how long it took
        # us to figure this out. Anyway, we need to convert the point to be
        # relative to self.camera here; otherwise we'll get bizarre,
        # nonsensical, and hard-to-debug results.
        coord3d = self.camera.getRelativePoint(self.render, coord3d)
        screenCoord = Point2()
        if not self.camLens.project(coord3d, screenCoord):
            log.debug("Attempting 3d-to-screen conversion on point outside of "
                      "camera's viewing frustum.")

        # Convert to a tuple to ensure no one else is keeping a reference
        # around.
        x, y = screenCoord
        return (x, y)

    def coordScreenTo3d(self, screenCoord):
        x, y = screenCoord
        screenPoint = Point2(x, y)

        # Do this calculation using simple geometry, rather than the absurd
        # collision-traversal nonsense we used to use. Thanks to
        #     https://www.panda3d.org/forums/viewtopic.php?t=5409
        # for pointing us at the right methods to make this work.

        # Get two points along the ray extending from the camera, in the
        # direction of the mouse click.
        nearPoint = Point3()
        farPoint = Point3()
        self.camLens.extrude(screenPoint, nearPoint, farPoint)

        # These points are relative to the camera, so need to be converted to
        # be relative to the render. Thanks to the example code (see link
        # above) for saving us probably some hours of debugging figuring that
        # one out again :)
        nearPoint = self.render.getRelativePoint(self.camera, nearPoint)
        farPoint  = self.render.getRelativePoint(self.camera, farPoint)

        intersection = Point3()
        if self.groundPlane.intersectsLine(intersection, nearPoint, farPoint):
            # Convert to a tuple to ensure no one else is keeping a reference
            # around.
            x, y, z = intersection
            return (x, y, z)

        # The ray didn't intersect the ground. This is almost certainly going
        # to happen at some point; all you have to do is find a way to aim the
        # camera (or manipulate the screen coordinate) so that the ray points
        # horizontally. But we don't have code to handle it, so for now just
        # abort.
        thisIsNotHandled()
Beispiel #25
0
    bottomLeftCurve = rotateCurveRight(transformCurve(
        points, 0.5, -0.5))  # (2n+c)+(n+c) + c
    topLeftCurve = transformCurve(points, -0.5, 0.5)  # n + c
    topRightCurve = transformCurve(points, 0.5, 0.5)  # n + c
    bottomRightCurve = rotateCurveLeft(transformCurve(
        points, -0.5, -0.5))  # (2n+c)+(n+c) + c
    for p in bottomLeftCurve:  # n
        newCurve.append(p)  # c
    for p in topLeftCurve:  # n
        newCurve.append(p)  # c
    for p in topRightCurve:  # n
        newCurve.append(p)  # c
    for p in bottomRightCurve:  # n
        newCurve.append(p)  # c
    points = newCurve  # c

for p in range(len(points) - 1):  # n
    seg = LineSegs()  # c
    seg.setThickness(3)  # c
    seg.draw_to(points[p][0], 0, points[p][2])  # c
    seg.draw_to(points[p + 1][0], 0, points[p + 1][2])  # c
    node = seg.create()  # c
    nodes.append(node)  # c

for node in nodes:  # n
    base.aspect2d.attach_new_node(node)  # c

base.run()  # c

# Total computational cost: d(12n) + 2n + c
Beispiel #26
0
def makeCubeOutline(mins, maxs, color, thickness=1.0):
    lines = LineSegs()
    lines.setColor(color)
    lines.setThickness(thickness)
    lines.move_to(mins)
    lines.draw_to(Point3(mins.get_x(), mins.get_y(), maxs.get_z()))
    lines.draw_to(Point3(mins.get_x(), maxs.get_y(), maxs.get_z()))
    lines.draw_to(Point3(mins.get_x(), maxs.get_y(), mins.get_z()))
    lines.draw_to(mins)
    lines.draw_to(Point3(maxs.get_x(), mins.get_y(), mins.get_z()))
    lines.draw_to(Point3(maxs.get_x(), mins.get_y(), maxs.get_z()))
    lines.draw_to(Point3(mins.get_x(), mins.get_y(), maxs.get_z()))
    lines.move_to(Point3(maxs.get_x(), mins.get_y(), maxs.get_z()))
    lines.draw_to(maxs)
    lines.draw_to(Point3(mins.get_x(), maxs.get_y(), maxs.get_z()))
    lines.move_to(maxs)
    lines.draw_to(Point3(maxs.get_x(), maxs.get_y(), mins.get_z()))
    lines.draw_to(Point3(mins.get_x(), maxs.get_y(), mins.get_z()))
    lines.move_to(Point3(maxs.get_x(), maxs.get_y(), mins.get_z()))
    lines.draw_to(Point3(maxs.get_x(), mins.get_y(), mins.get_z()))
    return lines.create()
Beispiel #27
0
class WartsApp(ShowBase):
    def __init__(self, graphicsInterface, backend, gameState):
        ShowBase.__init__(self)

        self.graphicsInterface = graphicsInterface
        self.backend = backend
        self.gameState = gameState

        self.groundNodes = None
        self.firstTick = True

        # This is available as a global, but pylint gives an undefined-variable
        # warning if we use it that way. Looking at
        #     https://www.panda3d.org/manual/index.php/ShowBase
        # I would have thought we could reference it as either
        # self.globalClock, direct.showbase.ShowBase.globalClock, or possibly
        # direct.showbase.globalClock, but none of those seems to work. To
        # avoid the pylint warnings, create self.globalClock manually.
        self.globalClock = ClockObject.getGlobalClock()

        # Set up event handling.
        self.mouseState = {}
        self.keys = {}
        self.setupEventHandlers()

        # Set up camera control.
        self.cameraHolder = self.render.attachNewNode('CameraHolder')
        self.cameraHolder.setPos(0, 0, 100)
        self.prevCameraHpr = (0, -80, 0)
        self.usingCustomCamera = True
        self.setCameraCustom()

        self.prevMousePos = None
        self.selectionBox = None
        self.selectionBoxNode = None
        self.selectionBoxOrigin = None

        # Define the ground plane by a normal (+z) and a point (the origin).
        self.groundPlane = core.Plane(core.Vec3(0, 0, 1), core.Point3(0, 0, 0))

        graphicsInterface.graphicsReady(self)

    def cleanup(self):
        pass

    # For backward compatibility.
    # TODO[#84]: Remove when old graphics goes away; have backend just call
    # tick() directly.
    def interfaceMessage(self, data):
        message = deserializeMessage(data)
        if isinstance(message, messages.Tick):
            self.tick()

        # Ignore everything else.

    def tick(self):
        # TODO: Multiple levels of log.debug. For now, this is too spammy, so
        # skip it.
        # log.debug("Graphics: tick()")

        if self.firstTick:
            if not self.gameState.hasSize:
                log.error("GameState must be assigned a size before first "
                          "tick().")
                return
            width, height = self.gameState.sizeInChunks
            self.groundNodes = [[None for _x in range(height)]
                                for _y in range(width)]
            for cx in range(width):
                for cy in range(height):
                    self.addGround((cx, cy),
                                   self.gameState.groundTypes[cx][cy])

            self.firstTick = False

        # For now, just call this every tick. Optimize later.
        self.rescanUnits()

    def addGround(self, chunkIndex, terrainType):
        cx, cy = chunkIndex
        wPos = Coord.fromCBU(chunk=(chunkIndex))

        if terrainType == 0:
            modelName = "green-ground.egg"
        else:
            modelName = "red-ground.egg"
            if terrainType != 1:
                log.warn("Unrecognized terrain type %d", terrainType)

        gPos1 = worldToGraphicsPos(wPos)
        gPos2 = worldToGraphicsPos(wPos + Distance.fromCBU(chunk=(1, 1)))

        # Figure out where we want the tile.
        goalCenterX = 0.5 * (gPos2[0] + gPos1[0])
        goalCenterY = 0.5 * (gPos2[1] + gPos1[1])
        goalWidthX = abs(gPos2[0] - gPos1[0])
        goalWidthY = abs(gPos2[1] - gPos1[1])

        model = self.loader.loadModel(getModelPath(modelName))

        # Put the model in the scene, but don't position it yet.
        rootNode = self.render.attachNewNode("")
        model.reparentTo(rootNode)

        # Rescale the model about its origin. The x and y coordinates of the
        # model's origin should be chosen as wherever it looks like the model's
        # center of mass is, so that rotation about the origin (in the xy
        # plane) feels natural.
        # TODO[#9]: Set a convention for model bounds so we don't have to do a
        # getTightBounds every time. This is dumb.
        # TODO[#3]: Or, as an alternative shorter-term solution, just define a
        # scale in the config files for the few models that aren't ours.
        bound1, bound2 = model.getTightBounds()
        modelWidthX = abs(bound2[0] - bound1[0])
        modelWidthY = abs(bound2[1] - bound1[1])

        # Scale it to the largest it can be while still fitting within the goal
        # rect. If the aspect ratio of the goal rect is different from that of
        # the model, then it'll only fill that rect in one dimension.
        # altScaleFactor is used for sanity checks below.
        scaleFactor, altScaleFactor = minmax(goalWidthX / modelWidthX,
                                             goalWidthY / modelWidthY)

        # Sanity check the scale factor.
        if scaleFactor <= 0.0:
            if scaleFactor == 0.0:
                log.warn("Ground %s will be scaled negatively!", chunkIndex)
            else:
                log.warn("Ground %s will be scaled to zero size.", chunkIndex)
        else:
            # TODO[#9]: Currently the example panda triggers this warning.
            # TODO[#3]: Magic numbers bad.
            if altScaleFactor / scaleFactor > 1.001:
                log.warn(
                    "Ground %s has different aspect ratio than "
                    "its model: model of size %.3g x %.3g being scaled "
                    "into %.3g x %.3g.", chunkIndex, modelWidthX, modelWidthY,
                    goalWidthX, goalWidthY)

        model.setScale(scaleFactor)

        # Place the model at z=0. The model's origin should be placed so that
        # this looks natural -- for most units this means it should be right at
        # the bottom of the model, but if we add any units that are intended to
        # float above the ground, then this can be accomplished by just
        # positioning the model above its origin.
        rootNode.setPos(goalCenterX, goalCenterY, 0.0)

        self.groundNodes[cx][cy] = rootNode

    def rescanUnits(self):
        """
        Check for units that have moved or been added/removed since the last
        scan. Update the display accordingly.
        """

        # TODO: Actually write this.
        pass

        # Divide all unit ids from my own list of units and the gamestate's
        # current list into three parts:
        #   - ids in both lists (potential moves)
        #   - ids only in the gamestate's list (additions)
        #   - ids only in my list (removals)
        #
        # Call:
        #     moveUnit()   for each potential move
        #     addUnit()    for each addition
        #     removeUnit() for each removal

    def moveUnit(self, uid, newPos):

        # TODO: Actually write this.
        pass

        # Set unit's node to move to new position.
        # Play "walk" animation
        # ...whatever else we used to do?

    def addUnit(self, uid, pos):

        # TODO: Actually write this.
        pass

        # Create unit/model/node as we used to.
        # Also add it to our uid->unit mapping.

    def removeUnit(self, uid, pos):

        # TODO: Actually write this.
        pass

        # Remove+cleanup unit/model/node as we used to.
        # Also remove it from our uid->unit mapping.

    def createSelectionBox(self, corner1, corner2):
        """
        Create a selection "box" given the coordinates of two opposite corners.
        The corners are given in world coordinates (well, 3d graphics
        coordinates).
        """

        assert self.selectionBox is None

        p1, p2, p3, p4 = self.convert3dBoxToScreen(corner1, corner2)
        x1, y1 = p1
        x2, y2 = p2
        x3, y3 = p3
        x4, y4 = p4

        # TODO[#3]: Magic numbers bad.
        self.selectionBox = LineSegs("SelectionBox")
        self.selectionBox.setThickness(3.0)
        self.selectionBox.setColor(0.0, 1.0, 0.25, 1.0)
        self.selectionBox.move_to(x1, 0, y1)
        self.selectionBox.draw_to(x2, 0, y2)
        self.selectionBox.draw_to(x3, 0, y3)
        self.selectionBox.draw_to(x4, 0, y4)
        self.selectionBox.draw_to(x1, 0, y1)

        self.selectionBoxNode = self.render2d.attachNewNode(
            self.selectionBox.create())

    def moveSelectionBox(self, corner1, corner2):
        assert self.selectionBox is not None

        p1, p2, p3, p4 = self.convert3dBoxToScreen(corner1, corner2)
        x1, y1 = p1
        x2, y2 = p2
        x3, y3 = p3
        x4, y4 = p4

        self.selectionBox.setVertex(0, x1, 0, y1)
        self.selectionBox.setVertex(1, x2, 0, y2)
        self.selectionBox.setVertex(2, x3, 0, y3)
        self.selectionBox.setVertex(3, x4, 0, y4)
        self.selectionBox.setVertex(4, x1, 0, y1)

    def removeSelectionBox(self):
        self.selectionBoxNode.removeNode()
        self.selectionBox = None
        self.selectionBoxNode = None

    def convert3dBoxToScreen(self, corner1, corner3):
        """
        Return screen coordinates of the 4 corners of a box, given in 3d
        coordinates. The box is specified using 2 opposite corners.
        """

        wx1, wy1, wz1 = corner1
        wx3, wy3, wz3 = corner3

        wx2, wy2 = (wx1, wy3)
        wx4, wy4 = (wx3, wy1)

        # Note: corner1 and corner2 could have nonzero z because floating-point
        # calculations, but they should at least be close. We'll just average
        # their z and not worry about it.
        wz2 = wz4 = 0.5 * (wz1 + wz3)

        p1 = self.coord3dToScreen((wx1, wy1, wz1))
        p2 = self.coord3dToScreen((wx2, wy2, wz2))
        p3 = self.coord3dToScreen((wx3, wy3, wz3))
        p4 = self.coord3dToScreen((wx4, wy4, wz4))

        return (p1, p2, p3, p4)

    def setCameraCustom(self):
        """
        Change to using our custom task to control the camera.
        """

        # Disable the default mouse-based camera control task, so we don't have
        # to fight with it for control of the camera.
        self.disableMouse()

        # Face the camera in the appropriate angle.
        self.camera.setHpr(self.prevCameraHpr)

        # Put it in the same location as the cameraHolder, and make it stay
        # put relative to the cameraHolder (so we can move the camera around by
        # changing the cameraHolder's position).
        self.camera.reparentTo(self.cameraHolder)
        self.camera.setPos(0, 0, 0)

        # Substitute our own camera control task.
        self.taskMgr.add(self.updateCameraTask, "UpdateCameraTask")

        self.usingCustomCamera = True

        # Need a task to handle mouse-dragging because there doesn't seem to be
        # a built-in mouseMove event.
        self.taskMgr.add(self.mouseMoveTask, "MouseMoveTask")

    def setCameraDefault(self):
        """
        Change to using the default mouse-based camera controls.
        """

        self.taskMgr.remove("UpdateCameraTask")

        # Save current location for when this control style is restored.
        self.prevCameraHpr = self.camera.getHpr()

        # Use the existing camera location, rather than jumping back to the one
        # from last time the default camera controller was active.
        # Copied from https://www.panda3d.org/manual/index.php/Mouse_Support
        mat = Mat4(self.camera.getMat())
        mat.invertInPlace()
        self.mouseInterfaceNode.setMat(mat)
        self.enableMouse()

        self.usingCustomCamera = False

    def toggleCameraStyle(self):
        """
        Switch to whichever style of camera control isn't currently active.
        """

        if self.usingCustomCamera:
            self.setCameraDefault()
        else:
            self.setCameraCustom()

    # We don't use task, but we can't remove it because the function signature
    # is from Panda3D.
    def updateCameraTask(self, task):  # pylint: disable=unused-argument
        """
        Move the camera sensibly.
        """

        dt = self.globalClock.getDt()
        translateSpeed = 30 * dt
        rotateSpeed = 50 * dt

        # Separately track whether the camera should translate in each of the 4
        # directions. These 4 are initialized based on the various inputs that
        # might tell us to scroll, and different inputs saying the same thing
        # don't stack. That way if we get inputs saying both "left" and
        # "right", they can cancel and the camera just won't move along that
        # axis -- even if, say, there are two inputs saying "left" and only one
        # saying "right'.
        moveLeft = self.keys["arrow_left"]
        moveRight = self.keys["arrow_right"]
        moveUp = self.keys["arrow_up"]
        moveDown = self.keys["arrow_down"]

        # Check if the mouse is over the window.
        if self.mouseWatcherNode.hasMouse():
            # Get the position.
            # Each coordinate is normalized to the interval [-1, 1].
            mousePos = self.mouseWatcherNode.getMouse()
            xPos, yPos = mousePos.getX(), mousePos.getY()
            # Only move if the mouse is close to the edge, and actually within
            # the window.
            if (1.0 - EDGE_SCROLL_WIDTH) < xPos <= 1.0:
                moveRight = 1
            if -(1.0 - EDGE_SCROLL_WIDTH) > xPos >= -1.0:
                moveLeft = 1
            if (1.0 - EDGE_SCROLL_WIDTH) < yPos <= 1.0:
                moveUp = 1
            if -(1.0 - EDGE_SCROLL_WIDTH) > yPos >= -1.0:
                moveDown = 1

        forward = translateSpeed * (moveUp - moveDown)
        sideways = translateSpeed * (moveRight - moveLeft)
        self.cameraHolder.setPos(self.cameraHolder, sideways, forward, 0)

        # Selection box logic
        if sideways != 0 or forward != 0:
            self.updateSelectionBox()

        rotate = rotateSpeed * (self.keys["a"] - self.keys["d"])
        self.cameraHolder.setHpr(self.cameraHolder, rotate, 0, 0)

        return Task.cont

    def zoomCamera(self, inward):
        """
        Zoom in or out.
        """

        dt = self.globalClock.getDt()
        zoomSpeed = 100 * dt

        zoom = -zoomSpeed if inward else zoomSpeed
        self.cameraHolder.setPos(self.cameraHolder, 0, 0, zoom)

    # We don't use task, but we can't remove it because the function signature
    # is from Panda3D.
    def mouseMoveTask(self, task):  # pylint: disable=unused-argument
        """
        Handle mouse movement.
        """

        mousePos = self.getMousePos()

        # NOTE: We don't handle clicking and dragging at the same time.
        if mousePos is not None and mousePos != self.prevMousePos:
            for (buttonId, state) in self.mouseState.iteritems():
                state.lastPos = mousePos
                if state.hasMoved:
                    self.handleMouseDragMove(buttonId, state.modifiers,
                                             state.startPos, mousePos)
                else:
                    startX, startY = state.startPos
                    mouseX, mouseY = mousePos
                    distance = math.hypot(mouseX - startX, mouseY - startY)
                    # TODO[#3]: Magic numbers bad.
                    # Check if the mouse has moved outside the dead zone.
                    if distance > 0.0314:
                        self.handleMouseDragStart(buttonId, state.modifiers,
                                                  state.startPos, mousePos)
                        state.hasMoved = True

        if mousePos != self.prevMousePos:
            self.prevMousePos = mousePos

        return Task.cont

    def pandaEventMouseDown(self, buttonId, modifiers):
        log.debug("Mouse down: button %s w/ mod %s", buttonId, modifiers)
        if buttonId in self.mouseState:
            # Call pandaEventMouseUp just to clear any state related to the
            # button being down, so we can handle this buttonDown event as if
            # it were a fresh press of the button.
            log.warn("Mouse button %s is already down.", buttonId)
            self.pandaEventMouseUp(buttonId)

        assert buttonId not in self.mouseState

        state = MouseButtonState(modifiers[:], self.getMousePos())
        self.mouseState[buttonId] = state

    def pandaEventMouseUp(self, buttonId):
        log.debug("Mouse up: button %s", buttonId)
        if buttonId not in self.mouseState:
            # Drop the event, since there's nothing to do.
            log.warn("Mouse button %s is already up.", buttonId)
            return

        state = self.mouseState[buttonId]

        if state.hasMoved:
            endPos = self.getMousePos()
            if endPos is None:
                endPos = state.lastPos
            self.handleMouseDragEnd(buttonId, state.modifiers, state.startPos,
                                    endPos)
        else:
            self.handleMouseClick(buttonId, state.modifiers, state.startPos)

        del self.mouseState[buttonId]

    def handleMouseClick(self, button, modifiers, pos):
        # Make sure the mouse is inside the screen
        # TODO: Move this check to pandaEventMouseUp?
        if self.mouseWatcherNode.hasMouse() and self.usingCustomCamera:
            x, y, _z = self.coordScreenTo3d(pos)
            uPos = graphicsToWorldPos((x, y))
            self.backend.worldClick(uPos, button, modifiers)

    def handleMouseDragStart(self, buttonId, modifiers, startPos, endPos):
        log.debug("Start dragging from %s to %s", startPos, endPos)

        if buttonId == 1 and modifiers == []:
            assert self.selectionBoxOrigin is None
            self.selectionBoxOrigin = self.coordScreenTo3d(startPos)
            endPos = self.coordScreenTo3d(endPos)
            self.createSelectionBox(self.selectionBoxOrigin, endPos)

    def handleMouseDragMove(self, buttonId, modifiers, startPos, endPos):
        log.debug("Continue dragging from %s to %s", startPos, endPos)

        if buttonId == 1 and modifiers == []:
            assert self.selectionBoxOrigin is not None
            endPos = self.coordScreenTo3d(endPos)
            self.moveSelectionBox(self.selectionBoxOrigin, endPos)

    def handleMouseDragEnd(self, buttonId, modifiers, startPos, endPos):
        log.debug("End dragging from %s to %s", startPos, endPos)

        # TODO: Do we need this check? What is the effect of only calling
        # removeSelectionBox() under this check?
        if buttonId == 1 and modifiers == []:
            # Actually select the units.
            startGPos = self.selectionBoxOrigin[:2]
            endGPos = self.coordScreenTo3d(endPos)[:2]
            startUPos = graphicsToWorldPos(startGPos)
            endUPos = graphicsToWorldPos(endGPos)
            self.backend.worldDrag(startUPos, endUPos, buttonId, modifiers)

            # Clear the selection box; we're done dragging.
            self.selectionBoxOrigin = None
            self.removeSelectionBox()

    def updateSelectionBox(self):
        if self.selectionBoxOrigin is not None:
            mousePos = self.getMousePos()
            if mousePos is not None:
                endPos = self.coordScreenTo3d(mousePos)
                self.moveSelectionBox(self.selectionBoxOrigin, endPos)

    def getMousePos(self):
        # Check if the mouse is over the window.
        if self.mouseWatcherNode.hasMouse():
            # Get the position.
            # Each coordinate is normalized to the interval [-1, 1].
            mousePoint = self.mouseWatcherNode.getMouse()
            # Create a copy of mousePoint rather than returning a reference to
            # it, because mousePoint will be modified in place by Panda.
            return (mousePoint.getX(), mousePoint.getY())
        else:
            return None

    def handleWindowClose(self):
        log.info("Window close requested -- shutting down client.")
        # When in Rome, send messages like the Romans do, I guess.
        # TODO: Get rid of messages, I think.
        message = cmessages.RequestQuit()
        self.graphicsInterface.graphicsMessage(message.serialize())

    def setupEventHandlers(self):
        def pushKey(key, value):
            self.keys[key] = value

        for key in [
                "arrow_up", "arrow_left", "arrow_right", "arrow_down", "w",
                "a", "d", "s"
        ]:
            self.keys[key] = False
            self.accept(key, pushKey, [key, True])
            self.accept("shift-%s" % key, pushKey, [key, True])
            self.accept("%s-up" % key, pushKey, [key, False])

        # Camera toggle.
        self.accept("f3", self.toggleCameraStyle, [])
        self.accept("shift-f3", self.toggleCameraStyle, [])

        # Center view.
        # self.accept("space", self.centerView, []) -- TODO

        # Handle mouse wheel.
        self.accept("wheel_up", self.zoomCamera, [True])
        self.accept("wheel_down", self.zoomCamera, [False])

        # Handle clicking.
        self.accept("mouse1", self.pandaEventMouseDown, [1, []])
        self.accept("mouse1-up", self.pandaEventMouseUp, [1])
        # TODO: Make sure this is always the right mouse button.
        self.accept("mouse3", self.pandaEventMouseDown, [3, []])
        self.accept("mouse3-up", self.pandaEventMouseUp, [3])

        # Handle clicking with modifier keys.
        self.accept("shift-mouse1", self.pandaEventMouseDown, [1, ["shift"]])
        self.accept("control-mouse1", self.pandaEventMouseDown,
                    [1, ["control"]])
        self.accept("shift-mouse3", self.pandaEventMouseDown, [3, ["shift"]])
        self.accept("control-mouse3", self.pandaEventMouseDown,
                    [3, ["control"]])

        # Handle window close request (clicking the X, Alt-F4, etc.)
        self.win.set_close_request_event("window-close")
        self.accept("window-close", self.handleWindowClose)

    def coord3dToScreen(self, coord3d):
        # Empirically, Lens.project takes coordinates in the *camera*'s
        # coordinate system, not its parent or the render. This was not very
        # clear from the documentation, and you'd be surprised how long it took
        # us to figure this out. Anyway, we need to convert the point to be
        # relative to self.camera here; otherwise we'll get bizarre,
        # nonsensical, and hard-to-debug results.
        coord3d = self.camera.getRelativePoint(self.render, coord3d)
        screenCoord = Point2()
        if not self.camLens.project(coord3d, screenCoord):
            log.debug("Attempting 3d-to-screen conversion on point outside of "
                      "camera's viewing frustum.")

        # Convert to a tuple to ensure no one else is keeping a reference
        # around.
        x, y = screenCoord
        return (x, y)

    def coordScreenTo3d(self, screenCoord):
        x, y = screenCoord
        screenPoint = Point2(x, y)

        # Do this calculation using simple geometry, rather than the absurd
        # collision-traversal nonsense we used to use. Thanks to
        #     https://www.panda3d.org/forums/viewtopic.php?t=5409
        # for pointing us at the right methods to make this work.

        # Get two points along the ray extending from the camera, in the
        # direction of the mouse click.
        nearPoint = Point3()
        farPoint = Point3()
        self.camLens.extrude(screenPoint, nearPoint, farPoint)

        # These points are relative to the camera, so need to be converted to
        # be relative to the render. Thanks to the example code (see link
        # above) for saving us probably some hours of debugging figuring that
        # one out again :)
        nearPoint = self.render.getRelativePoint(self.camera, nearPoint)
        farPoint = self.render.getRelativePoint(self.camera, farPoint)

        intersection = Point3()
        if self.groundPlane.intersectsLine(intersection, nearPoint, farPoint):
            # Convert to a tuple to ensure no one else is keeping a reference
            # around.
            x, y, z = intersection
            return (x, y, z)

        # The ray didn't intersect the ground. This is almost certainly going
        # to happen at some point; all you have to do is find a way to aim the
        # camera (or manipulate the screen coordinate) so that the ray points
        # horizontally. But we don't have code to handle it, so for now just
        # abort.
        thisIsNotHandled()
Beispiel #28
0
class Tmx2Bam():
    def __init__(self, input_file, output_file=None, prefabs=""):
        self.dir = os.path.dirname(input_file)
        self.depth = 0
        self.cardmaker = CardMaker("image")
        self.cardmaker.set_frame(-0.5, 0.5, -0.5, 0.5)
        self.linesegs = LineSegs()
        self.textnode = TextNode("text")

        self.tilesheets = []  # Every tsx file loaded.
        self.tiles = {}  # Every unique tile/card.
        self.node = NodePath("tmx_root")

        # load prefab models
        self.prefabs = {}
        if prefabs:
            loader = Loader.get_global_ptr()
            for prefab_node in loader.load_sync(prefabs).get_children():
                prefab_node.clear_transform()
                self.prefabs[prefab_node.name] = NodePath(prefab_node)

        self.tmx = ET.parse(input_file).getroot()
        self.xscale = int(self.tmx.get("tilewidth"))
        self.yscale = int(self.tmx.get("tileheight"))
        self.size = [0, 0]

        self.load_group(self.tmx)
        if output_file:
            self.export_bam(output_file)

    def attributes_to_tags(self, node, element):
        if not element == None:
            for property in element:
                if property.get("name") and property.get("value"):
                    node.set_tag(property.get("name"), property.get("value"))
            for key in element.keys():
                node.set_tag(key, element.get(key))

    def build_text(self, object):
        self.textnode.set_text(object[0].text)
        # TODO: set color
        # TODO: set wrap
        return self.textnode.generate()

    def build_polygon(self, object):
        self.linesegs.reset()
        points = object[0].get("points").split(" ")
        points = [tuple(map(float, i.split(","))) for i in points]
        startx = points[0][0] / self.xscale
        starty = points[0][1] / self.yscale
        self.linesegs.move_to(startx, -starty, 0)
        for point in points:
            x, y = point[0] / self.xscale, point[1] / self.yscale
            self.linesegs.draw_to(x, -y, 0)
        self.linesegs.draw_to(startx, -starty, 0)
        return self.linesegs.create()

    def build_rectangle(self, w, h):
        self.linesegs.reset()
        self.linesegs.move_to(0, 0, 0)
        self.linesegs.draw_to(w, 0, 0)
        self.linesegs.draw_to(w, -h, 0)
        self.linesegs.draw_to(0, -h, 0)
        self.linesegs.draw_to(0, 0, 0)
        return self.linesegs.create()

    def build_tile(self, tsx, id):
        tile = None
        # Cross-reference with self.prefabs in case there's a shape
        # corresponding with a tile's type
        use_prefab = False
        for tile in tsx.findall("tile"):
            if int(tile.get("id")) == id:
                type = tile.get("type")
                if type in self.prefabs:
                    geometry_node = NodePath(str(id))
                    self.prefabs[type].copy_to(geometry_node)
                    use_prefab = True
                break
        # Else we generate a card
        if not use_prefab:
            geometry = self.cardmaker.generate()
            geometry_node = NodePath(geometry)
            geometry_node.set_texture(tsx.get("texture"), 1)
            geometry_node.set_p(-90)
        geometry_node.set_transparency(True)
        # scale and offset UVs for single sprite
        columns = int(tsx.get("columns"))
        rows = int(tsx.get("rows"))
        w, h = 1 / columns, 1 / rows
        tile_x, tile_y = int(id % columns), int(id / (columns))
        u, v = (tile_x * w), 1 - ((tile_y * h) + h)
        for stage in geometry_node.find_all_texture_stages():
            geometry_node.set_texture(stage, tsx.get("texture"), 1)
            geometry_node.set_tex_scale(stage, w, h)
            geometry_node.set_tex_offset(stage, (u, v))
        self.attributes_to_tags(geometry_node, tile)
        return geometry_node

    def animated_tile(self, tsx, tile):
        node = NodePath("animated tile")
        sequence = SequenceNode("animated tile")
        duration = int(tile[0][0].get("duration"))
        if duration >= 9000:
            sequence.set_frame_rate(0)
        else:
            sequence.set_frame_rate(1000 / duration)
        for frame in tile[0]:
            tileid = int(frame.get("tileid"))
            tile_node = self.build_tile(tsx, tileid)
            sequence.add_child(tile_node.node())
        sequence.loop(True)
        node.attach_new_node(sequence)
        return node

    def get_tile(self, map_id):
        tileset, set_id = self.get_tileset(map_id)
        tsx = tileset.get("tsx")
        if map_id in self.tiles:  # if card is already stored
            node = self.tiles[map_id]  # use that one
        else:  # else build and store it
            is_special = False
            node = self.build_tile(tsx, set_id)
            for element in tsx:
                if element.tag == "tile":
                    if int(element.get("id")) == set_id:
                        # if it contains an element, it's always an animation
                        if len(element) > 0:
                            node = self.animated_tile(tsx, element)
                        self.attributes_to_tags(node, element)
                        break
            self.tiles[map_id] = node
        return node

    def load_layer(self, layer):
        layer_node = NodePath(layer.get("name"))
        static_tiles = NodePath("static")  # Static tiles to flatten
        flat_animated_tiles = NodePath("animated")  # Animated tiles to flatten
        dynamic_tiles = NodePath(
            "dynamic")  # All tiles unless otherwise specified (don't flatten)
        tile_groups = {}
        # should we flatten this layer
        store_data = flatten = False
        properties = layer.find("properties")
        if properties:
            for property in properties:
                if property.get("name") == "flatten":
                    flatten = True
                if property.get("name") == "store_data":
                    store_data = True
        # build all tiles in data as a grid of cards
        data = layer.find("data").text
        data = data.replace('\n', '')
        data = data.split(",")
        collumns = int(layer.get("width"))
        rows = int(layer.get("height"))
        self.size = [collumns, rows]
        for y in range(rows):
            for x in range(collumns):
                id = int(data[(y * collumns) + (x % collumns)])
                data[(y * collumns) + (x % collumns)] = id
                if id > 0:
                    tile = NodePath("tile")
                    self.get_tile(id).copy_to(tile)
                    if flatten:
                        if tile.find("**/+SequenceNode"):
                            tile.reparent_to(flat_animated_tiles)
                        else:
                            tile.reparent_to(static_tiles)
                    else:
                        tile.reparent_to(layer_node)
                    tile.set_pos(x, -y, 0)

        if flatten:
            if static_tiles.get_num_children() > 0:
                clear_all_tags(static_tiles)
                static_tiles.flatten_strong()
            if flat_animated_tiles.get_num_children() > 0:
                clear_all_tags(flat_animated_tiles)
                flat_animated_tiles = self.flatten_animated_tiles(
                    flat_animated_tiles)
            for t in (static_tiles, flat_animated_tiles):
                t.reparent_to(layer_node)

        if store_data:
            layer_node.set_python_tag("data", data)
        self.append_layer(layer_node, properties)

    def flatten_animated_tiles(self, group_node):
        # FIXME: hard to read: get_child() everywhere
        # Makes a new node for each frame using all its tiles
        # flatten the s*** out of the node and add to a new SequenceNode.
        tiles = group_node.get_children()
        flattened_sequence = SequenceNode(tiles[0].name)
        for a, animation in enumerate(tiles[0].node().get_children()):
            for f, frame in enumerate(animation.get_child(0).get_children()):
                combined_frame = NodePath("frame " + str(f))
                for tile in tiles:
                    new_np = NodePath("frame")
                    new_np.set_pos(tile.get_pos())
                    animation = tile.node().get_child(a).get_child(0)
                    new_np.attach_new_node(animation.get_child(f))
                    new_np.reparent_to(combined_frame)
                combined_frame.flattenStrong()
                flattened_sequence.add_child(combined_frame.node())
        framerate = animation.get_frame_rate()
        flattened_sequence.set_frame_rate(framerate)
        flattened_sequence.loop(True)
        return NodePath(flattened_sequence)

    def load_objectgroup(self, objectgroup):
        layer_node = NodePath(objectgroup.get("name"))
        for object in objectgroup:
            name = object.get("name")
            if not name: name = "object"
            node = NodePath(name)
            if len(object) > 0:
                # Has a type, so it's either a polygon, text, point or ellipse
                # Points and ellipses are just an empty for now.
                kind = object[0].tag
                if kind == "polygon":
                    node.attach_new_node(self.build_polygon(object))
                elif kind == "text":
                    node.attach_new_node(self.build_text(object))
                    node.set_p(-90)
                self.attributes_to_tags(node, object[0])
            else:  # Doesn't have a type, so it's either an image or a rectangle
                node = NodePath(name)
                w = float(object.get("width")) / self.xscale
                h = float(object.get("height")) / self.yscale
                if object.get("gid"):  # Has a gid, so it's an image
                    self.get_tile(int(object.get("gid"))).copy_to(node)
                    node.set_scale(w, h, 1)
                else:  # It's none of the above, so it's a rectangle
                    node.attach_new_node(self.build_rectangle(w, h))
            x = y = 0
            if object.get("x"):
                x = float(object.get("x")) / self.xscale
            if object.get("y"):
                y = float(object.get("y")) / self.yscale
            node.set_pos(x, -y, 0)
            self.attributes_to_tags(node, object)
            node.reparent_to(layer_node)
        self.append_layer(layer_node, objectgroup.find("properties"))

    def load_imagelayer(self, imagelayer):
        # FIXME: A lot of this stuff is repeated in build_tilcard
        image = imagelayer[0]
        right = int(image.get("width")) / self.xscale
        down = int(image.get("height")) / self.yscale
        self.cardmaker.set_frame(0, right, -down, 0)
        node = NodePath(self.cardmaker.generate())
        self.cardmaker.set_frame(0, 1, -1, 0)
        texture = Texture()
        texture.read(os.path.join(self.dir, image.get("source")))
        texture.setMagfilter(SamplerState.FT_nearest)
        texture.setMinfilter(SamplerState.FT_nearest)
        node.set_texture(texture)
        node.set_transparency(True)
        node.reparent_to(self.node)
        ox = imagelayer.get("offsetx")
        x, y = 0, 0
        if ox:
            x = float(ox) / self.xscale
        oy = imagelayer.get("offsety")
        if oy:
            y = float(oy) / self.yscale
        node.set_pos((x, -y, self.depth))
        node.set_p(-90)

    def load_group(self, group):
        for layer in group:
            if layer.tag == "tileset":
                self.load_tsx(layer)
            elif layer.tag == "layer":
                self.load_layer(layer)
            elif layer.tag == "objectgroup":
                self.load_objectgroup(layer)
            elif layer.tag == "imagelayer":
                self.load_imagelayer(layer)
            elif layer.tag == "group":
                self.load_group(layer)

    def append_layer(self, node, properties):
        self.attributes_to_tags(node, properties)
        node.set_z(self.depth)
        self.depth += 1
        if properties:
            for property in properties:
                if property.get("name") == "z":
                    node.set_z(int(property.get("value")))
                    self.depth -= 1
                    break
        node.reparent_to(self.node)

    def get_tileset(self, id):
        for tilesheet in self.tilesheets:
            if int(tilesheet.get("firstgid")) > id:
                break
            else:
                last = tilesheet
        id_in_sheet = id - int(last.get("firstgid"))
        return last, id_in_sheet,

    def load_tsx(self, layer):
        tsx_filename = layer.get("source")
        tsx = ET.parse(os.path.join(self.dir, tsx_filename)).getroot()
        # Load texture and store in the element tree.
        img_filename = tsx[0].get("source")
        texture = Texture()
        dir = os.path.join(self.dir, tsx_filename)
        place = os.path.join(os.path.split(dir)[0], img_filename)
        texture.read(place)
        texture.setMagfilter(SamplerState.FT_nearest)
        texture.setMinfilter(SamplerState.FT_nearest)
        tsx.set("texture", texture)
        columns = int(tsx.get("columns"))
        rows = int(tsx.get("tilecount")) // columns
        tsx.set("rows", str(rows))
        layer.set("tsx", tsx)
        self.tilesheets.append(layer)

    def export_bam(self, filename):
        print("Exporting as {}".format(filename))
        self.node.writeBamFile("{}".format(filename))