This commit is contained in:
Nicolas Predella 2026-05-06 11:12:13 +03:00
commit cf7749b9b7
30 changed files with 2282 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
__pycache__

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.13

47
README.md Normal file
View file

@ -0,0 +1,47 @@
# NodeSlicer
NodeSlicer is a high-performance Blender extension that provides a custom node-based interface for mesh slicing and G-code generation. By bypassing traditional Blender modifiers and using NumPy and Shapely for geometry processing, it offers a flexible and extensible workflow for 3D printing and procedural geometry tasks.
## Key Features
- **Custom Node Editor**: A dedicated node tree type for slicing workflows.
- **Fast Geometry Processing**: Uses NumPy for vectorized mesh operations and Shapely for robust 2D geometry manipulation.
- **G-code Generation**: Direct conversion of 2D contours (toolpaths) into G-code.
- **Live Preview**: Debug nodes to visualize generated toolpaths directly in the Blender viewport.
- **Extensible**: Modular architecture allowing for easy addition of new nodes and processing logic.
## Installation
### Requirements
- Blender 4.2 or newer (uses the new Extension system).
- Dependencies (bundled or installed via `uv`):
- `numpy`
- `shapely`
- `pyclipper`
### As a Blender Extension
1. Zip the repository contents (excluding `.venv`, `__pycache__`, etc.).
2. In Blender, go to `Edit > Preferences > Get Extensions`.
3. Click the arrow in the top right and select `Install from Disk`.
4. Select your zip file.
## Quick Start
1. Open the **Node Editor**.
2. Switch the tree type to **NodeSlicer Tree**.
3. Create a new tree.
4. Add an **Object Input** node and select a mesh object.
5. Connect it to a **Get Mesh** node.
6. Connect the mesh to **Slice Shells**.
7. Connect the slices to **Slices to Paths**, then **Paths to Gcode**.
8. Use a **Gcode To File** node to export your result.
## Documentation
- [Project Overview](doc/README.md)
- [Module Reference](doc/MODULES.md)
- [Node & Socket Reference](doc/NODES.md)
## License
This project is licensed under the GPL-3.0-or-later license. See `blender_manifest.toml` for details.

719
__init__.py Normal file
View file

