def intersect_idf_surfaces(idf):
    # type: (IDF) -> None
    """Intersect all surfaces in an IDF.

    :param idf: The IDF.
    """
    surfaces = idf.getsurfaces() + idf.getshadingsurfaces()
    try:
        ggr = idf.idfobjects["GLOBALGEOMETRYRULES"][0]
    except IndexError:
        ggr = None
    # get all the intersected surfaces
    adjacencies = get_adjacencies(surfaces)
    for surface in adjacencies:
        key, name = surface
        new_surfaces = adjacencies[surface]
        old_obj = idf.getobject(key.upper(), name)
        for i, new_coords in enumerate(new_surfaces, 1):
            new = idf.copyidfobject(old_obj)
            new.Name = "%s_%i" % (name, i)
            set_coords(new, new_coords, ggr)
        idf.removeidfobject(old_obj) def match_idf_surfaces(idf):
    # type: (IDF) -> None
    """Match all surfaces in an IDF.

    :param idf: The IDF.
    """
    surfaces = idf.getsurfaces() + idf.getshadingsurfaces()
    planes = getidfplanes(surfaces)
    matched = {}
    for distance in planes:
        for vector in planes[distance]:
            surfaces = planes[distance][vector]
            for surface in surfaces:
                set_unmatched_surface(surface, vector)
            matches = planes.get(-distance, {}).get(-vector, [])
            for s, m in product(surfaces, matches):
                if almostequal(s.coords, reversed(m.coords)):
                    matched[sorted_tuple(m, s)] = (m, s)

    for key in matched:
        set_matched_surfaces(*matched[key]) class Block(object):
    def __init__(
        self,
        name,  # type: str
        coordinates,  # type: Union[List[Tuple[float, float]], List[Tuple[int, int]]]
        height,  # type: float
        num_stories=1,  # type: int
        below_ground_stories=0,  # type: int
        below_ground_storey_height=2.5,  # type: float
        zoning="by_storey",  # type: str
        perim_depth=3.0,  # type: float
    ):
        # type: (...) -> None
        """Represents a single block for translation into an IDF.

        :param name: A name for the block.
        :param coordinates: A list of (x, y) tuples representing the building outline.
        :param height: The height of the block roof above ground level.
        :param num_stories: The total number of stories including basement stories. Default : 1.
        :param below_ground_stories: The number of stories below ground. Default : 0.
        :param below_ground_storey_height: The height of each basement storey. Default : 2.5.
        :param zoning: The zoning pattern of the block. Default : by_storey
        :param perim_depth: Depth of the perimeter zones if the core/perim zoning pattern is requested. Default : 3.0.

        """
        self.name = name
        if coordinates[0] == coordinates[-1]:
            coordinates.pop()
        self.coordinates = coordinates
        self.height = height
        self.num_stories = num_stories
        self.num_below_ground_stories = below_ground_stories
        self.below_ground_storey_height = below_ground_storey_height
        self.zoning = zoning
        self.perim_depth = perim_depth

    @property
    def stories(self):
        # type: () -> List[Dict[str, Any]]
        """A list of dicts of the surfaces of each storey in the block.

        :returns: list of dicts

        Example dict format::
            {'floors': [...],
             'ceilings': [...],
             'walls': [...],
             'roofs': [...],
             }

        """
        stories = []
        if self.num_below_ground_stories != 0:
            floor_no = -self.num_below_ground_stories
        else:
            floor_no = 0
        for floor, ceiling, wall, roof in zip(
            self.floors, self.ceilings, self.walls, self.roofs
        ):
            stories.append(
                {
                    "storey_no": floor_no,
                    "floors": floor,
                    "ceilings": ceiling,
                    "walls": wall,
                    "roofs": roof,
                }
            )
            floor_no += 1
        return stories

    @property
    def footprint(self):
        # type: () -> Polygon3D
        """Ground level outline of the block.

        :returns: A 2D outline of the block.

        """
        coordinates = [(v[0], v[1], 0) for v in self.coordinates]
        return Polygon3D(coordinates)

    @property
    def storey_height(self):
        # type: () -> float
        """Height of above ground stories.

        :returns: Average storey height.

        """
        return float(self.height) / (self.num_stories - self.num_below_ground_stories)

    @property
    def floor_heights(self):
        # type: () -> List[float]
        """Floor height for each storey in the block.

        :returns: A list of floor heights.

        """
        lfl = self.lowest_floor_level
        sh = self.storey_height
        floor_heights = [lfl + sh * i for i in range(self.num_stories)]
        return floor_heights

    @property
    def ceiling_heights(self):
        # type: () -> List[float]
        """Ceiling height for each storey in the block.

        :returns: A list of ceiling heights.

        """
        lfl = self.lowest_floor_level
        sh = self.storey_height
        ceiling_heights = [lfl + sh * (i + 1) for i in range(self.num_stories)]
        return ceiling_heights

    @property
    def lowest_floor_level(self):
        # type: () -> float
        """Floor level of the lowest basement storey.

        :returns: Lowest floor height.

        """
        return -(self.num_below_ground_stories * self.below_ground_storey_height)

    @property
    def walls(self):
        # type: () -> List[List[Polygon3D]]
        """Coordinates for each wall in the block.

        These are ordered as a list of lists, one for each storey.

        :returns: Coordinates for all walls.

        """
        walls = []
        for fh, ch in zip(self.floor_heights, self.ceiling_heights):
            floor_walls = [_make_wall(edge, fh, ch) for edge in self.footprint.edges]
            walls.append(floor_walls)
        return walls

    @property
    def floors(self):
        """Coordinates for each floor in the block.

        :returns: Coordinates for all floors.

        """
        floors = [
            [self.footprint.invert_orientation() + Vector3D(0, 0, fh)]
            for fh in self.floor_heights
        ]
        return floors

    @property
    def ceilings(self):
        """Coordinates for each ceiling in the block.

        :returns: Coordinates for all ceilings.

        """
        ceilings = [
            [self.footprint + Vector3D(0, 0, ch)] for ch in self.ceiling_heights[:-1]
        ]

        ceilings.append([])
        return ceilings

    @property
    def roofs(self):
        """Coordinates for each roof of the block.

        This returns a list with an entry for each floor for consistency with
        the other properties of the Block object, but should only have roof
        coordinates in the list in the final position.

        :returns: Coordinates for all roofs.

        """
        roofs = [[] for ch in self.ceiling_heights[:-1]]  # type: List[List[Polygon]]
        roofs.append([self.footprint + Vector3D(0, 0, self.height)])
        return roofs

    @property
    def surfaces(self):
        # type: () -> Dict[str, Any]
        """Coordinates for all the surfaces in the block.

        :returns: Coordinates for all surfaces.

        """
        return {
            "walls": self.walls,
            "ceilings": self.ceilings,
            "roofs": self.roofs,
            "floors": self.floors,
        } class Zone(object):
    """Represents a single zone for translation into an IDF."""

    def __init__(self, name, surfaces):
        # type: (str, Dict[str, Any]) -> None
        """Initialise the zone object.

        :param name: A name for the zone.
        :param surfaces: The surfaces that make up the zone.

        """
        self.name = name
        self.walls = [s for s in surfaces["walls"] if s.area > 0]
        self.floors = surfaces["floors"]
        self.roofs = surfaces["roofs"]
        self.ceilings = surfaces["ceilings"] @property
