knit-graphs 0.0.10__py3-none-any.whl → 0.0.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
knit_graphs/Yarn.py CHANGED
@@ -3,18 +3,22 @@
3
3
  This module contains the Yarn class and Yarn_Properties dataclass which together represent the physical yarn used in knitting patterns.
4
4
  The Yarn class manages the sequence of loops along a yarn and their floating relationships.
5
5
  """
6
+
6
7
  from __future__ import annotations
7
8
 
9
+ from collections.abc import Iterator
8
10
  from dataclasses import dataclass
9
- from typing import TYPE_CHECKING, Iterator, cast
10
-
11
- from networkx import DiGraph, dfs_edges, dfs_preorder_nodes
11
+ from typing import TYPE_CHECKING, Self, TypeVar, cast, overload
12
12
 
13
+ from knit_graphs.directed_loop_graph import Directed_Loop_Graph, Float_Edge
14
+ from knit_graphs.knit_graph_errors.knit_graph_error import Use_Cut_Yarn_ValueError
13
15
  from knit_graphs.Loop import Loop
14
16
 
15
17
  if TYPE_CHECKING:
16
18
  from knit_graphs.Knit_Graph import Knit_Graph
17
19
 
20
+ LoopT = TypeVar("LoopT", bound=Loop)
21
+
18
22
 
19
23
  @dataclass(frozen=True)
20
24
  class Yarn_Properties:
@@ -22,9 +26,8 @@ class Yarn_Properties:
22
26
 
23
27
  This frozen dataclass contains all the physical and visual properties that characterize a yarn, including its structure, weight, and appearance.
24
28
  """
29
+
25
30
  name: str = "yarn" # name (str): The name or identifier for this yarn type.
26
- plies: int = 2 # plies (int): The number of individual strands twisted together to form the yarn.
27
- weight: float = 28 # weight (float): The weight category or thickness of the yarn.
28
31
  color: str = "green" # color (str): The color of the yarn for visualization purposes.
29
32
 
30
33
  def __str__(self) -> str:
@@ -33,7 +36,7 @@ class Yarn_Properties:
33
36
  Returns:
34
37
  str: String representation in format "name(plies-weight,color)".
35
38
  """
36
- return f"{self.name}({self.plies}-{self.weight},{self.color})"
39
+ return f"{self.name}({self.color})"
37
40
 
38
41
  def __repr__(self) -> str:
39
42
  """Get the name of the yarn for debugging purposes.
@@ -52,7 +55,7 @@ class Yarn_Properties:
52
55
  """
53
56
  return Yarn_Properties()
54
57
 
55
- def __eq__(self, other: Yarn_Properties) -> bool:
58
+ def __eq__(self, other: object) -> bool:
56
59
  """Check equality with another Yarn_Properties instance.
57
60
 
58
61
  Args:
@@ -61,7 +64,7 @@ class Yarn_Properties:
61
64
  Returns:
62
65
  bool: True if all properties (name, plies, weight, color) are equal, False otherwise.
63
66
  """
64
- return self.name == other.name and self.plies == other.plies and self.weight == other.weight and self.color == other.color
67
+ return isinstance(other, Yarn_Properties) and self.name == other.name and self.color == other.color
65
68
 
66
69
  def __hash__(self) -> int:
67
70
  """Get hash value for use in sets and dictionaries.
@@ -69,48 +72,57 @@ class Yarn_Properties:
69
72
  Returns:
70
73
  int: Hash value based on all yarn properties.
71
74
  """
72
- return hash((self.name, self.plies, self.weight, self.color))
75
+ return hash((self.name, self.color))
73
76
 
74
77
 
75
- class Yarn:
78
+ class Yarn(Directed_Loop_Graph[LoopT, Float_Edge[LoopT]]):
76
79
  """A class to represent a yarn structure as a sequence of connected loops.
77
80
 
78
81
  The Yarn class manages a directed graph of loops representing the physical yarn path through a knitted structure.
79
82
  It maintains the sequential order of loops and their floating relationships, providing methods for navigation and manipulation of the yarn structure.
80
83
 
81
84
  Attributes:
