# mcedit2.rendering.selection — mceditlib 0.1 documentation

### Navigation

- index
- modules |
- mceditlib 0.1 documentation »
- Module code »

# Source code for mcedit2.rendering.selection

    """
        selection
    """
    from __future__ import absolute_import, division, print_function
    from OpenGL import GL
    from PySide import QtCore
    import logging
    import numpy
    from mcedit2.rendering import scenegraph, rendergraph, cubes
    from mcedit2.rendering.chunknode import ChunkGroupNode, ChunkNode
    from mcedit2.rendering.depths import DepthOffset
    from mcedit2.rendering.renderstates import _RenderstateAlphaBlendNode
    from mcedit2.rendering.scenegraph import VertexNode, RenderstateNode
    from mcedit2.rendering.vertexarraybuffer import VertexArrayBuffer
    from mcedit2.util import profiler
    from mcedit2.util.glutils import gl
    from mceditlib import faces
    from mceditlib.exceptions import ChunkNotPresent
    from mceditlib.selection import SectionBox, BoundingBox, SelectionBox, rayIntersectsBox

    log = logging.getLogger(__name__)

    class CullFaceRenderNode(rendergraph.RenderstateRenderNode):
        def enter(self):
            GL.glEnable(GL.GL_CULL_FACE)

        def exit(self):
            GL.glDisable(GL.GL_CULL_FACE)

    class NonAirMaskSelection(SelectionBox):

        def __init__(self, dimension, box):
            """

            :param dimension:
            :type dimension: mceditlib.worldeditor.WorldEditorDimension
            :return:
            :rtype:
            """
            super(NonAirMaskSelection, self).__init__()
            self.box = box
            self.mincx = box.mincx
            self.mincy = box.mincy
            self.mincz = box.mincz
            self.maxcx = box.maxcx
            self.maxcy = box.maxcy
            self.maxcz = box.maxcz

            self.dimension = dimension

        def box_mask(self, box):
            """

            :param box:
            :type box: BoundingBox
            :return:
            :rtype:
            """
            #if self.volume > 40:
            #    import pdb; pdb.set_trace()

            mask = numpy.zeros(shape=box.size, dtype=bool)

            for cx, cz in box.chunkPositions():
                try:
                    chunk = self.dimension.getChunk(cx, cz)
                except ChunkNotPresent:
                    continue
                for cy in box.sectionPositions(cx, cz):
                    section = chunk.getSection(cy)
                    if section is None:
                        continue
                    sectionBox = box.intersect(SectionBox(cx, cy, cz))
                    if sectionBox.volume == 0:
                        continue
                    slices = numpy.s_[
                        sectionBox.miny & 0xf:(sectionBox.miny & 0xf) + sectionBox.height,
                        sectionBox.minz & 0xf:(sectionBox.minz & 0xf) + sectionBox.length,
                        sectionBox.minx & 0xf:(sectionBox.minx & 0xf) + sectionBox.width,
                    ]
                    maskSlices = numpy.s_[
                        sectionBox.miny - box.miny:sectionBox.maxy - box.miny,
                        sectionBox.minz - box.minz:sectionBox.maxz - box.minz,
                        sectionBox.minx - box.minx:sectionBox.maxx - box.minx,
                    ]
                    blocks = section.Blocks
                    mask[maskSlices] = blocks[slices] != 0

            return mask

    class SelectionScene(scenegraph.Node):
        def __init__(self):
            """

            :return:
            :rtype:
            """
            super(SelectionScene, self).__init__()
            self.cullNode = RenderstateNode(CullFaceRenderNode)
            self.blendNode = RenderstateNode(_RenderstateAlphaBlendNode)
            self.groupNode = ChunkGroupNode()
            self.cullNode.addChild(self.blendNode)
            self.blendNode.addChild(self.groupNode)
            self.addChild(self.cullNode)
            self.loadTimer = QtCore.QTimer(timeout=self.loadMore)
            self.loadTimer.setInterval(0)
            self.loadTimer.start()
            self.renderSelection = None

        def __del__(self):
            self.loadTimer.stop()

        _selection = None
        @property
        def selection(self):
            return self._selection

        @selection.setter
        def selection(self, selection):
            if selection != self._selection:
                self._selection = selection
                self.updateSelection()

        _dimension = None
        @property
        def dimension(self):
            return self._dimension

        @dimension.setter
        def dimension(self, value):
            if value != self._dimension:
                self._dimension = value
                self.updateSelection()

        @property
        def filled(self):
            return self.cullNode.visible

        @filled.setter
        def filled(self, value):
            self.cullNode.visible = value

        def updateSelection(self):
            if self.dimension is None or self.selection is None:
                return

            selection = self.selection
            self.renderSelection = selection & NonAirMaskSelection(self.dimension, selection)
            self.groupNode.clear()
            self._loader = None
            if selection.chunkCount < 16:
                for _ in self.loadSections():
                    pass
                self.loadTimer.setInterval(333)

        _loader = None
        def loadMore(self):
            if self._loader is None:
                self._loader = self.loadSections()
            try:
                self._loader.next()
            except StopIteration:
                self._loader = None

        @profiler.iterator("SelectionScene")
        def loadSections(self):
            selection = self.renderSelection
            if selection is None:
                self.loadTimer.setInterval(333)
                return
            else:
                self.loadTimer.setInterval(0)

            for cx, cz in selection.chunkPositions():
                if self.groupNode.containsChunkNode((cx, cz)):
                    continue

                vertexArrays = []
                for cy in selection.sectionPositions(cx, cz):
                    box = SectionBox(cx, cy, cz).expand(1)
                    mask = selection.box_mask(box)
                    if mask is not None:
                        vertexArrays.extend(self.buildSection(mask, cy))
                if len(vertexArrays):
                    chunkNode = ChunkNode((cx, cz))
                    vertexNode = VertexNode(vertexArrays)
                    chunkNode.addChild(vertexNode)
                    self.groupNode.addChunkNode(chunkNode)
                yield
            self.loadTimer.setInterval(333)

        def exposedBlockMasks(self, mask):
            """
            Compare adjacent cells in the 3d mask along all three axes and return one mask for each cardinal direction.
            The returned masks contain the faces of each cell which are exposed in that direction and should be rendered.

            :param mask:
            :type mask: ndarray
            :return:
            :rtype: list[ndarray]
            """
            sy = sz = sx = 16
            exposedY = numpy.zeros((sy+1, sz, sx), dtype=bool)
            exposedZ = numpy.zeros((sy, sz+1, sx), dtype=bool)
            exposedX = numpy.zeros((sy, sz, sx+1), dtype=bool)

            exposedY[:] = mask[1:,   1:-1, 1:-1] != mask[ :-1, 1:-1, 1:-1]
            exposedZ[:] = mask[1:-1, 1:,   1:-1] != mask[1:-1,  :-1, 1:-1]
            exposedX[:] = mask[1:-1, 1:-1, 1:  ] != mask[1:-1, 1:-1,  :-1]

            exposedByFace = [
                exposedX[:, :, 1:],
                exposedX[:, :, :-1],
                exposedY[1:],
                exposedY[:-1],
                exposedZ[:, 1:],
                exposedZ[:, :-1],
            ]

            return exposedByFace

        def buildSection(self, sectionMask, cy):
            vertexArrays = []

            for (face, exposedFaceMask) in enumerate(self.exposedBlockMasks(sectionMask)):
                blockIndices = sectionMask[1:-1, 1:-1, 1:-1] & exposedFaceMask

                vertexBuffer = VertexArrayBuffer.fromIndices(face, blockIndices, False, False)
                if not len(vertexBuffer.vertex):
                    continue

                vertexBuffer.rgb[:] = faceShades[face]
                vertexBuffer.alpha[:] = 0x77
                vertexBuffer.vertex[..., 1] += cy << 4
                vertexArrays.append(vertexBuffer)

            return vertexArrays

    faceShades = {
        faces.FaceNorth: (0x33, 0x33, 0x99),
        faces.FaceSouth: (0x33, 0x33, 0x99),
        faces.FaceEast:  (0x44, 0x44, 0xCC),
        faces.FaceWest:  (0x44, 0x44, 0xCC),
        faces.FaceUp:    (0x55, 0x55, 0xFF),
        faces.FaceDown:  (0x22, 0x22, 0x77),
    }

    class SelectionBoxRenderNode(rendergraph.RenderNode):
        def drawSelf(self):
            box = self.sceneNode.selectionBox
            if box is None:
                return

            r, g, b, alpha = self.sceneNode.color
            with gl.glPushAttrib(GL.GL_DEPTH_BUFFER_BIT | GL.GL_ENABLE_BIT | GL.GL_POLYGON_BIT):
                GL.glDepthMask(False)
                GL.glEnable(GL.GL_BLEND)
                GL.glPolygonOffset(self.sceneNode.depth, self.sceneNode.depth)

                if self.sceneNode.filled:
                    # Filled box
                    GL.glColor(r, g, b, alpha)
                    cubes.drawBox(box)

                if self.sceneNode.wire:
                    # Wire box, thinner behind terrain
                    r, g, b, alpha = self.sceneNode.wireColor
                    GL.glColor(r, g, b, alpha)
                    GL.glLineWidth(2.0)
                    cubes.drawBox(box, cubeType=GL.GL_LINES)
                    GL.glDisable(GL.GL_DEPTH_TEST)
                    GL.glLineWidth(1.0)
                    cubes.drawBox(box, cubeType=GL.GL_LINES)

    class SelectionBoxNode(scenegraph.Node):
        RenderNodeClass = SelectionBoxRenderNode
        _selectionBox = None
        depth = DepthOffset.Selection
        wire = True

        _filled = True
        @property
        def filled(self):
            return self._filled

        @filled.setter
        def filled(self, value):
            self._filled = value
            self.dirty = True

        @property
        def selectionBox(self):
            return self._selectionBox

        @selectionBox.setter
        def selectionBox(self, value):
            self._selectionBox = value
            self.dirty = True

        _color = (.3, .3, 1, .3)
        @property
        def color(self):
            return self._color

        @color.setter
        def color(self, value):
            self._color = value
            self.dirty = True

        _wireColor = (1, 1, 1, .6)
        @property
        def wireColor(self):
            return self._wireColor

        @wireColor.setter
        def wireColor(self, value):
            self._wireColor = value
            self.dirty = True

    class SelectionFaceRenderNode(rendergraph.RenderNode):
        def drawSelf(self):
            box = self.sceneNode.selectionBox
            if box is None:
                return

            alpha = 0.16
            r, g, b = self.sceneNode.color
            with gl.glPushAttrib(GL.GL_DEPTH_BUFFER_BIT | GL.GL_ENABLE_BIT | GL.GL_LINE_BIT):
                GL.glDisable(GL.GL_DEPTH_TEST)
                GL.glDepthMask(False)
                GL.glEnable(GL.GL_BLEND)
                GL.glPolygonOffset(self.sceneNode.depth, self.sceneNode.depth)

                GL.glColor(1.0, 1.0, 1.0, 1.0)
                GL.glLineWidth(2.0)
                cubes.drawFace(box, self.sceneNode.face, GL.GL_LINE_STRIP)
                GL.glColor(r, g, b, alpha)
                GL.glEnable(GL.GL_DEPTH_TEST)
                cubes.drawFace(box, self.sceneNode.face)

    class SelectionFaceNode(scenegraph.Node):
        RenderNodeClass = SelectionFaceRenderNode
        _selectionBox = None
        _face = faces.FaceYIncreasing
        depth = DepthOffset.Selection

        @property
        def selectionBox(self):
            return self._selectionBox

        @selectionBox.setter
        def selectionBox(self, value):
            self._selectionBox = value
            self.dirty = True

        @property
        def face(self):
            return self._face

        @face.setter
        def face(self, value):
            self._face = value
            self.dirty = True

        _color = (.3, .3, 1)
        @property
        def color(self):
            return self._color

        @color.setter
        def color(self, value):
            self._color = value
            self.dirty = True

    def boxFaceUnderCursor(box, mouseRay):
        """
        Find the nearest face of the given bounding box that intersects with the given mouse ray
        and return (point, face)
        """
        nearPoint, mouseVector = mouseRay

        intersections = rayIntersectsBox(box, mouseRay)
        if intersections is None:
            return None, None

        point, face = intersections[0]

        # if the point is near the edge of the face, and the edge is facing away,
        # return the away-facing face

        dim = face.dimension

        dim1 = (dim + 1) % 3
        dim2 = (dim + 2) % 3

        # determine if a click was within self.edge_factor of the edge of a selection box side. if so, click through
        # to the opposite side
        edge_factor = 0.1

        for d in dim1, dim2:
            if not isinstance(d, int):
                assert False
            edge_width = box.size[d] * edge_factor
            faceNormal = [0, 0, 0]
            cameraBehind = False

            if point[d] - box.origin[d] < edge_width:
                faceNormal[d] = -1
                cameraBehind = nearPoint[d] - box.origin[d] > 0
            if point[d] - box.maximum[d] > -edge_width:
                faceNormal[d] = 1
                cameraBehind = nearPoint[d] - box.maximum[d] < 0

            if numpy.dot(faceNormal, mouseVector) > 0 or cameraBehind:
                # the face adjacent to the clicked edge faces away from the cam
                # xxxx this is where to allow iso views in face-on angles to grab edges
                # xxxx also change face highlight node to highlight this area
                return intersections[1] if len(intersections) > 1 else intersections[0]

        return point, face

### Navigation

- index
- modules |
- mceditlib 0.1 documentation »
- Module code »