def res_rotate_inst(inst: Entity, res: Property) -> None: """Rotate the instance around an axis. If `axis` is specified, it should be a normal vector and the instance will be rotated `angle` degrees around it. Otherwise, `angle` is a pitch-yaw-roll angle which is applied. `around` can be a point (local, pre-rotation) which is used as the origin. """ angles = Angle.from_str(inst['angles']) if 'axis' in res: orient = Matrix.axis_angle( Vec.from_str(inst.fixup.substitute(res['axis'])), conv_float(inst.fixup.substitute(res['angle'])), ) else: orient = Matrix.from_angle( Angle.from_str(inst.fixup.substitute(res['angle']))) try: offset = Vec.from_str(inst.fixup.substitute(res['around'])) except NoKeyError: pass else: origin = Vec.from_str(inst['origin']) inst['origin'] = origin + (-offset @ orient + offset) @ angles inst['angles'] = (orient @ angles).to_angle()
def test_old_rotation(py_c_vec) -> None: """Verify that the code matches the results from the earlier Vec.rotate code.""" Vec, Angle, Matrix, parse_vec_str = py_c_vec for pitch in range(0, 360, 15): for yaw in range(0, 360, 15): for roll in range(0, 360, 15): ang = Angle(pitch, yaw, roll) mat = Matrix.from_angle(ang) # Construct a matrix directly from 3 vector rotations. old_mat = Matrix() old_mat[0, 0], old_mat[0, 1], old_mat[0, 2] = old_rotate( Vec(x=1), pitch, yaw, roll) old_mat[1, 0], old_mat[1, 1], old_mat[1, 2] = old_rotate( Vec(y=1), pitch, yaw, roll) old_mat[2, 0], old_mat[2, 1], old_mat[2, 2] = old_rotate( Vec(z=1), pitch, yaw, roll) assert_rot(mat, old_mat, ang) old = old_rotate(Vec(128, 0, 0), pitch, yaw, roll) by_ang = Vec(128, 0, 0) @ ang by_mat = Vec(128, 0, 0) @ mat assert_vec(by_ang, old.x, old.y, old.z, ang, tol=1e-1) assert_vec(by_mat, old.x, old.y, old.z, ang, tol=1e-1)
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
def test(axis, equiv_ang: Py_Angle): for ang in range(0, 360, 15): assert_rot(Matrix.axis_angle(axis, ang), Matrix.from_angle(ang * equiv_ang), f'{axis} * {ang} != {equiv_ang}') # Inverse axis = reversed rotation. assert_rot(Matrix.axis_angle(-axis, ang), Matrix.from_angle(-ang * equiv_ang), f'{-axis} * {ang} != {equiv_ang}')
def test_matrix_roundtrip_pitch(py_c_vec): """Check converting to and from a Matrix does not change values.""" Vec, Angle, Matrix, parse_vec_str = py_c_vec # We can't directly check the resulted value, some of these produce # gimbal lock and can't be recovered. # So instead check the rotation matrix is the same. for pitch in range(0, 360, 45): old_ang = Angle(pitch, 0, 0) new_ang = Matrix.from_pitch(pitch).to_angle() assert_rot( Matrix.from_angle(old_ang), Matrix.from_angle(new_ang), (old_ang, new_ang), )
def res_add_placement_helper(inst: Entity, res: Property): """Add a placement helper to a specific tile. `Offset` and `normal` specify the position and direction out of the surface the helper should be added to. If `upDir` is specified, this is the direction of the top of the portal. """ orient = Matrix.from_angle(Angle.from_str(inst['angles'])) pos = conditions.resolve_offset(inst, res['offset', '0 0 0'], zoff=-64) normal = res.vec('normal', 0, 0, 1) @ orient up_dir: Optional[Vec] try: up_dir = Vec.from_str(res['upDir']) @ orient except LookupError: up_dir = None try: tile = tiling.TILES[(pos - 64 * normal).as_tuple(), normal.as_tuple()] except KeyError: LOGGER.warning('No tile at {} @ {}', pos, normal) return tile.add_portal_helper(up_dir)
def res_set_marker(inst: Entity, res: Property) -> None: """Set a marker at a specific position. Parameters: * `global`: If true, the position is an absolute position, ignoring this instance. * `name`: A name to store to identify this marker/item. * `pos`: The position or offset to use for the marker. """ origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) try: is_global = srctools.conv_bool( inst.fixup.substitute(res['global'], allow_invert=True)) except LookupError: is_global = False name = inst.fixup.substitute(res['name']).casefold() pos = Vec.from_str(inst.fixup.substitute(res['pos'])) if not is_global: pos = pos @ orient + origin mark = Marker(pos, name, inst) MARKERS.append(mark) LOGGER.debug('Marker added: {}', mark)
def make_corner( vmf: VMF, origin: Vec, start_dir: Vec, end_dir: Vec, size: int, config: Config, ) -> None: """Place a corner.""" angles = Matrix.from_basis(z=start_dir, x=end_dir) conditions.add_inst( vmf, origin=origin, angles=angles, file=config.inst_corner[int(size)], ) temp, visgroups = config.temp_corner[int(size)] if temp is not None: temp_solids = template_brush.import_template( vmf, temp, additional_visgroups=visgroups, origin=origin, angles=angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ).world motion_trigger(vmf, *temp_solids)
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 make_corner( vmf: VMF, origin: Vec, start_dir: Vec, end_dir: Vec, size: int, config: Config, ) -> None: angles = Matrix.from_basis(z=start_dir, x=end_dir).to_angle() vmf.create_ent( classname='func_instance', origin=origin, angles=angles, file=config.inst_corner[size], ) temp = config.temp_corner[size] if temp: temp_solids = template_brush.import_template( vmf, temp, origin=origin, angles=angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ).world motion_trigger(vmf, *temp_solids)
def add_item_coll(self, item: Item, inst: Entity) -> None: """Add the default collisions from an item definition for this instance.""" origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) for coll in item.collisions: self.add( (coll @ orient + origin).with_attrs(name=inst['targetname']))
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 __init__(self, inst: Entity, conf: Config, size: int) -> None: self.ent = inst self.conf = conf self.next = None self.no_prev = True self.size = size self.orient = Matrix.from_angle(Angle.from_str(inst['angles']))
class Node(Generic[ConfT]): """Represents a single node in the chain.""" item: Item = attr.ib(init=True) conf: ConfT = attr.ib(init=True) # Origin and angles of the instance. pos = attr.ib(init=False, default=attr.Factory( lambda self: Vec.from_str(self.item.inst['origin']), takes_self=True, )) orient = attr.ib(init=False, default=attr.Factory( lambda self: Matrix.from_angle( Angle.from_str(self.item.inst['angles'])), takes_self=True, )) # The links between nodes prev: Optional[Node[ConfT]] = attr.ib(default=None, init=False) next: Optional[Node[ConfT]] = attr.ib(default=None, init=False) @property def inst(self) -> Entity: """Return the relevant instance.""" return self.item.inst @classmethod def from_inst(cls, inst: Entity, conf: ConfT) -> Node[ConfT]: """Find the item for this instance, and return the node.""" name = inst['targetname'] try: return Node(connections.ITEMS[name], conf) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None
def __init__(self, ent: Entity) -> None: self.origin = Vec.from_str(ent['origin']) self.matrix = Matrix.from_angle(Angle.from_str(ent['angles'])) self.ent = ent self.has_input = False # We verify every node has an input if used. # DestType -> output. self.outputs: Dict[DestType, Optional[Node]] = dict.fromkeys( self.out_types, None) # Outputs fired when cubes reach this point. pass_outputs = [ out for out in ent.outputs if out.output.casefold() == self.pass_out_name ] self.has_pass = bool(pass_outputs) if self.has_pass: for out in pass_outputs: out.output = 'On' + PASS_OUT if ent['classname'].startswith('comp_'): # Remove the extra keyvalues we use. ent.keys = { 'classname': 'info_target', 'targetname': ent['targetname'], 'origin': ent['origin'], 'angles': ent['angles'], } ent.make_unique('_vac_node') elif not self.keep_ent: ent.remove()
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 _fill_norm_rotations() -> Dict[Tuple[Tuple[float, float, float], Tuple[ float, float, float]], Matrix, ]: """Given a norm->norm rotation, return the angles producing that.""" rotations = {} for norm_ax in 'xyz': for norm_mag in [-1, +1]: norm = Vec.with_axes(norm_ax, norm_mag) for angle_ax in ('pitch', 'yaw', 'roll'): for angle_mag in (-90, 90): angle = Matrix.from_angle( Angle.with_axes(angle_ax, angle_mag)) new_norm = norm @ angle if new_norm != norm: rotations[norm.as_tuple(), new_norm.as_tuple()] = angle # Assign a null rotation as well. rotations[norm.as_tuple(), norm.as_tuple()] = Matrix() rotations[norm.as_tuple(), (-norm).as_tuple()] = Matrix() return rotations
def test_gen_check(py_c_vec) -> None: """Do an exhaustive check on all rotation math using data from the engine.""" Vec, Angle, Matrix, parse_vec_str = py_c_vec X = Vec(x=1) Y = Vec(y=1) Z = Vec(z=1) with open('rotation.txt') as f: for line_num, line in enumerate(f, start=1): if not line.startswith('|'): # Skip other junk in the log. continue (pit, yaw, roll, for_x, for_y, for_z, left_x, left_y, left_z, up_x, up_y, up_z) = map(float, line[1:].split()) mat = Matrix.from_angle(Angle(pit, yaw, roll)) # Then check rotating vectors works correctly. # The engine actually gave us a right vector, so we need to flip that. assert_vec(X @ mat, for_x, for_y, for_z) assert_vec(Y @ mat, -left_x, -left_y, -left_z) assert_vec(Z @ mat, up_x, up_y, up_z) assert math.isclose(for_x, mat[0, 0], abs_tol=EPSILON) assert math.isclose(for_y, mat[0, 1], abs_tol=EPSILON) assert math.isclose(for_z, mat[0, 2], abs_tol=EPSILON) assert math.isclose(-left_x, mat[1, 0], abs_tol=EPSILON) assert math.isclose(-left_y, mat[1, 1], abs_tol=EPSILON) assert math.isclose(-left_z, mat[1, 2], abs_tol=EPSILON) assert math.isclose(up_x, mat[2, 0], abs_tol=EPSILON) assert math.isclose(up_y, mat[2, 1], abs_tol=EPSILON) assert math.isclose(up_z, mat[2, 2], abs_tol=EPSILON) # Also test Matrix.from_basis(). x = Vec(for_x, for_y, for_z) y = -Vec(left_x, left_y, left_z) z = Vec(up_x, up_y, up_z) assert_rot(Matrix.from_basis(x=x, y=y, z=z), mat) assert_rot(Matrix.from_basis(x=x, y=y), mat) assert_rot(Matrix.from_basis(y=y, z=z), mat) assert_rot(Matrix.from_basis(x=x, z=z), mat)
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_roll(py_c_vec): """Check converting to and from a Matrix does not change values.""" Vec, Angle, Matrix, parse_vec_str = py_c_vec for roll in range(0, 360, 45): if roll in (90, -90): # Don't test gimbal lock. continue mat = Matrix.from_roll(roll) assert_ang(mat.to_angle(), 0, 0, roll)
def test_bad_from_basis(py_c_vec) -> None: """Test invalid arguments to Matrix.from_basis()""" Vec, Angle, Matrix, parse_vec_str = py_c_vec v = Vec(0, 1, 0) with pytest.raises(TypeError): Matrix.from_basis() with pytest.raises(TypeError): Matrix.from_basis(x=v) with pytest.raises(TypeError): Matrix.from_basis(y=v) with pytest.raises(TypeError): Matrix.from_basis(z=v)
def __init__(self, pos: Vec, config: Config, radius: float) -> None: self.config = config self.prev: Optional[Node] = None self.next: Optional[Node] = None self.pos = pos self.radius = radius # Orientation of the segment up to the next. self.orient = Matrix() # The points for the cylinder, on these sides. self.points_prev: List[Vertex] = [] self.points_next: List[Vertex] = []
def res_alt_orientation(res: Property) -> Callable[[Entity], None]: """Apply an alternate orientation. "wall" makes the attaching surface in the -X direction, making obs rooms, corridors etc easier to build. The Z axis points in the former +X direction. "ceiling" flips the instance, making items such as droppers easier to build. The X axis remains unchanged. """ val = res.value.casefold() if val == 'wall': pose = Matrix.from_angle(-90, 180, 0) elif val in ('ceil', 'ceiling'): pose = Matrix.from_roll(180) else: raise ValueError(f'Unknown orientation type "{res.value}"!') def swap_orient(inst: Entity) -> None: """Apply the new orientation.""" inst['angles'] = pose @ Angle.from_str(inst['angles']) return swap_orient
def res_sendificator(vmf: VMF, inst: Entity): """Implement Sendificators.""" # For our version, we know which sendtor connects to what laser, # so we can couple the logic together (avoiding @sendtor_mutex). sendtor_name = inst['targetname'] sendtor = connections.ITEMS[sendtor_name] sendtor.enable_cmd += (Output( '', f'@{sendtor_name}_las_relay_*', 'Trigger', delay=0.01, ), ) for ind, conn in enumerate(list(sendtor.outputs), start=1): las_item = conn.to_item conn.remove() try: targ_offset, targ_normal = SENDTOR_TARGETS[las_item.name] except KeyError: LOGGER.warning('"{}" is not a Sendificator target!', las_item.name) continue orient = Matrix.from_angle(Angle.from_str(las_item.inst['angles'])) targ_offset = Vec.from_str( las_item.inst['origin']) + targ_offset @ orient targ_normal = targ_normal @ orient relay_name = f'@{sendtor_name}_las_relay_{ind}' relay = vmf.create_ent( 'logic_relay', targetname=relay_name, origin=targ_offset, angles=targ_normal.to_angle(), ) relay.add_out( Output('OnTrigger', '!self', 'RunScriptCode', '::sendtor_source <- self;'), Output('OnTrigger', '@sendtor_fire', 'Trigger'), ) if not las_item.inputs: # No other inputs, make it on always. PeTI automatically turns # it off when inputs are connected, which is annoying. las_item.inst.fixup['$start_enabled'] = '1' is_on = True else: is_on = las_item.inst.fixup.bool('$start_enabled') relay['StartDisabled'] = not is_on las_item.enable_cmd += (Output('', relay_name, 'Enable'), ) las_item.disable_cmd += (Output('', relay_name, 'Disable'), )
def test_ang_matrix_roundtrip(py_c_vec): """Check converting to and from a Matrix does not change values.""" Vec, Angle, Matrix, parse_vec_str = py_c_vec for p, y, r in iter_vec(range(0, 360, 90)): vert = Vec(x=1).rotate(p, y, r).z if vert < 0.99 or vert > 0.99: # If nearly vertical, gimbal lock prevents roundtrips. continue mat = Matrix.from_angle(Angle(p, y, r)) assert_ang(mat.to_angle(), p, y, r)
def res_antigel(inst: Entity) -> None: """Implement the Antigel marker.""" inst.remove() origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) pos = round(origin - 128 * orient.up(), 6) norm = round(orient.up(), 6) try: tiling.TILES[pos.as_tuple(), norm.as_tuple()].is_antigel = True except KeyError: LOGGER.warning('No tile to set antigel at {}, {}', pos, norm) texturing.ANTIGEL_LOCS.add((origin // 128).as_tuple())
def test_vec_basic_roll(py_c_vec): """Check each direction rotates appropriately in roll.""" Vec, Angle, Matrix, parse_vec_str = py_c_vec assert_vec(Vec(0, 200, 0) @ Matrix.from_roll(0), 0, 200, 0) assert_vec(Vec(0, 0, 150) @ Matrix.from_roll(0), 0, 0, 150) assert_vec(Vec(0, 200, 0) @ Matrix.from_roll(90), 0, 0, 200) assert_vec(Vec(0, 0, 150) @ Matrix.from_roll(90), 0, -150, 0) assert_vec(Vec(0, 200, 0) @ Matrix.from_roll(180), 0, -200, 0) assert_vec(Vec(0, 0, 150) @ Matrix.from_roll(180), 0, 0, -150) assert_vec(Vec(0, 200, 0) @ Matrix.from_roll(270), 0, 0, -200) assert_vec(Vec(0, 0, 150) @ Matrix.from_roll(270), 0, 150, 0)
def test_vec_basic_pitch(py_c_vec) -> None: """Check each direction rotates appropriately in pitch.""" Vec, Angle, Matrix, parse_vec_str = py_c_vec assert_vec(Vec(200, 0, 0) @ Matrix.from_pitch(0), 200, 0, 0) assert_vec(Vec(0, 0, 150) @ Matrix.from_pitch(0), 0, 0, 150) assert_vec(Vec(200, 0, 0) @ Matrix.from_pitch(90), 0, 0, -200) assert_vec(Vec(0, 0, 150) @ Matrix.from_pitch(90), 150, 0, 0) assert_vec(Vec(200, 0, 0) @ Matrix.from_pitch(180), -200, 0, 0) assert_vec(Vec(0, 0, 150) @ Matrix.from_pitch(180), 0, 0, -150) assert_vec(Vec(200, 0, 0) @ Matrix.from_pitch(270), 0, 0, 200) assert_vec(Vec(0, 0, 150) @ Matrix.from_pitch(270), -150, 0, 0)
def fix_single_straight( seg: Segment, over_name: str, join_points: dict[tuple[str, float, float, float], Segment], overlay_joins: dict[Segment, set[Segment]], ) -> None: """Figure out the correct rotation for 1-long straight antlines.""" # Check the U and V axis, to see if there's another antline on both # sides. If there is that's the correct orientation. orient = Matrix.from_angle(seg.normal.to_angle()) center = seg.start.copy() for off in [ orient.left(-8.0), orient.left(+8.0), orient.up(-8.0), orient.up(+8.0), ]: try: neigh = join_points[(over_name, ) + (center + off).as_tuple()] except KeyError: continue overlay_joins[seg].add(neigh) overlay_joins[neigh].add(seg) off_min = center - abs(off) off_max = center + abs(off) # If corners are on both opposite sides, we can be fairly confident # that's the correct orientation. If we don't have that (end of trail), # settle for one side. if seg.start == seg.end: # No points found. This is our best guess. seg.start = off_min seg.end = off_max elif seg.start != off_min or seg.end != off_max: # The other side is also present. Only override if we are on both # sides. if (over_name, ) + (center - off).as_tuple() in join_points: seg.start = off_min seg.end = off_max # Else: Both equal, we're fine. if seg.start == seg.end: raise ValueError( 'Cannot determine orientation ' 'for 1-wide straight ' 'antline at ({})!'.format(seg.start) )
def test_bbox_rotation( pitch: float, yaw: float, roll: float, ) -> None: """Test the rotation logic against the slow direct approach.""" ang = Angle(pitch, yaw, roll) bb_start = BBox(100, 200, 300, 300, 450, 600, contents=CollideType.ANTLINES, tags='blah') # Directly compute, by rotating all the angles, points = [ Vec(x, y, z) for x in [100, 300] for y in [200, 450] for z in [300, 600] ] result_ang = bb_start @ ang result_mat = bb_start @ Matrix.from_angle(ang) assert result_ang == result_mat bb_min, bb_max = Vec.bbox( point @ ang for point in points ) assert_bbox(result_mat, round(bb_min, 0), round(bb_max, 0), CollideType.ANTLINES, {'blah'})