82
- loop_graph (DiGraph): The directed graph loops connected by yarn-wise float edges.
83
85
  properties (Yarn_Properties): The physical and visual properties of this yarn.
84
86
  """
85
- FRONT_LOOPS: str = "Front_Loops"
86
- _BACK_LOOPS: str = "Back_Loops"
87
87
 
88
- def __init__(self, yarn_properties: None | Yarn_Properties = None, knit_graph: None | Knit_Graph = None):
88
+ def __init__(
89
+ self,
90
+ yarn_properties: Yarn_Properties | None = None,
91
+ knit_graph: Knit_Graph[LoopT] | None = None,
92
+ instance: int = 0,
93
+ loop_class: type[LoopT] | None = None,
94
+ ):
89
95
  """Initialize a yarn with the specified properties and optional knit graph association.
90
96
 
91
97
  Args:
92
98
  yarn_properties (None | Yarn_Properties, optional): The properties defining this yarn. If None, uses default properties. Defaults to standard properties.
93
99
  knit_graph (None | Knit_Graph, optional): The knit graph that will own this yarn. Can be None for standalone yarns. Defaults to None.
100
+ instance (int, optional): The instance of this yarn. As new yarns are formed by cuts, the instance will increase. Defaults to 0 (first instance of this yarn).
101
+ loop_class (type[LoopT], optional): The type of loop to create by default in this class. Defaults to Loop class.
94
102
  """
95
- self.loop_graph: DiGraph = DiGraph()
103
+ super().__init__()
104
+ self._instance: int = instance
105
+ self._is_cut: bool = False
106
+ self._loop_class: type[LoopT] = loop_class if loop_class is not None else cast(type[LoopT], Loop)
96
107
  if yarn_properties is None:
97
108
  yarn_properties = Yarn_Properties.default_yarn()
98
109
  self.properties: Yarn_Properties = yarn_properties
99
- self._first_loop: Loop | None = None
100
- self._last_loop: Loop | None = None
101
- self._knit_graph: None | Knit_Graph = knit_graph
110
+ self._first_loop: LoopT | None = None
111
+ self._last_loop: LoopT | None = None
112
+ self._knit_graph: Knit_Graph[LoopT] = knit_graph if knit_graph is not None else Knit_Graph[LoopT]()
113
+ if self not in self.knit_graph.yarns:
114
+ self.knit_graph.add_yarn(self)
102
115
 
103
116
  @property
104
- def knit_graph(self) -> None | Knit_Graph:
105
- """Get the knit graph that owns this yarn.
106
-
117
+ def knit_graph(self) -> Knit_Graph[LoopT]:
118
+ """
107
119
  Returns:
108
- None | Knit_Graph: The knit graph that owns this yarn, or None if not associated with a graph.
120
+ Knit_Graph | Knit_Graph: The knit graph that owns this yarn, or None if not associated with a graph.
109
121
  """
110
122
  return self._knit_graph
111
123
 
112
124
  @knit_graph.setter
113
- def knit_graph(self, knit_graph: Knit_Graph) -> None:
125
+ def knit_graph(self, knit_graph: Knit_Graph[LoopT]) -> None:
114
126
  """Set the knit graph that owns this yarn.
115
127
 
116
128
  Args:
@@ -118,88 +130,8 @@ class Yarn:
118
130
  """
119
131
  self._knit_graph = knit_graph
120
132
 
