Пример #1
0
def test_vec_identities(py_c_vec) -> None:
    """Check that vectors in the same axis as the rotation don't get spun."""
    Vec, Angle, Matrix, parse_vec_str = py_c_vec

    for ang in range(0, 360, 13):
        # Check the two constructors match.
        assert_rot(Matrix.from_pitch(ang), Matrix.from_angle(Angle(pitch=ang)))
        assert_rot(Matrix.from_yaw(ang), Matrix.from_angle(Angle(yaw=ang)))
        assert_rot(Matrix.from_roll(ang), Matrix.from_angle(Angle(roll=ang)))

        # Various magnitudes to test
        for mag in (-250, -1, 0, 1, 250):
            assert_vec(Vec(y=mag) @ Matrix.from_pitch(ang), 0, mag, 0)
            assert_vec(Vec(z=mag) @ Matrix.from_yaw(ang), 0, 0, mag)
            assert_vec(Vec(x=mag) @ Matrix.from_roll(ang), mag, 0, 0)
Пример #2
0
def test_matrix_roundtrip_yaw(py_c_vec):
    """Check converting to and from a Matrix does not change values."""
    Vec, Angle, Matrix, parse_vec_str = py_c_vec

    for yaw in range(0, 360, 45):
        mat = Matrix.from_yaw(yaw)
        assert_ang(mat.to_angle(), 0, yaw, 0)
Пример #3
0
def save_connectionpoint(item: Item, vmf: VMF) -> None:
    """Write connectionpoints to a VMF."""
    for side, points in item.antline_points.items():
        yaw = side.yaw
        inv_orient = Matrix.from_yaw(-yaw)
        for point in points:
            ant_pos = Vec(point.pos.x, -point.pos.y, -64)
            sign_pos = Vec(point.sign_off.x, -point.sign_off.y, -64)

            offset = (ant_pos - sign_pos) @ inv_orient
            try:
                skin = CONN_OFFSET_TO_SKIN[offset.as_tuple()]
            except KeyError:
                LOGGER.warning(
                    'Pos=({}), Sign=({}) -> ({}) is not a valid offset for signs!',
                    point.pos, point.sign_off, offset)
                continue
            pos: Vec = round((ant_pos + sign_pos) / 2.0 * 16.0, 0)

            vmf.create_ent(
                'bee2_editor_connectionpoint',
                origin=Vec(pos.x - 56, pos.y + 56, -64),
                angles=f'0 {yaw} 0',
                skin=skin,
                priority=point.priority,
                group_id='' if point.group is None else point.group,
            )
Пример #4
0
def build_collision(qc: QC, prop: PropPos, ref_mesh: Mesh) -> Optional[Mesh]:
    """Get the correct collision mesh for this model."""
    if prop.solidity is CollType.NONE:  # Non-solid
        return None
    elif prop.solidity is CollType.VPHYS or prop.solidity is CollType.BSP:
        if qc.phy_smd is None:
            return None
        try:
            return _coll_cache[qc.phy_smd]
        except KeyError:
            LOGGER.info('Parsing coll "{}"', qc.phy_smd)
            with open(qc.phy_smd, 'rb') as fb:
                coll = Mesh.parse_smd(fb)

            rot = Matrix.from_yaw(90)
            for tri in coll.triangles:
                for vert in tri:
                    vert.pos @= rot
                    vert.norm @= rot

            _coll_cache[qc.phy_smd] = coll
            return coll
    # Else, it's one of the three bounding box types.
    # We don't really care about which.
    bbox_min, bbox_max = Vec.bbox(vert.pos for tri in ref_mesh.triangles
                                  for vert in tri)
    return Mesh.build_bbox('static_prop', 'phy', bbox_min, bbox_max)
Пример #5
0
def test_vec_basic_yaw(py_c_vec) -> None:
    """Check each direction rotates appropriately in yaw."""
    Vec, Angle, Matrix, parse_vec_str = py_c_vec

    assert_vec(Vec(200, 0, 0) @ Matrix.from_yaw(0), 200, 0, 0)
    assert_vec(Vec(0, 150, 0) @ Matrix.from_yaw(0), 0, 150, 0)

    assert_vec(Vec(200, 0, 0) @ Matrix.from_yaw(90), 0, 200, 0)
    assert_vec(Vec(0, 150, 0) @ Matrix.from_yaw(90), -150, 0, 0)

    assert_vec(Vec(200, 0, 0) @ Matrix.from_yaw(180), -200, 0, 0)
    assert_vec(Vec(0, 150, 0) @ Matrix.from_yaw(180), 0, -150, 0)

    assert_vec(Vec(200, 0, 0) @ Matrix.from_yaw(270), 0, -200, 0)
    assert_vec(Vec(0, 150, 0) @ Matrix.from_yaw(270), 150, 0, 0)