def bounding_box(self):
    # type: () -> Polygon
    aligned = align_face(self)
    top_left = Vector3D(min(aligned.xs), max(aligned.ys), max(aligned.zs))
    bottom_left = Vector3D(min(aligned.xs), min(aligned.ys), min(aligned.zs))
    bottom_right = Vector3D(max(aligned.xs), min(aligned.ys), min(aligned.zs))
    top_right = Vector3D(max(aligned.xs), max(aligned.ys), max(aligned.zs))

    bbox = Polygon3D([top_left, bottom_left, bottom_right, top_right])
    return invert_align_face(self, bbox) class Polygon2D(Polygon):
    """Two-dimensional polygon."""

    n_dims = 2
    vector_class = Vector2D

    def __eq__(self, other):
        if self.__dict__ == other.__dict__:  # try the simple case first
            return True
        else:  # also cover same shape in different rotation
            if self.difference(other):
                return False
            if almostequal(self.normal_vector, other.normal_vector):
                return True
        return False

    @property
    def normal_vector(self):
        # type: () -> Vector3D
        as_3d = Polygon3D((v.x, v.y, 0) for v in self)
        return as_3d.normal_vector

    def project_to_3D(self, example3d):
        # type: (Polygon3D) -> Polygon3D
        """Project the 2D polygon rotated into 3D space.

        This is used to return a previously rotated 3D polygon back to its original orientation, or to to put polygons
        generated from pyclipper into the desired orientation.

        :param example3D: A 3D polygon in the desired plane.
        :returns: A 3D polygon.

        """
        points = self.points_matrix
        proj_axis = example3d.projection_axis
        a = example3d.distance
        v = example3d.normal_vector
        projected_points = project_to_3D(points, proj_axis, a, v)
        return Polygon3D(projected_points)

    @property
    def zs(self):
        # type: () -> List[float]
        return [0.0] * len(self.vertices) class Vector2D(Sized, Iterable):
    """Two dimensional point."""

    def __init__(self, *args):
        # type: (*Any) -> None
        self.args = list(args)
        self.x = float(args[0])
        self.y = float(args[1])
        self.z = 0.0

    def __iter__(self):
        # type: () -> Iterator
        return (i for i in self.args)

    def __repr__(self):
        # type: () -> str
        class_name = type(self).__name__
        return "{}({!r}, {!r})".format(class_name, *self.args)

    def __eq__(self, other):
        for a, b in zip(self, other):
            if a != b:
                return False
        return True

    def __sub__(self, other):
        # type: (Any) -> Union[Vector2D, Vector3D]
        return self.__class__(*[self[i] - other[i] for i in range(len(self))])

    def __add__(self, other):
        # type: (Any) -> Union[Vector2D, Vector3D]
        return self.__class__(*[self[i] + other[i] for i in range(len(self))])

    def __neg__(self):
        # type: () -> Union[Vector2D, Vector3D]
        return self.__class__(*inverse_vector(self))

    def __len__(self):
        # type: () -> int
        return len(self.args)

    def __getitem__(self, key):
        # type: (Union[int, slice]) -> Union[Any, List[Any]]
        return self.args[key]

    def __setitem__(self, key, value):
        self.args[key] = value

    def __hash__(self):
        return hash(self.x) ^ hash(self.y)

    def dot(self, other):
        # type: (Vector3D) -> np.float64
        return np.dot(self, other)

    def cross(self, other):
        # type: (Union[Vector2D, Vector3D]) -> np.ndarray
        return np.cross(self, other)

    @property
    def length(self):
        # type: () -> float
        """The length of a vector."""
        length = sum(x ** 2 for x in self.args) ** 0.5

        return length

    def closest(self, poly):
        # type: (Polygon3D) -> Optional[Any]
        """Find the closest vector in a polygon.

        :param poly: Polygon or Polygon3D

        """
        min_d = float("inf")
        closest_pt = None
        for pt2 in poly:
            direction = self - pt2
            sq_d = sum(x ** 2 for x in direction)
            if sq_d < min_d:
                min_d = sq_d
                closest_pt = pt2
        return closest_pt

    def normalize(self):
        # type: () -> Union[Vector2D, Vector3D]
        return self.set_length(1.0)

    def set_length(self, new_length):
        # type: (float) -> Union[Vector2D, Vector3D]
        current_length = self.length
        multiplier = new_length / current_length
        self.args = [i * multiplier for i in self.args]
        return self

    def invert(self):
        # type: () -> Union[Vector2D, Vector3D]
        return -self

    def as_array(self, dims=3):
        # type: (Union[Vector2D, Vector3D], int) -> np.ndarray
        """Convert a point to a numpy array.

        Converts a Vector3D to a numpy.array([x,y,z]) or a Vector2D to a numpy.array([x,y]).
        Ensures all values are floats since some other types cause problems in pyclipper (notably where sympy.Zero is
        used to represent 0.0).

        :param pt: The point to convert.
        :param dims: Number of dimensions {default : 3}.
        :returns: Vector as a Numpy array.

        """
        # handle Vector3D
        if dims == 3:
            return np.array([float(self.x), float(self.y), float(self.z)])
        # handle Vector2D
        elif dims == 2:
            return np.array([float(self.x), float(self.y)])
        else:
            raise ValueError("%s-dimensional vectors are not supported." % dims)

    def as_tuple(self, dims=3):
        # type: (Union[Vector2D, Vector3D, int]) -> Union[Tuple[float, float], Tuple[float, float, float]]
        """Convert a point to a numpy array.

        Convert a Vector3D to an (x,y,z) tuple or a Vector2D to an (x,y) tuple.
        Ensures all values are floats since some other types cause problems in pyclipper (notably where sympy.Zero is
        used to represent 0.0).

        :param pt: The point to convert.
        :param dims: Number of dimensions {default : 3}.
        :returns: Vector as a tuple.

        """
        # handle Vector3D
        if dims == 3:
            return float(self.x), float(self.y), float(self.z)
        # handle Vector2D
        elif dims == 2:
            return float(self.x), float(self.y)
        else:
            raise ValueError("%s-dimensional vectors are not supported." % dims)

    def relative_distance(self, v2):
        # type: (Vector3D) -> float
        """A distance function for sorting vectors by distance.

        This only provides relative distance, not actual distance since we only use it for sorting.

        :param v2: Another vector.
        :returns: Relative distance between two point vectors.

        """
        direction = self - v2
        return sum(x ** 2 for x in direction) class Vector3D(Vector2D):
    """Three dimensional point."""

    def __init__(
        self,
        x,  # type: Union[float, np.float64]
        y,  # type: Union[float, np.float64]
        z=0,  # type: Union[float, np.float64]
    ):
        # type: (...) -> None
        super(Vector3D, self).__init__(x, y, z)
        self.z = float(z)
        self.args = [self.x, self.y, self.z]

    def __repr__(self):
        # type: () -> str
        class_name = type(self).__name__
        return "{}({!r}, {!r}, {!r})".format(class_name, *self.args)

    def __hash__(self):
        # type: () -> int
        return hash(self.x) ^ hash(self.y) ^ hash(self.z) def export_to_obj(idf, fname=None, mtllib=None):
    # type: (IDF, Optional[str], Optional[str]) -> None
    """Export an OBJ file representation of the IDF.

    This can be used for viewing in tools which support the .obj format.

    :param idf: An IDF to export.
    :param fname: A filename for the .obj file. If None we try to base it on IDF.idfname and change the filetype.
    :param mtllib: The name of a .mtl file to be referenced from the .obj file. If None, we use default.mtl.
    """
    if not fname:
        name, _ext = os.path.splitext(idf.idfname)
        fname = "%s.obj" % name
    if not mtllib:
        name, _ext = os.path.splitext(fname)
        mtllib = "%s.mtl" % name
        shutil.copy(os.path.join(THIS_DIR, "default.mtl"), mtllib)
    obj_writer = ObjWriter()
    surfaces = idf.getsurfaces()
    subsurfaces = idf.getsubsurfaces()
    shading_surfaces = idf.getshadingsurfaces()
    obj_writer.from_surfaces(surfaces, subsurfaces, shading_surfaces)
    obj_writer.write(fname, mtllib) class PatchedIDF(BaseIDF):
    """Monkey-patched IDF.

    Patched to add read (to add additional functionality) and to fix copyidfobject and newidfobject.
    """

    def read(self):
        """Read the IDF file and the IDD file.

        If the IDD file had already been read, it will not be read again.

        Populates the following data structures::

        - idfobjects : list
        - model : list
        - idd_info : list
        - idd_index : dict
        """
        if self.getiddname() is None:
            errortxt = "IDD file needed to read the idf file. Set it using IDF.setiddname(iddfile)"
            raise IDDNotSetError(errortxt)
        (
            self.idfobjects,
            block,
            self.model,
            idd_info,
            idd_index,
            versiontuple,
        ) = idfreader1(
            self.idfname, self.iddname, self, commdct=self.idd_info, block=self.block
        )
        self.__class__.setidd(idd_info, idd_index, block, versiontuple)

    def newidfobject(self, key, aname="", **kwargs):
        # type: (str, str, **Any) -> EpBunch
        """Add a new idfobject to the model.

        If you don't specify a value for a field, the default value will be set.

        For example ::

            newidfobject("CONSTRUCTION")
            newidfobject("CONSTRUCTION",
                Name='Interior Ceiling_class',
                Outside_Layer='LW Concrete',
                Layer_2='soundmat')

        :param key: The type of IDF object. This must be in ALL_CAPS.
        :param aname: This parameter is not used. It is left there for backward compatibility.
        :param kwargs: Keyword arguments in the format `field=value` used to set fields in the EnergyPlus object.
        :returns: EpBunch object.
        """
        obj = newrawobject(self.model, self.idd_info, key)
        abunch = obj2bunch(self.model, self.idd_info, obj)
        if aname:
            warnings.warn(
                "The aname parameter should no longer be used (%s)." % aname,
                UserWarning,
            )
            namebunch(abunch, aname)
        self.idfobjects[key].append(abunch)  # type: Dict[str, Idf_MSequence]
        for k, v in kwargs.items():
            abunch[k] = v
        return abunch

    def copyidfobject(self, idfobject):
        # type: (EpBunch) -> EpBunch
        """Add an IDF object to the IDF.

        This has been monkey-patched to add the return value.

        :param idfobject: The IDF object to copy. Usually from another IDF, or it can be used to copy within this IDF.
        :returns: EpBunch object.
        """
        return addthisbunch(self.idfobjects, self.model, self.idd_info, idfobject, self) def set_default_constructions(idf):
    # type: (IDF) -> None
    """Set default constructions for surfaces in the model.

    :param idf: The IDF object.

    """
    constructions = [
        "Project Wall",
        "Project Partition",
        "Project Floor",
        "Project Flat Roof",
        "Project Ceiling",
        "Project Door",
    ]
    for construction in constructions:
        idf.newidfobject(
            "CONSTRUCTION", Name=construction, Outside_Layer="DefaultMaterial"
        )
    idf.newidfobject(
        "MATERIAL",
        Name="DefaultMaterial",
        Roughness="Rough",
        Thickness=0.1,
        Conductivity=0.1,
        Density=1000,
        Specific_Heat=1000,
    )

    idf.newidfobject(
        "CONSTRUCTION", Name="Project External Window", Outside_Layer="DefaultGlazing"
    )
    idf.newidfobject(
        "WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM",
        Name="DefaultGlazing",
        UFactor=2.7,
        Solar_Heat_Gain_Coefficient=0.763,
        Visible_Transmittance=0.8,
    )

    for surface in idf.getsurfaces():
        set_default_construction(surface)
    for subsurface in idf.getsubsurfaces():
        set_default_construction(subsurface) def set_wwr(
    idf, wwr=0.2, construction=None, force=False, wwr_map=None, orientation=None
):
    # type: (IDF, Optional[float], Optional[str], Optional[bool], Optional[dict], Optional[str]) -> None
    """Set the window to wall ratio on all external walls.

    :param idf: The IDF to edit.
    :param wwr: The window to wall ratio.
    :param construction: Name of a window construction.
    :param force: True to remove all subsurfaces before setting the WWR.
    :param wwr_map: Mapping from wall orientation (azimuth) to WWR, e.g. {180: 0.25, 90: 0.2}.
    :param orientation: One of "north", "east", "south", "west". Walls within 45 degrees will be affected.

    """
    try:
        ggr = idf.idfobjects["GLOBALGEOMETRYRULES"][0]  # type: Optional[Idf_MSequence]
    except IndexError:
        ggr = None

    # check orientation
    orientations = {
        "north": 0.0,
        "east": 90.0,
        "south": 180.0,
        "west": 270.0,
        None: None,
    }
    degrees = orientations.get(orientation, None)
    external_walls = filter(
        lambda x: x.Outside_Boundary_Condition.lower() == "outdoors",
        idf.getsurfaces("wall"),
    )
    external_walls = filter(
        lambda x: _has_correct_orientation(x, degrees), external_walls
    )
    subsurfaces = idf.getsubsurfaces()
    base_wwr = wwr
    for wall in external_walls:
        # get any subsurfaces on the wall
        wall_subsurfaces = list(
            filter(lambda x: x.Building_Surface_Name == wall.Name, subsurfaces)
        )
        if not all(_is_window(wss) for wss in wall_subsurfaces) and not force:
            raise ValueError(
                'Not all subsurfaces on wall "{name}" are windows. '
                "Use `force=True` to replace all subsurfaces.".format(name=wall.Name)
            )

        if wall_subsurfaces and not construction:
            constructions = list(
                {wss.Construction_Name for wss in wall_subsurfaces if _is_window(wss)}
            )
            if len(constructions) > 1:
                raise ValueError(
                    'Not all subsurfaces on wall "{name}" have the same construction'.format(
                        name=wall.Name
                    )
                )
            construction = constructions[0]
        # remove all subsurfaces
        for ss in wall_subsurfaces:
            idf.removeidfobject(ss)
        wwr = (wwr_map or {}).get(wall.azimuth) or base_wwr
        if not wwr:
            return
        coords = window_vertices_given_wall(wall, wwr)
        window = idf.newidfobject(
            "FENESTRATIONSURFACE:DETAILED",
            Name="%s window" % wall.Name,
            Surface_Type="Window",
            Construction_Name=construction or "",
            Building_Surface_Name=wall.Name,
            View_Factor_to_Ground="autocalculate",  # from the surface angle
        )
        window.setcoords(coords, ggr) def rotate(surfaces, angle):
    # type: (Union[List[EpBunch], Idf_MSequence], float) -> None
    """Rotate all surfaces by an angle.

    :param surfaces: A list of EpBunch objects or a mutable sequence.
    :param angle: An angle in degrees.

    """
    radians = np.deg2rad(angle)
    for s in surfaces:
        if not s.coords:
            warnings.warn(
                "%s was not affected by this operation since it does not define vertices."
                % s.Name
            )
            continue
        new_coords = rotate_coords(s.coords, radians)
        s.setcoords(new_coords) def scale(surfaces, factor, axes):
    # type: (Idf_MSequence, float, str) -> None
    """Scale all surfaces by a factor.

    :param surfaces: A list of EpBunch objects.
    :param factor: Factor to scale the surfaces by.
    :param axes: Axes to scale on.

    """
    for s in surfaces:
        if not s.coords:
            warnings.warn(
                "%s was not affected by this operation since it does not define vertices."
                % s.Name
            )
            continue
        new_coords = scale_coords(s.coords, factor, axes)
        s.setcoords(new_coords) def translate(
    surfaces, vector
):  # type: (Idf_MSequence, Union[Tuple[float, float], Vector2D, Vector3D]) -> None
    """Translate all surfaces by a vector.

    :param surfaces: A list of EpBunch objects.
    :param vector: Representation of a vector to translate by.

    """
    vector = Vector3D(*vector)
    for s in surfaces:
        if not s.coords:
            warnings.warn(
                "%s was not affected by this operation since it does not define vertices."
                % s.Name
            )
            continue
        new_coords = translate_coords(s.coords, vector)
        s.setcoords(new_coords) def translate_to_origin(idf):
    # type: (IDF) -> None
    """Move an IDF close to the origin so that it can be viewed in SketchUp.

    :param idf: The IDF to edit.

    """
    surfaces = idf.getsurfaces()
    subsurfaces = idf.getsubsurfaces()
    shading_surfaces = idf.getshadingsurfaces()

    min_x = min(min(Polygon3D(s.coords).xs) for s in surfaces)
    min_y = min(min(Polygon3D(s.coords).ys) for s in surfaces)

    translate(surfaces, (-min_x, -min_y))
    translate(subsurfaces, (-min_x, -min_y))
    translate(shading_surfaces, (-min_x, -min_y)) def view_idf(fname=None, idf_txt=None, test=False, idf=None):
    # type: (Optional[str], Optional[str], Optional[bool], Optional[IDF]) -> None
    """Display an IDF for inspection.

    :param fname: Path to the IDF.
    :param idf_txt: The string representation of an IDF.
    """
    from geomeppy import IDF

    try:
        plt.figure()
    except TclError:
        # this is as expected on the test server
        return
    if len([arg for arg in [fname, idf_txt, idf] if arg]) > 1:
        raise ValueError("Pass only one of fname, idf_txt or idf.")
    if not idf:
        # set the IDD for the version of EnergyPlus
        iddfhandle = StringIO(iddcurrent.iddtxt)
        if IDF.getiddname() is None:
            IDF.setiddname(iddfhandle)

        if fname:
            # import the IDF
            idf = IDF(fname)
        elif idf_txt:
            idf = IDF()
            idf.initreadtxt(idf_txt)
    # create the figure and add the surfaces
    ax = plt.axes(projection="3d")
    collections = _get_collections(idf, opacity=0.5)
    for c in collections:
        ax.add_collection3d(c)

    # calculate and set the axis limits
    limits = _get_limits(idf=idf)
    ax.set_xlim(limits["x"])
    ax.set_ylim(limits["y"])
    ax.set_zlim(limits["z"])

    if not test:
        plt.show() def core_perim_zone_coordinates(footprint, perim_depth):
    """Returns a dictionary with the coordinates of the core/perimeter zones and
    a list of tuples containing the coordinates for the core zone."""
    zones_perim = get_perims(footprint, get_core(footprint, perim_depth))
    core = get_core(footprint, perim_depth)

    zones_dict = {}
    for idx, zone_perim in enumerate(zones_perim, 1):
        zone_name = "Perimeter_Zone_%i" % idx
        perim_zones_coords = []
        for pts in zone_perim:
            perim_zones_coords.append(pts.as_tuple(dims=2))
        zones_dict[zone_name] = perim_zones_coords
    core_zone_coords = []
    for pts in core:
        core_zone_coords.append(pts.as_tuple(dims=2))

    zones_dict["Core_Zone"] = core_zone_coords
    return zones_dict, core_zone_coords