121
- def has_float(self, u: Loop, v: Loop) -> bool:
122
- """Check if there is a float edge between two loops on this yarn.
123
-
124
- Args:
125
- u (Loop): The first loop to check for float connection.
126
- v (Loop): The second loop to check for float connection.
127
-
128
- Returns:
129
- bool: True if there is a float edge between the loops, False otherwise.
130
- """
131
- return bool(self.loop_graph.has_edge(u, v))
132
-
133
- def add_loop_in_front_of_float(self, front_loop: Loop, u: Loop, v: Loop) -> None:
134
- """Record that a loop falls in front of the float between two other loops.
135
-
136
- Args:
137
- front_loop (Loop): The loop that is positioned in front of the float.
138
- u (Loop): The first loop in the float pair.
139
- v (Loop): The second loop in the float pair.
140
- """
141
- if not self.has_float(u, v):
142
- if self.has_float(v, u):
143
- self.add_loop_in_front_of_float(front_loop, v, u)
144
- else:
145
- return
146
- self.loop_graph.edges[u, v][self.FRONT_LOOPS].add(front_loop)
147
- front_loop.add_loop_in_front_of_float(u, v)
148
-
149
- def add_loop_behind_float(self, back_loop: Loop, u: Loop, v: Loop) -> None:
150
- """Record that a loop falls behind the float between two other loops.
151
-
152
- Args:
153
- back_loop (Loop): The loop that is positioned behind the float.
154
- u (Loop): The first loop in the float pair.
155
- v (Loop): The second loop in the float pair.
156
- """
157
- if not self.has_float(u, v):
158
- if self.has_float(v, u):
159
- self.add_loop_behind_float(back_loop, v, u)
160
- else:
161
- return
162
- self.loop_graph.edges[u, v][self._BACK_LOOPS].add(back_loop)
163
- back_loop.add_loop_behind_float(u, v)
164
-
165
- def get_loops_in_front_of_float(self, u: Loop, v: Loop) -> set[Loop]:
166
- """Get all loops positioned in front of the float between two loops.
167
-
168
- Args:
169
- u (Loop): The first loop in the float pair.
170
- v (Loop): The second loop in the float pair.
171
-
172
- Returns:
173
- set[Loop]: Set of loops positioned in front of the float between u and v, or empty set if no float exists.
174
- """
175
- if not self.has_float(u, v):
176
- if self.has_float(v, u):
177
- return self.get_loops_in_front_of_float(v, u)
178
- else:
179
- return set()
180
- else:
181
- return cast(set[Loop], self.loop_graph.edges[u, v][self.FRONT_LOOPS])
182
-
183
- def get_loops_behind_float(self, u: Loop, v: Loop) -> set[Loop]:
184
- """Get all loops positioned behind the float between two loops.
185
-
186
- Args:
187
- u (Loop): The first loop in the float pair.
188
- v (Loop): The second loop in the float pair.
189
-
190
- Returns:
191
- set[Loop]: Set of loops positioned behind the float between u and v, or empty set if no float exists.
192
- """
193
- if not self.has_float(u, v):
194
- if self.has_float(v, u):
195
- return self.get_loops_behind_float(v, u)
196
- else:
197
- return set()
198
- else:
199
- return cast(set[Loop], self.loop_graph.edges[u, v][self._BACK_LOOPS])
200
-
201
133
  @property
202
- def last_loop(self) -> Loop | None:
134
+ def last_loop(self) -> LoopT | None:
203
135
  """Get the most recently added loop at the end of the yarn.
204
136
 
205
137
  Returns:
@@ -208,7 +140,7 @@ class Yarn:
208
140
  return self._last_loop
209
141
 
210
142
  @property
211
- def first_loop(self) -> Loop | None:
143
+ def first_loop(self) -> LoopT | None:
212
144
  """Get the first loop at the beginning of the yarn.
213
145
 
214
146
  Returns:
@@ -225,6 +157,14 @@ class Yarn:
225
157
  """
226
158
  return self.last_loop is not None
227
159
 
160
+ @property
161
+ def is_cut(self) -> bool:
162
+ """
163
+ Returns:
164
+ bool: True if yarn has been cut and will no longer form loops, False otherwise.
165
+ """
166
+ return self._is_cut
167
+
228
168
  @property
229
169
  def yarn_id(self) -> str:
230
170
  """Get the string identifier for this yarn.