Пример #6
0
def load_connectionpoint(item: Item, ent: Entity) -> None:
    """Allow more conveniently defining connectionpoints."""
    origin = Vec.from_str(ent['origin'])
    angles = Angle.from_str(ent['angles'])
    if round(angles.pitch) != 0.0 or round(angles.roll) != 0.0:
        LOGGER.warning(
            "Connection Point at {} is not flat on the floor, PeTI doesn't allow this.",
            origin,
        )
        return
    try:
        side = ConnSide.from_yaw(round(angles.yaw))
    except ValueError:
        LOGGER.warning(
            "Connection Point at {} must point in a cardinal direction, not {}!",
            origin,
            angles,
        )
        return

    orient = Matrix.from_yaw(round(angles.yaw))

    center = (origin - (-56, 56, 0)) / 16
    center.z = 0
    center.y = -center.y
    try:
        offset = SKIN_TO_CONN_OFFSETS[ent['skin']] @ orient
    except KeyError:
        LOGGER.warning('Connection Point at {} has invalid skin "{}"!', origin)
        return
    ant_pos = Coord(round(center.x + offset.x), round(center.y - offset.y), 0)
    sign_pos = Coord(round(center.x - offset.x), round(center.y + offset.y), 0)

    group_str = ent['group_id']

    item.antline_points[side].append(
        AntlinePoint(ant_pos, sign_pos, conv_int(ent['priority']),
                     int(group_str) if group_str.strip() else None))
Пример #7
0
def add_glass_floorbeams(vmf: VMF, temp_name: str):
    """Add beams to separate large glass panels.

    The texture is assumed to match plasticwall004a's shape.
    """
    template = template_brush.get_template(temp_name)
    temp_world, temp_detail, temp_over = template.visgrouped()
    try:
        [beam_template] = temp_world + temp_detail  # type: Solid
    except ValueError:
        raise ValueError('Bad Glass Floorbeam template!')

    # Grab the 'end' side, which we move around.
    for side in beam_template.sides:
        if side.normal() == (-1, 0, 0):
            beam_end_face = side
            break
    else:
        raise ValueError('Not aligned to world...')

    separation = options.get(int, 'glass_floorbeam_sep') + 1
    separation *= 128

    # First we want to find all the groups of contiguous glass sections.
    # This is a mapping from some glass piece to its group list.
    groups = {}

    for (origin, normal), barr_type in BARRIERS.items():
        # Grating doesn't use it.
        if barr_type is not BarrierType.GLASS:
            continue

        normal = Vec(normal)

        if not normal.z:
            # Not walls.
            continue

        pos = Vec(origin) + normal * 62

        groups[pos.as_tuple()] = [pos]

    # Loop over every pos and check in the +x/y directions for another glass
    # piece. If there, merge the two lists and set every pos in the group to
    # point to the new list.
    # Once done, every unique list = a group.

    for pos_tup in groups.keys():
        pos = Vec(pos_tup)
        for off in ((128, 0, 0), (0, 128, 0)):
            neighbour = (pos + off).as_tuple()
            if neighbour in groups:
                our_group = groups[pos_tup]
                neigh_group = groups[neighbour]
                if our_group is neigh_group:
                    continue

                # Now merge the two lists. We then need to update all dict
                # locations to point to the new list.

                if len(neigh_group) > len(our_group):
                    small_group, large_group = our_group, neigh_group
                else:
                    small_group, large_group = neigh_group, our_group

                large_group.extend(small_group)
                for pos in small_group:
                    groups[pos.as_tuple()] = large_group

    # Remove duplicates objects by using the ID as key..
    groups = list({id(group): group for group in groups.values()}.values())

    # Side -> u, v or None

    for group in groups:
        bbox_min, bbox_max = Vec.bbox(group)
        dimensions = bbox_max - bbox_min

        # Our beams align to the smallest axis.
        if dimensions.y > dimensions.x:
            beam_ax = 'x'
            side_ax = 'y'
            rot = Matrix()
        else:
            beam_ax = 'y'
            side_ax = 'x'
            rot = Matrix.from_yaw(90)

        # Build min, max tuples for each axis in the other direction.
        # This tells us where the beams will be.
        beams: dict[float, tuple[float, float]] = {}

        # Add 128 so the first pos isn't a beam.
        offset = bbox_min[side_ax] + 128

        for pos in group:
            side_off = pos[side_ax]
            beam_off = pos[beam_ax]
            # Skip over non-'sep' positions..
            if (side_off - offset) % separation != 0:
                continue

            try:
                min_pos, max_pos = beams[side_off]
            except KeyError:
                beams[side_off] = beam_off, beam_off
            else:
                beams[side_off] = min(min_pos,
                                      beam_off), max(max_pos, beam_off)

        detail = vmf.create_ent('func_detail')

        for side_off, (min_off, max_off) in beams.items():
            for min_pos, max_pos in beam_hole_split(
                    beam_ax,
                    Vec.with_axes(side_ax, side_off, beam_ax, min_off, 'z',
                                  bbox_min),
                    Vec.with_axes(side_ax, side_off, beam_ax, max_off, 'z',
                                  bbox_min),
            ):

                if min_pos[beam_ax] >= max_pos[beam_ax]:
                    raise ValueError(min_pos, max_pos, beam_ax)

                # Make the beam.
                # Grab the end face and snap to the length we want.
                beam_end_off = max_pos[beam_ax] - min_pos[beam_ax]
                assert beam_end_off > 0, beam_end_off
                for plane in beam_end_face.planes:
                    plane.x = beam_end_off

                new_beam = beam_template.copy(vmf_file=vmf)
                new_beam.localise(min_pos, rot)
                detail.solids.append(new_beam)
