init
This commit is contained in:
commit
cf7749b9b7
30 changed files with 2282 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
__pycache__
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.13
|
||||||
47
README.md
Normal file
47
README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# NodeSlicer
|
||||||
|
|
||||||
|
NodeSlicer is a high-performance Blender extension that provides a custom node-based interface for mesh slicing and G-code generation. By bypassing traditional Blender modifiers and using NumPy and Shapely for geometry processing, it offers a flexible and extensible workflow for 3D printing and procedural geometry tasks.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Custom Node Editor**: A dedicated node tree type for slicing workflows.
|
||||||
|
- **Fast Geometry Processing**: Uses NumPy for vectorized mesh operations and Shapely for robust 2D geometry manipulation.
|
||||||
|
- **G-code Generation**: Direct conversion of 2D contours (toolpaths) into G-code.
|
||||||
|
- **Live Preview**: Debug nodes to visualize generated toolpaths directly in the Blender viewport.
|
||||||
|
- **Extensible**: Modular architecture allowing for easy addition of new nodes and processing logic.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- Blender 4.2 or newer (uses the new Extension system).
|
||||||
|
- Dependencies (bundled or installed via `uv`):
|
||||||
|
- `numpy`
|
||||||
|
- `shapely`
|
||||||
|
- `pyclipper`
|
||||||
|
|
||||||
|
### As a Blender Extension
|
||||||
|
1. Zip the repository contents (excluding `.venv`, `__pycache__`, etc.).
|
||||||
|
2. In Blender, go to `Edit > Preferences > Get Extensions`.
|
||||||
|
3. Click the arrow in the top right and select `Install from Disk`.
|
||||||
|
4. Select your zip file.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Open the **Node Editor**.
|
||||||
|
2. Switch the tree type to **NodeSlicer Tree**.
|
||||||
|
3. Create a new tree.
|
||||||
|
4. Add an **Object Input** node and select a mesh object.
|
||||||
|
5. Connect it to a **Get Mesh** node.
|
||||||
|
6. Connect the mesh to **Slice Shells**.
|
||||||
|
7. Connect the slices to **Slices to Paths**, then **Paths to Gcode**.
|
||||||
|
8. Use a **Gcode To File** node to export your result.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Project Overview](doc/README.md)
|
||||||
|
- [Module Reference](doc/MODULES.md)
|
||||||
|
- [Node & Socket Reference](doc/NODES.md)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the GPL-3.0-or-later license. See `blender_manifest.toml` for details.
|
||||||
719
__init__.py
Normal file
719
__init__.py
Normal file
|
|
@ -0,0 +1,719 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
bl_info = {
|
||||||
|
"name": "NodeSlicer",
|
||||||
|
"author": "OpenAI",
|
||||||
|
"version": (0, 1, 0),
|
||||||
|
"blender": (3, 6, 0),
|
||||||
|
"location": "Node Editor > Add",
|
||||||
|
"description": "Very small custom nodes example",
|
||||||
|
"category": "Node",
|
||||||
|
}
|
||||||
|
|
||||||
|
from math import floor
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from bpy.types import NodeTree, Node, NodeSocket, Operator, Menu
|
||||||
|
from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty
|
||||||
|
|
||||||
|
from .registry import register_class
|
||||||
|
from .core import slice
|
||||||
|
from .core.slice import Pather, slice_at_range
|
||||||
|
from .infra.blender_mesh_capture import SlicingObject, capture_mesh_edges
|
||||||
|
from .infra.blender_slice_nodes_generic import SliceNodeSocket
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bpy.stub_internal.rna_enums import OperatorReturnItems
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Node tree
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICETREE_NodeTree(NodeTree):
|
||||||
|
bl_idname = "SLICETREE_NodeTree"
|
||||||
|
bl_label = "NodeSlicer Tree"
|
||||||
|
bl_icon = "NODETREE"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Sockets
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class LabelNodeSocket(SliceNodeSocket):
|
||||||
|
default = ""
|
||||||
|
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||||
|
assert layout
|
||||||
|
layout.label(text=text or self.name)
|
||||||
|
|
||||||
|
class InputNodeSocket(SliceNodeSocket):
|
||||||
|
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||||
|
assert layout
|
||||||
|
if not self.is_output and self.is_linked:
|
||||||
|
layout.label(text=text or self.name)
|
||||||
|
else:
|
||||||
|
layout.prop(self, "value", text=text or self.name)
|
||||||
|
|
||||||
|
def _draw_as_ro(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||||
|
layout.label(text=text or self.name)
|
||||||
|
|
||||||
|
def _draw_as_rw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||||
|
layout.prop(self, "value", text=text or self.name)
|
||||||
|
|
||||||
|
class ReadonlySocket():
|
||||||
|
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||||
|
return self._draw_as_ro(context, layout, node, text) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICESOCK_Object(InputNodeSocket):
|
||||||
|
bl_label = "Object"
|
||||||
|
|
||||||
|
value: PointerProperty(
|
||||||
|
name="Object",
|
||||||
|
type=bpy.types.Object,
|
||||||
|
) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
color = (0.9, 0.7, 0.2, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
class SLICESOCK_Float_Common(InputNodeSocket):
|
||||||
|
bl_label = "Float"
|
||||||
|
|
||||||
|
value: FloatProperty(name="Float", default=0) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
color = (161/255, 161/255, 161/255, 1.0)
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICESOCK_FloatRo(ReadonlySocket, SLICESOCK_Float_Common): ...
|
||||||
|
@register_class
|
||||||
|
class SLICESOCK_Float(SLICESOCK_Float_Common): ...
|
||||||
|
|
||||||
|
|
||||||
|
class SLICESOCK_Int_Common(InputNodeSocket):
|
||||||
|
bl_label = "Integer"
|
||||||
|
|
||||||
|
value: IntProperty(name="Int", default=0) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
color = (161/255, 161/255, 161/255, 1.0)
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICESOCK_Int(SLICESOCK_Int_Common): ...
|
||||||
|
@register_class
|
||||||
|
class SLICESOCK_IntRo(ReadonlySocket, SLICESOCK_Int_Common): ...
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICESOCK_String(InputNodeSocket):
|
||||||
|
bl_label = "String"
|
||||||
|
|
||||||
|
value: StringProperty(name="String", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
color = (0.4, 0.7, 1.0, 1.0)
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICESOCK_Mesh(LabelNodeSocket):
|
||||||
|
bl_label = "Mesh"
|
||||||
|
default = None
|
||||||
|
color = (0, 214/255, 163/255, 1.0)
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICESOCK_Slices(LabelNodeSocket):
|
||||||
|
bl_label = "Slices"
|
||||||
|
default = []
|
||||||
|
color = (0, 214/255, 96/255, 1.0)
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICESOCK_Paths(LabelNodeSocket):
|
||||||
|
bl_label = "Paths"
|
||||||
|
default = []
|
||||||
|
color = (214/255, 0, 120/255, 1.0)
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICESOCK_Gcode(LabelNodeSocket):
|
||||||
|
bl_label = "Gcode"
|
||||||
|
default = ""
|
||||||
|
color = (1., 1., 1., 1.)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Nodes
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class SliceNode(Node, AutoBlIdName):
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ...
|
||||||
|
|
||||||
|
def add_output(self, bl_idname: str, name: str | None = None):
|
||||||
|
from .registry import get_label_by_name
|
||||||
|
return self.outputs.new(bl_idname, name or get_label_by_name(bl_idname))
|
||||||
|
|
||||||
|
def add_input(self, bl_idname: str, name: str | None = None, multi: bool = False):
|
||||||
|
from .registry import get_label_by_name
|
||||||
|
sock = self.inputs.new(bl_idname, name or get_label_by_name(bl_idname))
|
||||||
|
if multi:
|
||||||
|
sock.link_limit = 0
|
||||||
|
sock.display_shape = 'LIST'
|
||||||
|
return sock
|
||||||
|
|
||||||
|
# Basic Inputs
|
||||||
|
|
||||||
|
class InputNode(SliceNode):
|
||||||
|
input_type: str = ""
|
||||||
|
|
||||||
|
def __init_subclass__(cls, **kwargs):
|
||||||
|
super().__init_subclass__(**kwargs)
|
||||||
|
if "bl_label" not in cls.__dict__: cls.bl_label = cls.input_type
|
||||||
|
|
||||||
|
def init(self, context) -> None:
|
||||||
|
self.add_output(f"SLICESOCK_{self.bl_label}")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs):
|
||||||
|
return self.outputs.get(socket_name).value
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputString(InputNode):
|
||||||
|
input_type = "String"
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputFloat(InputNode):
|
||||||
|
input_type = "Float"
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputInt(InputNode):
|
||||||
|
input_type = "Int"
|
||||||
|
|
||||||
|
# Math
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_IntMath(SliceNode):
|
||||||
|
bl_label = "Integer Math"
|
||||||
|
|
||||||
|
operation: bpy.props.EnumProperty(
|
||||||
|
name="Operation",
|
||||||
|
items=[
|
||||||
|
('ADD', "Add", "Add the two values"),
|
||||||
|
('SUBTRACT', "Subtract", "Subtract the second value from the first"),
|
||||||
|
('MULTIPLY', "Multiply", "Multiply the two values"),
|
||||||
|
('DIVIDE', "Divide", "Integer division"),
|
||||||
|
('ABS', "Absolute", "Absolute value of the first value"),
|
||||||
|
('NEGATE', "Negate", "Negate the first value"),
|
||||||
|
('POWER', "Power", "First value raised to the power of the second"),
|
||||||
|
('MIN', "Minimum", "The smaller of the two values"),
|
||||||
|
('MAX', "Maximum", "The larger of the two values"),
|
||||||
|
('SIGN', "Sign", "The sign of the first value (-1, 0, or 1)"),
|
||||||
|
('DIVIDE_ROUND', "Divide Round", "Division rounded to the nearest integer"),
|
||||||
|
('DIVIDE_FLOOR', "Divide Floor", "Division rounded down"),
|
||||||
|
('DIVIDE_TRUNCATE', "Divide Truncate", "Division rounded towards zero"),
|
||||||
|
('MODULO_FLOORED', "Floored Modulo", "Modulo using floor division"),
|
||||||
|
('MODULO_TRUNCATED', "Truncated Modulo", "Modulo using truncated division"),
|
||||||
|
],
|
||||||
|
default='ADD'
|
||||||
|
) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output("SLICESOCK_IntRo", "Value")
|
||||||
|
|
||||||
|
self.add_input("SLICESOCK_Int", "Value")
|
||||||
|
self.add_input("SLICESOCK_Int", "Value")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "operation", text="")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> int:
|
||||||
|
a: int = _resolve_input(self.inputs[0])
|
||||||
|
b: int = _resolve_input(self.inputs[1])
|
||||||
|
|
||||||
|
op = self.operation
|
||||||
|
try:
|
||||||
|
if op == 'ADD': return a + b
|
||||||
|
if op == 'SUBTRACT': return a - b
|
||||||
|
if op == 'MULTIPLY': return a * b
|
||||||
|
if op == 'DIVIDE': return a // b if b != 0 else 0
|
||||||
|
if op == 'ABS': return abs(a)
|
||||||
|
if op == 'NEGATE': return -a
|
||||||
|
if op == 'POWER': return int(a ** b) if not (a == 0 and b < 0) else 0
|
||||||
|
if op == 'MIN': return min(a, b)
|
||||||
|
if op == 'MAX': return max(a, b)
|
||||||
|
if op == 'SIGN': return (a > 0) - (a < 0)
|
||||||
|
if op == 'DIVIDE_ROUND': return int(round(a / b)) if b != 0 else 0
|
||||||
|
if op == 'DIVIDE_FLOOR': return a // b if b != 0 else 0
|
||||||
|
if op == 'DIVIDE_TRUNCATE': return int(a / b) if b != 0 else 0
|
||||||
|
if op == 'MODULO_FLOORED': return a % b if b != 0 else 0
|
||||||
|
if op == 'MODULO_TRUNCATED': return a - (b * int(a / b)) if b != 0 else 0
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputObject(InputNode):
|
||||||
|
bl_label = "Object"
|
||||||
|
bl_icon = "OBJECT_DATA"
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GetMesh(SliceNode):
|
||||||
|
bl_label = "Get Mesh"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Object")
|
||||||
|
self.add_output("SLICESOCK_Mesh")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject | None:
|
||||||
|
if socket_name != "Mesh": return None
|
||||||
|
|
||||||
|
obj: Any = _resolve_input(self.inputs.get("Object"))
|
||||||
|
|
||||||
|
so = SlicingObject(obj, obj.name)
|
||||||
|
|
||||||
|
return so
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GetPaths(SliceNode):
|
||||||
|
bl_label = "Get Paths"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Object")
|
||||||
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
|
||||||
|
obj: Any = _resolve_input(self.inputs.get("Object"))
|
||||||
|
|
||||||
|
so = capture_mesh_edges(obj)
|
||||||
|
|
||||||
|
return so
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_SliceShells(SliceNode):
|
||||||
|
bl_label = "Slice Shells"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh")
|
||||||
|
self.add_input("SLICESOCK_Float", "Layer Height").value = 0.2
|
||||||
|
self.add_output("SLICESOCK_Slices")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
|
||||||
|
if socket_name != "Slices": return []
|
||||||
|
|
||||||
|
slicingmesh: SlicingObject | None = _resolve_input(self.inputs.get("Mesh"))
|
||||||
|
layer_height: float = _resolve_input(self.inputs.get("Layer Height"))
|
||||||
|
|
||||||
|
if not slicingmesh: return []
|
||||||
|
|
||||||
|
min = slicingmesh.mesh[:,0:3,2:3].min()
|
||||||
|
min = floor(min / layer_height) * layer_height
|
||||||
|
max = slicingmesh.mesh[:,0:3,2:3].max()
|
||||||
|
|
||||||
|
z_values = np.arange(min, max + layer_height, layer_height)
|
||||||
|
|
||||||
|
slices_group: list[list[np.ndarray]] | None = slice_at_range([slicingmesh.mesh], z_values)
|
||||||
|
assert slices_group
|
||||||
|
slices = slices_group[0]
|
||||||
|
|
||||||
|
return slices
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_BufferShells(SliceNode):
|
||||||
|
bl_label = "Buffer Shells"
|
||||||
|
|
||||||
|
shape_option: bpy.props.EnumProperty(
|
||||||
|
name="Miter",
|
||||||
|
items=[
|
||||||
|
('round', "Round", ""),
|
||||||
|
('square', "Square", ""),
|
||||||
|
('flat', "Flat", "")
|
||||||
|
]
|
||||||
|
) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Slices")
|
||||||
|
self.add_input("SLICESOCK_Float", "Distance").value = 0
|
||||||
|
self.add_output("SLICESOCK_Slices")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "shape_option")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
|
||||||
|
if socket_name != "Slices": return []
|
||||||
|
|
||||||
|
slices: list[np.ndarray] = _resolve_input(self.inputs.get("Slices"))
|
||||||
|
distance: float = _resolve_input(self.inputs.get("Distance"))
|
||||||
|
|
||||||
|
buffered_slices = slice.buffer_slice(slices, distance, miter=self.shape_option)
|
||||||
|
|
||||||
|
return buffered_slices
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_SlicesToPaths(SliceNode):
|
||||||
|
bl_label = "Slices to Paths"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Slices")
|
||||||
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
|
||||||
|
slices: list[np.ndarray] = _resolve_input(self.inputs.get("Slices"))
|
||||||
|
return slices
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_PathsToGcode(SliceNode):
|
||||||
|
bl_label = "Paths to Gcode"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths")
|
||||||
|
self.add_output("SLICESOCK_Gcode")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||||
|
paths: list[np.ndarray] = _resolve_input(self.inputs.get("Paths"))
|
||||||
|
pather: Pather = Pather(paths)
|
||||||
|
gcode: str = pather.to_gcode()
|
||||||
|
return gcode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GcodeBlock(SliceNode):
|
||||||
|
bl_label = "Gcode Block"
|
||||||
|
|
||||||
|
gcode: StringProperty(name="Gcode", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output("SLICESOCK_Gcode")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "gcode", text="")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||||
|
return self.gcode
|
||||||
|
|
||||||
|
### Joiners
|
||||||
|
|
||||||
|
class SliceNodeJoiner(SliceNode):
|
||||||
|
socket_type: str = ""
|
||||||
|
output_socket_type: str = ""
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output(self.output_socket_type or self.socket_type)
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
def is_active(input) -> bool:
|
||||||
|
return input.is_linked or getattr(input, 'value', False)
|
||||||
|
|
||||||
|
if len(self.inputs) == 0:
|
||||||
|
self.add_input(self.socket_type)
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_active(self.inputs[-1]):
|
||||||
|
self.add_input(self.socket_type)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if len(self.inputs) < 2: break
|
||||||
|
if not is_active(self.inputs[-1]) and not is_active(self.inputs[-2]):
|
||||||
|
self.inputs.remove(self.inputs[-2])
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_JoinGcode(SliceNodeJoiner):
|
||||||
|
bl_label = "Join Gcode"
|
||||||
|
socket_type = "SLICESOCK_Gcode"
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||||
|
gcode_input: list[str] = []
|
||||||
|
for input in self.inputs:
|
||||||
|
gcode_input += [_resolve_input(input)] or [""]
|
||||||
|
return "\n".join(gcode_input)
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_JoinSlices(SliceNodeJoiner):
|
||||||
|
bl_label = "Join Slices"
|
||||||
|
socket_type = "SLICESOCK_Slices"
|
||||||
|
|
||||||
|
operation: bpy.props.EnumProperty(
|
||||||
|
name="Operation",
|
||||||
|
items=[
|
||||||
|
('UNION', "Union", "Union of the slices"),
|
||||||
|
('INTERSECTION', "Intersection", "Intersection of the slices"),
|
||||||
|
('DIFFERENCE', "Subtract", "Clipping of the first slice"),
|
||||||
|
('XOR', "Exclusive or", "Exclusive or of the slices"),
|
||||||
|
],
|
||||||
|
default='UNION'
|
||||||
|
) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "operation", text="")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
|
||||||
|
accumulated: list[np.ndarray] = []
|
||||||
|
current: list[np.ndarray] = []
|
||||||
|
for input in self.inputs:
|
||||||
|
current =_resolve_input(input)
|
||||||
|
|
||||||
|
if not current:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not accumulated:
|
||||||
|
accumulated = current
|
||||||
|
current = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
accumulated = slice.boolean_slice(accumulated, current, self.operation)
|
||||||
|
current = []
|
||||||
|
|
||||||
|
return accumulated
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_JoinMesh(SliceNodeJoiner):
|
||||||
|
bl_label = "Join Mesh"
|
||||||
|
socket_type = "SLICESOCK_Mesh"
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject:
|
||||||
|
merged_mesh = SlicingObject()
|
||||||
|
|
||||||
|
for input in self.inputs:
|
||||||
|
mesh =_resolve_input(input)
|
||||||
|
if mesh:
|
||||||
|
merged_mesh.mesh = np.concatenate([merged_mesh.mesh, mesh.mesh], axis=0)
|
||||||
|
|
||||||
|
return merged_mesh
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_JoinPaths(SliceNodeJoiner):
|
||||||
|
bl_label = "Join Paths"
|
||||||
|
socket_type = "SLICESOCK_Paths"
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
|
||||||
|
merged_paths = []
|
||||||
|
|
||||||
|
for input in self.inputs:
|
||||||
|
paths = _resolve_input(input)
|
||||||
|
if paths:
|
||||||
|
merged_paths.extend(paths)
|
||||||
|
|
||||||
|
return merged_paths
|
||||||
|
|
||||||
|
### I/O
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_LoadGcode(SliceNode):
|
||||||
|
bl_label = "Load Gcode File"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_String", "Filepath")
|
||||||
|
self.add_output("SLICESOCK_Gcode")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||||
|
path = _resolve_input(self.inputs.get("Filepath"))
|
||||||
|
if not path: return ""
|
||||||
|
abs_path = bpy.path.abspath(path)
|
||||||
|
if os.path.exists(abs_path):
|
||||||
|
with open(abs_path, 'r', encoding='utf-8') as f:
|
||||||
|
return f.read()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
class ExecSliceNode(SliceNode):
|
||||||
|
button_label: str = 'Execute'
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.operator("slicenode.execute", text=self.button_label).node_name = self.name
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GcodeToFile(ExecSliceNode):
|
||||||
|
bl_label = "Gcode To File"
|
||||||
|
|
||||||
|
button_label = "Generate File"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Gcode")
|
||||||
|
self.add_input("SLICESOCK_String", "Filename")
|
||||||
|
|
||||||
|
def evaluate(self):
|
||||||
|
gcode: str = _resolve_input(self.inputs.get("Gcode"))
|
||||||
|
filename: str = _resolve_input(self.inputs.get("Filename"))
|
||||||
|
|
||||||
|
filepath = bpy.path.abspath(filename)
|
||||||
|
os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
|
||||||
|
|
||||||
|
with open(filepath, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(gcode)
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
|
bl_label = "Gcode to Mesh"
|
||||||
|
|
||||||
|
button_label = "Update Debug Mesh"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Gcode")
|
||||||
|
|
||||||
|
def evaluate(self):
|
||||||
|
gcode: str = _resolve_input(self.inputs.get("Gcode"))
|
||||||
|
if not gcode:
|
||||||
|
return 'empty'
|
||||||
|
|
||||||
|
segments = []
|
||||||
|
last_x, last_y, last_z = 0.0, 0.0, 0.0
|
||||||
|
last_e = 0.0
|
||||||
|
|
||||||
|
for line in gcode.splitlines():
|
||||||
|
line = line.split(';')[0].strip()
|
||||||
|
if not line: continue
|
||||||
|
|
||||||
|
parts = line.split()
|
||||||
|
cmd = parts[0].upper()
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
for p in parts[1:]:
|
||||||
|
if len(p) < 2: continue
|
||||||
|
try:
|
||||||
|
# Scale coordinates by 0.001 (mm to Blender units)
|
||||||
|
val = float(p[1:])
|
||||||
|
if p[0].upper() in 'XYZ':
|
||||||
|
val *= 0.001
|
||||||
|
params[p[0].upper()] = val
|
||||||
|
except ValueError: continue
|
||||||
|
|
||||||
|
new_x = params.get('X', last_x)
|
||||||
|
new_y = params.get('Y', last_y)
|
||||||
|
new_z = params.get('Z', last_z)
|
||||||
|
new_e = params.get('E', last_e)
|
||||||
|
|
||||||
|
if cmd == 'G1':
|
||||||
|
# Check if it's an extrusion move
|
||||||
|
# In absolute mode (typical), E must increase.
|
||||||
|
# In relative mode, E must be > 0.
|
||||||
|
# We'll assume absolute mode as per Pather.to_gcode() M82
|
||||||
|
is_extruding = new_e > last_e
|
||||||
|
|
||||||
|
if is_extruding:
|
||||||
|
segments.append(((last_x, last_y, last_z), (new_x, new_y, new_z)))
|
||||||
|
|
||||||
|
last_x, last_y, last_z = new_x, new_y, new_z
|
||||||
|
last_e = new_e
|
||||||
|
|
||||||
|
if not segments:
|
||||||
|
return 'no segments'
|
||||||
|
|
||||||
|
# Create or find "Debug" mesh object
|
||||||
|
mesh_name = "Debug"
|
||||||
|
if mesh_name in bpy.data.objects:
|
||||||
|
obj = bpy.data.objects[mesh_name]
|
||||||
|
mesh = obj.data
|
||||||
|
mesh.clear_geometry()
|
||||||
|
else:
|
||||||
|
mesh = bpy.data.meshes.new(mesh_name)
|
||||||
|
obj = bpy.data.objects.new(mesh_name, mesh)
|
||||||
|
assert bpy.context.collection
|
||||||
|
bpy.context.collection.objects.link(obj)
|
||||||
|
|
||||||
|
vertices = []
|
||||||
|
edges = []
|
||||||
|
for i, (p1, p2) in enumerate(segments):
|
||||||
|
vertices.extend([p1, p2])
|
||||||
|
edges.append((2*i, 2*i + 1))
|
||||||
|
|
||||||
|
mesh.from_pydata(vertices, edges, [])
|
||||||
|
mesh.update()
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_Test(ExecSliceNode):
|
||||||
|
bl_label = "Test"
|
||||||
|
|
||||||
|
button_label = "Test"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Gcode")
|
||||||
|
|
||||||
|
def evaluate(self):
|
||||||
|
_ = _resolve_input(self.inputs.get("Gcode"))
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Operator
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_OT_Execute(Operator):
|
||||||
|
bl_idname = "slicenode.execute"
|
||||||
|
bl_label = "Execute"
|
||||||
|
bl_description = "Execute"
|
||||||
|
|
||||||
|
node_name: bpy.props.StringProperty() # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
def execute(self, context) -> set['OperatorReturnItems']:
|
||||||
|
node_tree = context.space_data.edit_tree # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
|
||||||
|
node = node_tree.nodes[self.node_name]
|
||||||
|
_ = node.evaluate()
|
||||||
|
|
||||||
|
self.report({"INFO"}, f"Execution Successful")
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Menu
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_MT_Menu(Menu):
|
||||||
|
bl_idname = "SLICENODE_MT_Menu"
|
||||||
|
bl_label = "SliceNodes"
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
assert layout
|
||||||
|
layout.operator("node.add_node", text="String").type = "SLICENODE_InputString"
|
||||||
|
layout.operator("node.add_node", text="Float").type = "SLICENODE_InputFloat"
|
||||||
|
layout.operator("node.add_node", text="Integer").type = "SLICENODE_InputInt"
|
||||||
|
layout.operator("node.add_node", text="Integer Math").type = "SLICENODE_IntMath"
|
||||||
|
layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject"
|
||||||
|
layout.operator("node.add_node", text="Test").type = "SLICENODE_Test"
|
||||||
|
layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh"
|
||||||
|
layout.operator("node.add_node", text="Get Paths").type = "SLICENODE_GetPaths"
|
||||||
|
layout.separator()
|
||||||
|
layout.operator("node.add_node", text="Slice Shells").type = "SLICENODE_SliceShells"
|
||||||
|
layout.operator("node.add_node", text="Buffer Shells").type = "SLICENODE_BufferShells"
|
||||||
|
layout.operator("node.add_node", text="Slices to Paths").type = "SLICENODE_SlicesToPaths"
|
||||||
|
layout.operator("node.add_node", text="Paths to Gcode").type = "SLICENODE_PathsToGcode"
|
||||||
|
layout.separator()
|
||||||
|
layout.operator("node.add_node", text="Gcode Block").type = "SLICENODE_GcodeBlock"
|
||||||
|
layout.operator("node.add_node", text="Load Gcode").type = "SLICENODE_LoadGcode"
|
||||||
|
layout.separator()
|
||||||
|
layout.operator("node.add_node", text="Join Gcode").type = "SLICENODE_JoinGcode"
|
||||||
|
layout.operator("node.add_node", text="Join Mesh").type = "SLICENODE_JoinMesh"
|
||||||
|
layout.operator("node.add_node", text="Join Paths").type = "SLICENODE_JoinPaths"
|
||||||
|
layout.operator("node.add_node", text="Join Slices").type = "SLICENODE_JoinSlices"
|
||||||
|
layout.separator()
|
||||||
|
layout.operator("node.add_node", text="Gcode to File").type = "SLICENODE_GcodeToFile"
|
||||||
|
layout.operator("node.add_node", text="Gcode to Mesh").type = "SLICENODE_GcodeToMesh"
|
||||||
|
|
||||||
|
def draw_add_menu(self, context):
|
||||||
|
space = context.space_data
|
||||||
|
tree = getattr(space, "edit_tree", None)
|
||||||
|
if tree and tree.bl_idname == "SLICETREE_NodeTree":
|
||||||
|
self.layout.separator()
|
||||||
|
self.layout.menu("SLICENODE_MT_Menu")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Registration
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Load collected modules
|
||||||
|
modules = registry.get()
|
||||||
|
|
||||||
|
def register():
|
||||||
|
registry.blender_register_classes()
|
||||||
|
bpy.types.NODE_MT_add.append(draw_add_menu)
|
||||||
|
|
||||||
|
print('Nodeslicer Loaded')
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.types.NODE_MT_add.remove(draw_add_menu)
|
||||||
|
registry.blender_unregister_classes()
|
||||||
|
|
||||||
|
print('Nodeslicer Unloaded')
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
register()
|
||||||
71
blender_manifest.toml
Normal file
71
blender_manifest.toml
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
|
id = "nodeslicer"
|
||||||
|
version = "0.0.1"
|
||||||
|
name = "Nodeslicer"
|
||||||
|
tagline = "This is another extension"
|
||||||
|
maintainer = "Nicolas Predella"
|
||||||
|
type = "add-on"
|
||||||
|
|
||||||
|
tags = ["Node"]
|
||||||
|
|
||||||
|
blender_version_min = "4.2.0"
|
||||||
|
|
||||||
|
license = [
|
||||||
|
"SPDX:GPL-3.0-or-later",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Optional: required by some licenses.
|
||||||
|
# copyright = [
|
||||||
|
# "2002-2024 Developer Name",
|
||||||
|
# "1998 Company Name",
|
||||||
|
# ]
|
||||||
|
|
||||||
|
platforms = [
|
||||||
|
"windows-x64",
|
||||||
|
"macos-arm64",
|
||||||
|
"macos-x64",
|
||||||
|
"linux-x64",
|
||||||
|
]
|
||||||
|
|
||||||
|
wheels = [
|
||||||
|
"./wheels/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl",
|
||||||
|
"./wheels/pyclipper-1.4.0-cp311-cp311-win_amd64.whl",
|
||||||
|
"./wheels/pyclipper-1.4.0-cp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
|
||||||
|
"./wheels/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl",
|
||||||
|
"./wheels/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl",
|
||||||
|
"./wheels/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
|
||||||
|
"./wheels/shapely-2.1.2-cp311-cp311-win_amd64.whl",
|
||||||
|
|
||||||
|
"./wheels/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl",
|
||||||
|
"./wheels/pyclipper-1.4.0-cp313-cp313-win_amd64.whl",
|
||||||
|
"./wheels/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
|
||||||
|
"./wheels/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl",
|
||||||
|
"./wheels/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl",
|
||||||
|
"./wheels/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
|
||||||
|
"./wheels/shapely-2.1.2-cp313-cp313-win_amd64.whl",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Optional: add-ons can list which resources they will require:
|
||||||
|
# * files (for access of any filesystem operations)
|
||||||
|
# * network (for internet access)
|
||||||
|
# * clipboard (to read and/or write the system clipboard)
|
||||||
|
# * camera (to capture photos and videos)
|
||||||
|
# * microphone (to capture audio)
|
||||||
|
#
|
||||||
|
# If using network, remember to also check `bpy.app.online_access`
|
||||||
|
# https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
|
||||||
|
#
|
||||||
|
# For each permission it is important to also specify the reason why it is required.
|
||||||
|
# Keep this a single short sentence without a period (.) at the end.
|
||||||
|
# For longer explanations use the documentation or detail page.
|
||||||
|
#
|
||||||
|
[permissions]
|
||||||
|
files = "Import/export GCODE from/to disk"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
paths_exclude_pattern = [
|
||||||
|
"__pycache__/",
|
||||||
|
"/.git/",
|
||||||
|
"/*.zip",
|
||||||
|
]
|
||||||
8
core/geometry.py
Normal file
8
core/geometry.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import numpy as np
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
def crc32_array(a: np.ndarray) -> int:
|
||||||
|
if not a.size: return 0
|
||||||
|
ac = np.ascontiguousarray(a)
|
||||||
|
mv = memoryview(ac.data).cast("B")
|
||||||
|
return zlib.adler32(mv)
|
||||||
375
core/slice.py
Normal file
375
core/slice.py
Normal file
|
|
@ -0,0 +1,375 @@
|
||||||
|
from numpy import ndarray
|
||||||
|
from typing import Any
|
||||||
|
import numpy as np
|
||||||
|
import pyclipper
|
||||||
|
|
||||||
|
# Scaling factor for pyclipper (it works with integers)
|
||||||
|
SCALE = 1_000_000
|
||||||
|
|
||||||
|
def slice_at_range(models, z_values) -> list[list[np.ndarray]]:
|
||||||
|
if not models:
|
||||||
|
return []
|
||||||
|
|
||||||
|
mmps: list[list[np.ndarray]] = []
|
||||||
|
|
||||||
|
for model in models:
|
||||||
|
model_edges: np.ndarray = edges_from_model(model)
|
||||||
|
layered_edges: np.ndarray = layered_model(model_edges, z_values)
|
||||||
|
sliced_edges: list[np.ndarray] = slice_at_z(layered_edges)
|
||||||
|
|
||||||
|
all_model_paths: list[np.ndarray] = []
|
||||||
|
|
||||||
|
for z, e in zip(z_values, sliced_edges):
|
||||||
|
if e is None or len(e) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
zf = float(z)
|
||||||
|
|
||||||
|
# Build 3D segments directly
|
||||||
|
seg3d = e.copy()
|
||||||
|
seg3d[..., 2] = zf
|
||||||
|
|
||||||
|
# Build geometry for this slice
|
||||||
|
slice_paths = segments_to_slice(seg3d)
|
||||||
|
all_model_paths.extend(slice_paths)
|
||||||
|
|
||||||
|
mmps.append(all_model_paths)
|
||||||
|
return mmps
|
||||||
|
|
||||||
|
def edges_from_model(model): # returns a numpy: (facet, 3, 2, xyz)
|
||||||
|
pairs_indices = np.array([[0, 1], [1, 2], [2, 0]]) # Create an array of indices to select the pairs
|
||||||
|
return model[:, pairs_indices, :] # Use advanced indexing to create the array
|
||||||
|
|
||||||
|
def layered_model(models: np.ndarray, z_values: np.ndarray) -> np.ndarray:
|
||||||
|
models_array: np.ndarray = np.array(models)[None, :] # (1, F, 3, 2, 3)
|
||||||
|
m = len(z_values)
|
||||||
|
# array of zeros with shape (m, 1, 1, 1, 3)
|
||||||
|
z_offsets_full = np.zeros((m, 1, 1, 1, 3))
|
||||||
|
z_offsets_full[..., 2] = z_values[:, None, None, None]
|
||||||
|
|
||||||
|
layered = models_array - z_offsets_full # (m, F, 3, 2, 3)
|
||||||
|
return layered
|
||||||
|
|
||||||
|
def slice_at_z(edges, normal_vector=np.array([0,0,1]), tolerance=1e-15):
|
||||||
|
edges_offs = edges # Shape (m, n, 3, 2, 3)
|
||||||
|
|
||||||
|
# Obtain intersection points
|
||||||
|
line_vector = edges_offs[..., 1, :] - edges_offs[..., 0, :]
|
||||||
|
|
||||||
|
numerator = np.sum(normal_vector * -edges_offs[..., 0, :], axis=-1)
|
||||||
|
denominator = np.sum(normal_vector[np.newaxis, np.newaxis, :] * line_vector, axis=-1)
|
||||||
|
|
||||||
|
# Avoid division by zero
|
||||||
|
with np.errstate(divide='ignore', invalid='ignore'):
|
||||||
|
t = numerator / denominator
|
||||||
|
t[denominator == 0] = np.nan # Assign NaN where there's no valid intersection
|
||||||
|
t[(t < 0) | (t > 1)] = np.nan
|
||||||
|
|
||||||
|
intersections = edges_offs[..., 0, :] + t[..., np.newaxis] * line_vector
|
||||||
|
intersections[np.abs(intersections) < tolerance] = 0 # Zero out small values
|
||||||
|
|
||||||
|
all_segments = []
|
||||||
|
for itx in intersections:
|
||||||
|
mask = ~np.any(np.isnan(itx), axis=-1)
|
||||||
|
filtered_arr = itx[mask]
|
||||||
|
|
||||||
|
segment_count = filtered_arr.shape[-2] // 2
|
||||||
|
segments = filtered_arr.reshape(segment_count, 2, 3)
|
||||||
|
segments = np.around(segments, 6)
|
||||||
|
all_segments.append(segments)
|
||||||
|
|
||||||
|
return all_segments
|
||||||
|
|
||||||
|
def link_segments(segments: np.ndarray, tolerance=1e-6) -> list[np.ndarray]:
|
||||||
|
"""Links segments (N, 2, 3) into continuous paths."""
|
||||||
|
if len(segments) == 0: return []
|
||||||
|
z = segments[0, 0, 2]
|
||||||
|
pts = segments[:, :, :2]
|
||||||
|
|
||||||
|
def to_key(p): return tuple((p / tolerance).round().astype(np.int64))
|
||||||
|
|
||||||
|
adj = {}
|
||||||
|
for i in range(len(segments)):
|
||||||
|
p1, p2 = pts[i, 0], pts[i, 1]
|
||||||
|
k1, k2 = to_key(p1), to_key(p2)
|
||||||
|
if k1 == k2: continue
|
||||||
|
adj.setdefault(k1, []).append((k2, i))
|
||||||
|
adj.setdefault(k2, []).append((k1, i))
|
||||||
|
|
||||||
|
paths = []
|
||||||
|
used = set()
|
||||||
|
for i in range(len(segments)):
|
||||||
|
if i in used: continue
|
||||||
|
p1, p2 = pts[i, 0], pts[i, 1]
|
||||||
|
k1, k2 = to_key(p1), to_key(p2)
|
||||||
|
if k1 == k2: continue
|
||||||
|
|
||||||
|
path_pts = [p1, p2]
|
||||||
|
used.add(i)
|
||||||
|
|
||||||
|
# Forward
|
||||||
|
curr_k = k2
|
||||||
|
while True:
|
||||||
|
found = False
|
||||||
|
for next_k, idx in adj.get(curr_k, []):
|
||||||
|
if idx not in used:
|
||||||
|
pk1, pk2 = to_key(pts[idx, 0]), to_key(pts[idx, 1])
|
||||||
|
other_pt = pts[idx, 1] if pk1 == curr_k else pts[idx, 0]
|
||||||
|
path_pts.append(other_pt)
|
||||||
|
used.add(idx)
|
||||||
|
curr_k = to_key(other_pt)
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found: break
|
||||||
|
|
||||||
|
# Backward
|
||||||
|
curr_k = k1
|
||||||
|
while True:
|
||||||
|
found = False
|
||||||
|
for next_k, idx in adj.get(curr_k, []):
|
||||||
|
if idx not in used:
|
||||||
|
pk1, pk2 = to_key(pts[idx, 0]), to_key(pts[idx, 1])
|
||||||
|
other_pt = pts[idx, 1] if pk1 == curr_k else pts[idx, 0]
|
||||||
|
path_pts.insert(0, other_pt)
|
||||||
|
used.add(idx)
|
||||||
|
curr_k = to_key(other_pt)
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found: break
|
||||||
|
|
||||||
|
res = np.column_stack([np.array(path_pts), np.full(len(path_pts), z)])
|
||||||
|
paths.append(res)
|
||||||
|
return paths
|
||||||
|
|
||||||
|
def segments_to_slice(segments: np.ndarray) -> list[np.ndarray]:
|
||||||
|
|
||||||
|
linked = link_segments(segments)
|
||||||
|
if not linked: return []
|
||||||
|
|
||||||
|
pc = pyclipper.Pyclipper()
|
||||||
|
for p in linked:
|
||||||
|
# Pyclipper expects open paths (last point != first point) for ET_CLOSEDPOLYGON
|
||||||
|
path_pts = p[:, :2]
|
||||||
|
if len(path_pts) > 2 and np.allclose(path_pts[0], path_pts[-1]):
|
||||||
|
path_pts = path_pts[:-1]
|
||||||
|
|
||||||
|
scaled = (path_pts * SCALE).astype(np.int64).tolist()
|
||||||
|
pc.AddPath(scaled, pyclipper.PT_SUBJECT, closed = True)
|
||||||
|
|
||||||
|
# Use EVENODD for robust hole detection.
|
||||||
|
solution = pc.Execute(pyclipper.CT_UNION, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD)
|
||||||
|
|
||||||
|
z = segments[0, 0, 2]
|
||||||
|
res = []
|
||||||
|
for path in solution:
|
||||||
|
if not path: continue
|
||||||
|
fpath = np.array(path) / SCALE
|
||||||
|
fpath_3d = np.column_stack([fpath, np.full(len(fpath), z)])
|
||||||
|
# Close loop by appending the first point for internal consistency
|
||||||
|
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
|
||||||
|
res.append(fpath_3d)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def boolean_slice(subject: list[np.ndarray], object: list[np.ndarray], operation: str = 'UNION') -> list[np.ndarray]:
|
||||||
|
all_z_keys: set[int] = set()
|
||||||
|
subject_dict: dict[int, list[ndarray]] = {}
|
||||||
|
object_dict: dict[int, list[ndarray]] = {}
|
||||||
|
|
||||||
|
for s in subject:
|
||||||
|
z = int(round(s[0, 2] * 1000))
|
||||||
|
all_z_keys.add(z)
|
||||||
|
subject_dict.setdefault(z, []).append(s)
|
||||||
|
|
||||||
|
for s in object:
|
||||||
|
z = int(round(s[0, 2] * 1000))
|
||||||
|
all_z_keys.add(z)
|
||||||
|
object_dict.setdefault(z, []).append(s)
|
||||||
|
|
||||||
|
results: list[np.ndarray] = []
|
||||||
|
|
||||||
|
for k in all_z_keys:
|
||||||
|
subject_slices = subject_dict.get(k)
|
||||||
|
object_slices = object_dict.get(k)
|
||||||
|
|
||||||
|
if subject_slices is None:
|
||||||
|
assert object_slices
|
||||||
|
if operation == 'UNION':
|
||||||
|
for s in object_slices: results.append(s)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if object_slices is None:
|
||||||
|
assert subject_slices
|
||||||
|
if operation == 'INTERSECTION': continue
|
||||||
|
for s in subject_slices: results.append(s)
|
||||||
|
continue
|
||||||
|
|
||||||
|
z = subject_slices[0][0, 2]
|
||||||
|
|
||||||
|
pc = pyclipper.Pyclipper()
|
||||||
|
|
||||||
|
# Subject
|
||||||
|
for s in subject_slices:
|
||||||
|
path_pts = s[:, :2]
|
||||||
|
scaled = (path_pts * SCALE).astype(np.int64).tolist()
|
||||||
|
pc.AddPath(scaled, pyclipper.PT_SUBJECT, True)
|
||||||
|
|
||||||
|
# Object
|
||||||
|
for s in object_slices:
|
||||||
|
path_pts = s[:, :2]
|
||||||
|
scaled = (path_pts * SCALE).astype(np.int64).tolist()
|
||||||
|
pc.AddPath(scaled, pyclipper.PT_CLIP, True)
|
||||||
|
|
||||||
|
# Determine operation type. For now only UNION is supported.
|
||||||
|
op_type = pyclipper.CT_UNION
|
||||||
|
if operation == 'UNION': op_type = pyclipper.CT_UNION
|
||||||
|
elif operation == 'INTERSECTION': op_type = pyclipper.CT_INTERSECTION
|
||||||
|
elif operation == 'DIFFERENCE': op_type = pyclipper.CT_DIFFERENCE
|
||||||
|
elif operation == 'XOR': op_type = pyclipper.CT_XOR
|
||||||
|
|
||||||
|
solution = pc.Execute(op_type, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD)
|
||||||
|
|
||||||
|
for path in solution:
|
||||||
|
if not path: continue
|
||||||
|
fpath = np.array(path) / SCALE
|
||||||
|
fpath_3d = np.column_stack([fpath, np.full(len(fpath), z)])
|
||||||
|
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
|
||||||
|
results.append(fpath_3d)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def buffer_slice(slice_paths: list[np.ndarray], distance: float, miter='round', tolerance=.01) -> list[np.ndarray]:
|
||||||
|
|
||||||
|
if not slice_paths: return []
|
||||||
|
|
||||||
|
# Group paths by Z height to avoid flattening the entire model into 2D
|
||||||
|
groups: dict[int, list[np.ndarray]] = {}
|
||||||
|
for p in slice_paths:
|
||||||
|
z = p[0, 2]
|
||||||
|
# Use scaled integer for stable dictionary grouping of floats
|
||||||
|
z_key = int(round(z * 1000))
|
||||||
|
groups.setdefault(z_key, []).append(p)
|
||||||
|
|
||||||
|
join_types = {
|
||||||
|
'round': pyclipper.JT_ROUND,
|
||||||
|
'square': pyclipper.JT_SQUARE,
|
||||||
|
'flat': pyclipper.JT_SQUARE,
|
||||||
|
'miter': pyclipper.JT_MITER
|
||||||
|
}
|
||||||
|
jt = join_types.get(miter, pyclipper.JT_ROUND)
|
||||||
|
|
||||||
|
res = []
|
||||||
|
pco = pyclipper.PyclipperOffset(arc_tolerance=tolerance * SCALE)
|
||||||
|
|
||||||
|
# Buffer each layer independently
|
||||||
|
for z_key in sorted(groups.keys()):
|
||||||
|
layer_paths = groups[z_key]
|
||||||
|
z = layer_paths[0][0, 2]
|
||||||
|
|
||||||
|
pco.Clear()
|
||||||
|
scaled_paths = []
|
||||||
|
for p in layer_paths:
|
||||||
|
path_pts = p[:, :2]
|
||||||
|
# Strip redundant closing points
|
||||||
|
if len(path_pts) > 2 and np.allclose(path_pts[0], path_pts[-1]):
|
||||||
|
path_pts = path_pts[:-1]
|
||||||
|
|
||||||
|
if len(path_pts) < 3: continue
|
||||||
|
scaled_paths.append((path_pts * SCALE).astype(np.int64).tolist())
|
||||||
|
|
||||||
|
if not scaled_paths: continue
|
||||||
|
|
||||||
|
# Add all paths for this specific Z layer
|
||||||
|
pco.AddPaths(scaled_paths, jt, pyclipper.ET_CLOSEDPOLYGON)
|
||||||
|
solution = pco.Execute(distance * SCALE)
|
||||||
|
|
||||||
|
for path in solution:
|
||||||
|
if not path: continue
|
||||||
|
fpath = np.array(path) / SCALE
|
||||||
|
fpath_3d = np.column_stack([fpath, np.full(len(fpath), z)])
|
||||||
|
# Re-close loop
|
||||||
|
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
|
||||||
|
res.append(fpath_3d)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
def group_segments_by_z(segments: np.ndarray) -> dict[int, np.ndarray]:
|
||||||
|
z_values: np.ndarray = segments[:, 0, 2]
|
||||||
|
unique_z: np.ndarray = np.unique(z_values)
|
||||||
|
groups: dict[int, np.ndarray] = {int(round(z*1000)): segments[z_values == z] for z in unique_z}
|
||||||
|
return groups
|
||||||
|
|
||||||
|
############################################################
|
||||||
|
|
||||||
|
class Pather(object):
|
||||||
|
FEEDRATE: float = 1200. # mm/min
|
||||||
|
EXTRUSION_RATE: float = 0.017 # E per mm of path
|
||||||
|
|
||||||
|
def __init__(self, paths: list[np.ndarray], **kwargs) -> None:
|
||||||
|
self.paths = paths
|
||||||
|
self.bed_center_x: float = 250.
|
||||||
|
self.bed_center_y: float = 250.
|
||||||
|
|
||||||
|
def to_gcode(self) -> str:
|
||||||
|
"""
|
||||||
|
Convert a list of paths (3D numpy arrays) into G-code.
|
||||||
|
"""
|
||||||
|
if not self.paths:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def z_key(path) -> float:
|
||||||
|
return path[0, 2]
|
||||||
|
|
||||||
|
def segment_length_3d(p1, p2) -> float:
|
||||||
|
dx = p2[0] - p1[0]
|
||||||
|
dy = p2[1] - p1[1]
|
||||||
|
dz = p2[2] - p1[2]
|
||||||
|
return (dx * dx + dy * dy + dz * dz) ** 0.5
|
||||||
|
|
||||||
|
# Sort paths by Z height
|
||||||
|
lines = sorted(self.paths, key=z_key)
|
||||||
|
|
||||||
|
gcode: list[str] = [
|
||||||
|
"; Generated from NodeSlicer Paths (via Pyclipper)",
|
||||||
|
f"; FEEDRATE={self.FEEDRATE}",
|
||||||
|
f"; EXTRUSION_RATE={self.EXTRUSION_RATE}",
|
||||||
|
"G21 ; set units to mm",
|
||||||
|
"G90 ; absolute XYZ positioning",
|
||||||
|
"M82 ; absolute extrusion mode",
|
||||||
|
"G92 E0",
|
||||||
|
]
|
||||||
|
|
||||||
|
e = 0.0
|
||||||
|
last_z = -1.0
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
coords = line.tolist()
|
||||||
|
if len(coords) < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
x0, y0, z0 = coords[0]
|
||||||
|
|
||||||
|
# Layer change markers for PrusaSlicer
|
||||||
|
if abs(z0 - last_z) > 0.001:
|
||||||
|
gcode.append(f";LAYER_CHANGE")
|
||||||
|
gcode.append(f";Z:{z0:.3f}")
|
||||||
|
gcode.append(f";BEFORE_LAYER_CHANGE")
|
||||||
|
gcode.append(f"G92 E0") # Reset E on layer change
|
||||||
|
e = 0.0
|
||||||
|
gcode.append(f"G1 Z{z0:.3f} F720")
|
||||||
|
gcode.append(f";AFTER_LAYER_CHANGE")
|
||||||
|
last_z = z0
|
||||||
|
|
||||||
|
# Travel move to start of this path
|
||||||
|
gcode.append(f"G0 X{x0:.3f} Y{y0:.3f} Z{z0:.3f}")
|
||||||
|
gcode.append(";TYPE:External perimeter")
|
||||||
|
|
||||||
|
# Extrude along the line
|
||||||
|
for p1, p2 in zip(coords, coords[1:]):
|
||||||
|
seg_len: float = segment_length_3d(p1, p2)
|
||||||
|
e += seg_len * self.EXTRUSION_RATE
|
||||||
|
x, y, z = p2
|
||||||
|
gcode.append(
|
||||||
|
f"G1 X{x:.3f} Y{y:.3f} Z{z:.3f} E{e:.5f} F{self.FEEDRATE}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(gcode)
|
||||||
51
doc/MODULES.md
Normal file
51
doc/MODULES.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Module Reference
|
||||||
|
|
||||||
|
This file provides a mapping of modules to their primary responsibilities and key symbols.
|
||||||
|
|
||||||
|
## Root
|
||||||
|
|
||||||
|
- `__init__.py`:
|
||||||
|
- **Entry Point**: Extension registration (`register`, `unregister`).
|
||||||
|
- **Node Tree**: `SLICETREE_NodeTree` class.
|
||||||
|
- **Sockets**: Definitions for all `SLICESOCK_*` classes.
|
||||||
|
- **Nodes**: Definitions for all `SLICENODE_*` classes.
|
||||||
|
- **Evaluation Engine**:
|
||||||
|
- `_resolve_input()`: Recursively evaluates linked sockets.
|
||||||
|
- `_resolve_output()`: Computes node outputs with a caching system (`_EvalCache`) to avoid redundant calculations.
|
||||||
|
- `_follow_reroute()`: Handles Blender's Reroute nodes during evaluation.
|
||||||
|
- **Error Handling**: `NodeException` for user-facing evaluation errors.
|
||||||
|
|
||||||
|
- `registry.py`:
|
||||||
|
- **Auto-Registration**: Utility to track classes decorated with `@register_class`.
|
||||||
|
- **Resource Management**: Logic for dynamic icon and timer registration.
|
||||||
|
|
||||||
|
- `blender_manifest.toml`:
|
||||||
|
- Metadata for the Blender Extension platform.
|
||||||
|
- Lists required wheels (Shapely, PyClipper) for different platforms.
|
||||||
|
|
||||||
|
- `pyproject.toml`:
|
||||||
|
- Standard Python project configuration.
|
||||||
|
- Lists development and runtime dependencies.
|
||||||
|
|
||||||
|
## `/core` (Geometry & Slicing)
|
||||||
|
|
||||||
|
- `slice.py`:
|
||||||
|
- `slice_at_range()`: Top-level slicing function that iterates through Z-heights.
|
||||||
|
- `slice_at_z()`: Vectorized plane-mesh intersection using NumPy.
|
||||||
|
- `Pather`: Class that converts toolpaths into G-code strings. Handles layer change comments and extrusion calculations.
|
||||||
|
- `segments_to_slice()`: Utility to convert raw intersection segments into closed loops using Pyclipper and the Even-Odd fill rule.
|
||||||
|
- `link_segments()`: Logic for joining loose segments into continuous paths.
|
||||||
|
- `buffer_slice()`: Offset utility using PyclipperOffset. Groups paths by Z-height to ensure independent buffering of layers.
|
||||||
|
|
||||||
|
- `geometry.py`:
|
||||||
|
- `crc32_array()`: High-performance checksum generation for NumPy arrays, used for internal caching and data validation.
|
||||||
|
|
||||||
|
## `/infra` (Blender Integration)
|
||||||
|
|
||||||
|
- `blender_mesh_capture.py`:
|
||||||
|
- `SlicingObject`: Represents a single mesh as a NumPy triangle array. Computes bounding boxes and checksums.
|
||||||
|
- `SlicingCollection` / `SlicingGroup`: Hierarchical wrappers for managing multiple meshes and their metadata (e.g., Wipe Tower location).
|
||||||
|
- `objects_to_tris()`: Performance-critical function that extracts mesh data from Blender objects into NumPy arrays using `foreach_get`. Handles world-space transformation and unit scaling.
|
||||||
|
|
||||||
|
- `blender_bridge.py`:
|
||||||
|
- **Hierarchy Utilities**: Helpers like `get_all_children()` for traversing Blender's object tree.
|
||||||
68
doc/NODES.md
Normal file
68
doc/NODES.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Node & Socket Reference
|
||||||
|
|
||||||
|
## Node Sockets
|
||||||
|
|
||||||
|
| Socket ID | Type | Underlying Data Structure | Purpose |
|
||||||
|
|-----------|------|---------------------------|---------|
|
||||||
|
| `SLICESOCK_Object` | `bpy.types.Object` | Blender RNA Object | Reference to a Blender Object |
|
||||||
|
| `SLICESOCK_Float` | `float` | Python float | Numeric value |
|
||||||
|
| `SLICESOCK_Int` | `int` | Python int | Integer value |
|
||||||
|
| `SLICESOCK_String` | `str` | Python string | Text/Path value |
|
||||||
|
| `SLICESOCK_Mesh` | `SlicingObject` | NumPy NDArray (Triangles) | Internal mesh data (Transformed/Scaled) |
|
||||||
|
| `SLICESOCK_Slices` | `list[np.ndarray]` | List of closed paths (N, 3) | Closed manifold contours (Inside/Outside defined) |
|
||||||
|
| `SLICESOCK_Paths` | `list[np.ndarray]` | List of paths (N, 3) | Generic toolpaths (No defined interior) |
|
||||||
|
| `SLICESOCK_Gcode` | `str` | Multi-line string | Generated G-code text |
|
||||||
|
|
||||||
|
## Detailed Data Types
|
||||||
|
|
||||||
|
### SlicingObject (`SLICESOCK_Mesh`)
|
||||||
|
The `SlicingObject` class wraps a high-performance NumPy representation of a Blender mesh.
|
||||||
|
- **`.mesh`**: A NumPy `ndarray` of type `float64` with shape `(N, 4, 3)`.
|
||||||
|
- `N`: Number of triangles.
|
||||||
|
- `[i, 0:3, :]`: The three vertices of the $i$-th triangle.
|
||||||
|
- `[i, 3, :]`: The face normal of the $i$-th triangle.
|
||||||
|
- **Coordinates**: Transformed to world space and scaled by the Blender scene unit scale (multiplied by 1000 for mm-standard G-code).
|
||||||
|
|
||||||
|
### Slices (`SLICESOCK_Slices`)
|
||||||
|
A `list[np.ndarray]` (each `(N, 3)`) containing the results of the slicing operation.
|
||||||
|
- **Content**: Guaranteed to be closed loops forming a valid manifold boundary.
|
||||||
|
- **Z-Awareness**: Coordinates are stored as 3D `(X, Y, Z)` to maintain the correct layer height.
|
||||||
|
- **Topology**: Generated and cleaned using `pyclipper`.
|
||||||
|
|
||||||
|
### Paths (`SLICESOCK_Paths`)
|
||||||
|
A `list[np.ndarray]` (each `(N, 3)`) containing generic toolpaths.
|
||||||
|
- **Content**: Can be open or closed segments.
|
||||||
|
- **Z-Awareness**: Coordinates are stored as 3D `(X, Y, Z)`.
|
||||||
|
- **Source**: Typically generated from `Slices` boundaries via the `Slices to Paths` node.
|
||||||
|
|
||||||
|
### Gcode (`SLICESOCK_Gcode`)
|
||||||
|
A standard Python string containing formatted G-code.
|
||||||
|
- **Format**: `G0` for travels, `G1` for extrusions.
|
||||||
|
- **Extrusion**: Calculated as absolute `E` values based on Euclidean distance and a configurable `EXTRUSION_RATE`.
|
||||||
|
|
||||||
|
## Nodes
|
||||||
|
|
||||||
|
### Input Nodes
|
||||||
|
- `SLICENODE_InputFloat`: Single float input.
|
||||||
|
- `SLICENODE_InputInt`: Single integer input.
|
||||||
|
- `SLICENODE_InputString`: Single string/path input.
|
||||||
|
- `SLICENODE_InputObject`: Blender object picker.
|
||||||
|
- `SLICENODE_GcodeBlock`: Manual text area for entering raw G-code.
|
||||||
|
|
||||||
|
### Processing Nodes
|
||||||
|
- `SLICENODE_GetMesh`: Converts a Blender Object into internal `SlicingObject` format.
|
||||||
|
- `SLICENODE_SliceShells`: Slices a `SlicingObject` into 2D `Slices` at a given layer height.
|
||||||
|
- `SLICENODE_BufferShells`: Offsets/buffers 2D `Slices` using a "Distance" parameter. Buffers each Z-layer independently to maintain 3D structure and uses winding rules to correctly handle nested holes.
|
||||||
|
- `SLICENODE_SlicesToPaths`: Extracts the boundaries of `Slices` to create `Paths`.
|
||||||
|
- `SLICENODE_PathsToGcode`: Converts `Paths` into G-code blocks.
|
||||||
|
|
||||||
|
### Joiner Nodes (Variable Inputs)
|
||||||
|
- `SLICENODE_JoinMesh`: Merges multiple `SlicingObject` instances into one.
|
||||||
|
- `SLICENODE_JoinPaths`: Merges multiple paths.
|
||||||
|
- `SLICENODE_JoinGcode`: Concatenates multiple G-code strings with newline separators.
|
||||||
|
|
||||||
|
### Action Nodes (Executable)
|
||||||
|
- `SLICENODE_GcodeToFile`: Writes G-code string to a file on disk.
|
||||||
|
- `SLICENODE_GcodeToMesh`: Decodes G-code and creates a Blender mesh (Edges) for visualization/debugging.
|
||||||
|
- `SLICENODE_LoadGcode`: Reads a G-code file from disk into a string.
|
||||||
|
- `SLICENODE_Test`: Triggers evaluation for debugging purposes.
|
||||||
35
doc/README.md
Normal file
35
doc/README.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# NodeSlicer Technical Overview
|
||||||
|
|
||||||
|
NodeSlicer is a Blender extension designed for high-performance mesh slicing and G-code generation. It provides a node-based interface that abstracts away the complexity of 3D-to-2D geometry conversion, offering a pipeline for 3D printing workflows.
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
1. **Fast Slicing**: Bypasses the Blender CPU-bound Boolean modifiers by using vectorized plane-triangle intersection via NumPy.
|
||||||
|
2. **Robust Geometry**: Leverages Shapely (built on GEOS) for all 2D operations, ensuring topological correctness in offset/buffering and boundary extraction.
|
||||||
|
3. **Extensibility**: A custom Node Tree architecture allows for easy creation of new nodes for custom toolpath strategies or post-processing.
|
||||||
|
4. **Blender Integration**: Deeply integrated into Blender's UI while maintaining a decoupled geometry engine for maximum performance.
|
||||||
|
|
||||||
|
## System Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
UI[Blender Node Editor] --> Eval[Node Evaluation Engine]
|
||||||
|
Eval --> Capture[Mesh Capture - infra/blender_mesh_capture]
|
||||||
|
Capture --> Slice[Slicing Logic - core/slice]
|
||||||
|
Slice --> Gcode[G-code Generation - core/slice]
|
||||||
|
Gcode --> Export[File Export]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Lifecycle
|
||||||
|
|
||||||
|
- **Mesh Data**: Extracted from Blender as `(N, 4, 3)` NumPy arrays representing triangles and face normals.
|
||||||
|
- **2D Contours (Slices)**: Stored as a list of closed paths (NumPy arrays). Coordinates include Z-height. Topology (shells/holes) is managed via Pyclipper using the Even-Odd fill rule.
|
||||||
|
- **Toolpaths (Paths)**: Stored as a list of open or closed paths (NumPy arrays). These represent the actual motion lines of the 3D printer.
|
||||||
|
- **G-code**: Formatted strings that include travel moves (`G0`), extrusions (`G1`), and metadata (layer change comments).
|
||||||
|
|
||||||
|
## Important Files
|
||||||
|
|
||||||
|
- `__init__.py`: Node tree and evaluation engine definition.
|
||||||
|
- `core/slice.py`: Slicing and G-code generation algorithms.
|
||||||
|
- `infra/blender_mesh_capture.py`: High-performance mesh extraction from Blender.
|
||||||
|
- `registry.py`: Central registration of Blender classes.
|
||||||
7
infra/blender_bridge.py
Normal file
7
infra/blender_bridge.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
def get_all_children(obj):
|
||||||
|
children = []
|
||||||
|
for child in obj.children:
|
||||||
|
children += [child] + get_all_children(child)
|
||||||
|
return children
|
||||||
406
infra/blender_mesh_capture.py
Normal file
406
infra/blender_mesh_capture.py
Normal file
|
|
@ -0,0 +1,406 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from bpy.types import FloatAttribute, Object, Mesh
|
||||||
|
import bpy
|
||||||
|
import zlib, struct
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from numpy import dtype, float64
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
|
from ..core.geometry import crc32_array
|
||||||
|
from ..infra.blender_bridge import get_all_children
|
||||||
|
|
||||||
|
TYPES_NAME = 'DEFAULT'
|
||||||
|
|
||||||
|
class MeshCaptureException(Exception): pass
|
||||||
|
|
||||||
|
class SlicingObject():
|
||||||
|
name: str
|
||||||
|
parent: str
|
||||||
|
mesh: NDArray[float64]
|
||||||
|
|
||||||
|
def __init__(self, obj: Object | None = None, parent: str = "") -> None:
|
||||||
|
if not bpy.context.scene: raise Exception('No scene currently open!')
|
||||||
|
|
||||||
|
if obj: self.name = str(obj.name)
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||||
|
assert bpy.context.scene.unit_settings
|
||||||
|
scene_scale: float = bpy.context.scene.unit_settings.scale_length
|
||||||
|
|
||||||
|
if obj:
|
||||||
|
eval_objects = obj.evaluated_get(depsgraph)
|
||||||
|
self.mesh = objects_to_tris([eval_objects], 1000 * scene_scale)
|
||||||
|
else:
|
||||||
|
self.mesh = np.empty((0, 4, 3))
|
||||||
|
|
||||||
|
def offset(self, offset: NDArray):
|
||||||
|
self.mesh += offset
|
||||||
|
|
||||||
|
@property
|
||||||
|
def checksum(self) -> int:
|
||||||
|
import json
|
||||||
|
|
||||||
|
buf = bytearray()
|
||||||
|
buf.extend(struct.pack(">I", crc32_array(self.mesh)))
|
||||||
|
buf.extend(struct.pack(">I", zlib.crc32(self.name.encode("utf-8"))))
|
||||||
|
buf.extend(struct.pack(">I", zlib.crc32(self.parent.encode("utf-8"))))
|
||||||
|
|
||||||
|
return zlib.crc32(buf) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self) -> float: return self.mesh[:, :3, 2].max()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_x(self) -> float: return self.mesh[:, :3, 0].min()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_x(self) -> float: return self.mesh[:, :3, 0].max()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_y(self) -> float: return self.mesh[:, :3, 1].min()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_y(self) -> float: return self.mesh[:, :3, 1].max()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_xy(self) -> NDArray: return np.array([self.min_x, self.min_y, 0.0])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_xy(self) -> NDArray: return np.array([self.max_x, self.max_y, 0.0])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center_xy(self) -> NDArray: return (self.min_xy + self.max_xy) / 2.0
|
||||||
|
|
||||||
|
class SlicingCollection():
|
||||||
|
name: str
|
||||||
|
objects: list[SlicingObject]
|
||||||
|
|
||||||
|
def __init__(self, objs: list[Object], parent: str):
|
||||||
|
self.objects = [SlicingObject(obj, parent) for obj in objs]
|
||||||
|
self.objects = [o for o in self.objects if o.mesh.size]
|
||||||
|
self.name = parent
|
||||||
|
|
||||||
|
def offset(self, offset: NDArray):
|
||||||
|
for so in self.objects: so.offset(offset)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def checksum(self) -> int:
|
||||||
|
return crc32_array(np.array([o.checksum for o in self.objects]))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self) -> float:
|
||||||
|
height_all = [so.height for so in self.objects if np.any(so.mesh)]
|
||||||
|
if not height_all: return 0
|
||||||
|
return max(height_all)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def meshes(self) -> list[NDArray]:
|
||||||
|
return [o.mesh for o in self.objects]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def mesh_lengths_ids(self) -> NDArray:
|
||||||
|
return np.array([len(m) for m in self.meshes])
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def mesh_start_ids(self) -> NDArray:
|
||||||
|
lengths = self.mesh_lengths_ids
|
||||||
|
return np.insert(np.cumsum(lengths)[:-1], 0, 0)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def mesh_end_ids(self) -> NDArray:
|
||||||
|
starts = self.mesh_start_ids
|
||||||
|
lengths = self.mesh_lengths_ids
|
||||||
|
return starts + lengths - 1
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def all_verts(self) -> NDArray:
|
||||||
|
all_verts = np.vstack(self.meshes)[:, :3, :]
|
||||||
|
return all_verts.reshape(-1, 3)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def unique_verts(self) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
verts_list = []
|
||||||
|
idxs = []
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
for mesh in self.meshes:
|
||||||
|
verts = mesh[:, :3, :].reshape(-1, 3)
|
||||||
|
|
||||||
|
void_dtype = np.dtype((np.void, verts.dtype.itemsize * verts.shape[1]))
|
||||||
|
verts_void = verts.view(void_dtype).ravel()
|
||||||
|
|
||||||
|
unique_voids, inv = np.unique(verts_void, return_inverse=True)
|
||||||
|
|
||||||
|
unique_verts = unique_voids.view(verts.dtype).reshape(-1, 3)
|
||||||
|
|
||||||
|
verts_list.append(unique_verts)
|
||||||
|
idxs.append(inv.reshape(-1, 3) + offset)
|
||||||
|
|
||||||
|
offset += unique_verts.shape[0]
|
||||||
|
|
||||||
|
all_verts = np.concatenate(verts_list, axis=0)
|
||||||
|
all_idxs = np.vstack(idxs)
|
||||||
|
return all_verts, all_idxs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_x(self) -> float | None:
|
||||||
|
return min([so.min_x for so in self.objects if np.any(so.mesh)], default=None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_x(self) -> float | None:
|
||||||
|
return max([so.max_x for so in self.objects if np.any(so.mesh)], default=None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_y(self) -> float | None:
|
||||||
|
return min([so.min_y for so in self.objects if np.any(so.mesh)], default=None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_y(self) -> float | None:
|
||||||
|
return max([so.max_y for so in self.objects if np.any(so.mesh)], default=None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_xy(self) -> NDArray | None:
|
||||||
|
if self.min_x is None or self.min_y is None: return None
|
||||||
|
return np.array([self.min_x, self.min_y, 0.0])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_xy(self) -> NDArray | None:
|
||||||
|
if self.max_x is None or self.max_y is None: return None
|
||||||
|
return np.array([self.max_x, self.max_y, 0.0])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center_xy(self) -> NDArray | None:
|
||||||
|
if self.min_xy is None or self.max_xy is None: return None
|
||||||
|
return (self.min_xy + self.max_xy) / 2.0
|
||||||
|
|
||||||
|
class SlicingGroup():
|
||||||
|
|
||||||
|
collections: dict[str, SlicingCollection]
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
wipe_tower_xy: NDArray = np.array([0, 0])
|
||||||
|
wipe_tower_rotation_deg: float = 0
|
||||||
|
|
||||||
|
def __init__(self, selected_objs: list[Object]) -> None:
|
||||||
|
|
||||||
|
family = []
|
||||||
|
for obj in selected_objs:
|
||||||
|
family.append(obj)
|
||||||
|
family.extend(get_all_children(obj))
|
||||||
|
# Remove duplicates
|
||||||
|
family = list(set(family))
|
||||||
|
|
||||||
|
filtered_objs = [o for o in family if getattr(o, TYPES_NAME).object_type != 'Ignore']
|
||||||
|
|
||||||
|
def find_top_parent(o):
|
||||||
|
current = o
|
||||||
|
while current.parent is not None:
|
||||||
|
current = current.parent
|
||||||
|
return current
|
||||||
|
|
||||||
|
parents: dict[str, list[Object]] = {}
|
||||||
|
[parents.setdefault(find_top_parent(o).name, []).append(o) for o in filtered_objs]
|
||||||
|
|
||||||
|
self.collections = {}
|
||||||
|
for k, objects in parents.items():
|
||||||
|
self.collections[k] = SlicingCollection(objects, k)
|
||||||
|
|
||||||
|
self._extract_metadata(family)
|
||||||
|
|
||||||
|
def _extract_metadata(self, objs) -> None:
|
||||||
|
import math
|
||||||
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||||
|
scale_tx = 1000. * bpy.context.scene.unit_settings.scale_length #type: ignore
|
||||||
|
|
||||||
|
self.wipe_tower_xy = self.center_xy[0:2]
|
||||||
|
for obj in objs:
|
||||||
|
eval_obj = obj.evaluated_get(depsgraph)
|
||||||
|
if eval_obj.blendertoprusaslicer.object_type == 'WipeTower':
|
||||||
|
self.wipe_tower_xy = np.array(eval_obj.location[0:2]) * scale_tx
|
||||||
|
self.wipe_tower_rotation_deg = round(eval_obj.rotation_euler[2]*180/math.pi,5)
|
||||||
|
break
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def offset(self, offset: NDArray):
|
||||||
|
for k, so in self.collections.items(): so.offset(offset)
|
||||||
|
self.wipe_tower_xy = self.wipe_tower_xy + offset[0:2]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def checksum(self):
|
||||||
|
buf = bytearray()
|
||||||
|
for key, col in sorted(self.collections.items(), key=lambda kv: kv[0]):
|
||||||
|
key_bytes = key.encode('utf-8')
|
||||||
|
buf.extend(struct.pack(">I", len(key_bytes)))
|
||||||
|
buf.extend(key_bytes)
|
||||||
|
buf.extend(struct.pack(">I", col.checksum))
|
||||||
|
|
||||||
|
buf.extend(struct.pack(">I", crc32_array(self.wipe_tower_xy)))
|
||||||
|
buf.extend(struct.pack(">I", crc32_array(np.array(self.wipe_tower_rotation_deg))))
|
||||||
|
|
||||||
|
return zlib.crc32(buf) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self) -> float: return max([so.height for k, so in self.collections.items() if so.height], default=0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_x(self) -> float: return min([so.min_x for k, so in self.collections.items() if so.min_x], default=0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_x(self) -> float: return max([so.max_x for k, so in self.collections.items() if so.max_x], default=0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_y(self) -> float: return min([so.min_y for k, so in self.collections.items() if so.min_y], default=0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_y(self) -> float: return max([so.max_y for k, so in self.collections.items() if so.max_y], default=0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_xy(self) -> NDArray:
|
||||||
|
if self.min_x is None or self.min_y is None: return np.array([.0, .0, .0])
|
||||||
|
return np.array([self.min_x, self.min_y, 0.0])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_xy(self) -> NDArray:
|
||||||
|
if self.max_x is None or self.max_y is None: return np.array([.0, .0, .0])
|
||||||
|
return np.array([self.max_x, self.max_y, 0.0])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center_xy(self) -> NDArray:
|
||||||
|
if self.min_xy is None or self.max_xy is None: return np.array([.0, .0, .0])
|
||||||
|
return (self.min_xy + self.max_xy) / 2.0
|
||||||
|
|
||||||
|
class TriMesh():
|
||||||
|
mesh: Mesh
|
||||||
|
length_verts: int
|
||||||
|
length_tris: int
|
||||||
|
matrix_world: Any
|
||||||
|
|
||||||
|
def __init__(self, mesh: Mesh, v: int, t: int, m) -> None:
|
||||||
|
self.mesh = mesh
|
||||||
|
self.length_verts = v
|
||||||
|
self.length_tris = t
|
||||||
|
self.matrix_world = m
|
||||||
|
|
||||||
|
def objects_to_tris(objects: list[Object], scale) -> (NDArray[tuple[int, int, int], dtype[float]], NDArray[tuple[int, int], dtype[float]]): # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
tris_count: int = 0
|
||||||
|
meshes: list[TriMesh] = []
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
try:
|
||||||
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||||
|
mesh: Mesh = obj.to_mesh(depsgraph=depsgraph, preserve_all_data_layers=True)
|
||||||
|
except: continue
|
||||||
|
mesh.calc_loop_triangles()
|
||||||
|
|
||||||
|
cur_length = len(mesh.loop_triangles)
|
||||||
|
if cur_length == 0: continue
|
||||||
|
|
||||||
|
tris_count += cur_length
|
||||||
|
|
||||||
|
meshes += [TriMesh(
|
||||||
|
mesh,
|
||||||
|
len(mesh.vertices),
|
||||||
|
len(mesh.loop_triangles),
|
||||||
|
np.array(obj.matrix_world.transposed())
|
||||||
|
)]
|
||||||
|
|
||||||
|
tris_flat: np.ndarray[tuple[int], dtype[np.float64]] = np.empty(tris_count * 4 * 3, dtype=dtype(np.float64))
|
||||||
|
tris: np.ndarray[tuple[int, int, int], dtype[np.float64]] = tris_flat.reshape(-1, 4, 3)
|
||||||
|
|
||||||
|
col_idx = 0
|
||||||
|
for trimesh in meshes:
|
||||||
|
tris_v_i_flat: np.ndarray[tuple[int], dtype[np.int32]] = np.empty(trimesh.length_tris * 3, dtype=dtype(np.int32))
|
||||||
|
tris_v_n_flat: np.ndarray[tuple[int], dtype[np.float64]] = np.empty(trimesh.length_tris * 3, dtype=dtype(np.float64))
|
||||||
|
tris_verts_flat: np.ndarray[tuple[int], dtype[np.float64]] = np.empty(trimesh.length_verts * 3, dtype=dtype(np.float64))
|
||||||
|
|
||||||
|
trimesh.mesh.loop_triangles.foreach_get("vertices", tris_v_i_flat)
|
||||||
|
trimesh.mesh.loop_triangles.foreach_get("normal", tris_v_n_flat)
|
||||||
|
trimesh.mesh.vertices.foreach_get("co", tris_verts_flat)
|
||||||
|
|
||||||
|
tris_v_i: np.ndarray[tuple[int, int], dtype[np.int32]] = tris_v_i_flat.reshape((-1, 3))
|
||||||
|
tris_v_n: np.ndarray[tuple[int, int], dtype[np.float64]] = tris_v_n_flat.reshape((-1, 3))
|
||||||
|
tris_verts: np.ndarray[tuple[int, int], dtype[np.float64]] = tris_verts_flat.reshape((-1, 3))
|
||||||
|
|
||||||
|
homogeneous_verts: NDArray[float64] = np.hstack((tris_verts, np.ones((tris_verts.shape[0], 1)))) # type: ignore
|
||||||
|
tx_verts = homogeneous_verts @ trimesh.matrix_world
|
||||||
|
tx_verts = (tx_verts[:, :3]) * scale
|
||||||
|
|
||||||
|
homogeneous_norm: NDArray[float64] = np.hstack((tris_v_n, np.ones((tris_v_n.shape[0], 1)))) # type: ignore
|
||||||
|
tx_norm = homogeneous_norm @ trimesh.matrix_world.T
|
||||||
|
tx_norm = tx_norm[:, :3]
|
||||||
|
tx_norm = tx_norm / np.linalg.norm(tx_norm, axis=1, keepdims=True)
|
||||||
|
tx_norm = tx_norm[:, np.newaxis, :]
|
||||||
|
|
||||||
|
tx_tris = tx_verts[tris_v_i]
|
||||||
|
|
||||||
|
tris_coords_and_norm: NDArray[float64] = np.concatenate((tx_tris, tx_norm), axis=1)
|
||||||
|
|
||||||
|
tris[col_idx:col_idx + trimesh.length_tris,:] = tris_coords_and_norm
|
||||||
|
|
||||||
|
col_idx += trimesh.length_tris
|
||||||
|
|
||||||
|
return tris
|
||||||
|
|
||||||
|
def capture_mesh_edges(obj: Object) -> list[NDArray[float64]]:
|
||||||
|
try:
|
||||||
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||||
|
mesh: Mesh = obj.to_mesh(depsgraph=depsgraph, preserve_all_data_layers=True)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not mesh or not mesh.vertices:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
assert bpy.context.scene
|
||||||
|
assert bpy.context.scene.unit_settings
|
||||||
|
scene_scale: float = bpy.context.scene.unit_settings.scale_length
|
||||||
|
scale = 1000.0 * scene_scale
|
||||||
|
|
||||||
|
# Get vertices
|
||||||
|
num_verts = len(mesh.vertices)
|
||||||
|
verts_flat = np.empty(num_verts * 3, dtype=np.float64)
|
||||||
|
mesh.vertices.foreach_get("co", verts_flat)
|
||||||
|
verts = verts_flat.reshape(-1, 3)
|
||||||
|
|
||||||
|
# Apply world matrix transformation (consistent with objects_to_tris)
|
||||||
|
mat = np.array(obj.matrix_world.transposed())
|
||||||
|
homogeneous_verts = np.hstack((verts, np.ones((num_verts, 1))))
|
||||||
|
tx_verts = (homogeneous_verts @ mat)[:, :3] * scale
|
||||||
|
|
||||||
|
# Identify connectivity between consecutive indices
|
||||||
|
num_edges = len(mesh.edges)
|
||||||
|
connected = np.zeros(num_verts - 1, dtype=bool)
|
||||||
|
|
||||||
|
if num_edges > 0:
|
||||||
|
edges_flat = np.empty(num_edges * 2, dtype=np.int32)
|
||||||
|
mesh.edges.foreach_get("vertices", edges_flat)
|
||||||
|
edges = edges_flat.reshape(-1, 2)
|
||||||
|
|
||||||
|
v_min = np.min(edges, axis=1)
|
||||||
|
v_max = np.max(edges, axis=1)
|
||||||
|
|
||||||
|
# Check for edges connecting vertex i and i+1
|
||||||
|
mask = (v_max == v_min + 1)
|
||||||
|
consecutive_v_min = v_min[mask]
|
||||||
|
|
||||||
|
# Ensure indices are within bounds for the 'connected' array
|
||||||
|
valid_mask = (consecutive_v_min >= 0) & (consecutive_v_min < num_verts - 1)
|
||||||
|
connected[consecutive_v_min[valid_mask]] = True
|
||||||
|
|
||||||
|
# Split vertices into paths where connectivity is interrupted
|
||||||
|
split_at = np.where(~connected)[0] + 1
|
||||||
|
paths = np.split(tx_verts, split_at)
|
||||||
|
|
||||||
|
return [p for p in paths if p.size > 0]
|
||||||
|
finally:
|
||||||
|
# Compatibility with Blender 2.8+ to avoid memory leaks
|
||||||
|
# We check if the attribute exists as it's not used elsewhere in this file
|
||||||
|
if hasattr(obj, "to_mesh_clear"):
|
||||||
|
obj.to_mesh_clear()
|
||||||
110
infra/blender_slice_nodes_generic.py
Normal file
110
infra/blender_slice_nodes_generic.py
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from bpy.types import NodeSocket
|
||||||
|
|
||||||
|
class NodeException(Exception): ...
|
||||||
|
|
||||||
|
_EvalCache = dict[tuple[int, str], typing.Any]
|
||||||
|
|
||||||
|
class AutoBlIdName:
|
||||||
|
def __init_subclass__(cls, **kwargs):
|
||||||
|
super().__init_subclass__(**kwargs)
|
||||||
|
if "bl_idname" not in cls.__dict__:
|
||||||
|
cls.bl_idname = cls.__name__
|
||||||
|
|
||||||
|
class SliceNodeSocket(NodeSocket, AutoBlIdName):
|
||||||
|
default: typing.Any = None
|
||||||
|
color = (1., 1., 1., 1.)
|
||||||
|
|
||||||
|
def draw_color(self, context, node) -> tuple[float, float, float, float]: return self.color # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
|
||||||
|
## Evaluation helpers
|
||||||
|
|
||||||
|
def _follow_reroute(socket: NodeSocket) -> NodeSocket:
|
||||||
|
current: NodeSocket = socket
|
||||||
|
seen: set[int] = set()
|
||||||
|
|
||||||
|
while getattr(current.node, "bl_idname", "") == "NodeReroute":
|
||||||
|
assert current.node
|
||||||
|
ptr = current.node.as_pointer()
|
||||||
|
if ptr in seen:
|
||||||
|
break
|
||||||
|
seen.add(ptr)
|
||||||
|
|
||||||
|
reroute_input = current.node.inputs[0] if current.node.inputs else None
|
||||||
|
if not reroute_input or not reroute_input.is_linked:
|
||||||
|
raise NodeException("Loose reroute node found: ensure all reroute nodes are correctly linked")
|
||||||
|
|
||||||
|
current = reroute_input.links[0].from_socket
|
||||||
|
|
||||||
|
return current
|
||||||
|
|
||||||
|
def _resolve_input(
|
||||||
|
socket: SliceNodeSocket | None,
|
||||||
|
*,
|
||||||
|
cache: _EvalCache | None = None,
|
||||||
|
visited: set[tuple[int, str]] | None = None,
|
||||||
|
):
|
||||||
|
if socket is None:
|
||||||
|
raise Exception(f"The queried socket is non-existant")
|
||||||
|
|
||||||
|
assert socket.node
|
||||||
|
|
||||||
|
if not socket.is_linked:
|
||||||
|
if v := getattr(socket, "value", None): return v
|
||||||
|
else: return socket.default
|
||||||
|
|
||||||
|
assert socket.is_linked
|
||||||
|
assert socket.links
|
||||||
|
from_socket: NodeSocket = _follow_reroute(socket.links[0].from_socket)
|
||||||
|
|
||||||
|
return _resolve_output(
|
||||||
|
from_socket,
|
||||||
|
default=None,
|
||||||
|
cache=cache,
|
||||||
|
visited=visited,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _resolve_output(
|
||||||
|
socket: NodeSocket,
|
||||||
|
*,
|
||||||
|
default: typing.Any = None,
|
||||||
|
cache: _EvalCache | None = None,
|
||||||
|
visited: set[tuple[int, str]] | None = None,
|
||||||
|
):
|
||||||
|
|
||||||
|
cache = {} if cache is None else cache
|
||||||
|
visited = set() if visited is None else visited
|
||||||
|
|
||||||
|
assert socket.node
|
||||||
|
key = (socket.node.as_pointer(), socket.node.name)
|
||||||
|
if key in cache: return cache[key]
|
||||||
|
if key in visited: return default # cycle guard
|
||||||
|
|
||||||
|
visited.add(key)
|
||||||
|
try:
|
||||||
|
if not isinstance(socket.node, SliceNode):
|
||||||
|
return default
|
||||||
|
|
||||||
|
if getattr(socket.node, "mute", False):
|
||||||
|
# Attempt bypass: find a matching input socket by name or type
|
||||||
|
input_to_bypass = socket.node.inputs.get(socket.name)
|
||||||
|
if not input_to_bypass:
|
||||||
|
for inp in socket.node.inputs:
|
||||||
|
if inp.bl_idname == socket.bl_idname:
|
||||||
|
input_to_bypass = inp
|
||||||
|
break
|
||||||
|
|
||||||
|
if input_to_bypass:
|
||||||
|
value = _resolve_input(typing.cast(Any, input_to_bypass), cache=cache, visited=visited)
|
||||||
|
cache[key] = value
|
||||||
|
return value
|
||||||
|
return default
|
||||||
|
|
||||||
|
assert socket.node.evaluate_output
|
||||||
|
value = socket.node.evaluate_output(socket.name, cache=cache, visited=visited)
|
||||||
|
cache[key] = value
|
||||||
|
return value
|
||||||
|
finally:
|
||||||
|
visited.remove(key)
|
||||||
12
pyproject.toml
Normal file
12
pyproject.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[project]
|
||||||
|
name = "nodeslicer"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"fake-bpy-module>=20260317",
|
||||||
|
"numpy>=2.2.6",
|
||||||
|
"pyclipper>=1.4.0",
|
||||||
|
"shapely>=2.1.2",
|
||||||
|
]
|
||||||
77
registry.py
Normal file
77
registry.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
# registry.py
|
||||||
|
import bpy
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
_bpy_class_registry: list[type] = []
|
||||||
|
_timer_registry: list[Callable[..., int | None]] = []
|
||||||
|
|
||||||
|
# BPY CLASSES
|
||||||
|
def register_class(cls: type) -> type:
|
||||||
|
_bpy_class_registry.append(cls)
|
||||||
|
return cls
|
||||||
|
|
||||||
|
def get() -> list[type]:
|
||||||
|
return list(_bpy_class_registry)
|
||||||
|
|
||||||
|
def get_label_by_name(name: str) -> str | None:
|
||||||
|
for cls in get():
|
||||||
|
if cls.__name__ == name:
|
||||||
|
return getattr(cls, "bl_label", "No Label")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_class_by_name(name: str) -> type | None:
|
||||||
|
for cls in get():
|
||||||
|
if cls.__name__ == name:
|
||||||
|
return cls
|
||||||
|
return None
|
||||||
|
|
||||||
|
def blender_register_classes():
|
||||||
|
for module in get():
|
||||||
|
bpy.utils.register_class(module)
|
||||||
|
|
||||||
|
def blender_unregister_classes():
|
||||||
|
for module in get():
|
||||||
|
bpy.utils.unregister_class(module)
|
||||||
|
|
||||||
|
# TIMERS
|
||||||
|
def register_timer(clb: Callable[..., Any]):
|
||||||
|
_timer_registry.append(clb)
|
||||||
|
return clb
|
||||||
|
|
||||||
|
def get_timers() -> list[Callable[..., int | None]]:
|
||||||
|
return list(_timer_registry)
|
||||||
|
|
||||||
|
def blender_register_timers():
|
||||||
|
for timer in _timer_registry:
|
||||||
|
bpy.app.timers.register(timer, persistent=True)
|
||||||
|
|
||||||
|
def blender_unregister_timers():
|
||||||
|
for timer in _timer_registry:
|
||||||
|
bpy.app.timers.unregister(timer)
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from bpy.utils.previews import ImagePreviewCollection
|
||||||
|
_icons_pcoll: ImagePreviewCollection | None = None
|
||||||
|
|
||||||
|
def blender_register_icons():
|
||||||
|
global _icons_pcoll
|
||||||
|
|
||||||
|
_icons_pcoll = bpy.utils.previews.new()
|
||||||
|
|
||||||
|
my_icons_dir = os.path.join(os.path.dirname(__file__), "icons")
|
||||||
|
for filename in os.listdir(my_icons_dir):
|
||||||
|
_icons_pcoll.load(filename, os.path.join(my_icons_dir, filename), 'IMAGE')
|
||||||
|
|
||||||
|
def blender_unregister_icons():
|
||||||
|
global _icons_pcoll
|
||||||
|
|
||||||
|
if not _icons_pcoll: return
|
||||||
|
bpy.utils.previews.remove(_icons_pcoll)
|
||||||
|
del _icons_pcoll
|
||||||
|
|
||||||
|
def get_icon(icon_id: str) -> int:
|
||||||
|
global _icons_pcoll
|
||||||
|
|
||||||
|
if not _icons_pcoll: return -1
|
||||||
|
return _icons_pcoll[icon_id].icon_id
|
||||||
294
uv.lock
generated
Normal file
294
uv.lock
generated
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.11'",
|
||||||
|
"python_full_version < '3.11'",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fake-bpy-module"
|
||||||
|
version = "20260317"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3e/cf/acb601232f5bc8add9c10d701012c89dd3f84bb1a930c0dadee1b527ae5a/fake_bpy_module-20260317.tar.gz", hash = "sha256:b1d42c212286ea7e8fcaca0068b1f45574faff18d6fa1047aff8afbc58611251", size = 876605, upload-time = "2026-03-17T06:53:23.983Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/73/2f8c20f53ee3a9692cab552d02c7178afd15e1b15bb335b5d90feb943bbe/fake_bpy_module-20260317-py3-none-any.whl", hash = "sha256:399ac3f8479c236a023f632733df6bbb40af8340cc3c3c05b41f3b63bb732127", size = 1013802, upload-time = "2026-03-17T06:53:22.489Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeslicer"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "fake-bpy-module" },
|
||||||
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||||
|
{ name = "pyclipper" },
|
||||||
|
{ name = "shapely" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "fake-bpy-module", specifier = ">=20260317" },
|
||||||
|
{ name = "numpy", specifier = ">=2.2.6" },
|
||||||
|
{ name = "pyclipper", specifier = ">=1.4.0" },
|
||||||
|
{ name = "shapely", specifier = ">=2.1.2" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.2.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version < '3.11'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.4.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.11'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyclipper"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/21/3c06205bb407e1f79b73b7b4dfb3950bd9537c4f625a68ab5cc41177f5bc/pyclipper-1.4.0.tar.gz", hash = "sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1", size = 54489, upload-time = "2025-12-01T13:15:35.015Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/9f/a10173d32ecc2ce19a04d018163f3ca22a04c0c6ad03b464dcd32f9152a8/pyclipper-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bafad70d2679c187120e8c44e1f9a8b06150bad8c0aecf612ad7dfbfa9510f73", size = 264510, upload-time = "2025-12-01T13:14:46.551Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/c2/5490ddc4a1f7ceeaa0258f4266397e720c02db515b2ca5bc69b85676f697/pyclipper-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b74a9dd44b22a7fd35d65fb1ceeba57f3817f34a97a28c3255556362e491447", size = 139498, upload-time = "2025-12-01T13:14:48.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/0a/bea9102d1d75634b1a5702b0e92982451a1eafca73c4845d3dbe27eba13d/pyclipper-1.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a4d2736fb3c42e8eb1d38bf27a720d1015526c11e476bded55138a977c17d9d", size = 970974, upload-time = "2025-12-01T13:14:49.799Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/1b/097f8776d5b3a10eb7b443b632221f4ed825d892e79e05682f4b10a1a59c/pyclipper-1.4.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3b3630051b53ad2564cb079e088b112dd576e3d91038338ad1cc7915e0f14dc", size = 943315, upload-time = "2025-12-01T13:14:51.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/4d/17d6a3f1abf0f368d58f2309e80ee3761afb1fd1342f7780ab32ba4f0b1d/pyclipper-1.4.0-cp310-cp310-win32.whl", hash = "sha256:8d42b07a2f6cfe2d9b87daf345443583f00a14e856927782fde52f3a255e305a", size = 95286, upload-time = "2025-12-01T13:14:52.922Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/ca/b30138427ed122ec9b47980b943164974a2ec606fa3f71597033b9a9f9a6/pyclipper-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:6a97b961f182b92d899ca88c1bb3632faea2e00ce18d07c5f789666ebb021ca4", size = 104227, upload-time = "2025-12-01T13:14:54.013Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/e3/64cf7794319b088c288706087141e53ac259c7959728303276d18adc665d/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:adcb7ca33c5bdc33cd775e8b3eadad54873c802a6d909067a57348bcb96e7a2d", size = 264281, upload-time = "2025-12-01T13:14:55.47Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/cd/44ec0da0306fa4231e76f1c2cb1fa394d7bde8db490a2b24d55b39865f69/pyclipper-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd24849d2b94ec749ceac7c34c9f01010d23b6e9d9216cf2238b8481160e703d", size = 139426, upload-time = "2025-12-01T13:14:56.683Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/88/d8f6c6763ea622fe35e19c75d8b39ed6c55191ddc82d65e06bc46b26cb8e/pyclipper-1.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b6c8d75ba20c6433c9ea8f1a0feb7e4d3ac06a09ad1fd6d571afc1ddf89b869", size = 989649, upload-time = "2025-12-01T13:14:58.28Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/e9/ea7d68c8c4af3842d6515bedcf06418610ad75f111e64c92c1d4785a1513/pyclipper-1.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58e29d7443d7cc0e83ee9daf43927730386629786d00c63b04fe3b53ac01462c", size = 962842, upload-time = "2025-12-01T13:15:00.044Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/b7/0b4a272d8726e51ab05e2b933d8cc47f29757fb8212e38b619e170e6015c/pyclipper-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a8d2b5fb75ebe57e21ce61e79a9131edec2622ff23cc665e4d1d1f201bc1a801", size = 95098, upload-time = "2025-12-01T13:15:01.359Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/76/4901de2919198bb2bd3d989f86d4a1dff363962425bb2d63e24e6c990042/pyclipper-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:e9b973467d9c5fa9bc30bb6ac95f9f4d7c3d9fc25f6cf2d1cc972088e5955c01", size = 104362, upload-time = "2025-12-01T13:15:02.439Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/1b/7a07b68e0842324d46c03e512d8eefa9cb92ba2a792b3b4ebf939dafcac3/pyclipper-1.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140", size = 265676, upload-time = "2025-12-01T13:15:04.15Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/dd/8bd622521c05d04963420ae6664093f154343ed044c53ea260a310c8bb4d/pyclipper-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6", size = 140458, upload-time = "2025-12-01T13:15:05.76Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/06/6e3e241882bf7d6ab23d9c69ba4e85f1ec47397cbbeee948a16cf75e21ed/pyclipper-1.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca", size = 978235, upload-time = "2025-12-01T13:15:06.993Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/f4/3418c1cd5eea640a9fa2501d4bc0b3655fa8d40145d1a4f484b987990a75/pyclipper-1.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872", size = 961388, upload-time = "2025-12-01T13:15:08.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/94/c85401d24be634af529c962dd5d781f3cb62a67cd769534df2cb3feee97a/pyclipper-1.4.0-cp312-cp312-win32.whl", hash = "sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4", size = 95169, upload-time = "2025-12-01T13:15:10.098Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/77/dfea08e3b230b82ee22543c30c35d33d42f846a77f96caf7c504dd54fab1/pyclipper-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037", size = 104619, upload-time = "2025-12-01T13:15:11.592Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/d0/cbce7d47de1e6458f66a4d999b091640134deb8f2c7351eab993b70d2e10/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e", size = 264342, upload-time = "2025-12-01T13:15:12.697Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/cc/742b9d69d96c58ac156947e1b56d0f81cbacbccf869e2ac7229f2f86dc4e/pyclipper-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a", size = 139839, upload-time = "2025-12-01T13:15:13.911Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/48/dd301d62c1529efdd721b47b9e5fb52120fcdac5f4d3405cfc0d2f391414/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f", size = 972142, upload-time = "2025-12-01T13:15:15.477Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/bf/d493fd1b33bb090fa64e28c1009374d5d72fa705f9331cd56517c35e381e/pyclipper-1.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1", size = 952789, upload-time = "2025-12-01T13:15:17.042Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/88/b95ea8ea21ddca34aa14b123226a81526dd2faaa993f9aabd3ed21231604/pyclipper-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c", size = 94817, upload-time = "2025-12-01T13:15:18.724Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/42/0a1920d276a0e1ca21dc0d13ee9e3ba10a9a8aa3abac76cd5e5a9f503306/pyclipper-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832", size = 104007, upload-time = "2025-12-01T13:15:19.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/20/04d58c70f3ccd404f179f8dd81d16722a05a3bf1ab61445ee64e8218c1f8/pyclipper-1.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c", size = 265167, upload-time = "2025-12-01T13:15:20.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/2e/a570c1abe69b7260ca0caab4236ce6ea3661193ebf8d1bd7f78ccce537a5/pyclipper-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e", size = 139966, upload-time = "2025-12-01T13:15:22.036Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/3b/e0859e54adabdde8a24a29d3f525ebb31c71ddf2e8d93edce83a3c212ffc/pyclipper-1.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9", size = 968216, upload-time = "2025-12-01T13:15:23.18Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/6b/e3c4febf0a35ae643ee579b09988dd931602b5bf311020535fd9e5b7e715/pyclipper-1.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39", size = 954198, upload-time = "2025-12-01T13:15:24.522Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/74/728efcee02e12acb486ce9d56fa037120c9bf5b77c54bbdbaa441c14a9d9/pyclipper-1.4.0-cp314-cp314-win32.whl", hash = "sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b", size = 96951, upload-time = "2025-12-01T13:15:25.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/d7/7f4354e69f10a917e5c7d5d72a499ef2e10945312f5e72c414a0a08d2ae4/pyclipper-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826", size = 106782, upload-time = "2025-12-01T13:15:26.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/60/fc32c7a3d7f61a970511ec2857ecd09693d8ac80d560ee7b8e67a6d268c9/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303", size = 269880, upload-time = "2025-12-01T13:15:28.117Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/df/c4a72d3f62f0ba03ec440c4fff56cd2d674a4334d23c5064cbf41c9583f6/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9", size = 141706, upload-time = "2025-12-01T13:15:30.134Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/0b/cf55df03e2175e1e2da9db585241401e0bc98f76bee3791bed39d0313449/pyclipper-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286", size = 105308, upload-time = "2025-12-01T13:15:31.225Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608, upload-time = "2025-12-01T13:15:32.69Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/59/81050abdc9e5b90ffc2c765738c5e40e9abd8e44864aaa737b600f16c562/pyclipper-1.4.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98b2a40f98e1fc1b29e8a6094072e7e0c7dfe901e573bf6cfc6eb7ce84a7ae87", size = 126495, upload-time = "2025-12-01T13:15:33.743Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shapely"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/89/c3548aa9b9812a5d143986764dededfa48d817714e947398bdda87c77a72/shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f", size = 1825959, upload-time = "2025-09-24T13:50:00.682Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/8a/7ebc947080442edd614ceebe0ce2cdbd00c25e832c240e1d1de61d0e6b38/shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea", size = 1629196, upload-time = "2025-09-24T13:50:03.447Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/86/c9c27881c20d00fc409e7e059de569d5ed0abfcec9c49548b124ebddea51/shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f", size = 2951065, upload-time = "2025-09-24T13:50:05.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/8a/0ab1f7433a2a85d9e9aea5b1fbb333f3b09b309e7817309250b4b7b2cc7a/shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142", size = 3058666, upload-time = "2025-09-24T13:50:06.872Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/c6/5a30ffac9c4f3ffd5b7113a7f5299ccec4713acd5ee44039778a7698224e/shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4", size = 3966905, upload-time = "2025-09-24T13:50:09.417Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/72/e92f3035ba43e53959007f928315a68fbcf2eeb4e5ededb6f0dc7ff1ecc3/shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0", size = 4129260, upload-time = "2025-09-24T13:50:11.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/24/605901b73a3d9f65fa958e63c9211f4be23d584da8a1a7487382fac7fdc5/shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e", size = 1544301, upload-time = "2025-09-24T13:50:12.521Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/89/6db795b8dd3919851856bd2ddd13ce434a748072f6fdee42ff30cbd3afa3/shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f", size = 1722074, upload-time = "2025-09-24T13:50:13.909Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" },
|
||||||
|
]
|
||||||
BIN
wheels/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl
Normal file
BIN
wheels/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl
Normal file
Binary file not shown.
BIN
wheels/pyclipper-1.4.0-cp311-cp311-win_amd64.whl
Normal file
BIN
wheels/pyclipper-1.4.0-cp311-cp311-win_amd64.whl
Normal file
Binary file not shown.
Binary file not shown.
BIN
wheels/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl
Normal file
BIN
wheels/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl
Normal file
Binary file not shown.
Binary file not shown.
BIN
wheels/pyclipper-1.4.0-cp313-cp313-win_amd64.whl
Normal file
BIN
wheels/pyclipper-1.4.0-cp313-cp313-win_amd64.whl
Normal file
Binary file not shown.
BIN
wheels/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl
Normal file
BIN
wheels/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl
Normal file
Binary file not shown.
BIN
wheels/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl
Normal file
BIN
wheels/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl
Normal file
Binary file not shown.
Binary file not shown.
BIN
wheels/shapely-2.1.2-cp311-cp311-win_amd64.whl
Normal file
BIN
wheels/shapely-2.1.2-cp311-cp311-win_amd64.whl
Normal file
Binary file not shown.
BIN
wheels/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl
Normal file
BIN
wheels/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl
Normal file
Binary file not shown.
BIN
wheels/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl
Normal file
BIN
wheels/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl
Normal file
Binary file not shown.
Binary file not shown.
BIN
wheels/shapely-2.1.2-cp313-cp313-win_amd64.whl
Normal file
BIN
wheels/shapely-2.1.2-cp313-cp313-win_amd64.whl
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue