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 res_temp_reset_gridded(inst: Entity): """Temporary result - reset gridded state on a surface. Used for antline routers to undo ItemLightStrip's 4x4 texturing. This should be removed after geometry is done. """ pos = Vec(0, 0, -64) pos.localise( Vec.from_str(inst['origin']), Vec.from_str(inst['angles']) ) norm = Vec(z=-1).rotate_by_str(inst['angles']) for axis in 'xyz': # Don't realign things in the normal's axis - # those are already fine. if not norm[axis]: pos[axis] //= 128 pos[axis] *= 128 pos[axis] += 64 brush = SOLIDS.get(pos.as_tuple(), None) if brush is None: return if brush.color is template_brush.MAT_TYPES.white: brush.face.mat = const.WhitePan.WHITE_1x1 else: brush.face.mat = const.BlackPan.BLACK_1
def pose(f, rot): global FRAME # Calculate the piston's rotation. # First find the real position of the piston hinge. hinge_pos = Vec(-43, 0, 10.5) hinge_pos.x -= 64 hinge_pos.rotate(float(rot), 0, 0) hinge_pos.x += 64 # Where we want the end of the piston to be. anchor_point = Vec(z=-96, x=rot*1.5 + 96) piston_off = hinge_pos - anchor_point print(piston_off) piston_rot = math.degrees(math.atan2(piston_off.z, -piston_off.x)) f.write(frame_temp.format( time=FRAME, rot=-round(math.radians(rot), 6), # Cancel the effect of rot on pist_rot pist_rot=round(math.radians((piston_rot + rot) % 360), 6), len=-piston_off.mag(), marker=Vec(z=anchor_point.z, y=-anchor_point.x), )) FRAME += 1
def test_bbox() -> None: """Test the bounding box behaviour against a brute-force loop.""" rand = Random(1234) # Ensure reproducibility. SIZE = 128.0 # Build a set of points and keys. points = [(Vec(rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE)), Vec(rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE)), rand.getrandbits(64).to_bytes(8, 'little')) for _ in range(200)] tree = RTree() for a, b, data in points: tree.insert(a, b, data) # Pick a random bounding box. bb_min, bb_max = Vec.bbox( Vec( rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE), ), Vec( rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE), )) expected = [ data for a, b, data in points if Vec.bbox_intersect(*Vec.bbox(a, b), bb_min, bb_max) ] found = set(tree.find_bbox(bb_min, bb_max)) # Order is irrelevant, but duplicates must all match. assert sorted(expected) == sorted(found)
def test_reorder_helper() -> None: """Test the reorder helper.""" assert reorder((1, 2, 3), 'xyz', 0, 0, 0) == Vec(1, 2, 3) assert reorder((1, 2, 3), 'yzx', 0, 0, 0) == Vec(2, 3, 1) assert reorder((1, 2, 3), 'zyx', 0, 0, 0) == Vec(3, 2, 1) assert reorder((1, 2, 3), 'xzy', 0, 0, 0) == Vec(1, 3, 2) assert reorder((-10, 30, 0), 'xyz', 8, 6, 12) == Vec(-2, 36, 12)
def test_bbox_parse_block() -> None: """Test parsing of a block-shaped bbox from a VMF.""" vmf = VMF() ent = vmf.create_ent( 'bee2_collision_bbox', coll_deco=1, coll_physics=1, coll_grating=0, tags='standard excellent', ) ent.solids.append(vmf.make_prism(Vec(80, 10, 40), Vec(150, 220, 70)).solid) ent.solids.append(vmf.make_prism(Vec(-30, 45, 80), Vec(-20, 60, 120)).solid) bb2, bb1 = BBox.from_ent(ent) # Allow it to produce in either order. if bb1.min_x == -30: bb1, bb2 = bb2, bb1 assert_bbox( bb1, (80, 10, 40), (150, 220, 70), CollideType.DECORATION | CollideType.PHYSICS, {'standard', 'excellent'}, ) assert_bbox( bb2, (-30, 45, 80), (-20, 60, 120), CollideType.DECORATION | CollideType.PHYSICS, {'standard', 'excellent'}, )
def _make_support_table(): """Make a table of angle/offset values for each direction.""" for norm in (xp, xn, yp, yn, zp, zn): table = SUPPORT_POS[norm] = [] for x in range(0, 360, 90): ang = Vec(norm).to_angle(roll=x) table.append((ang, Vec(0, 0, -64).rotate(*ang)))
def _parse_smd_anim(file_iter: Iterator[Tuple[int, bytes]], bones: Dict[int, Bone]): """Parse the 'skeleton' section of SMDs.""" frames = {} time = -999999999 for line_num, line in file_iter: if line.startswith((b'//', b'#', b';')): continue if line.startswith(b'time'): try: time = int(line[4:]) except ValueError: raise ParseError(line_num, 'Invalid time value!') from None if time in frames: raise ValueError(line_num, 'Duplicate frame time {}!', time) frames[time] = [] elif line == b'end': return frames else: # Bone. try: byt_ind, byt_x, byt_y, byt_z, byt_pit, byt_yaw, byt_rol = line.split( ) pos = Vec(float(byt_x), float(byt_y), float(byt_z)) rot = Vec(float(byt_pit), float(byt_yaw), float(byt_rol)) except ValueError: raise ParseError(line_num, 'Invalid line!') from None try: bone = bones[int(byt_ind)] except KeyError: raise ParseError(line_num, 'Unknown bone index {}!', int(byt_ind)) frames[time].append(BoneFrame(bone, pos, rot)) raise ParseError('end', 'No end to skeleton section!')
def make_alpha_base(vmf: VMF, bbox_min: Vec, bbox_max: Vec, noise: SimplexNoise): """Add the base to a CutoutTile, using displacements.""" # We want to limit the size of brushes to 512, so the vertexes don't # get too far apart. # This now contains each point from beginning to end inclusive. x, y, z = bbox_min dim_x = bbox_max.x - bbox_min.x dim_y = bbox_max.y - bbox_min.y widths = [x for x in range(0, int(dim_x), 512)] + [dim_x] heights = [y for y in range(0, int(dim_y), 512)] + [dim_y] # Loop over two offset copies, so we get a min and max each time. for x1, x2 in zip(widths, widths[1:]): for y1, y2 in zip(heights, heights[1:]): # We place our displacement 1 unit above the surface, then offset # the verts down. brush = vmf.make_prism( Vec(x + x1, y + y1, z - FLOOR_DEPTH), Vec(x + x2, y + y2, z - FLOOR_DEPTH - 1), ) brush.top.mat = random.choice(MATS['floorbase_disp']) make_displacement( brush.top, offset=-1, noise=noise, ) vmf.add_brush(brush.solid)
def output_norm(self, dest: DestType=DestType.PRIMARY) -> Vec: """Return the flow direction at the end of this curve type.""" assert dest is DestType.PRIMARY if self.reversed: return Vec(y=-1).rotate(*self.angles) else: return Vec(z=-1).rotate(*self.angles)
def make_anim( root: Bone, sty_ang: Angle, new_bones: Dict[Tuple[float, float, float], Dict[Bone, Bone]], name: str, animation: Dict[int, List[BoneFrame]], ) -> None: """Generate the animation file.""" print(f' - {name}...', flush=True) mesh = Mesh({}, {}, []) # Add all the bones. mesh.bones[ROOT_NAME] = root for bone_map in new_bones.values(): for bone in bone_map.values(): mesh.bones[bone.name] = bone # Do the animation for frame_num, frames in animation.items(): mesh.animation[frame_num] = new_frames = [ BoneFrame(root, Vec(), Angle()), ] for angle, bone_map in new_bones.items(): new_frames.append(BoneFrame(bone_map[root], Vec(), Angle(angle))) for frame in frames: new_frames.append( BoneFrame(bone_map[frame.bone], frame.position, frame.rotation)) with open(name + '.smd', 'wb') as f: mesh.export(f)
def output_norm(self, dest: DestType = DestType.PRIMARY) -> Vec: """Return the flow direction at the end of the curve.""" assert dest is DestType.PRIMARY if self.reversed: return Vec(y=self.y) @ self.matrix else: return Vec(x=1, y=-self.y).norm() @ self.matrix
def test(x1, y1, z1, x2, y2, z2): """Check a Vec pair for incorrect comparisons.""" vec1 = Vec(x1, y1, z1) vec2 = Vec(x2, y2, z2) for op_func in comp_ops: if op_func is op.ne: # special-case - != uses or, not and corr_result = x1 != x2 or y1 != y2 or z1 != z2 else: corr_result = op_func(x1, x2) and op_func(y1, y2) and op_func( z1, z2) comp = ('Incorrect {{}} comparison for ' '({} {} {}) {} ({} {} {})'.format(x1, y1, z1, op_func.__name__, x2, y2, z2)) assert op_func(vec1, vec2) == corr_result, comp.format('Vec') assert op_func(vec1, Vec_tuple( x2, y2, z2)) == corr_result, comp.format('Vec_tuple') assert op_func(vec1, (x2, y2, z2)) == corr_result, comp.format('tuple') # Bare numbers compare magnitude.. assert op_func(vec1, x2) == op_func(vec1.mag(), x2), comp.format('x') assert op_func(vec1, y2) == op_func(vec1.mag(), y2), comp.format('y') assert op_func(vec1, z2) == op_func(vec1.mag(), z2), comp.format('z')
def join_markers(inst_a, inst_b, is_start=False): """Join two marker ents together with corners. This returns a list of solids used for the vphysics_motion trigger. """ origin_a = Vec.from_str(inst_a['ent']['origin']) origin_b = Vec.from_str(inst_b['ent']['origin']) norm_a = Vec(-1, 0, 0).rotate_by_str(inst_a['ent']['angles']) norm_b = Vec(-1, 0, 0).rotate_by_str(inst_b['ent']['angles']) config = inst_a['conf'] if norm_a == norm_b: # Either straight-line, or s-bend. dist = (origin_a - origin_b).mag() if origin_a + (norm_a * dist) == origin_b: make_straight( 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( origin_a, origin_b, norm_a, config, max_size=inst_a['size'], ) return try: corner_ang, flat_angle = CORNER_ANG[norm_a.as_tuple(), norm_b.as_tuple()] if origin_a[flat_angle] != origin_b[flat_angle]: # It needs to be flat in this angle! raise ValueError except ValueError: # The tubes need two corners to join together - abort for that. return else: make_bend( origin_a, origin_b, norm_a, norm_b, corner_ang, config, max_size=inst_a['size'], )
def blank(root_name: str) -> 'Mesh': """Create an empty mesh, with a single root bone.""" root_bone = Bone(root_name, None) return Mesh( {root_name: root_bone}, {0: [BoneFrame(root_bone, Vec(), Vec())]}, [], )
def make_tag_coop_inst(tag_loc: str): """Make the coop version of the tag instances. This needs to be shrunk, so all the logic entities are not spread out so much (coop tubes are small). This way we avoid distributing the logic. """ global TAG_COOP_INST_VMF TAG_COOP_INST_VMF = vmf = VMF.parse( os.path.join(tag_loc, TAG_GUN_COOP_INST) ) def logic_pos(): """Put the entities in a nice circle...""" while True: for ang in range(0, 44): ang *= 360/44 yield Vec(16*math.sin(ang), 16*math.cos(ang), 32) pos = logic_pos() # Move all entities that don't care about position to the base of the player for ent in TAG_COOP_INST_VMF.iter_ents(): if ent['classname'] == 'info_coop_spawn': # Remove the original spawn point from the instance. # That way it can overlay over other dropper instances. ent.remove() elif ent['classname'] in ('info_target', 'info_paint_sprayer'): pass else: ent['origin'] = next(pos) # These originally use the coop spawn point, but this doesn't # always work. Switch to the name of the player, which is much # more reliable. if ent['classname'] == 'logic_measure_movement': ent['measuretarget'] = '!player_blue' # Add in a trigger to start the gel gun, and reset the activated # gel whenever the player spawns. trig_brush = vmf.make_prism( Vec(-32, -32, 0), Vec(32, 32, 16), mat='tools/toolstrigger', ).solid start_trig = vmf.create_ent( classname='trigger_playerteam', target_team=3, # ATLAS spawnflags=1, # Clients only origin='0 0 8', ) start_trig.solids = [trig_brush] start_trig.add_out( # This uses the !activator as the target player so it must be via trigger. Output('OnStartTouchBluePlayer', '@gel_ui', 'Activate', delay=0, only_once=True), # Reset the gun to fire nothing. Output('OnStartTouchBluePlayer', '@blueisenabled', 'SetValue', 0, delay=0.1), Output('OnStartTouchBluePlayer', '@orangeisenabled', 'SetValue', 0, delay=0.1), )
def join_markers(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 = Vec(-1, 0, 0).rotate_by_str(mark_a.ent['angles']) norm_b = Vec(-1, 0, 0).rotate_by_str(mark_b.ent['angles']) config = mark_a.conf if norm_a == norm_b: # Either straight-line, or s-bend. dist = (origin_a - origin_b).mag() if origin_a + (norm_a * dist) == origin_b: make_straight( 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( origin_a, origin_b, norm_a, config, max_size=mark_a.size, ) return try: corner_ang, flat_angle = CORNER_ANG[norm_a.as_tuple(), norm_b.as_tuple()] if origin_a[flat_angle] != origin_b[flat_angle]: # It needs to be flat in this angle! raise ValueError except ValueError: # The tubes need two corners to join together - abort for that. return else: make_bend( origin_a, origin_b, norm_a, norm_b, corner_ang, config, max_size=mark_a.size, )
def vec_point(self, t: float, dest: DestType = DestType.PRIMARY) -> Vec: assert dest is not DestType.TERTIARY x, y = curve_point(64.0, t) if dest is DestType.SECONDARY: return self.origin + Vec(y, x, 0) @ self.matrix elif self.is_straight: return self.origin + Vec(y=128 * t) @ self.matrix else: return self.origin + Vec(-y, x, 0) @ self.matrix
def output_norm(self, dest: DestType = DestType.PRIMARY) -> Vec: """Return the flow direction at the end of this curve type.""" if dest is DestType.PRIMARY: return Vec(x=1) @ self.matrix elif dest is DestType.SECONDARY: return Vec(y=1) @ self.matrix elif dest is DestType.TERTIARY: return Vec(x=-1) @ self.matrix else: raise AssertionError(dest)
def output_norm(self, dest: DestType = DestType.PRIMARY) -> Vec: """Return the flow direction at the end of this curve type.""" if dest is DestType.SECONDARY: return Vec(x=1) @ self.matrix else: assert dest is DestType.PRIMARY if self.is_straight: return Vec(y=1) @ self.matrix else: return Vec(x=-1) @ self.matrix
def test_to_angle_roundtrip(py_c_vec): """Check Vec.to_angle() roundtrips.""" Vec, Angle, Matrix, parse_vec_str = py_c_vec for x, y, z in iter_vec((-1, 0, 1)): if x == y == z == 0: continue norm = Vec(x, y, z).norm() ang = norm.to_angle() assert_vec(Vec(x=1).rotate(*ang), norm.x, norm.y, norm.z, ang)
def save_embeddedvoxel(item: Item, vmf: VMF) -> None: """Save embedded voxel volumes.""" for bbox_min, bbox_max in bounding_boxes(item.embed_voxels): vmf.create_ent('bee2_editor_embeddedvoxel').solids.append( vmf.make_prism( Vec(bbox_min) * 128 + (-64.0, -64.0, -192.0), Vec(bbox_max) * 128 + (+64.0, +64.0, -64.0), # Entirely ignored, but makes it easier to distinguish. 'tools/toolshint', ).solid)
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 = Vec(-1, 0, 0).rotate_by_str(mark_a.ent['angles']) norm_b = Vec(-1, 0, 0).rotate_by_str(mark_b.ent['angles']) config = mark_a.conf if norm_a == norm_b: # Either straight-line, or s-bend. dist = (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 side_off_a == side_off_b: make_bend( vmf, origin_a, origin_b, norm_a, norm_b, config, max_size=mark_a.size, )
def rotate(self, angles: Vec, origin: Vec=(0, 0, 0)): """Rotate this template, and return a new template with those angles.""" new_axis = {} origin = Vec(origin) for norm, (mat, axis_u, axis_v, rot) in self._axes.items(): axis_u = axis_u.localise(origin, angles) axis_v = axis_v.localise(origin, angles) norm = Vec(norm).rotate(*angles) new_axis[norm.as_tuple()] = mat, axis_u, axis_v, rot return ScalingTemplate(self.id, new_axis)
def vec_point(self, t: float, dest: DestType = DestType.PRIMARY) -> Vec: """Return the position this far through the given curve.""" x, y = curve_point(64.0, t) if dest is DestType.PRIMARY: return self.origin + Vec(y, x, 0) @ self.matrix elif dest is DestType.SECONDARY: return self.origin + Vec(y=128 * t) @ self.matrix elif dest is DestType.TERTIARY: return self.origin + Vec(-y, x, 0) @ self.matrix else: raise AssertionError(dest)
def test_bbox_parse_plane(axis: str, mins: tuple3, maxes: tuple3) -> None: """Test parsing planar bboxes from a VMF. With 5 skip sides, the brush is flattened into the remaining plane. """ vmf = VMF() ent = vmf.create_ent('bee2_collision_bbox', coll_solid=1) prism = vmf.make_prism(Vec(80, 10, 40), Vec(150, 220, 70), mat='tools/toolsskip') getattr(prism, axis).mat = 'tools/toolsclip' ent.solids.append(prism.solid) [bbox] = BBox.from_ent(ent) assert_bbox(bbox, mins, maxes, CollideType.SOLID, set())
def __init__( self, model: str, origin: Vec, angles: Vec, scaling: float, visleafs: List[int], solidity: int, flags: StaticPropFlags = StaticPropFlags.NONE, skin: int = 0, min_fade: float = 0, max_fade: float = 0, lighting_origin: Vec = None, fade_scale: float = -1, min_dx_level: int = 0, max_dx_level: int = 0, min_cpu_level: int = 0, max_cpu_level: int = 0, min_gpu_level: int = 0, max_gpu_level: int = 0, tint: Vec = Vec(255, 255, 255), # Rendercolor renderfx: int = 255, disable_on_xbox: bool = False, ) -> None: self.model = model self.origin = origin self.angles = angles self.scaling = scaling self.visleafs = visleafs self.solidity = solidity self.flags = flags self.skin = skin self.min_fade = min_fade self.max_fade = max_fade if lighting_origin is None: self.lighting = Vec(origin) else: self.lighting = lighting_origin self.fade_scale = fade_scale self.min_dx_level = min_dx_level self.max_dx_level = max_dx_level self.min_cpu_level = min_cpu_level self.max_cpu_level = max_cpu_level self.min_gpu_level = min_gpu_level self.max_gpu_level = max_gpu_level self.tint = Vec(tint) self.renderfx = renderfx self.disable_on_xbox = disable_on_xbox
def rotate(self, angles: Vec, origin: Optional[Vec] = None) -> 'ScalingTemplate': """Rotate this template, and return a new template with those angles.""" new_axis = {} if origin is None: origin = Vec() for norm, (mat, axis_u, axis_v, rot) in self._axes.items(): axis_u = axis_u.localise(origin, angles) axis_v = axis_v.localise(origin, angles) v_norm = Vec(norm).rotate(*angles) new_axis[v_norm.as_tuple()] = mat, axis_u, axis_v, rot return ScalingTemplate(self.id, new_axis)
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_bbox_vecs() -> None: """Test that the vector properties don't return the same object.""" bb = BBox(30, 60, 80, 120, 451, 730) assert bb.mins == Vec(30.0, 60.0, 80.0) assert bb.mins is not bb.mins assert bb.maxes == Vec(120.0, 451.0, 730.0) assert bb.maxes is not bb.maxes assert bb.size == Vec(90.0, 391.0, 650.0) assert bb.size is not bb.size assert bb.center == Vec(75.0, 255.5, 405.0) assert bb.center is not bb.center