Пример #8
0
 def _init_orient(self) -> Matrix:
     """We need to rotate the orient, because items have forward as negative X."""
     rot = Matrix.from_angle(Angle.from_str(self.ent['angles']))
     return Matrix.from_yaw(180) @ rot
Пример #9
0
def compile_func(
    mdl_key: Tuple[Set[PropPos], bool],
    temp_folder: Path,
    mdl_name: str,
    lookup_model: Callable[[str], Tuple[QC, Model]],
) -> None:
    """Build this merged model."""
    LOGGER.info('Compiling {}...', mdl_name)
    prop_pos, has_coll = mdl_key

    # Unify these properties.
    surfprops = set()  # type: Set[str]
    cdmats = set()  # type: Set[str]
    contents = set()  # type: Set[int]

    for prop in prop_pos:
        qc, mdl = lookup_model(prop.model)
        assert mdl is not None, prop.model
        surfprops.add(mdl.surfaceprop.casefold())
        cdmats.update(mdl.cdmaterials)
        contents.add(mdl.contents)

    if len(surfprops) > 1:
        raise ValueError('Multiple surfaceprops? Should be filtered out.')

    if len(contents) > 1:
        raise ValueError('Multiple contents? Should be filtered out.')

    [surfprop] = surfprops
    [phy_content_type] = contents

    ref_mesh = Mesh.blank('static_prop')
    coll_mesh = None  #  type: Optional[Mesh]

    for prop in prop_pos:
        qc, mdl = lookup_model(prop.model)
        try:
            child_ref = _mesh_cache[qc, prop.skin]
        except KeyError:
            LOGGER.info('Parsing ref "{}"', qc.ref_smd)
            with open(qc.ref_smd, 'rb') as fb:
                child_ref = Mesh.parse_smd(fb)

            if prop.skin != 0 and prop.skin < len(mdl.skins):
                # We need to rename the materials to match the skin.
                swap_skins = dict(zip(mdl.skins[0], mdl.skins[prop.skin]))
                for tri in child_ref.triangles:
                    tri.mat = swap_skins.get(tri.mat, tri.mat)

            # For some reason all the SMDs are rotated badly, but only
            # if we append them.
            rot = Matrix.from_yaw(90)
            for tri in child_ref.triangles:
                for vert in tri:
                    vert.pos @= rot
                    vert.norm @= rot

            _mesh_cache[qc, prop.skin] = child_ref

        child_coll = build_collision(qc, prop, child_ref)

        offset = Vec(prop.x, prop.y, prop.z)
        angles = Angle(prop.pit, prop.yaw, prop.rol)

        ref_mesh.append_model(child_ref, angles, offset,
                              prop.scale * qc.ref_scale)

        if has_coll and child_coll is not None:
            if coll_mesh is None:
                coll_mesh = Mesh.blank('static_prop')
            coll_mesh.append_model(child_coll, angles, offset,
                                   prop.scale * qc.phy_scale)

    with (temp_folder / 'reference.smd').open('wb') as fb:
        ref_mesh.export(fb)

    # Generate  a  blank animation.
    with (temp_folder / 'anim.smd').open('wb') as fb:
        Mesh.blank('static_prop').export(fb)

    if coll_mesh is not None:
        with (temp_folder / 'physics.smd').open('wb') as fb:
            coll_mesh.export(fb)

    with (temp_folder / 'model.qc').open('w') as f:
        f.write(
            QC_TEMPLATE.format(
                path=mdl_name,
                surf=surfprop,
                # For $contents, we need to decompose out each bit.
                # This is the same as BSP's flags in public/bsp_flags.h
                # However only a few types are allowable.
                contents=' '.join([
                    cont for mask, cont in [
                        (0x1, '"solid"'),
                        (0x8, '"grate"'),
                        (0x2000000, '"monster"'),
                        (0x20000000, '"ladder"'),
                    ] if mask & phy_content_type
                    # 0 needs to produce this value.
                ]) or '"notsolid"',
            ))

        for mat in sorted(cdmats):
            f.write('$cdmaterials "{}"\n'.format(mat))

        if coll_mesh is not None:
            f.write(QC_COLL_TEMPLATE)