@@ -234,100 +174,194 @@ class Yarn:
234
174
  """
235
175
  return str(self.properties)
236
176
 
237
- def __str__(self) -> str:
238
- """Get the string representation of this yarn.
177
+ @property
178
+ def float_iter(self) -> Iterator[tuple[LoopT, LoopT]]:
179
+ """
180
+ Returns:
181
+ Iterator[tuple[Loop, Loop]]: An iterator over tuples of connected loops representing the yarn path.
182
+ """
183
+ if self.first_loop is None:
184
+ return iter([])
185
+ return self.dfs_edges(self.first_loop)
186
+
187
+ def loops_in_front_of_floats(self) -> Iterator[tuple[LoopT, LoopT, set[LoopT]]]:
188
+ """Get all float segments with loops positioned in front of them.
239
189
 
240
190
  Returns:
241
- str: The yarn identifier string.
191
+ list[tuple[Loop, Loop, set[Loop]]]: List of tuples containing the two loops defining each float and the set of loops positioned in front of that float.
192
+ Only includes floats that have loops in front of them.
242
193
  """
243
- return self.yarn_id
194
+ return ((u, v, d.front_loops) for u, v, d in self.edge_iter if len(d.front_loops) > 0)
244
195
 
245
- def __repr__(self) -> str:
246
- """Get the representation string of this yarn for debugging.
196
+ def loops_behind_floats(self) -> Iterator[tuple[LoopT, LoopT, set[LoopT]]]:
197
+ """Get all float segments with loops positioned behind them.
247
198
 
248
199
  Returns:
249
- str: The representation of the yarn properties.
200
+ list[tuple[Loop, Loop, set[Loop]]]: List of tuples containing the two loops defining each float and the set of loops positioned behind that float.
201
+ Only includes floats that have loops behind them.
250
202
  """
251
- return repr(self.properties)
203
+ return ((u, v, d.back_loops) for u, v, d in self.edge_iter if len(d.back_loops) > 0)
252
204
 
253
- def __hash__(self) -> int:
254
- """Get the hash value of this yarn for use in sets and dictionaries.
205
+ def next_loop(self, loop: LoopT) -> LoopT | None:
206
+ """
207
+ Args:
208
+ loop (LoopT): The loop to find the next loop from.
255
209
 
256
210
  Returns:
257
- int: Hash value based on the yarn properties.
211
+ LoopT | None: The next loop on yarn after the specified loop, or None if it's the last loop.
212
+
213
+ Raises:
214
+ KeyError: If the specified loop is not on this yarn.
215
+ """
216
+ if loop not in self:
217
+ raise KeyError(f"Loop {loop} is not on Yarn")
218
+ successors = self.successors(loop)
219
+ return successors.pop() if len(successors) > 0 else None
220
+
221
+ def prior_loop(self, loop: LoopT) -> LoopT | None:
222
+ """
223
+ Args:
224
+ loop (Loop): The loop to find the prior loop from.
225
+
226
+ Returns:
227
+ Loop | None: The prior loop on yarn before the specified loop, or None if it's the first loop.
228
+
229
+ Raises:
230
+ KeyError: If the specified loop is not on this yarn.
258
231
  """
259
- return hash(self.properties)
232
+ if loop not in self:
233
+ raise KeyError(f"Loop {loop} is not on Yarn")
234
+ predecessors = self.predecessors(loop)
235
+ if len(predecessors) > 0:
236
+ return predecessors.pop()
237
+ else:
238
+ return None
260
239
 
261
- def __len__(self) -> int:
262
- """Get the number of loops on this yarn.
240
+ def has_float(self, u: LoopT, v: LoopT) -> bool:
241
+ """Check if there is a float edge between two loops on this yarn.
242
+
243
+ Args:
244
+ u (Loop): The first loop to check for float connection.
245
+ v (Loop): The second loop to check for float connection.
263
246
 
264
247
  Returns:
265
- int: The total number of loops on this yarn.
248
+ bool: True if there is a float edge between the loops, False otherwise.
266
249
  """
267
- return len(self.loop_graph.nodes)
250
+ return bool(self.has_edge(u, v))
268
251
 
269
- def make_loop_on_end(self) -> Loop:
252
+ def get_loops_in_front_of_float(self, u: LoopT, v: LoopT) -> set[LoopT]:
253
+ """Get all loops positioned in front of the float between two loops.
254
+
255
+ Args:
256
+ u (Loop): The first loop in the float pair.
257
+ v (Loop): The second loop in the float pair.
258
+
259
+ Returns:
260
+ set[Loop]: Set of loops positioned in front of the float between u and v, or empty set if no float exists.
261
+ """
262
+ if not self.has_float(u, v):
263
+ if self.has_float(v, u):
264
+ return self.get_loops_in_front_of_float(v, u)
265
+ else:
266
+ return set()
267
+ else:
268
+ return self.get_edge(u, v).front_loops
269
+
270
+ def get_loops_behind_float(self, u: LoopT, v: LoopT) -> set[LoopT]:
271
+ """Get all loops positioned behind the float between two loops.
272
+
273
+ Args:
274
+ u (Loop): The first loop in the float pair.
275
+ v (Loop): The second loop in the float pair.
276
+
277
+ Returns:
278
+ set[Loop]: Set of loops positioned behind the float between u and v, or empty set if no float exists.
279
+ """
280
+ if not self.has_float(u, v):
281
+ if self.has_float(v, u):
282
+ return self.get_loops_behind_float(v, u)
283
+ else:
284
+ return set()
285
+ else:
286
+ return self.get_edge(u, v).back_loops
287
+
288
+ def make_loop_on_end(self) -> LoopT:
270
289
  """Create and add a new loop at the end of the yarn.
271
290
 
272
291
  Returns:
273
292
  Loop: The newly created loop that was added to the end of this yarn.
274
293
  """
275
294
  loop_id = self._next_loop_id()
276
- loop = Loop(loop_id, self)
277
- return self.add_loop_to_end(loop)
295
+ return self.add_loop_to_end(self._new_loop(loop_id))
278
296
 
279
297
  def _next_loop_id(self) -> int:
280
- """Get the ID for the next loop to be added to this yarn.
281
-
298
+ """
282
299
  Returns:
283
300
  int: The ID of the next loop to be added to this yarn based on the knit graph or, if no knit graph is associated with this yarn, based on the last loop on this yarn.
284
301
  """
285
- if self.knit_graph is not None:
286
- if self.knit_graph.last_loop is None:
287
- return 0
288
- else:
289
- return self.knit_graph.last_loop.loop_id + 1
290
- elif self.last_loop is None:
302
+ if self.knit_graph.last_loop is None:
291
303
  return 0
292
304
  else:
293
- return self.last_loop.loop_id + 1
305
+ return self.knit_graph.last_loop.loop_id + 1
306
+
307
+ def _new_loop(self, loop_id: int) -> LoopT:
308
+ """
309
+ Args:
310
+ loop_id (int): The loop id to create a new loop.
311
+
312
+ Returns:
313
+ LoopT: A loop on this yarn with the given id.
314
+
315
+ Raises:
316
+ Use_Cut_Yarn_ValueError: If the yarn has been cut and can no longer form loops.
317
+ """
318
+ if self.is_cut:
319
+ raise Use_Cut_Yarn_ValueError(self)
320
+ elif self.first_loop is not None:
321
+ return self.first_loop.__class__(loop_id, self, self.knit_graph) # type: ignore[arg-type]
322
+ else:
323
+ return self._loop_class(loop_id, self, self.knit_graph) # type: ignore[arg-type]
294
324
 
295
- def remove_loop(self, loop: Loop) -> None:
325
+ def remove_loop(self, loop: LoopT | int) -> None:
296
326
  """
297
327
  Remove the given loop from the yarn.
298
328
  Reconnects any neighboring loops to form a new float with the positioned in-front-of or behind the original floats positioned accordingly.
299
329
  Resets the first_loop and last_loop properties if the removed loop was the tail of the yarn.
300
330
  Args:
301
- loop (Loop): The loop to remove from the yarn.
331
+ loop (LoopT): The loop to remove from the yarn.
302
332
 
303
333
  Raises:
304
334
  KeyError: The given loop does not exist in the yarn.
305
335
  """
306
336
  if loop not in self:
307
- raise KeyError(f'Loop {loop} does not exist on yarn {self}.')
337
+ raise KeyError(f"Loop {loop} does not exist on yarn {self}.")
338
+ elif isinstance(loop, int):
339
+ loop = self[loop]
308
340
  prior_loop = self.prior_loop(loop)