import itertools
from typing import Any, Dict, List, Optional, Union  # noqa
from eppy.bunch_subclass import EpBunch  # noqa
from eppy.idf_msequence import Idf_MSequence  # noqa
from .geom.intersect_match import intersect_idf_surfaces, match_idf_surfaces
from .builder import Block, Zone
from .geom.polygons import bounding_box, Polygon2D  # noqa
from .geom.vectors import Vector2D, Vector3D  # noqa
from .io.obj import export_to_obj
from .patches import PatchedIDF
from .recipes import (
    set_default_constructions,
    set_wwr,
    rotate,
    scale,
    translate,
    translate_to_origin,
)
from .view_geometry import view_idf
from .geom.core_perim import core_perim_zone_coordinates
"""
This module contains the implementation of `geomeppy.IDF`.
"""




def new_idf(fname):
    # type: (str) -> IDF
    """Create a new blank IDF.

    :param fname: A name for the new IDF.

    """
    idf = IDF()
    idf.new(fname)
    return idf


class IDF(PatchedIDF):
    """Geometry-enabled IDF class, usable in the same way as Eppy's IDF.

    This adds geometry functionality to Eppy's IDF class.

    """

    def intersect_match(self):
        # type: () -> None
        """Intersect all surfaces in the IDF, then set boundary conditions."""
        self.intersect()
        self.match()

    def intersect(self):
        # type: () -> None
        """Intersect all surfaces in the IDF."""
        intersect_idf_surfaces(self)

    def match(self):
        # type: () -> None
        """Set boundary conditions for all surfaces in the IDF."""
        match_idf_surfaces(self)

    def translate_to_origin(self):
        # type: () -> None
        """Move an IDF close to the origin so that it can be viewed in SketchUp."""
        translate_to_origin(self)

    def translate(self, vector):
        # type: (Vector2D) -> None
        """Move the IDF in the direction given by a vector.

        :param vector: A vector to translate by.

        """
        surfaces = self.getsurfaces()
        translate(surfaces, vector)
        subsurfaces = self.getsubsurfaces()
        translate(subsurfaces, vector)
        shadingsurfaces = self.getshadingsurfaces()
        translate(shadingsurfaces, vector)

    def rotate(self, angle, anchor=None):
        # type: (float, Optional[Union[Vector2D, Vector3D]]) -> None
        """Rotate the IDF counterclockwise by the angle given.

        :param angle: Angle (in degrees) to rotate by.
        :param anchor: Point around which to rotate. Default is the centre of the the IDF's bounding box.

        """
        anchor = anchor or self.centroid
        surfaces = self.getsurfaces()
        subsurfaces = self.getsubsurfaces()
        shadingsurfaces = self.getshadingsurfaces()
        self.translate(-anchor)
        rotate(surfaces, angle)
        rotate(subsurfaces, angle)
        rotate(shadingsurfaces, angle)
        self.translate(anchor)

    def scale(self, factor, anchor=None, axes="xy"):
        # type: (float, Optional[Union[Vector2D, Vector3D]], str) -> None
        """Scale the IDF by a scaling factor.

        :param factor: Factor to scale by.
        :param anchor: Point to scale around. Default is the centre of the the IDF's bounding box.
        :param axes: Axes to scale on. Default 'xy'.

        """
        anchor = anchor or self.centroid
        surfaces = self.getsurfaces()
        subsurfaces = self.getsubsurfaces()
        shadingsurfaces = self.getshadingsurfaces()
        self.translate(-anchor)
        scale(surfaces, factor, axes)
        scale(subsurfaces, factor, axes)
        scale(shadingsurfaces, factor, axes)
        self.translate(anchor)


    def set_default_constructions(self):