@ -0,0 +1,719 @@
from __future__ import annotations
bl_info = {
"name": "NodeSlicer",
"author": "OpenAI",
"version": (0, 1, 0),
"blender": (3, 6, 0),
"location": "Node Editor > Add",
"description": "Very small custom nodes example",
"category": "Node",
}
from math import floor
from typing import TYPE_CHECKING, Any
import bpy
import os
import numpy as np
from bpy.types import NodeTree, Node, NodeSocket, Operator, Menu
from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty
from .registry import register_class
from .core import slice
from .core.slice import Pather, slice_at_range
from .infra.blender_mesh_capture import SlicingObject, capture_mesh_edges
from .infra.blender_slice_nodes_generic import SliceNodeSocket
if TYPE_CHECKING:
from bpy.stub_internal.rna_enums import OperatorReturnItems
# -----------------------------------------------------------------------------
# Node tree
# -----------------------------------------------------------------------------
@register_class
class SLICETREE_NodeTree(NodeTree):
bl_idname = "SLICETREE_NodeTree"
bl_label = "NodeSlicer Tree"
bl_icon = "NODETREE"
# -----------------------------------------------------------------------------
# Sockets
# -----------------------------------------------------------------------------
class LabelNodeSocket(SliceNodeSocket):
default = ""
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
assert layout
layout.label(text=text or self.name)
class InputNodeSocket(SliceNodeSocket):
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
assert layout
if not self.is_output and self.is_linked:
layout.label(text=text or self.name)
else:
layout.prop(self, "value", text=text or self.name)
def _draw_as_ro(self, context: Any, layout: Any, node: Any, text: str | None):
layout.label(text=text or self.name)
def _draw_as_rw(self, context: Any, layout: Any, node: Any, text: str | None):
layout.prop(self, "value", text=text or self.name)
class ReadonlySocket():
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
return self._draw_as_ro(context, layout, node, text) # pyright: ignore[reportAttributeAccessIssue]
@register_class
class SLICESOCK_Object(InputNodeSocket):
bl_label = "Object"
value: PointerProperty(
name="Object",
type=bpy.types.Object,
) # pyright: ignore[reportInvalidTypeForm]
color = (0.9, 0.7, 0.2, 1.0)
class SLICESOCK_Float_Common(InputNodeSocket):
bl_label = "Float"
value: FloatProperty(name="Float", default=0) # pyright: ignore[reportInvalidTypeForm]
color = (161/255, 161/255, 161/255, 1.0)
@register_class
class SLICESOCK_FloatRo(ReadonlySocket, SLICESOCK_Float_Common): ...
@register_class
class SLICESOCK_Float(SLICESOCK_Float_Common): ...
class SLICESOCK_Int_Common(InputNodeSocket):
bl_label = "Integer"
value: IntProperty(name="Int", default=0) # pyright: ignore[reportInvalidTypeForm]
color = (161/255, 161/255, 161/255, 1.0)
@register_class
class SLICESOCK_Int(SLICESOCK_Int_Common): ...
@register_class
class SLICESOCK_IntRo(ReadonlySocket, SLICESOCK_Int_Common): ...
@register_class
class SLICESOCK_String(InputNodeSocket):
bl_label = "String"
value: StringProperty(name="String", default="") # pyright: ignore[reportInvalidTypeForm]
color = (0.4, 0.7, 1.0, 1.0)
@register_class
class SLICESOCK_Mesh(LabelNodeSocket):
bl_label = "Mesh"
default = None
color = (0, 214/255, 163/255, 1.0)
@register_class
class SLICESOCK_Slices(LabelNodeSocket):
bl_label = "Slices"
default = []
color = (0, 214/255, 96/255, 1.0)
@register_class
class SLICESOCK_Paths(LabelNodeSocket):
bl_label = "Paths"
default = []
color = (214/255, 0, 120/255, 1.0)
@register_class
class SLICESOCK_Gcode(LabelNodeSocket):
bl_label = "Gcode"
default = ""
color = (1., 1., 1., 1.)
# -----------------------------------------------------------------------------
# Nodes
# -----------------------------------------------------------------------------
class SliceNode(Node, AutoBlIdName):
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ...
def add_output(self, bl_idname: str, name: str | None = None):
from .registry import get_label_by_name
return self.outputs.new(bl_idname, name or get_label_by_name(bl_idname))
def add_input(self, bl_idname: str, name: str | None = None, multi: bool = False):
from .registry import get_label_by_name
sock = self.inputs.new(bl_idname, name or get_label_by_name(bl_idname))
if multi:
sock.link_limit = 0
sock.display_shape = 'LIST'
return sock
# Basic Inputs
class InputNode(SliceNode):
input_type: str = ""
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if "bl_label" not in cls.__dict__: cls.bl_label = cls.input_type
def init(self, context) -> None:
self.add_output(f"SLICESOCK_{self.bl_label}")
def evaluate_output(self, socket_name: str, **_kwargs):
return self.outputs.get(socket_name).value
@register_class
class SLICENODE_InputString(InputNode):
input_type = "String"
@register_class
class SLICENODE_InputFloat(InputNode):
input_type = "Float"
@register_class
class SLICENODE_InputInt(InputNode):
input_type = "Int"
# Math
@register_class
class SLICENODE_IntMath(SliceNode):
bl_label = "Integer Math"
operation: bpy.props.EnumProperty(
name="Operation",
items=[
('ADD', "Add", "Add the two values"),
('SUBTRACT', "Subtract", "Subtract the second value from the first"),
('MULTIPLY', "Multiply", "Multiply the two values"),
('DIVIDE', "Divide", "Integer division"),
('ABS', "Absolute", "Absolute value of the first value"),
('NEGATE', "Negate", "Negate the first value"),
('POWER', "Power", "First value raised to the power of the second"),
('MIN', "Minimum", "The smaller of the two values"),
('MAX', "Maximum", "The larger of the two values"),
('SIGN', "Sign", "The sign of the first value (-1, 0, or 1)"),
('DIVIDE_ROUND', "Divide Round", "Division rounded to the nearest integer"),
('DIVIDE_FLOOR', "Divide Floor", "Division rounded down"),
('DIVIDE_TRUNCATE', "Divide Truncate", "Division rounded towards zero"),
('MODULO_FLOORED', "Floored Modulo", "Modulo using floor division"),
('MODULO_TRUNCATED', "Truncated Modulo", "Modulo using truncated division"),
],
default='ADD'
) # pyright: ignore[reportInvalidTypeForm]
def init(self, context):
self.add_output("SLICESOCK_IntRo", "Value")
self.add_input("SLICESOCK_Int", "Value")
self.add_input("SLICESOCK_Int", "Value")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "operation", text="")
def evaluate_output(self, socket_name: str, **_kwargs) -> int:
a: int = _resolve_input(self.inputs[0])
b: int = _resolve_input(self.inputs[1])
op = self.operation
try:
if op == 'ADD': return a + b
if op == 'SUBTRACT': return a - b
if op == 'MULTIPLY': return a * b
if op == 'DIVIDE': return a // b if b != 0 else 0
if op == 'ABS': return abs(a)
if op == 'NEGATE': return -a
if op == 'POWER': return int(a ** b) if not (a == 0 and b < 0) else 0
if op == 'MIN': return min(a, b)
if op == 'MAX': return max(a, b)
if op == 'SIGN': return (a > 0) - (a < 0)
if op == 'DIVIDE_ROUND': return int(round(a / b)) if b != 0 else 0
if op == 'DIVIDE_FLOOR': return a // b if b != 0 else 0
if op == 'DIVIDE_TRUNCATE': return int(a / b) if b != 0 else 0
if op == 'MODULO_FLOORED': return a % b if b != 0 else 0
if op == 'MODULO_TRUNCATED': return a - (b * int(a / b)) if b != 0 else 0
except Exception:
return 0
return 0
@register_class
class SLICENODE_InputObject(InputNode):
bl_label = "Object"
bl_icon = "OBJECT_DATA"
@register_class
class SLICENODE_GetMesh(SliceNode):
bl_label = "Get Mesh"
def init(self, context):
self.add_input("SLICESOCK_Object")
self.add_output("SLICESOCK_Mesh")
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject | None:
if socket_name != "Mesh": return None
obj: Any = _resolve_input(self.inputs.get("Object"))
so = SlicingObject(obj, obj.name)
return so
@register_class
class SLICENODE_GetPaths(SliceNode):
bl_label = "Get Paths"
def init(self, context):
self.add_input("SLICESOCK_Object")
self.add_output("SLICESOCK_Paths")
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
obj: Any = _resolve_input(self.inputs.get("Object"))
so = capture_mesh_edges(obj)
return so
@register_class
class SLICENODE_SliceShells(SliceNode):
bl_label = "Slice Shells"
def init(self, context):
self.add_input("SLICESOCK_Mesh")
self.add_input("SLICESOCK_Float", "Layer Height").value = 0.2
self.add_output("SLICESOCK_Slices")
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
if socket_name != "Slices": return []
slicingmesh: SlicingObject | None = _resolve_input(self.inputs.get("Mesh"))
layer_height: float = _resolve_input(self.inputs.get("Layer Height"))
if not slicingmesh: return []
min = slicingmesh.mesh[:,0:3,2:3].min()
min = floor(min / layer_height) * layer_height
max = slicingmesh.mesh[:,0:3,2:3].max()
z_values = np.arange(min, max + layer_height, layer_height)
slices_group: list[list[np.ndarray]] | None = slice_at_range([slicingmesh.mesh], z_values)
assert slices_group
slices = slices_group[0]
return slices
@register_class
class SLICENODE_BufferShells(SliceNode):
bl_label = "Buffer Shells"
shape_option: bpy.props.EnumProperty(
name="Miter",
items=[
('round', "Round", ""),
('square', "Square", ""),
('flat', "Flat", "")
]
) # pyright: ignore[reportInvalidTypeForm]
def init(self, context):
self.add_input("SLICESOCK_Slices")
self.add_input("SLICESOCK_Float", "Distance").value = 0
self.add_output("SLICESOCK_Slices")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "shape_option")
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
if socket_name != "Slices": return []
slices: list[np.ndarray] = _resolve_input(self.inputs.get("Slices"))
distance: float = _resolve_input(self.inputs.get("Distance"))
buffered_slices = slice.buffer_slice(slices, distance, miter=self.shape_option)
return buffered_slices
@register_class
class SLICENODE_SlicesToPaths(SliceNode):
bl_label = "Slices to Paths"
def init(self, context):
self.add_input("SLICESOCK_Slices")
self.add_output("SLICESOCK_Paths")
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
slices: list[np.ndarray] = _resolve_input(self.inputs.get("Slices"))
return slices
@register_class
class SLICENODE_PathsToGcode(SliceNode):
bl_label = "Paths to Gcode"
def init(self, context):
self.add_input("SLICESOCK_Paths")
self.add_output("SLICESOCK_Gcode")
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
paths: list[np.ndarray] = _resolve_input(self.inputs.get("Paths"))
pather: Pather = Pather(paths)
gcode: str = pather.to_gcode()
return gcode
@register_class
class SLICENODE_GcodeBlock(SliceNode):
bl_label = "Gcode Block"
gcode: StringProperty(name="Gcode", default="") # pyright: ignore[reportInvalidTypeForm]
def init(self, context):
self.add_output("SLICESOCK_Gcode")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "gcode", text="")
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
return self.gcode
### Joiners
class SliceNodeJoiner(SliceNode):
socket_type: str = ""
output_socket_type: str = ""
def init(self, context):
self.add_output(self.output_socket_type or self.socket_type)
def update(self) -> None:
def is_active(input) -> bool:
return input.is_linked or getattr(input, 'value', False)
if len(self.inputs) == 0:
self.add_input(self.socket_type)
return
if is_active(self.inputs[-1]):
self.add_input(self.socket_type)
while True:
if len(self.inputs) < 2: break
if not is_active(self.inputs[-1]) and not is_active(self.inputs[-2]):
self.inputs.remove(self.inputs[-2])
continue
break
@register_class
class SLICENODE_JoinGcode(SliceNodeJoiner):
bl_label = "Join Gcode"
socket_type = "SLICESOCK_Gcode"
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
gcode_input: list[str] = []
for input in self.inputs:
gcode_input += [_resolve_input(input)] or [""]
return "\n".join(gcode_input)
@register_class
class SLICENODE_JoinSlices(SliceNodeJoiner):
bl_label = "Join Slices"
socket_type = "SLICESOCK_Slices"
operation: bpy.props.EnumProperty(
name="Operation",
items=[
('UNION', "Union", "Union of the slices"),
('INTERSECTION', "Intersection", "Intersection of the slices"),
('DIFFERENCE', "Subtract", "Clipping of the first slice"),
('XOR', "Exclusive or", "Exclusive or of the slices"),
],
default='UNION'
) # pyright: ignore[reportInvalidTypeForm]
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "operation", text="")
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
accumulated: list[np.ndarray] = []
current: list[np.ndarray] = []
for input in self.inputs:
current =_resolve_input(input)
if not current:
continue
if not accumulated:
accumulated = current
current = []
continue
accumulated = slice.boolean_slice(accumulated, current, self.operation)
current = []
return accumulated
@register_class
class SLICENODE_JoinMesh(SliceNodeJoiner):
bl_label = "Join Mesh"
socket_type = "SLICESOCK_Mesh"
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject:
merged_mesh = SlicingObject()
for input in self.inputs:
mesh =_resolve_input(input)
if mesh:
merged_mesh.mesh = np.concatenate([merged_mesh.mesh, mesh.mesh], axis=0)
return merged_mesh
@register_class
class SLICENODE_JoinPaths(SliceNodeJoiner):
bl_label = "Join Paths"
socket_type = "SLICESOCK_Paths"
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
merged_paths = []
for input in self.inputs:
paths = _resolve_input(input)
if paths:
merged_paths.extend(paths)
return merged_paths
### I/O
@register_class
class SLICENODE_LoadGcode(SliceNode):
bl_label = "Load Gcode File"
def init(self, context):
self.add_input("SLICESOCK_String", "Filepath")
self.add_output("SLICESOCK_Gcode")
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
path = _resolve_input(self.inputs.get("Filepath"))
if not path: return ""
abs_path = bpy.path.abspath(path)
if os.path.exists(abs_path):
with open(abs_path, 'r', encoding='utf-8') as f:
return f.read()
return ""
class ExecSliceNode(SliceNode):
button_label: str = 'Execute'
def draw_buttons(self, context, layout):
assert layout
layout.operator("slicenode.execute", text=self.button_label).node_name = self.name
@register_class
class SLICENODE_GcodeToFile(ExecSliceNode):
bl_label = "Gcode To File"
button_label = "Generate File"
def init(self, context):
self.add_input("SLICESOCK_Gcode")
self.add_input("SLICESOCK_String", "Filename")
def evaluate(self):
gcode: str = _resolve_input(self.inputs.get("Gcode"))
filename: str = _resolve_input(self.inputs.get("Filename"))
filepath = bpy.path.abspath(filename)
os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
with open(filepath, "w", encoding="utf-8", newline="\n") as f:
f.write(gcode)
return 'ok'
@register_class
class SLICENODE_GcodeToMesh(ExecSliceNode):
bl_label = "Gcode to Mesh"
button_label = "Update Debug Mesh"
def init(self, context):
self.add_input("SLICESOCK_Gcode")
def evaluate(self):
gcode: str = _resolve_input(self.inputs.get("Gcode"))
if not gcode:
return 'empty'
segments = []
last_x, last_y, last_z = 0.0, 0.0, 0.0
last_e = 0.0
for line in gcode.splitlines():
line = line.split(';')[0].strip()
if not line: continue
parts = line.split()
cmd = parts[0].upper()
params = {}
for p in parts[1:]:
if len(p) < 2: continue
try:
# Scale coordinates by 0.001 (mm to Blender units)
val = float(p[1:])
if p[0].upper() in 'XYZ':
val *= 0.001
params[p[0].upper()] = val
except ValueError: continue
new_x = params.get('X', last_x)
new_y = params.get('Y', last_y)
new_z = params.get('Z', last_z)
new_e = params.get('E', last_e)
if cmd == 'G1':
# Check if it's an extrusion move
# In absolute mode (typical), E must increase.
# In relative mode, E must be > 0.
# We'll assume absolute mode as per Pather.to_gcode() M82
is_extruding = new_e > last_e
if is_extruding:
segments.append(((last_x, last_y, last_z), (new_x, new_y, new_z)))
last_x, last_y, last_z = new_x, new_y, new_z
last_e = new_e
if not segments:
return 'no segments'
# Create or find "Debug" mesh object
mesh_name = "Debug"
if mesh_name in bpy.data.objects:
obj = bpy.data.objects[mesh_name]
mesh = obj.data
mesh.clear_geometry()
else:
mesh = bpy.data.meshes.new(mesh_name)
obj = bpy.data.objects.new(mesh_name, mesh)
assert bpy.context.collection
bpy.context.collection.objects.link(obj)
vertices = []
edges = []
for i, (p1, p2) in enumerate(segments):
vertices.extend([p1, p2])
edges.append((2*i, 2*i + 1))
mesh.from_pydata(vertices, edges, [])
mesh.update()
return 'ok'
@register_class
class SLICENODE_Test(ExecSliceNode):
bl_label = "Test"
button_label = "Test"
def init(self, context):
self.add_input("SLICESOCK_Gcode")
def evaluate(self):
_ = _resolve_input(self.inputs.get("Gcode"))
return 'ok'
# -----------------------------------------------------------------------------
# Operator
# -----------------------------------------------------------------------------
@register_class
class SLICENODE_OT_Execute(Operator):
bl_idname = "slicenode.execute"
bl_label = "Execute"
bl_description = "Execute"
node_name: bpy.props.StringProperty() # pyright: ignore[reportInvalidTypeForm]
def execute(self, context) -> set['OperatorReturnItems']:
node_tree = context.space_data.edit_tree # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
node = node_tree.nodes[self.node_name]
_ = node.evaluate()
self.report({"INFO"}, f"Execution Successful")
return {"FINISHED"}
# -----------------------------------------------------------------------------
# Menu
# -----------------------------------------------------------------------------
@register_class
class SLICENODE_MT_Menu(Menu):
bl_idname = "SLICENODE_MT_Menu"
bl_label = "SliceNodes"
def draw(self, context):
layout = self.layout
assert layout
layout.operator("node.add_node", text="String").type = "SLICENODE_InputString"
layout.operator("node.add_node", text="Float").type = "SLICENODE_InputFloat"
layout.operator("node.add_node", text="Integer").type = "SLICENODE_InputInt"
layout.operator("node.add_node", text="Integer Math").type = "SLICENODE_IntMath"
layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject"
layout.operator("node.add_node", text="Test").type = "SLICENODE_Test"
layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh"
layout.operator("node.add_node", text="Get Paths").type = "SLICENODE_GetPaths"
layout.separator()
layout.operator("node.add_node", text="Slice Shells").type = "SLICENODE_SliceShells"
layout.operator("node.add_node", text="Buffer Shells").type = "SLICENODE_BufferShells"
layout.operator("node.add_node", text="Slices to Paths").type = "SLICENODE_SlicesToPaths"
layout.operator("node.add_node", text="Paths to Gcode").type = "SLICENODE_PathsToGcode"
layout.separator()
layout.operator("node.add_node", text="Gcode Block").type = "SLICENODE_GcodeBlock"
layout.operator("node.add_node", text="Load Gcode").type = "SLICENODE_LoadGcode"
layout.separator()
layout.operator("node.add_node", text="Join Gcode").type = "SLICENODE_JoinGcode"
layout.operator("node.add_node", text="Join Mesh").type = "SLICENODE_JoinMesh"
layout.operator("node.add_node", text="Join Paths").type = "SLICENODE_JoinPaths"
layout.operator("node.add_node", text="Join Slices").type = "SLICENODE_JoinSlices"
layout.separator()
layout.operator("node.add_node", text="Gcode to File").type = "SLICENODE_GcodeToFile"
layout.operator("node.add_node", text="Gcode to Mesh").type = "SLICENODE_GcodeToMesh"
def draw_add_menu(self, context):
space = context.space_data
tree = getattr(space, "edit_tree", None)
if tree and tree.bl_idname == "SLICETREE_NodeTree":
self.layout.separator()
self.layout.menu("SLICENODE_MT_Menu")
# -----------------------------------------------------------------------------
# Registration
# -----------------------------------------------------------------------------
### Load collected modules
modules = registry.get()
def register():
registry.blender_register_classes()
bpy.types.NODE_MT_add.append(draw_add_menu)
print('Nodeslicer Loaded')
def unregister():
bpy.types.NODE_MT_add.remove(draw_add_menu)
registry.blender_unregister_classes()
print('Nodeslicer Unloaded')
if __name__ == "__main__":
register()

