예제 #1
0
def make_bend(
    vmf: VMF,
    origin_a: Vec,
    origin_b: Vec,
    norm_a: Vec,
    norm_b: Vec,
    config,
    max_size: int,
    is_start=False,
) -> None:
    """Make a corner and the straight sections leading into it."""
    off = origin_b - origin_a
    # The distance to move first, then second.
    first_movement = round(Vec.dot(off, norm_a))
    sec_movement = round(Vec.dot(off, norm_b))

    # The size of the corner ranges from 1-3. It's
    # limited by the user's setting and the distance we have in each direction
    corner_size = int(
        min(
            first_movement // 128,
            sec_movement // 128,
            3,
            max_size + 1,
        ))

    straight_a = first_movement - (corner_size - 1) * 128
    straight_b = sec_movement - corner_size * 128

    if corner_size < 1:
        return  # No room!

    if straight_a > 0:
        make_straight(
            vmf,
            origin_a,
            norm_a,
            straight_a,
            config,
            is_start,
        )

    corner_origin = origin_a + norm_a * straight_a
    make_corner(
        vmf,
        corner_origin,
        norm_a,
        norm_b,
        corner_size - 1,
        config,
    )

    if straight_b > 0:
        make_straight(
            vmf,
            origin_b - (straight_b * norm_b),
            norm_b,
            straight_b,
            config,
        )
예제 #2
0
파일: fizzler.py 프로젝트: BenVlodgi/BEE2.4
    def _texture_fit(
        self,
        side: Side,
        tex_size: float,
        field_length: float,
        fizz: Fizzler,
        neg: Vec,
        pos: Vec,
        is_laserfield=False,
    ) -> None:
        """Calculate the texture offsets required for fitting a texture."""
        if side.vaxis.vec() != -fizz.up_axis:
            # Rotate it
            rot_angle = side.normal().rotation_around()
            for _ in range(4):
                side.uaxis = side.uaxis.rotate(rot_angle)
                side.vaxis = side.vaxis.rotate(rot_angle)
                if side.vaxis.vec() == -fizz.up_axis:
                    break
            else:
                LOGGER.warning("Can't fix rotation for {} -> {}", side.vaxis, fizz.up_axis)

        side.uaxis.offset = -(tex_size / field_length) * neg.dot(side.uaxis.vec())
        side.vaxis.offset = -(tex_size / 128) * neg.dot(side.vaxis.vec())

        #  The above fits it correctly, except it's vertically half-offset.
        # For laserfields that's what we want, for fizzlers we want it normal.
        if not is_laserfield:
            side.vaxis.offset += tex_size / 2

        side.uaxis.scale = field_length / tex_size
        side.vaxis.scale = 128 / tex_size

        side.uaxis.offset %= tex_size
        side.vaxis.offset %= tex_size
예제 #3
0
def compute_visleafs(
    coll_data: List[Tuple[Vec, float, Vec, float]],
    vis_tree_top: VisTree,
) -> List[int]:
    """Compute the visleafs this rope is present in."""
    # Each tree node defines a plane. For each side we touch, we need to
    # continue looking down that side of the tree for visleafs.
    # We need to do this individually for each segment pair. That way
    # we correctly handle cases like ropes encircling a room without entering it.
    used_leafs: Set[int] = set()

    # Check if we collide with either side of the tree (or both).
    # This just involves doing a sphere-plane check with each side of the node.
    # If both are on one side, the whole segment cannot cross.
    for point1, radius1, point2, radius2 in coll_data:
        todo_trees: List[VisTree] = [vis_tree_top]
        for tree in todo_trees:
            off1 = Vec.dot(tree.plane_norm, point1) - tree.plane_dist
            off2 = Vec.dot(tree.plane_norm, point2) - tree.plane_dist
            if off1 >= -radius1 or off2 >= -radius2:
                if isinstance(tree.child_neg, VisLeaf):
                    used_leafs.add(tree.child_neg.id)
                else:
                    todo_trees.append(tree.child_neg)
            if off1 <= radius1 or off2 <= radius2:
                if isinstance(tree.child_pos, VisLeaf):
                    used_leafs.add(tree.child_pos.id)
                else:
                    todo_trees.append(tree.child_pos)

    return sorted(used_leafs)
예제 #4
0
파일: fizzler.py 프로젝트: prrg/BEE2.4
    def _texture_fit(
        self,
        side: Side,
        tex_size: float,
        field_length: float,
        fizz: Fizzler,
        neg: Vec,
        pos: Vec,
        is_laserfield=False,
    ) -> None:
        """Calculate the texture offsets required for fitting a texture."""
        if side.vaxis.vec() != -fizz.up_axis:
            # Rotate it
            rot_angle = side.normal().rotation_around()
            for _ in range(4):
                side.uaxis = side.uaxis.rotate(rot_angle)
                side.vaxis = side.vaxis.rotate(rot_angle)
                if side.vaxis.vec() == -fizz.up_axis:
                    break
            else:
                LOGGER.warning("Can't fix rotation for {} -> {}", side.vaxis, fizz.up_axis)

        side.uaxis.offset = -(tex_size / field_length) * neg.dot(side.uaxis.vec())
        side.vaxis.offset = -(tex_size / 128) * neg.dot(side.vaxis.vec())

        #  The above fits it correctly, except it's vertically half-offset.
        # For laserfields that's what we want, for fizzlers we want it normal.
        if not is_laserfield:
            side.vaxis.offset += tex_size / 2

        side.uaxis.scale = field_length / tex_size
        side.vaxis.scale = 128 / tex_size

        side.uaxis.offset %= tex_size
        side.vaxis.offset %= tex_size
예제 #5
0
 def make_cap(orig, norm):
     # Recompute the UVs to use the first bit of the cable.
     points = [
         Vertex(
             point.pos,
             norm,
             lerp(Vec.dot(point.norm, node.orient.up()), -1, 1,
                  node.config.u_min, node.config.u_max),
             lerp(Vec.dot(point.norm, node.orient.left()), -1, 1, 0, v_max),
             point.links,
         ) for point in orig
     ]
     mesh.triangles.append(Triangle(mat, points[0], points[1], points[2]))
     for a, b in zip(points[2:], points[3:]):
         mesh.triangles.append(Triangle(mat, points[0], a, b))