309
341
  next_loop = self.next_loop(loop)
310
- if isinstance(prior_loop, Loop) and isinstance(next_loop, Loop): # Loop is between two floats to be merged.
342
+ if prior_loop is not None and next_loop is not None: # Loop is between two floats to be merged.
311
343
  front_of_float_loops = self.get_loops_in_front_of_float(prior_loop, loop)
312
344
  front_of_float_loops.update(self.get_loops_in_front_of_float(loop, next_loop))
313
345
  back_of_float_loops = self.get_loops_behind_float(prior_loop, loop)
314
346
  back_of_float_loops.update(self.get_loops_behind_float(loop, next_loop))
315
- self.loop_graph.remove_node(loop)
316
- self.loop_graph.add_edge(prior_loop, next_loop, Front_Loops=front_of_float_loops, Back_Loops=back_of_float_loops)
347
+ super().remove_loop(loop)
348
+ self.add_edge(
349
+ prior_loop,
350
+ next_loop,
351
+ Float_Edge[LoopT](front_loops=front_of_float_loops, back_loops=back_of_float_loops),
352
+ )
317
353
  for front_loop in front_of_float_loops:
318
354
  front_loop.add_loop_in_front_of_float(prior_loop, next_loop)
319
355
  for back_loop in back_of_float_loops:
320
356
  back_loop.add_loop_behind_float(prior_loop, next_loop)
321
- return
322
- if next_loop is None: # This was the last loop, make the prior loop the last loop.
323
- assert loop is self.last_loop
324
- self._last_loop = prior_loop
325
- if prior_loop is None: # This was the first loop, make the next loop the first loop.
326
- assert loop is self.first_loop
327
- self._first_loop = next_loop
328
- self.loop_graph.remove_node(loop)
329
-
330
- def add_loop_to_end(self, loop: Loop) -> Loop:
357
+ else:
358
+ super().remove_loop(loop)
359
+ if next_loop is None: # This was the last loop, make the prior loop the last loop.
360
+ self._last_loop = prior_loop
361
+ if prior_loop is None: # This was the first loop, make the next loop the first loop.
362
+ self._first_loop = next_loop
363
+
364
+ def add_loop_to_end(self, loop: LoopT) -> LoopT:
331
365
  """Add an existing loop to the end of this yarn and associated knit graph.
332
366
 
333
367
  Args:
@@ -341,124 +375,128 @@ class Yarn:
341
375
  self.knit_graph.add_loop(loop)
342
376
  return loop
343
377
 
344
- def insert_loop(self, loop: Loop, prior_loop: Loop | None = None) -> None:
378
+ def insert_loop(self, loop: LoopT, prior_loop: LoopT | None = None) -> None:
345
379
  """Insert a loop into the yarn sequence after the specified prior loop.
346
380
 
347
381
  Args:
348
382
  loop (Loop): The loop to be added to this yarn.
349
383
  prior_loop (Loop | None): The loop that should come before this loop on the yarn. If None, defaults to the last loop (adding to end of yarn).
350
384
  """
351
- self.loop_graph.add_node(loop)
352
- if not self.has_loops:
385
+ super().add_loop(loop)
386
+ if self.last_loop is None:
353
387
  self._last_loop = loop
354
388
  self._first_loop = loop
355
389
  return
356
- elif prior_loop is None:
390
+ if prior_loop is None:
357
391
  prior_loop = self.last_loop
358
- self.loop_graph.add_edge(prior_loop, loop, Front_Loops=set(), Back_Loops=set())
392
+ super().add_edge(prior_loop, loop, Float_Edge[LoopT]())
359
393
  if prior_loop == self.last_loop:
360
394
  self._last_loop = loop
361
395
 
362
- def next_loop(self, loop: Loop) -> Loop | None:
363
- """Get the loop that follows the specified loop on this yarn.
396
+ def add_loop_in_front_of_float(self, front_loop: LoopT, start_of_float: LoopT) -> None:
397
+ """Record that a loop falls in front of the float between two other loops.
364
398
 
365
399
  Args:
366
- loop (Loop): The loop to find the next loop from.
367
-
368
- Returns:
369
- Loop | None: The next loop on yarn after the specified loop, or None if it's the last loop.
370
-
371
- Raises:
372
- KeyError: If the specified loop is not on this yarn.
400
+ front_loop (Loop): The loop that is positioned in front of the float.
401
+ start_of_float (Loop): The first loop in the float pair.
373
402
  """