71
blender_manifest.toml Normal file
View file

@ -0,0 +1,71 @@
schema_version = "1.0.0"
id = "nodeslicer"
version = "0.0.1"
name = "Nodeslicer"
tagline = "This is another extension"
maintainer = "Nicolas Predella"
type = "add-on"
tags = ["Node"]
blender_version_min = "4.2.0"
license = [
"SPDX:GPL-3.0-or-later",
]
# Optional: required by some licenses.
# copyright = [
# "2002-2024 Developer Name",
# "1998 Company Name",
# ]
platforms = [
"windows-x64",
"macos-arm64",
"macos-x64",
"linux-x64",
]
wheels = [
"./wheels/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl",
"./wheels/pyclipper-1.4.0-cp311-cp311-win_amd64.whl",
"./wheels/pyclipper-1.4.0-cp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
"./wheels/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl",
"./wheels/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl",
"./wheels/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
"./wheels/shapely-2.1.2-cp311-cp311-win_amd64.whl",
"./wheels/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl",
"./wheels/pyclipper-1.4.0-cp313-cp313-win_amd64.whl",
"./wheels/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
"./wheels/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl",
"./wheels/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl",
"./wheels/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
"./wheels/shapely-2.1.2-cp313-cp313-win_amd64.whl",
]
# Optional: add-ons can list which resources they will require:
# * files (for access of any filesystem operations)
# * network (for internet access)
# * clipboard (to read and/or write the system clipboard)
# * camera (to capture photos and videos)
# * microphone (to capture audio)
#
# If using network, remember to also check `bpy.app.online_access`
# https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
#
# For each permission it is important to also specify the reason why it is required.
# Keep this a single short sentence without a period (.) at the end.
# For longer explanations use the documentation or detail page.
#
[permissions]
files = "Import/export GCODE from/to disk"
[build]
paths_exclude_pattern = [
"__pycache__/",
"/.git/",
"/*.zip",
]

8
core/geometry.py Normal file
View file

@ -0,0 +1,8 @@
import numpy as np
import zlib
def crc32_array(a: np.ndarray) -> int:
if not a.size: return 0
ac = np.ascontiguousarray(a)
mv = memoryview(ac.data).cast("B")
return zlib.adler32(mv)

375
core/slice.py Normal file
View file