예제 #6
0
def compute_orients(nodes: Iterable[Node]) -> None:
    """Compute the appropriate orientation for each node."""
    # This is based on the info at:
    # https://janakiev.com/blog/framing-parametric-curves/
    tangents: Dict[Node, Vec] = {}
    all_nodes: Set[Node] = set()
    for node in nodes:
        if node.prev is node.next is None:
            continue
        node_prev = node.prev if node.prev is not None else node
        node_next = node.next if node.next is not None else node
        tangents[node] = (node_next.pos - node_prev.pos).norm()
        all_nodes.add(node)

    while all_nodes:
        node1 = all_nodes.pop()
        node1 = node1.find_start()
        tanj1 = tangents[node1]
        # Start with an arbitrary roll for the first orientation.
        node1.orient = Matrix.from_angle(tanj1.to_angle())
        while node1.next is not None:
            node2 = node1.next
            all_nodes.discard(node2)
            tanj1 = tangents[node1]
            tanj2 = tangents[node2]
            b = Vec.cross(tanj1, tanj2)
            if b.mag_sq() < 0.001:
                node2.orient = node1.orient.copy()
            else:
                b = b.norm()
                phi = math.acos(Vec.dot(tanj1, tanj2))
                up = node1.orient.up() @ Matrix.axis_angle(
                    b, math.degrees(phi))
                node2.orient = Matrix.from_basis(x=tanj2, z=up)
            node1 = node2
예제 #7
0
def bsp_collision(point: Vec, planes: List[Tuple[Vec, Vec]]) -> bool:
    """Check if the given position is inside a BSP node."""
    for pos, norm in planes:
        off = pos - point
        # This is the actual distance, so we'll use a rather large
        # "epsilon" to catch objects close to the edges.
        if Vec.dot(off, norm) < -0.1:
            return False
    return True
예제 #8
0
 def contains(self, point: Vec) -> bool:
     """Check if the given position is inside the volume."""
     for convex in self.collision:
         for pos, norm in convex:
             off = pos - point
             # This is the actual distance, so we'll use a rather large
             # "epsilon" to catch objects close to the edges.
             if Vec.dot(off, norm) < -0.1:
                 break  # Outside a plane, it doesn't match this convex.
         else:  # Inside all these planes, it's inside.
             return True
     return False  # All failed, not present.
예제 #9
0
def res_transfer_bullseye(inst: Entity, props: Property):
    """Transfer catapult targets and placement helpers from one tile to another."""
    start_pos = conditions.resolve_offset(inst, props['start_pos', ''])
    end_pos = conditions.resolve_offset(inst, props['end_pos', ''])
    start_norm = props.vec('start_norm', 0, 0, 1).rotate_by_str(inst['angles'])
    end_norm = props.vec('end_norm', 0, 0, 1).rotate_by_str(inst['angles'])

    try:
        start_tile = tiling.TILES[
            (start_pos - 64 * start_norm).as_tuple(),
            start_norm.as_tuple()
        ]
    except KeyError:
        LOGGER.warning('"{}": Cannot find tile to transfer from at {}, {}!'.format(
            inst['targetname'],
            start_pos,
            start_norm
        ))
        return

    end_tile = tiling.TileDef.ensure(
        end_pos - 64 * end_norm,
        end_norm,
    )
    # Now transfer the stuff.
    if start_tile.has_oriented_portal_helper:
        # We need to rotate this.
        orient = start_tile.portal_helper_orient.copy()
        # If it's directly opposite, just mirror - we have no clue what the
        # intent is.
        if Vec.dot(start_norm, end_norm) != -1.0:
            # Use the dict to compute the rotation to apply.
            orient.rotate(*NORM_ROTATIONS[
                start_norm.as_tuple(),
                end_norm.as_tuple()
            ])
        end_tile.add_portal_helper(orient)
    elif start_tile.has_portal_helper:
        # Non-oriented, don't orient.
        end_tile.add_portal_helper()
    start_tile.remove_portal_helper(all=True)

    if start_tile.bullseye_count:
        end_tile.bullseye_count = start_tile.bullseye_count
        start_tile.bullseye_count = 0
        # Then transfer the targets across.
        for plate in faithplate.PLATES.values():
            if getattr(plate, 'target', None) is start_tile:
                plate.target = end_tile
예제 #10
0
def find_closest(
    all_nodes: Iterable[Tuple[Vec, List[Tuple[Vec, nodes.Node]]]],
    node: nodes.Node,
    src_point: Vec,
    src_norm: Vec,
) -> nodes.Node:
    """Search through all the nodes to find the one most aligned to this."""
    best_node: Optional[nodes.Node] = None
    best_dist = math.inf

    # We're looking for if the point is inside the cylinder projecting out
    # of the node.
    for targ_norm, node_lst in all_nodes:
        if Vec.dot(src_norm, targ_norm) < ANG_THRESHOLD:
            continue
        for targ_point, targ in node_lst:
            if node is targ:
                continue
            # First check if we're beyond the target point
            off = (src_point - targ_point)
            dist = -off.dot(src_norm)
            # On the other side, or not better than what we've got.
            # Allow a tiny amount of overlap.
            if dist < -2.0 or dist > best_dist:
                continue
            # Now project the point onto the target's plane.
            # If inside, we've found it.
            if (off + dist * targ_norm).mag_sq() <= (64 * 64):
                best_node = targ
                best_dist = dist

    if best_node is None:
        if node.ent['targetname']:
            name = ' "{}"'.format(node.ent["targetname"])
        else:
            name = ''
        raise ValueError('No destination found for '
                         f'junction {name} at ({node.origin})!')
    # Mark the node as having an input, for sanity checking purposes.
    # Note that nodes can have multiple inputs, if they're merging paths.
    best_node.has_input = True

    return best_node
예제 #11
0
def interpolate_all(nodes: Set[Node]) -> None:
    """Produce nodes in-between each user-made node."""
    # Create the nodes and put them in a seperate list, then add them
    # to the actual nodes list second. This way sections that have been interpolated
    # don't affect the interpolation of neighbouring sections.

    segments: List[List[Node]] = []
    for node1 in nodes:
        if node1.next is None or node1.config.segments <= 0:
            continue
        node2 = node1.next
        interp_type = node1.config.interp
        func = globals()['interpolate_' + interp_type.name.casefold()]
        points = func(node1, node2, node1.config.segments)

        for a, b in zip(points, points[1:]):
            a.next = b
            b.prev = a
        points[0].prev = node1
        points[-1].next = node2
        segments.append(points)

    for points in segments:
        nodes.update(points)
        points[0].prev.next = points[0]
        points[-1].next.prev = points[-1]

    # Finally, split nodes with too much of an angle between them - we can't smooth.
    for node in list(nodes):
        if node.prev is None or node.next is None:
            continue
        off1 = node.pos - node.prev.pos
        off2 = node.next.pos - node.pos
        if Vec.dot(off1, off2) < 0.7:
            new_node = Node(node.pos.copy(), node.config, node.radius)
            nodes.add(new_node)
            new_node.next = node.next
            node.next.prev = new_node
            node.next = None
