Skip to content

compas_nest.datastructures

compas_nest.datastructures

COMPAS data structures for nesting input, replicating the OpenNest C# nest_geo and nest_sheets containers.

nest_geo

Bases: Data

Container for the parts (polylines with optional holes) to nest.

Replicates the role of the OpenNest C# nest_geo class.

Parameters:

Name Type Description Default
parts list[dict]

Each part is a dict {"outline": Polyline, "holes": [Polyline, ...], "copies": int, "attributes": [Geometry, ...]}.

None
name str
None

Attributes:

Name Type Description
parts list[dict]

The parts. Index into this list is the part index returned by the engines.

Source code in compas_nest/datastructures.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
class nest_geo(Data):
    """Container for the parts (polylines with optional holes) to nest.

    Replicates the role of the OpenNest C# ``nest_geo`` class.

    Parameters
    ----------
    parts : list[dict], optional
        Each part is a dict ``{"outline": Polyline, "holes": [Polyline, ...], "copies": int, "attributes": [Geometry, ...]}``.
    name : str, optional

    Attributes
    ----------
    parts : list[dict]
        The parts. Index into this list is the *part index* returned by the engines.
    """

    def __init__(self, parts=None, name=None):
        super().__init__(name=name)
        self.parts = parts or []

    @property
    def __data__(self):
        return {
            "parts": [
                {
                    "outline": part["outline"],
                    "holes": list(part.get("holes", [])),
                    "copies": int(part.get("copies", 1)),
                    "rotations": int(part.get("rotations", 0)),
                    "attributes": list(part.get("attributes", [])),
                }
                for part in self.parts
            ]
        }

    @classmethod
    def __from_data__(cls, data):
        return cls(parts=data["parts"])

    def add_part(self, outline, holes=None, copies=1, attributes=None, rotations=0):
        """Add a part.

        Parameters
        ----------
        outline : :class:`compas.geometry.Polyline`
            Closed outer ring of the part.
        holes : list[:class:`compas.geometry.Polyline`], optional
            Interior holes.
        copies : int, optional
            Number of identical copies to nest.
        attributes : list[:class:`compas.geometry.Geometry`], optional
            Extra geometry carried along with the part through placement (opennest engine feature).
        rotations : int, optional
            Per-part rotation override. ``0`` (default) = use the solver's global rotation setting;
            ``N`` = this part may only use ``N`` orientations (360/N degree steps); ``1`` = fixed
            (no rotation, e.g. grain direction). Lets rectangular and freeform parts share one nest
            with different rotation rules.

        Returns
        -------
        int
            The index of the added part.
        """
        self.parts.append(
            {
                "outline": outline,
                "holes": list(holes or []),
                "copies": int(copies),
                "rotations": max(0, int(rotations)),
                "attributes": list(attributes or []),
            }
        )
        return len(self.parts) - 1

    def _flatten_parts(self, expand_copies):
        """Flatten parts to the flat arrays the engines consume (parts stay in their raw frame).

        Parameters
        ----------
        expand_copies : bool
            If True, repeat each part ``copies`` times (physics engine, which has no quantity input)
            and return an ``origin_index`` map. If False, return per-part ``quantities`` (NFP engine).

        Returns
        -------
        dict
            Keys: ``vertex_counts``, ``xy``, ``hole_counts``, ``hole_vertex_counts``, ``hole_xy``,
            and either ``origin_index`` (expand) or ``quantities``.
        """
        vertex_counts = []
        xy = []
        hole_counts = []
        hole_vertex_counts = []
        hole_xy = []
        origin_index = []
        quantities = []
        # Per-part rotation override, one entry per emitted part (per instance when copies are
        # expanded, per part otherwise) — index-aligned with vertex_counts, i.e. part_count. 0 = global.
        rotations = []

        for part_index, part in enumerate(self.parts):
            copies = max(1, int(part.get("copies", 1)))
            reps = copies if expand_copies else 1
            if not expand_copies:
                quantities.append(copies)
            rot = max(0, int(part.get("rotations", 0)))
            for _ in range(reps):
                n, flat = _ring_xy(part["outline"])
                vertex_counts.append(n)
                xy.extend(flat)
                holes = part.get("holes", [])
                hole_counts.append(len(holes))
                for h in holes:
                    hn, hflat = _ring_xy(h)
                    hole_vertex_counts.append(hn)
                    hole_xy.extend(hflat)
                origin_index.append(part_index)
                rotations.append(rot)

        result = {
            "vertex_counts": vertex_counts,
            "xy": xy,
            "hole_counts": hole_counts,
            "hole_vertex_counts": hole_vertex_counts,
            "hole_xy": hole_xy,
            "rotations": rotations,
        }
        if expand_copies:
            result["origin_index"] = origin_index
        else:
            result["quantities"] = quantities
        return result

add_part(outline, holes=None, copies=1, attributes=None, rotations=0)

Add a part.

Parameters:

Name Type Description Default
outline :class:`compas.geometry.Polyline`

Closed outer ring of the part.

required
holes list[:class:`compas.geometry.Polyline`]

Interior holes.