@ -0,0 +1,375 @@
from numpy import ndarray
from typing import Any
import numpy as np
import pyclipper
# Scaling factor for pyclipper (it works with integers)
SCALE = 1_000_000
def slice_at_range(models, z_values) -> list[list[np.ndarray]]:
if not models:
return []
mmps: list[list[np.ndarray]] = []
for model in models:
model_edges: np.ndarray = edges_from_model(model)
layered_edges: np.ndarray = layered_model(model_edges, z_values)
sliced_edges: list[np.ndarray] = slice_at_z(layered_edges)
all_model_paths: list[np.ndarray] = []
for z, e in zip(z_values, sliced_edges):
if e is None or len(e) == 0:
continue
zf = float(z)
# Build 3D segments directly
seg3d = e.copy()
seg3d[..., 2] = zf
# Build geometry for this slice
slice_paths = segments_to_slice(seg3d)
all_model_paths.extend(slice_paths)
mmps.append(all_model_paths)
return mmps
def edges_from_model(model): # returns a numpy: (facet, 3, 2, xyz)
pairs_indices = np.array([[0, 1], [1, 2], [2, 0]]) # Create an array of indices to select the pairs
return model[:, pairs_indices, :] # Use advanced indexing to create the array
def layered_model(models: np.ndarray, z_values: np.ndarray) -> np.ndarray:
models_array: np.ndarray = np.array(models)[None, :] # (1, F, 3, 2, 3)
m = len(z_values)
# array of zeros with shape (m, 1, 1, 1, 3)
z_offsets_full = np.zeros((m, 1, 1, 1, 3))
z_offsets_full[..., 2] = z_values[:, None, None, None]
layered = models_array - z_offsets_full # (m, F, 3, 2, 3)
return layered
def slice_at_z(edges, normal_vector=np.array([0,0,1]), tolerance=1e-15):
edges_offs = edges # Shape (m, n, 3, 2, 3)
# Obtain intersection points
line_vector = edges_offs[..., 1, :] - edges_offs[..., 0, :]
numerator = np.sum(normal_vector * -edges_offs[..., 0, :], axis=-1)
denominator = np.sum(normal_vector[np.newaxis, np.newaxis, :] * line_vector, axis=-1)
# Avoid division by zero
with np.errstate(divide='ignore', invalid='ignore'):
t = numerator / denominator
t[denominator == 0] = np.nan # Assign NaN where there's no valid intersection
t[(t < 0) | (t > 1)] = np.nan
intersections = edges_offs[..., 0, :] + t[..., np.newaxis] * line_vector
intersections[np.abs(intersections) < tolerance] = 0 # Zero out small values
all_segments = []
for itx in intersections:
mask = ~np.any(np.isnan(itx), axis=-1)
filtered_arr = itx[mask]
segment_count = filtered_arr.shape[-2] // 2
segments = filtered_arr.reshape(segment_count, 2, 3)
segments = np.around(segments, 6)
all_segments.append(segments)
return all_segments
def link_segments(segments: np.ndarray, tolerance=1e-6) -> list[np.ndarray]:
"""Links segments (N, 2, 3) into continuous paths."""
if len(segments) == 0: return []
z = segments[0, 0, 2]
pts = segments[:, :, :2]
def to_key(p): return tuple((p / tolerance).round().astype(np.int64))
adj = {}
for i in range(len(segments)):
p1, p2 = pts[i, 0], pts[i, 1]
k1, k2 = to_key(p1), to_key(p2)
if k1 == k2: continue
adj.setdefault(k1, []).append((k2, i))
adj.setdefault(k2, []).append((k1, i))
paths = []
used = set()
for i in range(len(segments)):
if i in used: continue
p1, p2 = pts[i, 0], pts[i, 1]
k1, k2 = to_key(p1), to_key(p2)
if k1 == k2: continue
path_pts = [p1, p2]
used.add(i)
# Forward
curr_k = k2
while True:
found = False
for next_k, idx in adj.get(curr_k, []):
if idx not in used:
pk1, pk2 = to_key(pts[idx, 0]), to_key(pts[idx, 1])
other_pt = pts[idx, 1] if pk1 == curr_k else pts[idx, 0]
path_pts.append(other_pt)
used.add(idx)
curr_k = to_key(other_pt)
found = True
break
if not found: break
# Backward
curr_k = k1
while True:
found = False
for next_k, idx in adj.get(curr_k, []):
if idx not in used:
pk1, pk2 = to_key(pts[idx, 0]), to_key(pts[idx, 1])
other_pt = pts[idx, 1] if pk1 == curr_k else pts[idx, 0]
path_pts.insert(0, other_pt)
used.add(idx)
curr_k = to_key(other_pt)
found = True
break
if not found: break
res = np.column_stack([np.array(path_pts), np.full(len(path_pts), z)])
paths.append(res)
return paths
def segments_to_slice(segments: np.ndarray) -> list[np.ndarray]:
linked = link_segments(segments)
if not linked: return []
pc = pyclipper.Pyclipper()
for p in linked:
# Pyclipper expects open paths (last point != first point) for ET_CLOSEDPOLYGON
path_pts = p[:, :2]
if len(path_pts) > 2 and np.allclose(path_pts[0], path_pts[-1]):
path_pts = path_pts[:-1]
scaled = (path_pts * SCALE).astype(np.int64).tolist()
pc.AddPath(scaled, pyclipper.PT_SUBJECT, closed = True)
# Use EVENODD for robust hole detection.
solution = pc.Execute(pyclipper.CT_UNION, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD)
z = segments[0, 0, 2]
res = []
for path in solution:
if not path: continue
fpath = np.array(path) / SCALE
fpath_3d = np.column_stack([fpath, np.full(len(fpath), z)])
# Close loop by appending the first point for internal consistency
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
res.append(fpath_3d)
return res
def boolean_slice(subject: list[np.ndarray], object: list[np.ndarray], operation: str = 'UNION') -> list[np.ndarray]:
all_z_keys: set[int] = set()
subject_dict: dict[int, list[ndarray]] = {}
object_dict: dict[int, list[ndarray]] = {}
for s in subject:
z = int(round(s[0, 2] * 1000))
all_z_keys.add(z)
subject_dict.setdefault(z, []).append(s)
for s in object:
z = int(round(s[0, 2] * 1000))
all_z_keys.add(z)
object_dict.setdefault(z, []).append(s)
results: list[np.ndarray] = []
for k in all_z_keys:
subject_slices = subject_dict.get(k)
object_slices = object_dict.get(k)
if subject_slices is None:
assert object_slices
if operation == 'UNION':
for s in object_slices: results.append(s)
continue
if object_slices is None:
assert subject_slices
if operation == 'INTERSECTION': continue
for s in subject_slices: results.append(s)
continue
z = subject_slices[0][0, 2]
pc = pyclipper.Pyclipper()
# Subject
for s in subject_slices:
path_pts = s[:, :2]
scaled = (path_pts * SCALE).astype(np.int64).tolist()
pc.AddPath(scaled, pyclipper.PT_SUBJECT, True)
# Object
for s in object_slices:
path_pts = s[:, :2]
scaled = (path_pts * SCALE).astype(np.int64).tolist()
pc.AddPath(scaled, pyclipper.PT_CLIP, True)
# Determine operation type. For now only UNION is supported.
op_type = pyclipper.CT_UNION
if operation == 'UNION': op_type = pyclipper.CT_UNION
elif operation == 'INTERSECTION': op_type = pyclipper.CT_INTERSECTION
elif operation == 'DIFFERENCE': op_type = pyclipper.CT_DIFFERENCE
elif operation == 'XOR': op_type = pyclipper.CT_XOR
solution = pc.Execute(op_type, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD)
for path in solution:
if not path: continue
fpath = np.array(path) / SCALE
fpath_3d = np.column_stack([fpath, np.full(len(fpath), z)])
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
results.append(fpath_3d)
return results
def buffer_slice(slice_paths: list[np.ndarray], distance: float, miter='round', tolerance=.01) -> list[np.ndarray]:
if not slice_paths: return []
# Group paths by Z height to avoid flattening the entire model into 2D
groups: dict[int, list[np.ndarray]] = {}
for p in slice_paths:
z = p[0, 2]
# Use scaled integer for stable dictionary grouping of floats
z_key = int(round(z * 1000))
groups.setdefault(z_key, []).append(p)
join_types = {
'round': pyclipper.JT_ROUND,
'square': pyclipper.JT_SQUARE,
'flat': pyclipper.JT_SQUARE,
'miter': pyclipper.JT_MITER
}
jt = join_types.get(miter, pyclipper.JT_ROUND)
res = []
pco = pyclipper.PyclipperOffset(arc_tolerance=tolerance * SCALE)
# Buffer each layer independently
for z_key in sorted(groups.keys()):
layer_paths = groups[z_key]
z = layer_paths[0][0, 2]
pco.Clear()
scaled_paths = []
for p in layer_paths:
path_pts = p[:, :2]
# Strip redundant closing points
if len(path_pts) > 2 and np.allclose(path_pts[0], path_pts[-1]):
path_pts = path_pts[:-1]
if len(path_pts) < 3: continue
scaled_paths.append((path_pts * SCALE).astype(np.int64).tolist())
if not scaled_paths: continue
# Add all paths for this specific Z layer
pco.AddPaths(scaled_paths, jt, pyclipper.ET_CLOSEDPOLYGON)
solution = pco.Execute(distance * SCALE)
for path in solution:
if not path: continue
fpath = np.array(path) / SCALE
fpath_3d = np.column_stack([fpath, np.full(len(fpath), z)])
# Re-close loop
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
res.append(fpath_3d)
return res
def group_segments_by_z(segments: np.ndarray) -> dict[int, np.ndarray]:
z_values: np.ndarray = segments[:, 0, 2]
unique_z: np.ndarray = np.unique(z_values)
groups: dict[int, np.ndarray] = {int(round(z*1000)): segments[z_values == z] for z in unique_z}
return groups
############################################################
class Pather(object):
FEEDRATE: float = 1200. # mm/min
EXTRUSION_RATE: float = 0.017 # E per mm of path
def __init__(self, paths: list[np.ndarray], **kwargs) -> None:
self.paths = paths
self.bed_center_x: float = 250.
self.bed_center_y: float = 250.
def to_gcode(self) -> str:
"""
Convert a list of paths (3D numpy arrays) into G-code.
"""
if not self.paths:
return ""
def z_key(path) -> float:
return path[0, 2]
def segment_length_3d(p1, p2) -> float:
dx = p2[0] - p1[0]
dy = p2[1] - p1[1]
dz = p2[2] - p1[2]
return (dx * dx + dy * dy + dz * dz) ** 0.5
# Sort paths by Z height
lines = sorted(self.paths, key=z_key)
gcode: list[str] = [
"; Generated from NodeSlicer Paths (via Pyclipper)",
f"; FEEDRATE={self.FEEDRATE}",
f"; EXTRUSION_RATE={self.EXTRUSION_RATE}",
"G21 ; set units to mm",
"G90 ; absolute XYZ positioning",
"M82 ; absolute extrusion mode",
"G92 E0",
]
e = 0.0
last_z = -1.0
for line in lines:
coords = line.tolist()
if len(coords) < 2:
continue
x0, y0, z0 = coords[0]
# Layer change markers for PrusaSlicer
if abs(z0 - last_z) > 0.001:
gcode.append(f";LAYER_CHANGE")
gcode.append(f";Z:{z0:.3f}")
gcode.append(f";BEFORE_LAYER_CHANGE")
gcode.append(f"G92 E0") # Reset E on layer change
e = 0.0
gcode.append(f"G1 Z{z0:.3f} F720")
gcode.append(f";AFTER_LAYER_CHANGE")
last_z = z0
# Travel move to start of this path
gcode.append(f"G0 X{x0:.3f} Y{y0:.3f} Z{z0:.3f}")
gcode.append(";TYPE:External perimeter")
# Extrude along the line
for p1, p2 in zip(coords, coords[1:]):
seg_len: float = segment_length_3d(p1, p2)
e += seg_len * self.EXTRUSION_RATE
x, y, z = p2
gcode.append(
f"G1 X{x:.3f} Y{y:.3f} Z{z:.3f} E{e:.5f} F{self.FEEDRATE}"
)
return "\n".join(gcode)

51
doc/MODULES.md Normal file
View file

@ -0,0 +1,51 @@
# Module Reference
This file provides a mapping of modules to their primary responsibilities and key symbols.
## Root
- `__init__.py`:
- **Entry Point**: Extension registration (`register`, `unregister`).
- **Node Tree**: `SLICETREE_NodeTree` class.
- **Sockets**: Definitions for all `SLICESOCK_*` classes.
- **Nodes**: Definitions for all `SLICENODE_*` classes.
- **Evaluation Engine**:
- `_resolve_input()`: Recursively evaluates linked sockets.
- `_resolve_output()`: Computes node outputs with a caching system (`_EvalCache`) to avoid redundant calculations.
- `_follow_reroute()`: Handles Blender's Reroute nodes during evaluation.
- **Error Handling**: `NodeException` for user-facing evaluation errors.
- `registry.py`:
- **Auto-Registration**: Utility to track classes decorated with `@register_class`.
- **Resource Management**: Logic for dynamic icon and timer registration.
- `blender_manifest.toml`:
- Metadata for the Blender Extension platform.
- Lists required wheels (Shapely, PyClipper) for different platforms.
- `pyproject.toml`:
- Standard Python project configuration.
- Lists development and runtime dependencies.
## `/core` (Geometry & Slicing)
- `slice.py`:
- `slice_at_range()`: Top-level slicing function that iterates through Z-heights.
- `slice_at_z()`: Vectorized plane-mesh intersection using NumPy.
- `Pather`: Class that converts toolpaths into G-code strings. Handles layer change comments and extrusion calculations.
- `segments_to_slice()`: Utility to convert raw intersection segments into closed loops using Pyclipper and the Even-Odd fill rule.
- `link_segments()`: Logic for joining loose segments into continuous paths.
- `buffer_slice()`: Offset utility using PyclipperOffset. Groups paths by Z-height to ensure independent buffering of layers.
- `geometry.py`:
- `crc32_array()`: High-performance checksum generation for NumPy arrays, used for internal caching and data validation.
## `/infra` (Blender Integration)
- `blender_mesh_capture.py`:
- `SlicingObject`: Represents a single mesh as a NumPy triangle array. Computes bounding boxes and checksums.
- `SlicingCollection` / `SlicingGroup`: Hierarchical wrappers for managing multiple meshes and their metadata (e.g., Wipe Tower location).
- `objects_to_tris()`: Performance-critical function that extracts mesh data from Blender objects into NumPy arrays using `foreach_get`. Handles world-space transformation and unit scaling.
- `blender_bridge.py`:
- **Hierarchy Utilities**: Helpers like `get_all_children()` for traversing Blender's object tree.

68
doc/NODES.md Normal file
View file

