commit cf7749b9b7b7b3a0989bc5a0d7a3b5acbd536e81 Author: Nicolas Predella Date: Wed May 6 11:12:13 2026 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..3a4f41e --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d072e0d --- /dev/null +++ b/README.md @@ -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. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..531fea1 --- /dev/null +++ b/__init__.py @@ -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() \ No newline at end of file diff --git a/blender_manifest.toml b/blender_manifest.toml new file mode 100644 index 0000000..2be5f3b --- /dev/null +++ b/blender_manifest.toml @@ -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", +] diff --git a/core/geometry.py b/core/geometry.py new file mode 100644 index 0000000..204c813 --- /dev/null +++ b/core/geometry.py @@ -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) \ No newline at end of file diff --git a/core/slice.py b/core/slice.py new file mode 100644 index 0000000..d686d7d --- /dev/null +++ b/core/slice.py @@ -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) diff --git a/doc/MODULES.md b/doc/MODULES.md new file mode 100644 index 0000000..36020ea --- /dev/null +++ b/doc/MODULES.md @@ -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. diff --git a/doc/NODES.md b/doc/NODES.md new file mode 100644 index 0000000..7e2a109 --- /dev/null +++ b/doc/NODES.md @@ -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. diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..eb7c494 --- /dev/null +++ b/doc/README.md @@ -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. diff --git a/infra/blender_bridge.py b/infra/blender_bridge.py new file mode 100644 index 0000000..7f1d5ae --- /dev/null +++ b/infra/blender_bridge.py @@ -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 \ No newline at end of file diff --git a/infra/blender_mesh_capture.py b/infra/blender_mesh_capture.py new file mode 100644 index 0000000..b9f5761 --- /dev/null +++ b/infra/blender_mesh_capture.py @@ -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() \ No newline at end of file diff --git a/infra/blender_slice_nodes_generic.py b/infra/blender_slice_nodes_generic.py new file mode 100644 index 0000000..ecc3205 --- /dev/null +++ b/infra/blender_slice_nodes_generic.py @@ -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) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..710e497 --- /dev/null +++ b/pyproject.toml @@ -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", +] diff --git a/registry.py b/registry.py new file mode 100644 index 0000000..70734cc --- /dev/null +++ b/registry.py @@ -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 \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..f49e3b4 --- /dev/null +++ b/uv.lock @@ -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" }, +] diff --git a/wheels/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl b/wheels/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl new file mode 100644 index 0000000..641d711 Binary files /dev/null and b/wheels/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl differ diff --git a/wheels/pyclipper-1.4.0-cp311-cp311-win_amd64.whl b/wheels/pyclipper-1.4.0-cp311-cp311-win_amd64.whl new file mode 100644 index 0000000..5635093 Binary files /dev/null and b/wheels/pyclipper-1.4.0-cp311-cp311-win_amd64.whl differ diff --git a/wheels/pyclipper-1.4.0-cp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/wheels/pyclipper-1.4.0-cp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 0000000..3176f95 Binary files /dev/null and b/wheels/pyclipper-1.4.0-cp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/wheels/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl b/wheels/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl new file mode 100644 index 0000000..17a0c69 Binary files /dev/null and b/wheels/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl differ diff --git a/wheels/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/wheels/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 0000000..c6d0d06 Binary files /dev/null and b/wheels/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/wheels/pyclipper-1.4.0-cp313-cp313-win_amd64.whl b/wheels/pyclipper-1.4.0-cp313-cp313-win_amd64.whl new file mode 100644 index 0000000..fdf4767 Binary files /dev/null and b/wheels/pyclipper-1.4.0-cp313-cp313-win_amd64.whl differ diff --git a/wheels/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl b/wheels/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl new file mode 100644 index 0000000..9ed6bdd Binary files /dev/null and b/wheels/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl differ diff --git a/wheels/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl b/wheels/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl new file mode 100644 index 0000000..72eaf6d Binary files /dev/null and b/wheels/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl differ diff --git a/wheels/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/wheels/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 0000000..9ab8d75 Binary files /dev/null and b/wheels/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/wheels/shapely-2.1.2-cp311-cp311-win_amd64.whl b/wheels/shapely-2.1.2-cp311-cp311-win_amd64.whl new file mode 100644 index 0000000..ee3d19a Binary files /dev/null and b/wheels/shapely-2.1.2-cp311-cp311-win_amd64.whl differ diff --git a/wheels/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl b/wheels/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl new file mode 100644 index 0000000..71979d9 Binary files /dev/null and b/wheels/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl differ diff --git a/wheels/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl b/wheels/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl new file mode 100644 index 0000000..3a1ddc9 Binary files /dev/null and b/wheels/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl differ diff --git a/wheels/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/wheels/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 0000000..cfa9c5c Binary files /dev/null and b/wheels/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/wheels/shapely-2.1.2-cp313-cp313-win_amd64.whl b/wheels/shapely-2.1.2-cp313-cp313-win_amd64.whl new file mode 100644 index 0000000..97e6293 Binary files /dev/null and b/wheels/shapely-2.1.2-cp313-cp313-win_amd64.whl differ