예제 #12
0
def find_closest(
    all_nodes: Iterable[Tuple[Vec, List[Tuple[Vec, nodes.Node]]]],
    node: nodes.Node,
    src_type: nodes.DestType,
) -> nodes.Node:
    """Search through all the nodes to find the one most aligned to this."""
    src_point = node.vec_point(1.0, src_type)
    src_norm = node.output_norm(src_type)

    best_node: Optional[nodes.Node] = None
    best_dist = math.inf

    # We're looking for if the point is inside the cylinder projecting out
    # of the node.
    for targ_norm, node_lst in all_nodes:
        if Vec.dot(src_norm, targ_norm) < ANG_THRESHOLD:
            continue
        for targ_point, targ in node_lst:
            if node is targ:
                continue
            # First check if we're beyond the target point
            off = (src_point - targ_point)
            dist = -off.dot(src_norm)
            # On the other side, or not better than what we've got.
            # Allow a tiny amount of overlap.
            if dist < -8.0 or dist > best_dist:
                continue
            # Now project the point onto the target's plane.
            # If inside, we've found it.
            if (off + dist * targ_norm).mag_sq() <= (64 * 64):
                best_node = targ
                best_dist = dist

    if best_node is None:
        raise ValueError(
            f'No destination found for {src_type.value} output of {node}! Junction at {src_point}.'
        )
    return best_node