@ -0,0 +1,68 @@
# Node & Socket Reference
## Node Sockets
| Socket ID | Type | Underlying Data Structure | Purpose |
|-----------|------|---------------------------|---------|
| `SLICESOCK_Object` | `bpy.types.Object` | Blender RNA Object | Reference to a Blender Object |
| `SLICESOCK_Float` | `float` | Python float | Numeric value |
| `SLICESOCK_Int` | `int` | Python int | Integer value |
| `SLICESOCK_String` | `str` | Python string | Text/Path value |
| `SLICESOCK_Mesh` | `SlicingObject` | NumPy NDArray (Triangles) | Internal mesh data (Transformed/Scaled) |
| `SLICESOCK_Slices` | `list[np.ndarray]` | List of closed paths (N, 3) | Closed manifold contours (Inside/Outside defined) |
| `SLICESOCK_Paths` | `list[np.ndarray]` | List of paths (N, 3) | Generic toolpaths (No defined interior) |
| `SLICESOCK_Gcode` | `str` | Multi-line string | Generated G-code text |
## Detailed Data Types
### SlicingObject (`SLICESOCK_Mesh`)
The `SlicingObject` class wraps a high-performance NumPy representation of a Blender mesh.
- **`.mesh`**: A NumPy `ndarray` of type `float64` with shape `(N, 4, 3)`.
- `N`: Number of triangles.
- `[i, 0:3, :]`: The three vertices of the $i$-th triangle.
- `[i, 3, :]`: The face normal of the $i$-th triangle.
- **Coordinates**: Transformed to world space and scaled by the Blender scene unit scale (multiplied by 1000 for mm-standard G-code).
### Slices (`SLICESOCK_Slices`)
A `list[np.ndarray]` (each `(N, 3)`) containing the results of the slicing operation.
- **Content**: Guaranteed to be closed loops forming a valid manifold boundary.
- **Z-Awareness**: Coordinates are stored as 3D `(X, Y, Z)` to maintain the correct layer height.
- **Topology**: Generated and cleaned using `pyclipper`.
### Paths (`SLICESOCK_Paths`)
A `list[np.ndarray]` (each `(N, 3)`) containing generic toolpaths.
- **Content**: Can be open or closed segments.
- **Z-Awareness**: Coordinates are stored as 3D `(X, Y, Z)`.
- **Source**: Typically generated from `Slices` boundaries via the `Slices to Paths` node.
### Gcode (`SLICESOCK_Gcode`)
A standard Python string containing formatted G-code.
- **Format**: `G0` for travels, `G1` for extrusions.
- **Extrusion**: Calculated as absolute `E` values based on Euclidean distance and a configurable `EXTRUSION_RATE`.
## Nodes
### Input Nodes
- `SLICENODE_InputFloat`: Single float input.
- `SLICENODE_InputInt`: Single integer input.
- `SLICENODE_InputString`: Single string/path input.
- `SLICENODE_InputObject`: Blender object picker.
- `SLICENODE_GcodeBlock`: Manual text area for entering raw G-code.
### Processing Nodes
- `SLICENODE_GetMesh`: Converts a Blender Object into internal `SlicingObject` format.
- `SLICENODE_SliceShells`: Slices a `SlicingObject` into 2D `Slices` at a given layer height.
- `SLICENODE_BufferShells`: Offsets/buffers 2D `Slices` using a "Distance" parameter. Buffers each Z-layer independently to maintain 3D structure and uses winding rules to correctly handle nested holes.
- `SLICENODE_SlicesToPaths`: Extracts the boundaries of `Slices` to create `Paths`.
- `SLICENODE_PathsToGcode`: Converts `Paths` into G-code blocks.
### Joiner Nodes (Variable Inputs)
- `SLICENODE_JoinMesh`: Merges multiple `SlicingObject` instances into one.
- `SLICENODE_JoinPaths`: Merges multiple paths.
- `SLICENODE_JoinGcode`: Concatenates multiple G-code strings with newline separators.
### Action Nodes (Executable)
- `SLICENODE_GcodeToFile`: Writes G-code string to a file on disk.
- `SLICENODE_GcodeToMesh`: Decodes G-code and creates a Blender mesh (Edges) for visualization/debugging.
- `SLICENODE_LoadGcode`: Reads a G-code file from disk into a string.
- `SLICENODE_Test`: Triggers evaluation for debugging purposes.

35
doc/README.md Normal file
View file

@ -0,0 +1,35 @@
# NodeSlicer Technical Overview
NodeSlicer is a Blender extension designed for high-performance mesh slicing and G-code generation. It provides a node-based interface that abstracts away the complexity of 3D-to-2D geometry conversion, offering a pipeline for 3D printing workflows.
## Core Principles
1. **Fast Slicing**: Bypasses the Blender CPU-bound Boolean modifiers by using vectorized plane-triangle intersection via NumPy.
2. **Robust Geometry**: Leverages Shapely (built on GEOS) for all 2D operations, ensuring topological correctness in offset/buffering and boundary extraction.
3. **Extensibility**: A custom Node Tree architecture allows for easy creation of new nodes for custom toolpath strategies or post-processing.
4. **Blender Integration**: Deeply integrated into Blender's UI while maintaining a decoupled geometry engine for maximum performance.
## System Architecture
```mermaid
graph TD
UI[Blender Node Editor] --> Eval[Node Evaluation Engine]
Eval --> Capture[Mesh Capture - infra/blender_mesh_capture]
Capture --> Slice[Slicing Logic - core/slice]
Slice --> Gcode[G-code Generation - core/slice]
Gcode --> Export[File Export]
```
## Data Lifecycle
- **Mesh Data**: Extracted from Blender as `(N, 4, 3)` NumPy arrays representing triangles and face normals.
- **2D Contours (Slices)**: Stored as a list of closed paths (NumPy arrays). Coordinates include Z-height. Topology (shells/holes) is managed via Pyclipper using the Even-Odd fill rule.
- **Toolpaths (Paths)**: Stored as a list of open or closed paths (NumPy arrays). These represent the actual motion lines of the 3D printer.
- **G-code**: Formatted strings that include travel moves (`G0`), extrusions (`G1`), and metadata (layer change comments).
## Important Files
- `__init__.py`: Node tree and evaluation engine definition.
- `core/slice.py`: Slicing and G-code generation algorithms.
- `infra/blender_mesh_capture.py`: High-performance mesh extraction from Blender.
- `registry.py`: Central registration of Blender classes.

7
infra/blender_bridge.py Normal file
View file

@ -0,0 +1,7 @@
from __future__ import annotations
def get_all_children(obj):
children = []
for child in obj.children:
children += [child] + get_all_children(child)
return children

View file