None
copies int

Number of identical copies to nest.

1
attributes list[:class:`compas.geometry.Geometry`]

Extra geometry carried along with the part through placement (opennest engine feature).

None
rotations int

Per-part rotation override. 0 (default) = use the solver's global rotation setting; N = this part may only use N orientations (360/N degree steps); 1 = fixed (no rotation, e.g. grain direction). Lets rectangular and freeform parts share one nest with different rotation rules.

0

Returns:

Type Description
int

The index of the added part.

Source code in compas_nest/datastructures.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def add_part(self, outline, holes=None, copies=1, attributes=None, rotations=0):
    """Add a part.

    Parameters
    ----------
    outline : :class:`compas.geometry.Polyline`
        Closed outer ring of the part.
    holes : list[:class:`compas.geometry.Polyline`], optional
        Interior holes.
    copies : int, optional
        Number of identical copies to nest.
    attributes : list[:class:`compas.geometry.Geometry`], optional
        Extra geometry carried along with the part through placement (opennest engine feature).
    rotations : int, optional
        Per-part rotation override. ``0`` (default) = use the solver's global rotation setting;
        ``N`` = this part may only use ``N`` orientations (360/N degree steps); ``1`` = fixed
        (no rotation, e.g. grain direction). Lets rectangular and freeform parts share one nest
        with different rotation rules.

    Returns
    -------
    int
        The index of the added part.
    """
    self.parts.append(
        {
            "outline": outline,
            "holes": list(holes or []),
            "copies": int(copies),
            "rotations": max(0, int(rotations)),
            "attributes": list(attributes or []),
        }
    )
    return len(self.parts) - 1

nest_sheets

Bases: Data

Container for the sheets (polylines with optional holes) to nest into.

Replicates the role of the OpenNest C# nest_sheets class.

Parameters:

Name Type Description Default
sheets list[dict]

Each sheet is a dict {"outline": Polyline, "holes": [Polyline, ...]}.

None
name str
None

Attributes:

Name Type Description
sheets list[dict]

The sheets. Index into this list is the sheet_id returned by the engines.

Source code in compas_nest/datastructures.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
class nest_sheets(Data):
    """Container for the sheets (polylines with optional holes) to nest into.

    Replicates the role of the OpenNest C# ``nest_sheets`` class.

    Parameters
    ----------
    sheets : list[dict], optional
        Each sheet is a dict ``{"outline": Polyline, "holes": [Polyline, ...]}``.
    name : str, optional

    Attributes
    ----------
    sheets : list[dict]
        The sheets. Index into this list is the ``sheet_id`` returned by the engines.
    """

    def __init__(self, sheets=None, name=None):
        super().__init__(name=name)
        self.sheets = sheets or []

    @property
    def __data__(self):
        return {
            "sheets": [
                {"outline": sheet["outline"], "holes": list(sheet.get("holes", []))}
                for sheet in self.sheets
            ]
        }

    @classmethod
    def __from_data__(cls, data):
        return cls(sheets=data["sheets"])

    def add_sheet(self, outline, holes=None):
        """Add a sheet.

        Parameters
        ----------
        outline : :class:`compas.geometry.Polyline`
            Closed outer boundary of the sheet.
        holes : list[:class:`compas.geometry.Polyline`], optional
            Forbidden interior regions parts must avoid.

        Returns
        -------
        int
            The index of the added sheet.
        """
        self.sheets.append({"outline": outline, "holes": list(holes or [])})
        return len(self.sheets) - 1

    @classmethod
    def from_size(cls, width, height, count=1, gap=None):
        """Build ``count`` rectangular sheets laid out left to right.

        Parameters
        ----------
        width, height : float
            Sheet dimensions.
        count : int, optional
            Number of identical sheets.
        gap : float, optional
            Horizontal spacing between sheet origins in addition to ``width`` (defaults to ``0.1 * width``).

        Returns
        -------
        :class:`nest_sheets`
        """
        gap = 0.1 * width if gap is None else gap
        sheets = cls()
        for i in range(count):
            ox = i * (width + gap)
            outline = Polyline(
                [
                    [ox, 0.0, 0.0],
                    [ox + width, 0.0, 0.0],
                    [ox + width, height, 0.0],
                    [ox, height, 0.0],
                    [ox, 0.0, 0.0],
                ]
            )
            sheets.add_sheet(outline)
        return sheets

    def origins(self):
        """Return the world origin (bbox min x, y) of each sheet.

        The engines work in a sheet-local frame; placements are returned relative to this origin.

        Returns
        -------
        list[tuple[float, float]]
        """
        out = []
        for sheet in self.sheets:
            pts = sheet["outline"].points
            ox = min(float(p[0]) for p in pts)
            oy = min(float(p[1]) for p in pts)
            out.append((ox, oy))
        return out

    def to_arrays(self):
        """Flatten sheets to flat arrays in their local frame (each sheet normalised to its bbox min).

        Returns
        -------
        dict
            Keys: ``vertex_counts``, ``xy``, ``hole_counts``, ``hole_vertex_counts``, ``hole_xy``, ``origins``.
        """
        vertex_counts = []
        xy = []
        hole_counts = []
        hole_vertex_counts = []
        hole_xy = []
        origins = self.origins()

        for sheet, (ox, oy) in zip(self.sheets, origins):
            n, flat = _ring_xy(sheet["outline"], ox, oy)
            vertex_counts.append(n)
            xy.extend(flat)
            holes = sheet.get("holes", [])
            hole_counts.append(len(holes))
            for h in holes:
                hn, hflat = _ring_xy(h, ox, oy)
                hole_vertex_counts.append(hn)
                hole_xy.extend(hflat)

        return {
            "vertex_counts": vertex_counts,
            "xy": xy,
            "hole_counts": hole_counts,
            "hole_vertex_counts": hole_vertex_counts,
            "hole_xy": hole_xy,
            "origins": origins,
        }

