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)
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)
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, )
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)
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)
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))
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)
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
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)
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, )