@ -0,0 +1,406 @@
from __future__ import annotations
from bpy.types import FloatAttribute, Object, Mesh
import bpy
import zlib, struct
from functools import cached_property
import numpy as np
from numpy import dtype, float64
from numpy.typing import NDArray
from typing import Any, cast
from ..core.geometry import crc32_array
from ..infra.blender_bridge import get_all_children
TYPES_NAME = 'DEFAULT'
class MeshCaptureException(Exception): pass
class SlicingObject():
name: str
parent: str
mesh: NDArray[float64]
def __init__(self, obj: Object | None = None, parent: str = "") -> None:
if not bpy.context.scene: raise Exception('No scene currently open!')
if obj: self.name = str(obj.name)
self.parent = parent
depsgraph = bpy.context.evaluated_depsgraph_get()
assert bpy.context.scene.unit_settings
scene_scale: float = bpy.context.scene.unit_settings.scale_length
if obj:
eval_objects = obj.evaluated_get(depsgraph)
self.mesh = objects_to_tris([eval_objects], 1000 * scene_scale)
else:
self.mesh = np.empty((0, 4, 3))
def offset(self, offset: NDArray):
self.mesh += offset
@property
def checksum(self) -> int:
import json
buf = bytearray()
buf.extend(struct.pack(">I", crc32_array(self.mesh)))
buf.extend(struct.pack(">I", zlib.crc32(self.name.encode("utf-8"))))
buf.extend(struct.pack(">I", zlib.crc32(self.parent.encode("utf-8"))))
return zlib.crc32(buf) & 0xFFFFFFFF
@property
def height(self) -> float: return self.mesh[:, :3, 2].max()
@property
def min_x(self) -> float: return self.mesh[:, :3, 0].min()
@property
def max_x(self) -> float: return self.mesh[:, :3, 0].max()
@property
def min_y(self) -> float: return self.mesh[:, :3, 1].min()
@property
def max_y(self) -> float: return self.mesh[:, :3, 1].max()
@property
def min_xy(self) -> NDArray: return np.array([self.min_x, self.min_y, 0.0])
@property
def max_xy(self) -> NDArray: return np.array([self.max_x, self.max_y, 0.0])
@property
def center_xy(self) -> NDArray: return (self.min_xy + self.max_xy) / 2.0
class SlicingCollection():
name: str
objects: list[SlicingObject]
def __init__(self, objs: list[Object], parent: str):
self.objects = [SlicingObject(obj, parent) for obj in objs]
self.objects = [o for o in self.objects if o.mesh.size]
self.name = parent
def offset(self, offset: NDArray):
for so in self.objects: so.offset(offset)
@property
def checksum(self) -> int:
return crc32_array(np.array([o.checksum for o in self.objects]))
@property
def height(self) -> float:
height_all = [so.height for so in self.objects if np.any(so.mesh)]
if not height_all: return 0
return max(height_all)
@property
def meshes(self) -> list[NDArray]:
return [o.mesh for o in self.objects]
@cached_property
def mesh_lengths_ids(self) -> NDArray:
return np.array([len(m) for m in self.meshes])
@cached_property
def mesh_start_ids(self) -> NDArray:
lengths = self.mesh_lengths_ids
return np.insert(np.cumsum(lengths)[:-1], 0, 0)
@cached_property
def mesh_end_ids(self) -> NDArray:
starts = self.mesh_start_ids
lengths = self.mesh_lengths_ids
return starts + lengths - 1
@cached_property
def all_verts(self) -> NDArray:
all_verts = np.vstack(self.meshes)[:, :3, :]
return all_verts.reshape(-1, 3)
@cached_property
def unique_verts(self) -> tuple[np.ndarray, np.ndarray]:
verts_list = []
idxs = []
offset = 0
for mesh in self.meshes:
verts = mesh[:, :3, :].reshape(-1, 3)
void_dtype = np.dtype((np.void, verts.dtype.itemsize * verts.shape[1]))
verts_void = verts.view(void_dtype).ravel()
unique_voids, inv = np.unique(verts_void, return_inverse=True)
unique_verts = unique_voids.view(verts.dtype).reshape(-1, 3)
verts_list.append(unique_verts)
idxs.append(inv.reshape(-1, 3) + offset)
offset += unique_verts.shape[0]
all_verts = np.concatenate(verts_list, axis=0)
all_idxs = np.vstack(idxs)
return all_verts, all_idxs
@property
def min_x(self) -> float | None:
return min([so.min_x for so in self.objects if np.any(so.mesh)], default=None)
@property
def max_x(self) -> float | None:
return max([so.max_x for so in self.objects if np.any(so.mesh)], default=None)
@property
def min_y(self) -> float | None:
return min([so.min_y for so in self.objects if np.any(so.mesh)], default=None)
@property
def max_y(self) -> float | None:
return max([so.max_y for so in self.objects if np.any(so.mesh)], default=None)
@property
def min_xy(self) -> NDArray | None:
if self.min_x is None or self.min_y is None: return None
return np.array([self.min_x, self.min_y, 0.0])
@property
def max_xy(self) -> NDArray | None:
if self.max_x is None or self.max_y is None: return None
return np.array([self.max_x, self.max_y, 0.0])
@property
def center_xy(self) -> NDArray | None:
if self.min_xy is None or self.max_xy is None: return None
return (self.min_xy + self.max_xy) / 2.0
class SlicingGroup():
collections: dict[str, SlicingCollection]
# Metadata
wipe_tower_xy: NDArray = np.array([0, 0])
wipe_tower_rotation_deg: float = 0
def __init__(self, selected_objs: list[Object]) -> None:
family = []
for obj in selected_objs:
family.append(obj)
family.extend(get_all_children(obj))
# Remove duplicates
family = list(set(family))
filtered_objs = [o for o in family if getattr(o, TYPES_NAME).object_type != 'Ignore']
def find_top_parent(o):
current = o
while current.parent is not None:
current = current.parent
return current
parents: dict[str, list[Object]] = {}
[parents.setdefault(find_top_parent(o).name, []).append(o) for o in filtered_objs]
self.collections = {}
for k, objects in parents.items():
self.collections[k] = SlicingCollection(objects, k)
self._extract_metadata(family)
def _extract_metadata(self, objs) -> None:
import math
depsgraph = bpy.context.evaluated_depsgraph_get()
scale_tx = 1000. * bpy.context.scene.unit_settings.scale_length #type: ignore
self.wipe_tower_xy = self.center_xy[0:2]
for obj in objs:
eval_obj = obj.evaluated_get(depsgraph)
if eval_obj.blendertoprusaslicer.object_type == 'WipeTower':
self.wipe_tower_xy = np.array(eval_obj.location[0:2]) * scale_tx
self.wipe_tower_rotation_deg = round(eval_obj.rotation_euler[2]*180/math.pi,5)
break
return
def offset(self, offset: NDArray):
for k, so in self.collections.items(): so.offset(offset)
self.wipe_tower_xy = self.wipe_tower_xy + offset[0:2]
@property
def checksum(self):
buf = bytearray()
for key, col in sorted(self.collections.items(), key=lambda kv: kv[0]):
key_bytes = key.encode('utf-8')
buf.extend(struct.pack(">I", len(key_bytes)))
buf.extend(key_bytes)
buf.extend(struct.pack(">I", col.checksum))
buf.extend(struct.pack(">I", crc32_array(self.wipe_tower_xy)))
buf.extend(struct.pack(">I", crc32_array(np.array(self.wipe_tower_rotation_deg))))
return zlib.crc32(buf) & 0xFFFFFFFF
@property
def height(self) -> float: return max([so.height for k, so in self.collections.items() if so.height], default=0)
@property
def min_x(self) -> float: return min([so.min_x for k, so in self.collections.items() if so.min_x], default=0)
@property
def max_x(self) -> float: return max([so.max_x for k, so in self.collections.items() if so.max_x], default=0)
@property
def min_y(self) -> float: return min([so.min_y for k, so in self.collections.items() if so.min_y], default=0)
@property
def max_y(self) -> float: return max([so.max_y for k, so in self.collections.items() if so.max_y], default=0)
@property
def min_xy(self) -> NDArray:
if self.min_x is None or self.min_y is None: return np.array([.0, .0, .0])
return np.array([self.min_x, self.min_y, 0.0])
@property
def max_xy(self) -> NDArray:
if self.max_x is None or self.max_y is None: return np.array([.0, .0, .0])
return np.array([self.max_x, self.max_y, 0.0])
@property
def center_xy(self) -> NDArray:
if self.min_xy is None or self.max_xy is None: return np.array([.0, .0, .0])
return (self.min_xy + self.max_xy) / 2.0
class TriMesh():
mesh: Mesh
length_verts: int
length_tris: int
matrix_world: Any
def __init__(self, mesh: Mesh, v: int, t: int, m) -> None:
self.mesh = mesh
self.length_verts = v
self.length_tris = t
self.matrix_world = m
def objects_to_tris(objects: list[Object], scale) -> (NDArray[tuple[int, int, int], dtype[float]], NDArray[tuple[int, int], dtype[float]]): # pyright: ignore[reportInvalidTypeForm]
tris_count: int = 0
meshes: list[TriMesh] = []
for obj in objects:
try:
depsgraph = bpy.context.evaluated_depsgraph_get()
mesh: Mesh = obj.to_mesh(depsgraph=depsgraph, preserve_all_data_layers=True)
except: continue
mesh.calc_loop_triangles()
cur_length = len(mesh.loop_triangles)
if cur_length == 0: continue
tris_count += cur_length
meshes += [TriMesh(
mesh,
len(mesh.vertices),
len(mesh.loop_triangles),
np.array(obj.matrix_world.transposed())
)]
tris_flat: np.ndarray[tuple[int], dtype[np.float64]] = np.empty(tris_count * 4 * 3, dtype=dtype(np.float64))
tris: np.ndarray[tuple[int, int, int], dtype[np.float64]] = tris_flat.reshape(-1, 4, 3)
col_idx = 0
for trimesh in meshes:
tris_v_i_flat: np.ndarray[tuple[int], dtype[np.int32]] = np.empty(trimesh.length_tris * 3, dtype=dtype(np.int32))
tris_v_n_flat: np.ndarray[tuple[int], dtype[np.float64]] = np.empty(trimesh.length_tris * 3, dtype=dtype(np.float64))
tris_verts_flat: np.ndarray[tuple[int], dtype[np.float64]] = np.empty(trimesh.length_verts * 3, dtype=dtype(np.float64))
trimesh.mesh.loop_triangles.foreach_get("vertices", tris_v_i_flat)
trimesh.mesh.loop_triangles.foreach_get("normal", tris_v_n_flat)
trimesh.mesh.vertices.foreach_get("co", tris_verts_flat)
tris_v_i: np.ndarray[tuple[int, int], dtype[np.int32]] = tris_v_i_flat.reshape((-1, 3))
tris_v_n: np.ndarray[tuple[int, int], dtype[np.float64]] = tris_v_n_flat.reshape((-1, 3))
tris_verts: np.ndarray[tuple[int, int], dtype[np.float64]] = tris_verts_flat.reshape((-1, 3))
homogeneous_verts: NDArray[float64] = np.hstack((tris_verts, np.ones((tris_verts.shape[0], 1)))) # type: ignore
tx_verts = homogeneous_verts @ trimesh.matrix_world
tx_verts = (tx_verts[:, :3]) * scale
homogeneous_norm: NDArray[float64] = np.hstack((tris_v_n, np.ones((tris_v_n.shape[0], 1)))) # type: ignore
tx_norm = homogeneous_norm @ trimesh.matrix_world.T
tx_norm = tx_norm[:, :3]
tx_norm = tx_norm / np.linalg.norm(tx_norm, axis=1, keepdims=True)
tx_norm = tx_norm[:, np.newaxis, :]
tx_tris = tx_verts[tris_v_i]
tris_coords_and_norm: NDArray[float64] = np.concatenate((tx_tris, tx_norm), axis=1)
tris[col_idx:col_idx + trimesh.length_tris,:] = tris_coords_and_norm
col_idx += trimesh.length_tris
return tris
def capture_mesh_edges(obj: Object) -> list[NDArray[float64]]:
try:
depsgraph = bpy.context.evaluated_depsgraph_get()
mesh: Mesh = obj.to_mesh(depsgraph=depsgraph, preserve_all_data_layers=True)
except Exception:
return []
if not mesh or not mesh.vertices:
return []
try:
assert bpy.context.scene
assert bpy.context.scene.unit_settings
scene_scale: float = bpy.context.scene.unit_settings.scale_length
scale = 1000.0 * scene_scale
# Get vertices
num_verts = len(mesh.vertices)
verts_flat = np.empty(num_verts * 3, dtype=np.float64)
mesh.vertices.foreach_get("co", verts_flat)
verts = verts_flat.reshape(-1, 3)
# Apply world matrix transformation (consistent with objects_to_tris)
mat = np.array(obj.matrix_world.transposed())
homogeneous_verts = np.hstack((verts, np.ones((num_verts, 1))))
tx_verts = (homogeneous_verts @ mat)[:, :3] * scale
# Identify connectivity between consecutive indices
num_edges = len(mesh.edges)
connected = np.zeros(num_verts - 1, dtype=bool)
if num_edges > 0:
edges_flat = np.empty(num_edges * 2, dtype=np.int32)
mesh.edges.foreach_get("vertices", edges_flat)
edges = edges_flat.reshape(-1, 2)
v_min = np.min(edges, axis=1)
v_max = np.max(edges, axis=1)
# Check for edges connecting vertex i and i+1
mask = (v_max == v_min + 1)
consecutive_v_min = v_min[mask]
# Ensure indices are within bounds for the 'connected' array
valid_mask = (consecutive_v_min >= 0) & (consecutive_v_min < num_verts - 1)
connected[consecutive_v_min[valid_mask]] = True
# Split vertices into paths where connectivity is interrupted
split_at = np.where(~connected)[0] + 1
paths = np.split(tx_verts, split_at)
return [p for p in paths if p.size > 0]
finally:
# Compatibility with Blender 2.8+ to avoid memory leaks
# We check if the attribute exists as it's not used elsewhere in this file
if hasattr(obj, "to_mesh_clear"):
obj.to_mesh_clear()

View file