Пример #10
0
def combine_group(
    compiler: ModelCompiler,
    props: List[StaticProp],
    lookup_model: Callable[[str], Tuple[QC, Model]],
) -> StaticProp:
    """Merge the given props together, compiling a model if required."""

    # We want to allow multiple props to reuse the same model.
    # To do this try and match prop groups to each other, by "unifying"
    # them into a consistent orientation.
    #
    # If there are matches in different orientations, they're most likely
    # 90 degree or other rotations in the yaw axis. So we compute the average,
    # and subtract that out.

    avg_pos = Vec()
    avg_yaw = 0.0

    visleafs = set()  # type: Set[int]

    for prop in props:
        avg_pos += prop.origin
        avg_yaw += prop.angles.yaw
        visleafs.update(prop.visleafs)

    # Snap to nearest 15 degrees to keep the models themselves not
    # strangely rotated.
    avg_yaw = round(avg_yaw / (15 * len(props))) * 15.0
    avg_pos /= len(props)
    yaw_rot = Matrix.from_yaw(-avg_yaw)

    prop_pos = set()
    for prop in props:
        origin = round((prop.origin - avg_pos) @ yaw_rot, 7)
        angles = round(Vec(prop.angles), 7)
        angles.y -= avg_yaw
        try:
            coll = CollType(prop.solidity)
        except ValueError:
            raise ValueError('Unknown prop_static collision type '
                             '{} for "{}" at {}!'.format(
                                 prop.solidity,
                                 prop.model,
                                 prop.origin,
                             ))
        prop_pos.add(
            PropPos(
                origin.x,
                origin.y,
                origin.z,
                angles.x,
                angles.y,
                angles.z,
                prop.model,
                prop.skin,
                prop.scaling,
                coll,
            ))
    # We don't want to build collisions if it's not used.
    has_coll = any(pos.solidity is not CollType.NONE for pos in prop_pos)
    mdl_name, result = compiler.get_model(
        (frozenset(prop_pos), has_coll),
        compile_func,
        lookup_model,
    )

    # Many of these we require to be the same, so we can read them
    # from any of the component props.
    return StaticProp(
        model=mdl_name,
        origin=avg_pos,
        angles=Angle(0, avg_yaw - 90, 0),
        scaling=1.0,
        visleafs=sorted(visleafs),
        solidity=(CollType.VPHYS if has_coll else CollType.NONE).value,
        flags=props[0].flags,
        lighting_origin=avg_pos,
        tint=props[0].tint,
        renderfx=props[0].renderfx,
    )