예제 #13
0
파일: barriers.py 프로젝트: BEEmod/BEE2.4
def make_barriers(vmf: VMF):
    """Make barrier entities. get_tex is vbsp.get_tex."""
    glass_temp = template_brush.get_scaling_template(
        options.get(str, "glass_template"))
    grate_temp = template_brush.get_scaling_template(
        options.get(str, "grating_template"))
    hole_temp_small: List[Solid]
    hole_temp_lrg_diag: List[Solid]
    hole_temp_lrg_cutout: List[Solid]
    hole_temp_lrg_square: List[Solid]

    # Avoid error without this package.
    if HOLES:
        # Grab the template solids we need.
        hole_combined_temp = template_brush.get_template(
            options.get(str, 'glass_hole_temp'))
        hole_world, hole_detail, _ = hole_combined_temp.visgrouped({'small'})
        hole_temp_small = hole_world + hole_detail
        hole_world, hole_detail, _ = hole_combined_temp.visgrouped(
            {'large_diagonal'})
        hole_temp_lrg_diag = hole_world + hole_detail
        hole_world, hole_detail, _ = hole_combined_temp.visgrouped(
            {'large_cutout'})
        hole_temp_lrg_cutout = hole_world + hole_detail
        hole_world, hole_detail, _ = hole_combined_temp.visgrouped(
            {'large_square'})
        hole_temp_lrg_square = hole_world + hole_detail
    else:
        hole_temp_small = hole_temp_lrg_diag = hole_temp_lrg_cutout = hole_temp_lrg_square = []

    floorbeam_temp = options.get(str, 'glass_floorbeam_temp')

    if options.get_itemconf('BEE_PELLET:PelletGrating', False):
        # Merge together these existing filters in global_pti_ents
        vmf.create_ent(
            origin=options.get(Vec, 'global_pti_ents_loc'),
            targetname='@grating_filter',
            classname='filter_multi',
            filtertype=0,
            negated=0,
            filter01='@not_pellet',
            filter02='@not_paint_bomb',
        )
    else:
        # Just skip paint bombs.
        vmf.create_ent(
            origin=options.get(Vec, 'global_pti_ents_loc'),
            targetname='@grating_filter',
            classname='filter_activator_class',
            negated=1,
            filterclass='prop_paint_bomb',
        )

    # Group the positions by planes in each orientation.
    # This makes them 2D grids which we can optimise.
    # (normal_dist, positive_axis, type) -> [(x, y)]
    slices: Dict[Tuple[Tuple[float, float, float], bool, BarrierType],
                 Dict[Tuple[int, int], False]] = defaultdict(dict)
    # We have this on the 32-grid so we can cut squares for holes.

    for (origin_tup, normal_tup), barr_type in BARRIERS.items():
        origin = Vec(origin_tup)
        normal = Vec(normal_tup)
        norm_axis = normal.axis()
        u, v = origin.other_axes(norm_axis)
        norm_pos = Vec.with_axes(norm_axis, origin)
        slice_plane = slices[
            norm_pos.as_tuple(),  # distance from origin to this plane.
            normal[norm_axis] > 0, barr_type, ]
        for u_off in [-48, -16, 16, 48]:
            for v_off in [-48, -16, 16, 48]:
                slice_plane[int((u + u_off) // 32),
                            int((v + v_off) // 32), ] = True

    # Remove pane sections where the holes are. We then generate those with
    # templates for slanted parts.
    for (origin_tup, norm_tup), hole_type in HOLES.items():
        barr_type = BARRIERS[origin_tup, norm_tup]

        origin = Vec(origin_tup)
        normal = Vec(norm_tup)
        norm_axis = normal.axis()
        u, v = origin.other_axes(norm_axis)
        norm_pos = Vec.with_axes(norm_axis, origin)
        slice_plane = slices[norm_pos.as_tuple(), normal[norm_axis] > 0,
                             barr_type, ]
        if hole_type is HoleType.LARGE:
            offsets = (-80, -48, -16, 16, 48, 80)
        else:
            offsets = (-16, 16)
        for u_off in offsets:
            for v_off in offsets:
                # Remove these squares, but keep them in the dict
                # so we can check if there was glass there.
                uv = (
                    int((u + u_off) // 32),
                    int((v + v_off) // 32),
                )
                if uv in slice_plane:
                    slice_plane[uv] = False
                # These have to be present, except for the corners
                # on the large hole.
                elif abs(u_off) != 80 or abs(v_off) != 80:
                    u_ax, v_ax = Vec.INV_AXIS[norm_axis]
                    LOGGER.warning(
                        'Hole tried to remove missing tile at ({})?',
                        Vec.with_axes(norm_axis, norm_pos, u_ax, u + u_off,
                                      v_ax, v + v_off),
                    )

        # Now generate the curved brushwork.

        if barr_type is BarrierType.GLASS:
            front_temp = glass_temp
        elif barr_type is BarrierType.GRATING:
            front_temp = grate_temp
        else:
            raise NotImplementedError

        angles = normal.to_angle()
        hole_temp: List[Tuple[List[Solid], Matrix]] = []

        # This is a tricky bit. Two large templates would collide
        # diagonally, and we allow the corner glass to not be present since
        # the hole doesn't actually use that 32x32 segment.
        # So we need to determine which of 3 templates to use.
        corn_angles = angles.copy()
        if hole_type is HoleType.LARGE:
            for corn_angles.roll in (0, 90, 180, 270):
                corn_mat = Matrix.from_angle(corn_angles)

                corn_dir = Vec(y=1, z=1) @ corn_angles
                hole_off = origin + 128 * corn_dir
                diag_type = HOLES.get(
                    (hole_off.as_tuple(), normal.as_tuple()),
                    None,
                )
                corner_pos = origin + 80 * corn_dir
                corn_u, corn_v = corner_pos.other_axes(norm_axis)
                corn_u = int(corn_u // 32)
                corn_v = int(corn_v // 32)

                if diag_type is HoleType.LARGE:
                    # There's another large template to this direction.
                    # Just have 1 generate both combined, so the brushes can
                    # be more optimal. To pick, arbitrarily make the upper one
                    # be in charge.
                    if corn_v > v // 32:
                        hole_temp.append((hole_temp_lrg_diag, corn_mat))
                    continue
                if (corn_u, corn_v) in slice_plane:
                    hole_temp.append((hole_temp_lrg_square, corn_mat))
                else:
                    hole_temp.append((hole_temp_lrg_cutout, corn_mat))

        else:
            hole_temp.append((hole_temp_small, Matrix.from_angle(angles)))

        def solid_pane_func(off1: float, off2: float, mat: str) -> List[Solid]:
            """Given the two thicknesses, produce the curved hole from the template."""
            off_min = 64 - max(off1, off2)
            off_max = 64 - min(off1, off2)
            new_brushes = []
            for brushes, matrix in hole_temp:
                for orig_brush in brushes:
                    brush = orig_brush.copy(vmf_file=vmf)
                    new_brushes.append(brush)
                    for face in brush.sides:
                        face.mat = mat
                        for point in face.planes:
                            if point.x > 64:
                                point.x = off_max
                            else:
                                point.x = off_min
                        face.localise(origin, matrix)
                        # Increase precision, these are small detail brushes.
                        face.lightmap = 8
            return new_brushes

        make_glass_grating(
            vmf,
            origin,
            normal,
            barr_type,
            front_temp,
            solid_pane_func,
        )

    for (plane_pos, is_pos, barr_type), pos_slice in slices.items():
        plane_pos = Vec(plane_pos)
        norm_axis = plane_pos.axis()
        normal = Vec.with_axes(norm_axis, 1 if is_pos else -1)

        if barr_type is BarrierType.GLASS:
            front_temp = glass_temp
        elif barr_type is BarrierType.GRATING:
            front_temp = grate_temp
        else:
            raise NotImplementedError

        u_axis, v_axis = Vec.INV_AXIS[norm_axis]

        for min_u, min_v, max_u, max_v in grid_optimise(pos_slice):
            # These are two points in the origin plane, at the borders.
            pos_min = Vec.with_axes(
                norm_axis,
                plane_pos,
                u_axis,
                min_u * 32,
                v_axis,
                min_v * 32,
            )
            pos_max = Vec.with_axes(
                norm_axis,
                plane_pos,
                u_axis,
                max_u * 32 + 32,
                v_axis,
                max_v * 32 + 32,
            )

            def solid_pane_func(pos1: float, pos2: float,
                                mat: str) -> List[Solid]:
                """Make the solid brush."""
                return [
                    vmf.make_prism(
                        pos_min + normal * (64.0 - pos1),
                        pos_max + normal * (64.0 - pos2),
                        mat=mat,
                    ).solid
                ]

            make_glass_grating(
                vmf,
                (pos_min + pos_max) / 2 + 63 * normal,
                normal,
                barr_type,
                front_temp,
                solid_pane_func,
            )
            # Generate hint brushes, to ensure sorting is done correctly.
            [hint] = solid_pane_func(0, 4.0, consts.Tools.SKIP)
            for side in hint:
                if abs(Vec.dot(side.normal(), normal)) > 0.99:
                    side.mat = consts.Tools.HINT
            vmf.add_brush(hint)

    if floorbeam_temp:
        LOGGER.info('Adding Glass floor beams...')
        add_glass_floorbeams(vmf, floorbeam_temp)
        LOGGER.info('Done!')
예제 #14
0
def join_markers(vmf: VMF, mark_a: Marker, mark_b: Marker, is_start: bool=False) -> None:
    """Join two marker ents together with corners."""
    origin_a = Vec.from_str(mark_a.ent['origin'])
    origin_b = Vec.from_str(mark_b.ent['origin'])

    norm_a = mark_a.orient.forward()
    norm_b = mark_b.orient.forward()

    config = mark_a.conf

    LOGGER.debug(
        'Connect markers: {} @ {} -> {} @ {}, dot={}\n{}',
        origin_a, norm_a,
        origin_b, norm_b,
        Vec.dot(norm_a, norm_b),
        config,
    )

    if norm_a == norm_b:
        # Either straight-line, or s-bend.
        dist = round((origin_a - origin_b).mag())

        if origin_a + (norm_a * dist) == origin_b:
            make_straight(
                vmf,
                origin_a,
                norm_a,
                dist,
                config,
                is_start,
            )
        # else: S-bend, we don't do the geometry for this..
        return

    if norm_a == -norm_b:
        # U-shape bend..
        make_ubend(
            vmf,
            origin_a,
            origin_b,
            norm_a,
            config,
            max_size=mark_a.size,
        )
        return

    # Lastly try a regular curve. Check they are on the same plane.
    side_dir = Vec.cross(norm_a, norm_b)
    side_off_a = side_dir.dot(origin_a)
    side_off_b = side_dir.dot(origin_b)
    if abs(side_off_a - side_off_b) < 1e-6:
        make_bend(
            vmf,
            origin_a,
            origin_b,
            norm_a,
            norm_b,
            config,
            max_size=mark_a.size,
        )
    else:
        LOGGER.warning(
            'Cannot connect markers: {} @ {} -> {} @ {}\n '
            'Sides: {:.12f} {:.12f}\n{}',
            origin_a, norm_a,
            origin_b, norm_b,
            side_off_a, side_off_b,
            config,
        )
예제 #15
0
def entity_finder(ctx: Context):
    """Finds the closest entity of a given type."""
    target_cache = {}  # type: Dict[tuple, Entity]

    for finder in ctx.vmf.by_class['comp_entity_finder']:
        finder.remove()
        targ_class = finder['targetcls']
        targ_radius = conv_float(finder['radius'])
        targ_ref = finder['targetref']
        blacklist = finder['blacklist'].casefold()
        # Restrict to actually valid dot products.
        targ_fov = max(
            0.0, min(180.0, conv_float(finder['searchfov', '180.0'], 180.0)))

        # This will never find things, ignore that.
        if targ_fov == 0.0:
            LOGGER.warning(
                'Entity finder at <{}>! has FOV of 0, ignoring!',
                finder['origin'],
            )
            targ_fov = 180.0
        targ_dot = math.cos(math.radians(targ_fov))
        normal = Vec(x=1).rotate_by_str(finder['angles'])

        targ_pos = Vec.from_str(finder['origin'])
        if targ_ref:
            for ent in ctx.vmf.search(targ_ref):
                targ_pos = Vec.from_str(ent['origin'])
                break
            else:
                LOGGER.warning(
                    'Can\'t find ref entity named "{}" '
                    'for entity finder at <{}>!',
                    targ_ref,
                    finder['origin'],
                )

        key = (targ_class, targ_radius, blacklist,
               targ_fov) + targ_pos.as_tuple()
        try:
            found_ent = target_cache[key]
        except KeyError:
            found_ent = None
            cur_dist = float('inf')
            targ_ent = None

            if blacklist.endswith('*'):
                blacklist = blacklist[:-1]

                def blacklist_func(name: str) -> bool:
                    """Check if the name matches the blacklist, with wildcards."""
                    return name.casefold().startswith(blacklist)
            elif blacklist:

                def blacklist_func(name: str) -> bool:
                    """Check if the name matches the blacklist exactly."""
                    return name.casefold() == blacklist
            else:

                def blacklist_func(name: str) -> bool:
                    """No blacklist."""
                    return False

            for targ_ent in ctx.vmf.by_class[targ_class]:
                if blacklist_func(targ_ent['targetname']):
                    continue

                offset = (Vec.from_str(targ_ent['origin']) - targ_pos)
                dist_to = offset.mag()

                # If at the same point, direction is meaningless.
                # Treat that as always passing the FOV check.
                if dist_to != 0.0 and Vec.dot(offset.norm(),
                                              normal) < targ_dot:
                    continue

                if targ_radius == 0 or dist_to < targ_radius:
                    if cur_dist > dist_to:
                        found_ent = targ_ent
                        cur_dist = dist_to
            del targ_ent
            if found_ent is None:
                LOGGER.warning(
                    'Cannot find valid {} entity within {} units '
                    'for entity finder at <{}>! (fov={}, in direction {})',
                    targ_class,
                    targ_radius,
                    finder['origin'],
                    targ_fov,
                    normal,
                )
                continue
            target_cache[key] = found_ent

        found_ent.outputs.extend(finder.outputs)
        finder.outputs.clear()

        # If the ent has no targetname, give it one.
        if not found_ent['targetname']:
            found_ent['targetname'] = '_found_entity_1'
            found_ent.make_unique()

        # If specified, teleport to the item's location.
        if conv_bool(finder['teleporttarget']):
            found_ent['origin'] = targ_pos

        for ind in itertools.count(1):
            kv_mode_str = finder['kv{}_mode'.format(ind)].casefold()
            if kv_mode_str == '':
                break

            try:
                kv_mode = FinderModes(kv_mode_str)
            except ValueError:
                LOGGER.warning(
                    'Unknown mode "{}" '
                    'for entity finder at <{}>!',
                    kv_mode_str,
                    finder['origin'],
                )
                continue

            kv_src = finder['kv{}_src'.format(ind)]
            kv_dest = finder['kv{}_dest'.format(ind)]

            # All modes need the destination keyvalue.
            if not kv_dest:
                LOGGER.warning(
                    'No destination keyvalue set '
                    'for entity finder at <{}>, transformation #{}!',
                    finder['origin'],
                    ind,
                )
                continue

            needs_src, needs_known = NEEDS[kv_mode]

            known_ent = None
            if needs_known:
                known_ent_name = finder['kv{}_known'.format(ind)]
                if not known_ent_name:
                    LOGGER.warning(
                        'No known entity specified for entity finder at '
                        '<{}>, but one required for transformation #{}!',
                        finder['origin'],
                        ind,
                    )
                for known_ent in ctx.vmf.search(known_ent_name):
                    break
                if known_ent is None:
                    LOGGER.warning(
                        'Can\'t find known entity named '
                        '"{}" for entity finder at <{}>, transformation #{}!',
                        known_ent_name,
                        finder['origin'],
                        ind,
                    )
                    continue

            if needs_src and not kv_src:
                LOGGER.warning(
                    'No source keyvalue set '
                    'for entity finder at <{}>, transformation {}!',
                    finder['origin'],
                    ind,
                )
                continue

            if kv_mode is FinderModes.CONST_TARG:
                # Set constant value on the found ent.
                found_ent[kv_dest] = kv_src
            elif kv_mode is FinderModes.CONST_KNOWN:
                # Set constant value on known entity.
                known_ent[kv_dest] = kv_src
            elif kv_mode is FinderModes.TARG_TO_KNOWN:
                known_ent[kv_dest] = found_ent[kv_src]
            elif kv_mode is FinderModes.KNOWN_TO_TARG:
                found_ent[kv_dest] = known_ent[kv_src]
            elif kv_mode is FinderModes.OUTPUT_MERGE:
                name = '!' + kv_dest.lstrip('!').casefold()
                for out in known_ent.outputs:
                    if out.target.casefold() == name:
                        out.target = found_ent['targetname']
            else:
                raise AssertionError('Unknown mode {}'.format(kv_mode))
예제 #16
0
def generate(sources: List[nodes.Spawner]) -> List[Animation]:
    """Generate all the animations, one by one."""
    anims = [Animation(node) for node in sources]

    for anim in anims:
        node = anim.cur_node
        speed = anim.start_node.speed / FPS
        offset = anim.start_node.origin.copy()

        # To keep the speed constant, keep track of any extra we need to offset
        # into the next node.
        overshoot = anim.start_overshoot

        # To generate, we alternate between making a single node, and then
        # making the straight section.

        while True:
            # First, check to see if we need to branch off.
            # If we're secondary, we are the branch off and so don't need to
            # do that again.
            if anim.curve_type is DestType.PRIMARY:
                if DestType.SECONDARY in node.out_types:
                    anims.append(anim.tee(node, DestType.SECONDARY, overshoot))
                if DestType.TERTIARY in node.out_types:
                    anims.append(anim.tee(node, DestType.TERTIARY, overshoot))

            needs_out = node.has_pass
            overshoot = 0

            seg_len = node.path_len(anim.curve_type)
            seg_frames = math.ceil((seg_len - overshoot) / speed)
            for i in range(int(seg_frames)):
                # Make each frame.
                pos = (overshoot + speed * i) / seg_len
                if needs_out and pos > 0.5:
                    anim.pass_points.append((anim.duration, node))
                    needs_out = False
                # Place the point.
                last_loc = node.vec_point(pos, anim.curve_type)
                anim.add_point(last_loc - offset)

            # If short, we might not have placed the output.
            if needs_out:
                anim.pass_points.append((anim.duration, node))

            # Recalculate the new overshoot.
            overshoot += speed * seg_frames - seg_len
            overshoot = 0

            if isinstance(node, nodes.Destroyer):
                # We reached the end, finalise!
                anim.end_node = node
                anim.add_point(node.origin - offset)
                break

            # Now generate the straight part between this node and the next.
            next_node = node.outputs[anim.curve_type]
            cur_end = node.vec_point(1.0, anim.curve_type)
            straight_off = next_node.vec_point(0.0) - cur_end

            if next_node in anim.history:
                raise ValueError(f"Vactube junction at {next_node.origin} "
                                 f"loops back onto itself!")

            # Only generate the straight part if the nodes aren't overlapping.
            if Vec.dot(straight_off, next_node.input_norm()) > 0:
                straight_dist = straight_off.mag()
                seg_frames = math.ceil((straight_dist - overshoot) / speed)

                for i in range(int(seg_frames)):
                    # Make each frame.
                    pos = cur_end + (
                        (overshoot + speed * i) / straight_dist) * straight_off
                    anim.add_point(pos - offset)

                overshoot += (speed * seg_frames) - straight_dist

            # And advance to the next node.
            anim.cur_node = node = next_node
            anim.history.append(node)

            # We only do secondary for the first node, we always continue
            # to the primary value.
            anim.curve_type = DestType.PRIMARY

    return anims
예제 #17
0
파일: barriers.py 프로젝트: BEEmod/BEE2.4
def make_glass_grating(
    vmf: VMF,
    ent_pos: Vec,
    normal: Vec,
    barr_type: BarrierType,
    front_temp: template_brush.ScalingTemplate,
    solid_func: Callable[[float, float, str], List[Solid]],
):
    """Make all the brushes needed for glass/grating.

    solid_func() is called with two offsets from the voxel edge, and returns a
    matching list of solids. This allows doing holes and normal panes with the
    same function.
    barrier_type is either 'glass' or 'grating'.
    """

    if barr_type is BarrierType.GLASS:
        main_ent = vmf.create_ent('func_detail')
        player_clip_mat = consts.Tools.PLAYER_CLIP_GLASS
        tex_cat = 'glass'
    else:
        player_clip_mat = consts.Tools.PLAYER_CLIP_GRATE
        main_ent = vmf.create_ent(
            'func_brush',
            renderfx=14,  # Constant Glow
            solidity=1,  # Never solid
            origin=ent_pos,
        )
        tex_cat = 'grating'
    # The actual glass/grating brush - 0.5-1.5 units back from the surface.
    main_ent.solids = solid_func(0.5, 1.5, consts.Tools.NODRAW)

    for face in main_ent.sides():
        if abs(Vec.dot(normal, face.normal())) > 0.99:
            texturing.apply(texturing.GenCat.SPECIAL, face, tex_cat)
            front_temp.apply(face, change_mat=False)

    if normal.z == 0:
        # If vertical, we don't care about footsteps.
        # So just use 'normal' clips.
        player_clip = vmf.create_ent('func_detail')
        player_clip_mat = consts.Tools.PLAYER_CLIP
    else:
        # This needs to be a func_brush, otherwise the clip texture data
        # will be merged with other clips.
        player_clip = vmf.create_ent(
            'func_brush',
            solidbsp=1,
            origin=ent_pos,
        )
        # We also need a func_detail clip, which functions on portals.
        # Make it thinner, so it doesn't impact footsteps.
        player_thin_clip = vmf.create_ent('func_detail')
        player_thin_clip.solids = solid_func(0.5, 3.5,
                                             consts.Tools.PLAYER_CLIP)

    player_clip.solids = solid_func(0, 4, player_clip_mat)

    if barr_type is BarrierType.GRATING:
        # Add the VPhysics clip.
        phys_clip = vmf.create_ent(
            'func_clip_vphysics',
            filtername='@grating_filter',
            origin=ent_pos,
            StartDisabled=0,
        )
        phys_clip.solids = solid_func(0, 2, consts.Tools.TRIGGER)
예제 #18
0
def res_antlaser(vmf: VMF, res: Property) -> object:
    """The condition to generate AntLasers and Antline Corners.

    This is executed once to modify all instances.
    """
    conf_inst_corner = instanceLocs.resolve('<item_bee2_antline_corner>',
                                            silent=True)
    conf_inst_laser = instanceLocs.resolve(res['instance'])
    conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64)
    conf_las_start = Vec(z=res.float('LasStart') - 64)
    conf_rope_off = res.vec('RopePos')
    conf_toggle_targ = res['toggleTarg', '']

    beam_conf = res.find_key('BeamKeys', or_blank=True)
    glow_conf = res.find_key('GlowKeys', or_blank=True)
    cable_conf = res.find_key('CableKeys', or_blank=True)

    if beam_conf:
        # Grab a copy of the beam spawnflags so we can set our own options.
        conf_beam_flags = beam_conf.int('spawnflags')
        # Mask out certain flags.
        conf_beam_flags &= (
            0
            | 1  # Start On
            | 2  # Toggle
            | 4  # Random Strike
            | 8  # Ring
            | 16  # StartSparks
            | 32  # EndSparks
            | 64  # Decal End
            #| 128  # Shade Start
            #| 256  # Shade End
            #| 512  # Taper Out
        )
    else:
        conf_beam_flags = 0

    conf_outputs = [
        Output.parse(prop) for prop in res
        if prop.name in ('onenabled', 'ondisabled')
    ]

    # Find all the markers.
    nodes: dict[str, Node] = {}

    for inst in vmf.by_class['func_instance']:
        filename = inst['file'].casefold()
        name = inst['targetname']
        if filename in conf_inst_laser:
            node_type = NodeType.LASER
        elif filename in conf_inst_corner:
            node_type = NodeType.CORNER
        else:
            continue

        try:
            # Remove the item - it's no longer going to exist after
            # we're done.
            item = connections.ITEMS.pop(name)
        except KeyError:
            raise ValueError('No item for "{}"?'.format(name)) from None
        pos = Vec.from_str(inst['origin'])
        orient = Matrix.from_angle(Angle.from_str(inst['angles']))
        if node_type is NodeType.CORNER:
            timer_delay = item.inst.fixup.int('$timer_delay')
            # We treat inf, 1, 2 and 3 as the same, to get around the 1 and 2 not
            # being selectable issue.
            pos = CORNER_POS[max(0, timer_delay - 3) % 8] @ orient + pos
        nodes[name] = Node(node_type, inst, item, pos, orient)

    if not nodes:
        # None at all.
        return conditions.RES_EXHAUSTED

    # Now find every connected group, recording inputs, outputs and links.
    todo = set(nodes.values())

    groups: list[Group] = []

    while todo:
        start = todo.pop()
        # Synthesise the Item used for logic.
        # We use a random info_target to manage the IO data.
        group = Group(start, start.type)
        groups.append(group)
        for node in group.nodes:
            # If this node has no non-node outputs, destroy the antlines.
            has_output = False
            node.is_grouped = True

            for conn in list(node.item.outputs):
                neighbour = conn.to_item
                neigh_node = nodes.get(neighbour.name, None)
                todo.discard(neigh_node)
                if neigh_node is None or neigh_node.type is not node.type:
                    # Not a node or different item type, it must therefore
                    # be a target of our logic.
                    conn.from_item = group.item
                    has_output = True
                    continue
                elif not neigh_node.is_grouped:
                    # Another node.
                    group.nodes.append(neigh_node)
                # else: True, node already added.

                # For nodes, connect link.
                conn.remove()
                group.links.add(frozenset({node, neigh_node}))

            # If we have a real output, we need to transfer it.
            # Otherwise we can just destroy it.
            if has_output:
                node.item.transfer_antlines(group.item)
            else:
                node.item.delete_antlines()

            # Do the same for inputs, so we can catch that.
            for conn in list(node.item.inputs):
                neighbour = conn.from_item
                neigh_node = nodes.get(neighbour.name, None)
                todo.discard(neigh_node)
                if neigh_node is None or neigh_node.type is not node.type:
                    # Not a node or different item type, it must therefore
                    # be a target of our logic.
                    conn.to_item = group.item
                    node.had_input = True
                    continue
                elif not neigh_node.is_grouped:
                    # Another node.
                    group.nodes.append(neigh_node)
                # else: True, node already added.

                # For nodes, connect link.
                conn.remove()
                group.links.add(frozenset({neigh_node, node}))

    # Now every node is in a group. Generate the actual entities.
    for group in groups:
        # We generate two ent types. For each marker, we add a sprite
        # and a beam pointing at it. Then for each connection
        # another beam.

        # Choose a random item name to use for our group.
        base_name = group.nodes[0].item.name

        out_enable = [Output('', '', 'FireUser2')]
        out_disable = [Output('', '', 'FireUser1')]
        if group.type is NodeType.LASER:
            for output in conf_outputs:
                if output.output.casefold() == 'onenabled':
                    out_enable.append(output.copy())
                else:
                    out_disable.append(output.copy())

        group.item.enable_cmd = tuple(out_enable)
        group.item.disable_cmd = tuple(out_disable)

        if group.type is NodeType.LASER and conf_toggle_targ:
            # Make the group info_target into a texturetoggle.
            toggle = group.item.inst
            toggle['classname'] = 'env_texturetoggle'
            toggle['target'] = conditions.local_name(group.nodes[0].inst,
                                                     conf_toggle_targ)

        # Node -> index for targetnames.
        indexes: dict[Node, int] = {}

        # For antline corners, the antline segments.
        segments: list[antlines.Segment] = []

        # frozenset[Node] unpacking isn't clear.
        node_a: Node
        node_b: Node

        if group.type is NodeType.CORNER:
            for node_a, node_b in group.links:
                # Place a straight antline between each connected node.
                # If on the same plane, we only need one. If not, we need to
                # do one for each plane it's in.
                offset = node_b.pos - node_a.pos
                up_a = node_a.orient.up()
                up_b = node_b.orient.up()
                plane_a = Vec.dot(node_a.pos, up_a)
                plane_b = Vec.dot(node_b.pos, up_b)
                if Vec.dot(up_a, up_b) > 0.9:
                    if abs(plane_a - plane_b) > 1e-6:
                        LOGGER.warning(
                            'Antline corners "{}" - "{}" '
                            'are on different planes',
                            node_a.item.name,
                            node_b.item.name,
                        )
                        continue
                    u = node_a.orient.left()
                    v = node_a.orient.forward()
                    # Which are we aligned to?
                    if abs(Vec.dot(offset, u)) < 1e-6 or abs(Vec.dot(
                            offset, v)) < 1e-6:
                        forward = offset.norm()
                        group.add_ant_straight(
                            up_a,
                            node_a.pos + 8.0 * forward,
                            node_b.pos - 8.0 * forward,
                        )
                    else:
                        LOGGER.warning(
                            'Antline corners "{}" - "{}" '
                            'are not directly aligned',
                            node_a.item.name,
                            node_b.item.name,
                        )
                else:
                    # We expect them be aligned to each other.
                    side = Vec.cross(up_a, up_b)
                    if abs(Vec.dot(side, offset)) < 1e-6:
                        mid1 = node_a.pos + Vec.dot(offset, up_b) * up_b
                        mid2 = node_b.pos - Vec.dot(offset, up_a) * up_a
                        if mid1 != mid2:
                            LOGGER.warning(
                                'Midpoint mismatch: {} != {} for "{}" - "{}"',
                                mid1,
                                mid2,
                                node_a.item.name,
                                node_b.item.name,
                            )
                        group.add_ant_straight(
                            up_a,
                            node_a.pos + 8.0 * (mid1 - node_a.pos).norm(),
                            mid1,
                        )
                        group.add_ant_straight(
                            up_b,
                            node_b.pos + 8.0 * (mid2 - node_b.pos).norm(),
                            mid2,
                        )

        # For cables, it's a bit trickier than the beams.
        # The cable ent itself is the one which decides what it links to,
        # so we need to potentially make endpoint cables at locations with
        # only "incoming" lines.
        # So this dict is either a targetname to indicate cables with an
        # outgoing connection, or the entity for endpoints without an outgoing
        # connection.
        cable_points: dict[Node, Union[Entity, str]] = {}

        for i, node in enumerate(group.nodes, start=1):
            indexes[node] = i
            node.item.name = base_name

            if group.type is NodeType.CORNER:
                node.inst.remove()
                # Figure out whether we want a corner at this point, or
                # just a regular dot. If a non-node input was provided it's
                # always a corner. Otherwise it's one if there's an L, T or X
                # junction.
                use_corner = True
                norm = node.orient.up().as_tuple()
                if not node.had_input:
                    neighbors = [
                        mag * direction for direction in [
                            node.orient.forward(),
                            node.orient.left(),
                        ] for mag in [-8.0, 8.0]
                        if ((node.pos + mag * direction).as_tuple(),
                            norm) in group.ant_seg
                    ]
                    if len(neighbors) == 2:
                        [off1, off2] = neighbors
                        if Vec.dot(off1, off2) < -0.99:
                            # ---o---, merge together. The endpoints we want
                            # are the other ends of the two segments.
                            group.add_ant_straight(
                                node.orient.up(),
                                group.rem_ant_straight(norm, node.pos + off1),
                                group.rem_ant_straight(norm, node.pos + off2),
                            )
                            use_corner = False
                    elif len(neighbors) == 1:
                        # o-----, merge.
                        [offset] = neighbors
                        group.add_ant_straight(
                            node.orient.up(),
                            group.rem_ant_straight(norm, node.pos + offset),
                            node.pos - offset,
                        )
                        use_corner = False
                if use_corner:
                    segments.append(
                        antlines.Segment(
                            antlines.SegType.CORNER,
                            round(node.orient.up(), 3),
                            Vec(node.pos),
                            Vec(node.pos),
                        ))
            elif group.type is NodeType.LASER:
                sprite_pos = node.pos + conf_glow_height @ node.orient

                if glow_conf:
                    # First add the sprite at the right height.
                    sprite = vmf.create_ent('env_sprite')
                    for prop in glow_conf:
                        sprite[prop.name] = conditions.resolve_value(
                            node.inst, prop.value)

                    sprite['origin'] = sprite_pos
                    sprite['targetname'] = NAME_SPR(base_name, i)
                elif beam_conf:
                    # If beams but not sprites, we need a target.
                    vmf.create_ent(
                        'info_target',
                        origin=sprite_pos,
                        targetname=NAME_SPR(base_name, i),
                    )

                if beam_conf:
                    # Now the beam going from below up to the sprite.
                    beam_pos = node.pos + conf_las_start @ node.orient
                    beam = vmf.create_ent('env_beam')
                    for prop in beam_conf:
                        beam[prop.name] = conditions.resolve_value(
                            node.inst, prop.value)

                    beam['origin'] = beam['targetpoint'] = beam_pos
                    beam['targetname'] = NAME_BEAM_LOW(base_name, i)
                    beam['LightningStart'] = beam['targetname']
                    beam['LightningEnd'] = NAME_SPR(base_name, i)
                    beam['spawnflags'] = conf_beam_flags | 128  # Shade Start

        segments += set(group.ant_seg.values())
        if group.type is NodeType.CORNER and segments:
            group.item.antlines.add(
                antlines.Antline(group.item.name + '_antline', segments))

        if group.type is NodeType.LASER and beam_conf:
            for i, (node_a, node_b) in enumerate(group.links):
                beam = vmf.create_ent('env_beam')
                conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys')
                beam['origin'] = beam['targetpoint'] = node_a.pos
                beam['targetname'] = NAME_BEAM_CONN(base_name, i)
                beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a])
                beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b])
                beam['spawnflags'] = conf_beam_flags

        if group.type is NodeType.LASER and cable_conf:
            build_cables(
                vmf,
                group,
                cable_points,
                base_name,
                beam_conf,
                conf_rope_off,
            )

    return conditions.RES_EXHAUSTED
예제 #19
0
def parse_antlines(vmf: VMF) -> tuple[
    dict[str, list[Antline]],
    dict[int, list[Segment]]
]:
    """Convert overlays in the map into Antline objects.

    This returns two dicts. The first maps targetnames to lists of antlines.
    The second maps solid IDs to segments, for assigning TileDefs to them.
    """
    # We want to reconstruct the shape of the antline path.
    # To do that we find the neighbouring points for each overlay.

    LOGGER.info('Parsing antlines...')

    # segment -> found neighbours of it.
    overlay_joins: defaultdict[Segment, set[Segment]] = defaultdict(set)

    segment_to_name: dict[Segment, str] = {}

    # Points on antlines where two can connect. For corners that's each side,
    # for straight it's each end. Combine that with the targetname
    # so we only join related antlines.
    join_points: dict[tuple[str, float, float, float], Segment] = {}

    mat_straight = consts.Antlines.STRAIGHT
    mat_corner = consts.Antlines.CORNER

    side_to_seg: dict[int, list[Segment]] = {}
    antlines: dict[str, list[Antline]] = {}

    for over in vmf.by_class['info_overlay']:
        mat = over['material']
        origin = Vec.from_str(over['basisorigin'])
        normal = Vec.from_str(over['basisnormal'])
        orient = Matrix.from_angle(Angle.from_str(over['angles']))

        if mat == mat_corner:
            seg_type = SegType.CORNER
            start = end = origin

            # One on each side - we know the size.
            points = [
                origin + orient.left(-8.0),
                origin + orient.left(+8.0),
                origin + orient.forward(-8.0),
                origin + orient.forward(+8.0),
            ]
        elif mat == mat_straight:
            seg_type = SegType.STRAIGHT

            # We want to determine the length first.
            long_axis = orient.left()
            side_axis = orient.forward()

            # The order of these isn't correct, but we need the neighbours to
            # fix that.
            start, end = overlay_bounds(over)
            # For whatever reason, Valve sometimes generates antlines which are
            # shortened by 1 unit. So snap those to grid.
            start = round(start / 16, 0) * 16
            end = round(end / 16, 0) * 16

            if math.isclose(Vec.dot(end - start, long_axis), 16.0):
                # Special case.
                # 1-wide antlines don't have the correct
                # rotation, pointing always in the U axis.
                # So we need to figure that out to get the correct links.
                # For now just create the segment with dummy values.
                start = end = origin
                points = []
            else:
                offset: Vec = round(abs(8 * side_axis), 0)
                start += offset
                end -= offset

                points = [start, end]
        else:
            # It's not an antline.
            continue

        seg = Segment(seg_type, normal, start, end)
        segment_to_name[seg] = over_name = over['targetname']

        for side_id in over['sides'].split():
            side_to_seg.setdefault(int(side_id), []).append(seg)

        for point in points:
            # Lookup the point to see if we've already checked it.
            # If not, write us into that spot.
            neighbour = join_points.setdefault(
                (over_name, ) + point.as_tuple(),
                seg,
            )
            if neighbour is seg:
                # None found
                continue
            overlay_joins[neighbour].add(seg)
            overlay_joins[seg].add(neighbour)

        # Remove original from the map.
        over.remove()

    # Now fix the square straight segments.
    for seg, over_name in segment_to_name.items():
        if seg.type is SegType.STRAIGHT and seg.start == seg.end:
            fix_single_straight(seg, over_name, join_points, overlay_joins)

    # Now, finally compute each continuous section.
    for start_seg, over_name in segment_to_name.items():
        try:
            neighbours = overlay_joins[start_seg]
        except KeyError:
            continue  # Done already.

        if len(neighbours) != 1:
            continue
        # Found a start point!
        segments = [start_seg]

        for segment in segments:
            neighbours = overlay_joins.pop(segment)
            # Except KeyError: this segment's already done??
            for neighbour in neighbours:
                if neighbour not in segments:
                    segments.append(neighbour)

        antlines.setdefault(over_name, []).append(Antline(over_name, segments))

    LOGGER.info('Done! ({} antlines)'.format(sum(map(len, antlines.values()))))
    return antlines, side_to_seg