@ -0,0 +1,110 @@
from __future__ import annotations
import typing
from bpy.types import NodeSocket
class NodeException(Exception): ...
_EvalCache = dict[tuple[int, str], typing.Any]
class AutoBlIdName:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if "bl_idname" not in cls.__dict__:
cls.bl_idname = cls.__name__
class SliceNodeSocket(NodeSocket, AutoBlIdName):
default: typing.Any = None
color = (1., 1., 1., 1.)
def draw_color(self, context, node) -> tuple[float, float, float, float]: return self.color # pyright: ignore[reportIncompatibleMethodOverride]
## Evaluation helpers
def _follow_reroute(socket: NodeSocket) -> NodeSocket:
current: NodeSocket = socket
seen: set[int] = set()
while getattr(current.node, "bl_idname", "") == "NodeReroute":
assert current.node
ptr = current.node.as_pointer()
if ptr in seen:
break
seen.add(ptr)
reroute_input = current.node.inputs[0] if current.node.inputs else None
if not reroute_input or not reroute_input.is_linked:
raise NodeException("Loose reroute node found: ensure all reroute nodes are correctly linked")
current = reroute_input.links[0].from_socket
return current
def _resolve_input(
socket: SliceNodeSocket | None,
*,
cache: _EvalCache | None = None,
visited: set[tuple[int, str]] | None = None,
):
if socket is None:
raise Exception(f"The queried socket is non-existant")
assert socket.node
if not socket.is_linked:
if v := getattr(socket, "value", None): return v
else: return socket.default
assert socket.is_linked
assert socket.links
from_socket: NodeSocket = _follow_reroute(socket.links[0].from_socket)
return _resolve_output(
from_socket,
default=None,
cache=cache,
visited=visited,
)
def _resolve_output(
socket: NodeSocket,
*,
default: typing.Any = None,
cache: _EvalCache | None = None,
visited: set[tuple[int, str]] | None = None,
):
cache = {} if cache is None else cache
visited = set() if visited is None else visited
assert socket.node
key = (socket.node.as_pointer(), socket.node.name)
if key in cache: return cache[key]
if key in visited: return default # cycle guard
visited.add(key)
try:
if not isinstance(socket.node, SliceNode):
return default
if getattr(socket.node, "mute", False):
# Attempt bypass: find a matching input socket by name or type
input_to_bypass = socket.node.inputs.get(socket.name)
if not input_to_bypass:
for inp in socket.node.inputs:
if inp.bl_idname == socket.bl_idname:
input_to_bypass = inp
break
if input_to_bypass:
value = _resolve_input(typing.cast(Any, input_to_bypass), cache=cache, visited=visited)
cache[key] = value
return value
return default
assert socket.node.evaluate_output
value = socket.node.evaluate_output(socket.name, cache=cache, visited=visited)
cache[key] = value
return value
finally:
visited.remove(key)

12
pyproject.toml Normal file
View file

@ -0,0 +1,12 @@
[project]
name = "nodeslicer"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"fake-bpy-module>=20260317",
"numpy>=2.2.6",
"pyclipper>=1.4.0",
"shapely>=2.1.2",
]

77
registry.py Normal file
View file

@ -0,0 +1,77 @@
# registry.py
import bpy
from typing import Any, Callable
_bpy_class_registry: list[type] = []
_timer_registry: list[Callable[..., int | None]] = []
# BPY CLASSES
def register_class(cls: type) -> type:
_bpy_class_registry.append(cls)
return cls
def get() -> list[type]:
return list(_bpy_class_registry)
def get_label_by_name(name: str) -> str | None:
for cls in get():
if cls.__name__ == name:
return getattr(cls, "bl_label", "No Label")
return None
def get_class_by_name(name: str) -> type | None:
for cls in get():
if cls.__name__ == name:
return cls
return None
def blender_register_classes():
for module in get():
bpy.utils.register_class(module)
def blender_unregister_classes():
for module in get():
bpy.utils.unregister_class(module)
# TIMERS
def register_timer(clb: Callable[..., Any]):
_timer_registry.append(clb)
return clb
def get_timers() -> list[Callable[..., int | None]]:
return list(_timer_registry)
def blender_register_timers():
for timer in _timer_registry:
bpy.app.timers.register(timer, persistent=True)
def blender_unregister_timers():
for timer in _timer_registry:
bpy.app.timers.unregister(timer)
import os
from bpy.utils.previews import ImagePreviewCollection
_icons_pcoll: ImagePreviewCollection | None = None
def blender_register_icons():
global _icons_pcoll
_icons_pcoll = bpy.utils.previews.new()
my_icons_dir = os.path.join(os.path.dirname(__file__), "icons")
for filename in os.listdir(my_icons_dir):
_icons_pcoll.load(filename, os.path.join(my_icons_dir, filename), 'IMAGE')
def blender_unregister_icons():
global _icons_pcoll
if not _icons_pcoll: return
bpy.utils.previews.remove(_icons_pcoll)
del _icons_pcoll
def get_icon(icon_id: str) -> int:
global _icons_pcoll
if not _icons_pcoll: return -1
return _icons_pcoll[icon_id].icon_id

294
uv.lock generated Normal file
View file