374
- if loop not in self:
375
- raise KeyError(f"Loop {loop} is not on Yarn")
376
- successors = [*self.loop_graph.successors(loop)]
377
- if len(successors) > 0:
378
- return cast(Loop, successors[0])
379
- else:
380
- return None
381
-
382
- def prior_loop(self, loop: Loop) -> Loop | None:
383
- """Get the loop that precedes the specified loop on this yarn.
403
+ end_of_float = self.next_loop(start_of_float)
404
+ if end_of_float is not None:
405
+ self.get_edge(start_of_float, end_of_float).add_loop_in_front_of_float(front_loop)
406
+ front_loop.add_loop_in_front_of_float(start_of_float, end_of_float)
384
407
 
408
+ def remove_loop_relative_to_floats(self, loop: LoopT) -> None:
409
+ """
410
+ Removes the given loop from positions relative to floats along this yarn.
385
411
  Args:
386
- loop (Loop): The loop to find the prior loop from.
387
-
388
- Returns:
389
- Loop | None: The prior loop on yarn before the specified loop, or None if it's the first loop.
390
-
391
- Raises:
392
- KeyError: If the specified loop is not on this yarn.
412
+ loop (LoopT): The loop to remove from positions relative to floats on this yarn.
393
413
  """
394
- if loop not in self:
395
- raise KeyError(f"Loop {loop} is not on Yarn")
396
- predecessors = [*self.loop_graph.predecessors(loop)]
397
- if len(predecessors) > 0:
398
- return cast(Loop, predecessors[0])
399
- else:
400
- return None
414
+ for _u, _v, float_data in self.edge_iter:
415
+ float_data.remove_loop_relative_to_floats(loop)
401
416
 
402
- def __contains__(self, item: Loop | int) -> bool:
403
- """Check if a loop is contained on this yarn.
417
+ def add_loop_behind_float(self, back_loop: LoopT, start_of_float: LoopT) -> None:
418
+ """Record that a loop falls in front of the float between two other loops.
404
419
 
405
420
  Args:
406
- item (Loop | int): The loop or loop ID being checked for in the yarn.
421
+ back_loop (Loop): The loop that is positioned behind the float.
422
+ start_of_float (Loop): The first loop in the float pair.
423
+ """
424
+ end_of_float = self.next_loop(start_of_float)
425
+ if end_of_float is not None:
426
+ self.get_edge(start_of_float, end_of_float).add_loop_behind_float(back_loop)
427
+ back_loop.add_loop_behind_float(start_of_float, end_of_float)
428
+
429
+ def cut_yarn(self) -> Self:
430
+ """Cut yarn to make it no longer active and create a new yarn instance of the same type.
407
431
 
408
432
  Returns:
409
- bool: True if the loop is on the yarn, False otherwise.
433
+ Yarn[LoopT]: New yarn of the same type after cutting this yarn.
410
434
  """
411
- return bool(self.loop_graph.has_node(item))
435
+ self._is_cut = True
436
+ return self.__class__(self.properties, self.knit_graph, self._instance + 1, self._loop_class)
412
437
 
413
- def __iter__(self) -> Iterator[Loop]:
414
- """Iterate over loops on this yarn in sequence from first to last.
438
+ def __str__(self) -> str:
439
+ """Get the string representation of this yarn.
415
440
 
416
441
  Returns:
417
- Iterator[Loop]: An iterator over the loops on this yarn in their natural sequence order.
442
+ str: The yarn identifier string.
418
443
  """
419
- return cast(Iterator[Loop], dfs_preorder_nodes(self.loop_graph, self.first_loop))
444
+ return self.yarn_id
420
445
 
421
- def edge_iter(self) -> Iterator[tuple[Loop, Loop]]:
422
- """Iterate over the edges between loops on this yarn.
446
+ def __repr__(self) -> str:
447
+ """Get the representation string of this yarn for debugging.
423
448
 
424
449
  Returns:
425
- Iterator[tuple[Loop, Loop]]: An iterator over tuples of connected loops representing the yarn path.
450
+ str: The representation of the yarn properties.
426
451
  """