add_sheet(outline, holes=None)

Add a sheet.

Parameters:

Name Type Description Default
outline :class:`compas.geometry.Polyline`

Closed outer boundary of the sheet.

required
holes list[:class:`compas.geometry.Polyline`]

Forbidden interior regions parts must avoid.

None

Returns:

Type Description
int

The index of the added sheet.

Source code in compas_nest/datastructures.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def add_sheet(self, outline, holes=None):
    """Add a sheet.

    Parameters
    ----------
    outline : :class:`compas.geometry.Polyline`
        Closed outer boundary of the sheet.
    holes : list[:class:`compas.geometry.Polyline`], optional
        Forbidden interior regions parts must avoid.

    Returns
    -------
    int
        The index of the added sheet.
    """
    self.sheets.append({"outline": outline, "holes": list(holes or [])})
    return len(self.sheets) - 1

from_size(width, height, count=1, gap=None) classmethod

Build count rectangular sheets laid out left to right.

Parameters:

Name Type Description Default
width float

Sheet dimensions.

required
height float

Sheet dimensions.

required
count int

Number of identical sheets.

1
gap float

Horizontal spacing between sheet origins in addition to width (defaults to 0.1 * width).

None

Returns:

Type Description
class:`nest_sheets`
Source code in compas_nest/datastructures.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
@classmethod
def from_size(cls, width, height, count=1, gap=None):
    """Build ``count`` rectangular sheets laid out left to right.

    Parameters
    ----------
    width, height : float
        Sheet dimensions.
    count : int, optional
        Number of identical sheets.
    gap : float, optional
        Horizontal spacing between sheet origins in addition to ``width`` (defaults to ``0.1 * width``).

    Returns
    -------
    :class:`nest_sheets`
    """
    gap = 0.1 * width if gap is None else gap
    sheets = cls()
    for i in range(count):
        ox = i * (width + gap)
        outline = Polyline(
            [
                [ox, 0.0, 0.0],
                [ox + width, 0.0, 0.0],
                [ox + width, height, 0.0],
                [ox, height, 0.0],
                [ox, 0.0, 0.0],
            ]
        )
        sheets.add_sheet(outline)
    return sheets

origins()

Return the world origin (bbox min x, y) of each sheet.

The engines work in a sheet-local frame; placements are returned relative to this origin.

Returns:

Type Description
list[tuple[float, float]]
Source code in compas_nest/datastructures.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
def origins(self):
    """Return the world origin (bbox min x, y) of each sheet.

    The engines work in a sheet-local frame; placements are returned relative to this origin.

    Returns
    -------
    list[tuple[float, float]]
    """
    out = []
    for sheet in self.sheets:
        pts = sheet["outline"].points
        ox = min(float(p[0]) for p in pts)
        oy = min(float(p[1]) for p in pts)
        out.append((ox, oy))
    return out

to_arrays()

Flatten sheets to flat arrays in their local frame (each sheet normalised to its bbox min).

Returns:

Type Description
dict

Keys: vertex_counts, xy, hole_counts, hole_vertex_counts, hole_xy, origins.

Source code in compas_nest/datastructures.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
def to_arrays(self):
    """Flatten sheets to flat arrays in their local frame (each sheet normalised to its bbox min).

    Returns
    -------
    dict
        Keys: ``vertex_counts``, ``xy``, ``hole_counts``, ``hole_vertex_counts``, ``hole_xy``, ``origins``.
    """
    vertex_counts = []
    xy = []
    hole_counts = []
    hole_vertex_counts = []
    hole_xy = []
    origins = self.origins()

    for sheet, (ox, oy) in zip(self.sheets, origins):
        n, flat = _ring_xy(sheet["outline"], ox, oy)
        vertex_counts.append(n)
        xy.extend(flat)
        holes = sheet.get("holes", [])
        hole_counts.append(len(holes))
        for h in holes:
            hn, hflat = _ring_xy(h, ox, oy)
            hole_vertex_counts.append(hn)
            hole_xy.extend(hflat)

    return {
        "vertex_counts": vertex_counts,
        "xy": xy,
        "hole_counts": hole_counts,
        "hole_vertex_counts": hole_vertex_counts,
        "hole_xy": hole_xy,
        "origins": origins,
    }