@ -0,0 +1,294 @@
version = 1
revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.11'",
"python_full_version < '3.11'",
]
[[package]]
name = "fake-bpy-module"
version = "20260317"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3e/cf/acb601232f5bc8add9c10d701012c89dd3f84bb1a930c0dadee1b527ae5a/fake_bpy_module-20260317.tar.gz", hash = "sha256:b1d42c212286ea7e8fcaca0068b1f45574faff18d6fa1047aff8afbc58611251", size = 876605, upload-time = "2026-03-17T06:53:23.983Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/73/2f8c20f53ee3a9692cab552d02c7178afd15e1b15bb335b5d90feb943bbe/fake_bpy_module-20260317-py3-none-any.whl", hash = "sha256:399ac3f8479c236a023f632733df6bbb40af8340cc3c3c05b41f3b63bb732127", size = 1013802, upload-time = "2026-03-17T06:53:22.489Z" },
]
[[package]]
name = "nodeslicer"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "fake-bpy-module" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "pyclipper" },
{ name = "shapely" },
]
[package.metadata]
requires-dist = [
{ name = "fake-bpy-module", specifier = ">=20260317" },
{ name = "numpy", specifier = ">=2.2.6" },
{ name = "pyclipper", specifier = ">=1.4.0" },
{ name = "shapely", specifier = ">=2.1.2" },
]
[[package]]
name = "numpy"
version = "2.2.6"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
{ url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
{ url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
{ url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
{ url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
{ url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
{ url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
{ url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
{ url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
{ url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
{ url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
{ url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
{ url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
{ url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
{ url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
{ url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
{ url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
{ url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
{ url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
{ url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
{ url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
{ url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
{ url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
{ url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
{ url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
{ url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
{ url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
{ url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
{ url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
{ url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
{ url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
{ url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
{ url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
{ url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
{ url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
{ url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
{ url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
{ url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
{ url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
{ url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
{ url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
{ url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
{ url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
{ url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
{ url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
{ url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
{ url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
{ url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
]
[[package]]
name = "numpy"
version = "2.4.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.11'",
]
sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" },
{ url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" },
{ url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" },
{ url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" },
{ url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" },
{ url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" },
{ url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" },
{ url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" },
{ url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" },
{ url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" },
{ url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" },
{ url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" },
{ url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" },
{ url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" },
{ url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" },
{ url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" },
{ url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" },
{ url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" },
{ url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" },
{ url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" },
{ url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" },
{ url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" },
{ url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" },
{ url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" },
{ url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" },
{ url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" },
{ url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" },
{ url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" },
{ url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" },
{ url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" },
{ url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" },
{ url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" },
{ url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" },
{ url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" },
{ url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" },
{ url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" },
{ url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" },
{ url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" },
{ url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" },
{ url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" },
{ url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" },
{ url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" },
{ url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" },
{ url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" },
{ url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" },
{ url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" },
{ url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" },
{ url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" },
{ url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" },
{ url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" },
{ url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" },
{ url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" },
{ url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" },
{ url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" },
{ url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" },
{ url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" },
{ url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" },
{ url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" },
{ url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" },
{ url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" },
{ url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" },
{ url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" },
{ url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" },
{ url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" },
{ url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" },
{ url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" },
{ url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" },
{ url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" },
{ url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" },
]
[[package]]
name = "pyclipper"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/21/3c06205bb407e1f79b73b7b4dfb3950bd9537c4f625a68ab5cc41177f5bc/pyclipper-1.4.0.tar.gz", hash = "sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1", size = 54489, upload-time = "2025-12-01T13:15:35.015Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/9f/a10173d32ecc2ce19a04d018163f3ca22a04c0c6ad03b464dcd32f9152a8/pyclipper-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bafad70d2679c187120e8c44e1f9a8b06150bad8c0aecf612ad7dfbfa9510f73", size = 264510, upload-time = "2025-12-01T13:14:46.551Z" },
{ url = "https://files.pythonhosted.org/packages/e0/c2/5490ddc4a1f7ceeaa0258f4266397e720c02db515b2ca5bc69b85676f697/pyclipper-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b74a9dd44b22a7fd35d65fb1ceeba57f3817f34a97a28c3255556362e491447", size = 139498, upload-time = "2025-12-01T13:14:48.31Z" },
{ url = "https://files.pythonhosted.org/packages/3b/0a/bea9102d1d75634b1a5702b0e92982451a1eafca73c4845d3dbe27eba13d/pyclipper-1.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a4d2736fb3c42e8eb1d38bf27a720d1015526c11e476bded55138a977c17d9d", size = 970974, upload-time = "2025-12-01T13:14:49.799Z" },
{ url = "https://files.pythonhosted.org/packages/8b/1b/097f8776d5b3a10eb7b443b632221f4ed825d892e79e05682f4b10a1a59c/pyclipper-1.4.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3b3630051b53ad2564cb079e088b112dd576e3d91038338ad1cc7915e0f14dc", size = 943315, upload-time = "2025-12-01T13:14:51.266Z" },
{ url = "https://files.pythonhosted.org/packages/fd/4d/17d6a3f1abf0f368d58f2309e80ee3761afb1fd1342f7780ab32ba4f0b1d/pyclipper-1.4.0-cp310-cp310-win32.whl", hash = "sha256:8d42b07a2f6cfe2d9b87daf345443583f00a14e856927782fde52f3a255e305a", size = 95286, upload-time = "2025-12-01T13:14:52.922Z" },
{ url = "https://files.pythonhosted.org/packages/53/ca/b30138427ed122ec9b47980b943164974a2ec606fa3f71597033b9a9f9a6/pyclipper-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:6a97b961f182b92d899ca88c1bb3632faea2e00ce18d07c5f789666ebb021ca4", size = 104227, upload-time = "2025-12-01T13:14:54.013Z" },
{ url = "https://files.pythonhosted.org/packages/de/e3/64cf7794319b088c288706087141e53ac259c7959728303276d18adc665d/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:adcb7ca33c5bdc33cd775e8b3eadad54873c802a6d909067a57348bcb96e7a2d", size = 264281, upload-time = "2025-12-01T13:14:55.47Z" },
{ url = "https://files.pythonhosted.org/packages/34/cd/44ec0da0306fa4231e76f1c2cb1fa394d7bde8db490a2b24d55b39865f69/pyclipper-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd24849d2b94ec749ceac7c34c9f01010d23b6e9d9216cf2238b8481160e703d", size = 139426, upload-time = "2025-12-01T13:14:56.683Z" },
{ url = "https://files.pythonhosted.org/packages/ad/88/d8f6c6763ea622fe35e19c75d8b39ed6c55191ddc82d65e06bc46b26cb8e/pyclipper-1.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b6c8d75ba20c6433c9ea8f1a0feb7e4d3ac06a09ad1fd6d571afc1ddf89b869", size = 989649, upload-time = "2025-12-01T13:14:58.28Z" },
{ url = "https://files.pythonhosted.org/packages/ff/e9/ea7d68c8c4af3842d6515bedcf06418610ad75f111e64c92c1d4785a1513/pyclipper-1.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58e29d7443d7cc0e83ee9daf43927730386629786d00c63b04fe3b53ac01462c", size = 962842, upload-time = "2025-12-01T13:15:00.044Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b7/0b4a272d8726e51ab05e2b933d8cc47f29757fb8212e38b619e170e6015c/pyclipper-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a8d2b5fb75ebe57e21ce61e79a9131edec2622ff23cc665e4d1d1f201bc1a801", size = 95098, upload-time = "2025-12-01T13:15:01.359Z" },
{ url = "https://files.pythonhosted.org/packages/3a/76/4901de2919198bb2bd3d989f86d4a1dff363962425bb2d63e24e6c990042/pyclipper-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:e9b973467d9c5fa9bc30bb6ac95f9f4d7c3d9fc25f6cf2d1cc972088e5955c01", size = 104362, upload-time = "2025-12-01T13:15:02.439Z" },
{ url = "https://files.pythonhosted.org/packages/90/1b/7a07b68e0842324d46c03e512d8eefa9cb92ba2a792b3b4ebf939dafcac3/pyclipper-1.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140", size = 265676, upload-time = "2025-12-01T13:15:04.15Z" },
{ url = "https://files.pythonhosted.org/packages/6b/dd/8bd622521c05d04963420ae6664093f154343ed044c53ea260a310c8bb4d/pyclipper-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6", size = 140458, upload-time = "2025-12-01T13:15:05.76Z" },
{ url = "https://files.pythonhosted.org/packages/7a/06/6e3e241882bf7d6ab23d9c69ba4e85f1ec47397cbbeee948a16cf75e21ed/pyclipper-1.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca", size = 978235, upload-time = "2025-12-01T13:15:06.993Z" },
{ url = "https://files.pythonhosted.org/packages/cf/f4/3418c1cd5eea640a9fa2501d4bc0b3655fa8d40145d1a4f484b987990a75/pyclipper-1.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872", size = 961388, upload-time = "2025-12-01T13:15:08.467Z" },
{ url = "https://files.pythonhosted.org/packages/ac/94/c85401d24be634af529c962dd5d781f3cb62a67cd769534df2cb3feee97a/pyclipper-1.4.0-cp312-cp312-win32.whl", hash = "sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4", size = 95169, upload-time = "2025-12-01T13:15:10.098Z" },
{ url = "https://files.pythonhosted.org/packages/97/77/dfea08e3b230b82ee22543c30c35d33d42f846a77f96caf7c504dd54fab1/pyclipper-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037", size = 104619, upload-time = "2025-12-01T13:15:11.592Z" },
{ url = "https://files.pythonhosted.org/packages/67/d0/cbce7d47de1e6458f66a4d999b091640134deb8f2c7351eab993b70d2e10/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e", size = 264342, upload-time = "2025-12-01T13:15:12.697Z" },
{ url = "https://files.pythonhosted.org/packages/ce/cc/742b9d69d96c58ac156947e1b56d0f81cbacbccf869e2ac7229f2f86dc4e/pyclipper-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a", size = 139839, upload-time = "2025-12-01T13:15:13.911Z" },
{ url = "https://files.pythonhosted.org/packages/db/48/dd301d62c1529efdd721b47b9e5fb52120fcdac5f4d3405cfc0d2f391414/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f", size = 972142, upload-time = "2025-12-01T13:15:15.477Z" },
{ url = "https://files.pythonhosted.org/packages/07/bf/d493fd1b33bb090fa64e28c1009374d5d72fa705f9331cd56517c35e381e/pyclipper-1.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1", size = 952789, upload-time = "2025-12-01T13:15:17.042Z" },
{ url = "https://files.pythonhosted.org/packages/cf/88/b95ea8ea21ddca34aa14b123226a81526dd2faaa993f9aabd3ed21231604/pyclipper-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c", size = 94817, upload-time = "2025-12-01T13:15:18.724Z" },
{ url = "https://files.pythonhosted.org/packages/ba/42/0a1920d276a0e1ca21dc0d13ee9e3ba10a9a8aa3abac76cd5e5a9f503306/pyclipper-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832", size = 104007, upload-time = "2025-12-01T13:15:19.763Z" },
{ url = "https://files.pythonhosted.org/packages/1a/20/04d58c70f3ccd404f179f8dd81d16722a05a3bf1ab61445ee64e8218c1f8/pyclipper-1.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c", size = 265167, upload-time = "2025-12-01T13:15:20.844Z" },
{ url = "https://files.pythonhosted.org/packages/bd/2e/a570c1abe69b7260ca0caab4236ce6ea3661193ebf8d1bd7f78ccce537a5/pyclipper-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e", size = 139966, upload-time = "2025-12-01T13:15:22.036Z" },
{ url = "https://files.pythonhosted.org/packages/e8/3b/e0859e54adabdde8a24a29d3f525ebb31c71ddf2e8d93edce83a3c212ffc/pyclipper-1.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9", size = 968216, upload-time = "2025-12-01T13:15:23.18Z" },
{ url = "https://files.pythonhosted.org/packages/f6/6b/e3c4febf0a35ae643ee579b09988dd931602b5bf311020535fd9e5b7e715/pyclipper-1.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39", size = 954198, upload-time = "2025-12-01T13:15:24.522Z" },
{ url = "https://files.pythonhosted.org/packages/fc/74/728efcee02e12acb486ce9d56fa037120c9bf5b77c54bbdbaa441c14a9d9/pyclipper-1.4.0-cp314-cp314-win32.whl", hash = "sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b", size = 96951, upload-time = "2025-12-01T13:15:25.79Z" },
{ url = "https://files.pythonhosted.org/packages/e3/d7/7f4354e69f10a917e5c7d5d72a499ef2e10945312f5e72c414a0a08d2ae4/pyclipper-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826", size = 106782, upload-time = "2025-12-01T13:15:26.945Z" },
{ url = "https://files.pythonhosted.org/packages/63/60/fc32c7a3d7f61a970511ec2857ecd09693d8ac80d560ee7b8e67a6d268c9/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303", size = 269880, upload-time = "2025-12-01T13:15:28.117Z" },
{ url = "https://files.pythonhosted.org/packages/49/df/c4a72d3f62f0ba03ec440c4fff56cd2d674a4334d23c5064cbf41c9583f6/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9", size = 141706, upload-time = "2025-12-01T13:15:30.134Z" },
{ url = "https://files.pythonhosted.org/packages/c5/0b/cf55df03e2175e1e2da9db585241401e0bc98f76bee3791bed39d0313449/pyclipper-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286", size = 105308, upload-time = "2025-12-01T13:15:31.225Z" },
{ url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608, upload-time = "2025-12-01T13:15:32.69Z" },
{ url = "https://files.pythonhosted.org/packages/18/59/81050abdc9e5b90ffc2c765738c5e40e9abd8e44864aaa737b600f16c562/pyclipper-1.4.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98b2a40f98e1fc1b29e8a6094072e7e0c7dfe901e573bf6cfc6eb7ce84a7ae87", size = 126495, upload-time = "2025-12-01T13:15:33.743Z" },
]
[[package]]
name = "shapely"
version = "2.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/89/c3548aa9b9812a5d143986764dededfa48d817714e947398bdda87c77a72/shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f", size = 1825959, upload-time = "2025-09-24T13:50:00.682Z" },
{ url = "https://files.pythonhosted.org/packages/ce/8a/7ebc947080442edd614ceebe0ce2cdbd00c25e832c240e1d1de61d0e6b38/shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea", size = 1629196, upload-time = "2025-09-24T13:50:03.447Z" },
{ url = "https://files.pythonhosted.org/packages/c8/86/c9c27881c20d00fc409e7e059de569d5ed0abfcec9c49548b124ebddea51/shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f", size = 2951065, upload-time = "2025-09-24T13:50:05.266Z" },
{ url = "https://files.pythonhosted.org/packages/50/8a/0ab1f7433a2a85d9e9aea5b1fbb333f3b09b309e7817309250b4b7b2cc7a/shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142", size = 3058666, upload-time = "2025-09-24T13:50:06.872Z" },
{ url = "https://files.pythonhosted.org/packages/bb/c6/5a30ffac9c4f3ffd5b7113a7f5299ccec4713acd5ee44039778a7698224e/shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4", size = 3966905, upload-time = "2025-09-24T13:50:09.417Z" },
{ url = "https://files.pythonhosted.org/packages/9c/72/e92f3035ba43e53959007f928315a68fbcf2eeb4e5ededb6f0dc7ff1ecc3/shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0", size = 4129260, upload-time = "2025-09-24T13:50:11.183Z" },
{ url = "https://files.pythonhosted.org/packages/42/24/605901b73a3d9f65fa958e63c9211f4be23d584da8a1a7487382fac7fdc5/shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e", size = 1544301, upload-time = "2025-09-24T13:50:12.521Z" },
{ url = "https://files.pythonhosted.org/packages/e1/89/6db795b8dd3919851856bd2ddd13ce434a748072f6fdee42ff30cbd3afa3/shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f", size = 1722074, upload-time = "2025-09-24T13:50:13.909Z" },
{ url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" },
{ url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" },
{ url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" },
{ url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" },
{ url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" },
{ url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" },
{ url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" },
{ url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" },
{ url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" },
{ url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" },
{ url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" },
{ url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" },
{ url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" },
{ url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" },
{ url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" },
{ url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" },
{ url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" },
{ url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" },
{ url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" },
{ url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" },
{ url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" },
{ url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" },
{ url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" },
{ url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" },
{ url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" },
{ url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" },
{ url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" },
{ url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" },
{ url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" },
{ url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" },
{ url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" },
{ url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" },
{ url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" },
{ url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" },
{ url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" },
{ url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" },
{ url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" },
{ url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" },
{ url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" },
{ url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" },
{ url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" },
{ url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" },
{ url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" },
{ url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" },
{ url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" },
{ url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" },
{ url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" },
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.