427
- return cast(Iterator[tuple[Loop, Loop]], dfs_edges(self.loop_graph, self.first_loop))
452
+ return repr(self.properties)
428
453
 
429
- def loops_in_front_of_floats(self) -> list[tuple[Loop, Loop, set[Loop]]]:
430
- """Get all float segments with loops positioned in front of them.
454
+ def __hash__(self) -> int:
455
+ """Get the hash value of this yarn for use in sets and dictionaries.
431
456
 
432
457
  Returns:
433
- list[tuple[Loop, Loop, set[Loop]]]: List of tuples containing the two loops defining each float and the set of loops positioned in front of that float.
434
- Only includes floats that have loops in front of them.
458
+ int: Hash value based on the yarn properties.
435
459
  """
436
- return [(u, v, self.get_loops_in_front_of_float(u, v)) for u, v in self.edge_iter()
437
- if len(self.get_loops_in_front_of_float(u, v)) > 0]
460
+ return hash((self._instance, self.properties))
438
461
 
439
- def loops_behind_floats(self) -> list[tuple[Loop, Loop, set[Loop]]]:
440
- """Get all float segments with loops positioned behind them.
462
+ def __iter__(self) -> Iterator[LoopT]:
463
+ """Iterate over loops on this yarn in sequence from first to last.
441
464
 
442
465
  Returns:
443
- list[tuple[Loop, Loop, set[Loop]]]: List of tuples containing the two loops defining each float and the set of loops positioned behind that float.
444
- Only includes floats that have loops behind them.
466
+ Iterator[Loop]: An iterator over the loops on this yarn in their natural sequence order.
445
467
  """
446
- return [(u, v, self.get_loops_behind_float(u, v)) for u, v in self.edge_iter()
447
- if len(self.get_loops_behind_float(u, v)) > 0]
468
+ if self.first_loop is None:
469
+ return iter([])
470
+ return self.dfs_preorder_loops(self.first_loop)
471
+
472
+ @overload
473
+ def __getitem__(self, item: int) -> LoopT: ...
474
+
475
+ @overload
476
+ def __getitem__(self, item: tuple[LoopT | int, LoopT | int]) -> Float_Edge[LoopT]: ...
477
+
478
+ @overload
479
+ def __getitem__(self, item: slice) -> list[LoopT]: ...
448
480
 
449
- def __getitem__(self, item: int) -> Loop:
481
+ def __getitem__(
482
+ self, item: int | tuple[LoopT | int, LoopT | int] | slice
483
+ ) -> LoopT | Float_Edge[LoopT] | list[LoopT]:
450
484
  """Get a loop by its ID from this yarn.
451
485
 
452
486
  Args:
453
- item (int): The loop ID to retrieve from this yarn.
487
+ item (int | tuple[LoopT | int, LoopT | int] | slice):
488
+ The loop ,loop ID, or float between two loops to retrieve from this yarn.
489
+ If given a slice, it will retrieve the elements between the specified indices in the standard ordering of loops along the yarn.
454
490
 
455
491
  Returns:
456
- Loop: The loop on the yarn with the matching ID.
492
+ LoopT: The loop on the yarn with the matching ID.
493
+ Float_Edge[LoopT]: The float data for the given pair of loops forming a float.
494
+ list[LoopT]: The loops in the slice of the yarn based on their ordering along the yarn.
457
495
 
458
496
  Raises:
459
- KeyError: If the loop ID is not found on this yarn.
497
+ KeyError: If the item is not found on this yarn.
460
498
  """
461
- if item not in self:
462
- raise KeyError(f"{item} is not a loop on yarn {self}")
499
+ if isinstance(item, slice):
500
+ return list(self)[item]
463
501
  else:
464
- return cast(Loop, self.loop_graph.nodes[item])
502
+ return super().__getitem__(item)