From 31d34577e74123f2c21571f7735c767177d0dcc6 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Wed, 26 Aug 2026 12:54:30 +0300 Subject: [PATCH] up --- core/nodeslicer/nodes/mesh.py | 14 +-- core/nodeslicer/tree.py | 40 +++++++-- core/pdf_exporter/writer.py | 8 +- core/slice.py | 2 +- doc/NODES.md | 124 +++++++++++++++----------- infra/blender/blender_mesh_capture.py | 6 +- tests/test_pdf_exporter.py | 8 +- tests/test_wipe_tower.py | 4 +- 8 files changed, 123 insertions(+), 83 deletions(-) diff --git a/core/nodeslicer/nodes/mesh.py b/core/nodeslicer/nodes/mesh.py index fa31780..86a5934 100644 --- a/core/nodeslicer/nodes/mesh.py +++ b/core/nodeslicer/nodes/mesh.py @@ -251,7 +251,7 @@ class SLICENODE_SeparateMeshByRange(SliceNode): so_new.mesh = so.mesh[mask] so_new.transforms = [dict(t) for t in getattr(so, "transforms", [])] so_new.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties() - so_new.object_properties = list(getattr(so, "object_properties", [])) + so_new.pdf_object_properties = list(getattr(so, "pdf_object_properties", [])) so_new.modifiers = so.modifiers.copy() if so.modifiers else [] for k, v in so.attributes.items(): so_new.attributes[k] = { @@ -436,7 +436,7 @@ def _copy_slicing_object(so: SlicingObject) -> SlicingObject: new_so.extruder = so.extruder new_so.transforms = [dict(t) for t in getattr(so, "transforms", [])] new_so.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties() - new_so.object_properties = list(getattr(so, "object_properties", [])) + new_so.pdf_object_properties = list(getattr(so, "pdf_object_properties", [])) return new_so def _copy_slicing_collection(sc: SlicingCollection) -> SlicingCollection: @@ -778,7 +778,7 @@ def _assign_properties_to_item(geom: Any, props: list[tuple[str, str]]) -> Any: return None elif isinstance(geom, SlicePaths): copied = geom.copy_with(paths=list(geom)) - copied.object_properties = resolve_object_properties(copied, props) + copied.pdf_object_properties = resolve_object_properties(copied, props) return copied elif isinstance(geom, (list, tuple)): return [_assign_properties_to_item(item, props) for item in geom] @@ -786,23 +786,23 @@ def _assign_properties_to_item(geom: Any, props: list[tuple[str, str]]) -> Any: copied = _copy_slicing_group(geom) for coll in copied.collections.values(): for obj in coll.objects: - obj.object_properties = resolve_object_properties(obj, props) + obj.pdf_object_properties = resolve_object_properties(obj, props) return copied elif isinstance(geom, SlicingCollection): copied = _copy_slicing_collection(geom) for obj in copied.objects: - obj.object_properties = resolve_object_properties(obj, props) + obj.pdf_object_properties = resolve_object_properties(obj, props) return copied elif isinstance(geom, SlicingObject): copied = _copy_slicing_object(geom) - copied.object_properties = resolve_object_properties(copied, props) + copied.pdf_object_properties = resolve_object_properties(copied, props) return copied return geom @register_class class SLICENODE_AssignPropertyToObject(SliceNode): - bl_label = "Assign Property to Object" + bl_label = "Assign PDF Object Properties" def init(self, context): self.add_input("SLICESOCK_Mesh", "Geometry") diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index 33ecccb..af5c275 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -497,9 +497,6 @@ class SLICENODE_MT_DataInput(Menu): layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject" layout.operator("node.add_node", text="Collection Objects").type = "SLICENODE_CollectionObjects" layout.operator("node.add_node", text="Settings").type = "SLICENODE_InputSettings" - layout.operator("node.add_node", text="Wipe Tower").type = "SLICENODE_WipeTower" - layout.operator("node.add_node", text="Offset Object").type = "SLICENODE_OffsetObject" - layout.operator("node.add_node", text="PrusaSlicer Config").type = "SLICENODE_PrusaslicerConf" layout.separator() layout.operator("node.add_node", text="Printer").type = "SLICENODE_InputPrinter" layout.operator("node.add_node", text="Printer List").type = "SLICENODE_PrinterList" @@ -515,9 +512,6 @@ class SLICENODE_MT_Geometry(Menu): assert layout layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh" layout.operator("node.add_node", text="Rename Mesh").type = "SLICENODE_RenameMesh" - layout.operator("node.add_node", text="Assign PDF Properties").type = "SLICENODE_AssignPdfProperties" - layout.operator("node.add_node", text="Assign Property to Object").type = "SLICENODE_AssignPropertyToObject" - layout.operator("node.add_node", text="Assign Modifiers").type = "SLICENODE_AssignModifiers" layout.operator("node.add_node", text="Transform Geometry").type = "SLICENODE_TransformGeometry" layout.operator("node.add_node", text="Drop Geometry").type = "SLICENODE_DropGeometry" layout.operator("node.add_node", text="Separate Mesh").type = "SLICENODE_SeparateMesh" @@ -541,6 +535,35 @@ class SLICENODE_MT_Geometry(Menu): layout.operator("node.add_node", text="Math").type = "SLICENODE_FloatMath" layout.operator("node.add_node", text="Integer Math").type = "SLICENODE_IntMath" +@register_class +class SLICENODE_MT_PrusaSlicer(Menu): + bl_idname = "SLICENODE_MT_PrusaSlicer" + bl_label = "PrusaSlicer" + + def draw(self, context): + layout = self.layout + assert layout + layout.operator("node.add_node", text="PrusaSlicer Config").type = "SLICENODE_PrusaslicerConf" + layout.operator("node.add_node", text="Assign Modifiers").type = "SLICENODE_AssignModifiers" + layout.operator("node.add_node", text="Wipe Tower").type = "SLICENODE_WipeTower" + layout.operator("node.add_node", text="Offset Object").type = "SLICENODE_OffsetObject" + layout.separator() + layout.operator("node.add_node", text="PrusaSlicer Slice").type = "SLICENODE_PrusaslicerSlice" + layout.operator("node.add_node", text="PrusaSlicer Preview").type = "SLICENODE_PrusaslicerPreview" + +@register_class +class SLICENODE_MT_PdfExporter(Menu): + bl_idname = "SLICENODE_MT_PdfExporter" + bl_label = "PDF Exporter" + + def draw(self, context): + layout = self.layout + assert layout + layout.operator("node.add_node", text="Assign PDF Properties").type = "SLICENODE_AssignPdfProperties" + layout.operator("node.add_node", text="Assign PDF Object Properties").type = "SLICENODE_AssignPropertyToObject" + layout.separator() + layout.operator("node.add_node", text="Mesh to PDF").type = "SLICENODE_MeshToPdf" + @register_class class SLICENODE_MT_Gcode(Menu): bl_idname = "SLICENODE_MT_Gcode" @@ -580,7 +603,6 @@ class SLICENODE_MT_ActionIO(Menu): layout = self.layout assert layout layout.operator("node.add_node", text="Gcode to File").type = "SLICENODE_GcodeToFile" - layout.operator("node.add_node", text="Mesh to PDF").type = "SLICENODE_MeshToPdf" layout.operator("node.add_node", text="Gcode to Text Block").type = "SLICENODE_GcodeToTextBlock" layout.operator("node.add_node", text="Start Print").type = "SLICENODE_StartPrint" layout.separator() @@ -588,8 +610,6 @@ class SLICENODE_MT_ActionIO(Menu): layout.operator("node.add_node", text="Airtable Submit").type = "SLICENODE_AirtableSubmit" layout.separator() layout.operator("node.add_node", text="Gcode to Preview Mesh").type = "SLICENODE_GcodeToMesh" - layout.operator("node.add_node", text="PrusaSlicer Preview").type = "SLICENODE_PrusaslicerPreview" - layout.operator("node.add_node", text="PrusaSlicer Slice").type = "SLICENODE_PrusaslicerSlice" layout.operator("node.add_node", text="Executor").type = "SLICENODE_Executor" layout.operator("node.add_node", text="Test").type = "SLICENODE_Test" @@ -620,6 +640,8 @@ class SLICENODE_MT_Menu(Menu): assert layout layout.menu("SLICENODE_MT_DataInput") layout.menu("SLICENODE_MT_Geometry") + layout.menu("SLICENODE_MT_PrusaSlicer") + layout.menu("SLICENODE_MT_PdfExporter") layout.menu("SLICENODE_MT_Gcode") layout.menu("SLICENODE_MT_Joiners") layout.menu("SLICENODE_MT_ActionIO") diff --git a/core/pdf_exporter/writer.py b/core/pdf_exporter/writer.py index 5e7fe50..7b7aa28 100644 --- a/core/pdf_exporter/writer.py +++ b/core/pdf_exporter/writer.py @@ -171,7 +171,7 @@ def render_object_properties_table( """ objs_with_props = [ so for so in slicing_objs - if getattr(so, "object_properties", None) is not None and len(getattr(so, "object_properties", [])) > 0 + if getattr(so, "pdf_object_properties", getattr(so, "object_properties", None)) is not None and len(getattr(so, "pdf_object_properties", getattr(so, "object_properties", []))) > 0 ] if not objs_with_props: return @@ -203,7 +203,7 @@ def render_object_properties_table( label_color = (0.35, 0.35, 0.35) for so in objs_with_props: - raw_props = getattr(so, "object_properties", []) + raw_props = getattr(so, "pdf_object_properties", getattr(so, "object_properties", [])) props = resolve_object_properties(so, raw_props) if not props: continue @@ -999,7 +999,7 @@ class export_mesh_to_pdf: objs_with_props = [ so for so in self.slicing_objs - if getattr(so, "object_properties", None) is not None and len(getattr(so, "object_properties", [])) > 0 + if getattr(so, "pdf_object_properties", getattr(so, "object_properties", None)) is not None and len(getattr(so, "pdf_object_properties", getattr(so, "object_properties", []))) > 0 ] w_table_mm = 0.0 @@ -1015,7 +1015,7 @@ class export_mesh_to_pdf: if objs_with_props: w_table_mm = (self.font_size_pt / MM_TO_PT) * 25.0 h_props_total_mm = sum( - (1 + len(getattr(so, "object_properties", []))) * cell_height_mm + (1 + len(getattr(so, "pdf_object_properties", getattr(so, "object_properties", [])))) * cell_height_mm for so in objs_with_props ) + len(objs_with_props) * gap_mm if h_table_col_mm > 0.0: diff --git a/core/slice.py b/core/slice.py index b4f65fa..41fb74e 100644 --- a/core/slice.py +++ b/core/slice.py @@ -165,7 +165,7 @@ class SlicePaths(list[np.ndarray]): layer_height: float first_layer_height: float | None transforms: list[dict[str, Any]] - object_properties: list[tuple[str, str]] + pdf_object_properties: list[tuple[str, str]] pdf_properties: PdfProperties name: str objects_info: dict[str, Any] | None diff --git a/doc/NODES.md b/doc/NODES.md index 267d1d7..0de7c56 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -43,41 +43,21 @@ A standard Python string containing formatted G-code. All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** category. -### Data & Input +#### Data & Input - **Object**: Blender object picker. - **Float**: Single float input. - **Integer**: Single integer input. - **String**: Single string/path input. - **Settings**: Read-only/write configuration settings dictionary. -- **Wipe Tower**: Configures the Wipe Tower printbed position (`wipe_tower_position`) passed to 3MF metadata (`Prusa_Slicer_wipe_tower_information.xml`) without injecting into profile INI settings. - - **Position X**: X coordinate of the wipe tower on the printbed. - - **Position Y**: Y coordinate of the wipe tower on the printbed. -- **Offset Object**: Configures the object placement transform offset (`offset_object`) applied to `` inside 3MF model build data (`3D/3dmodel.model`) without injecting into profile INI settings. - - **Offset X**: X translation offset for the build item. - - **Offset Y**: Y translation offset for the build item. - - **Offset Z**: Z translation offset for the build item. +- **Printer**: Printer name picker. +- **Printer List**: List of configured printers. - **Printer Intrinsics**: Configures bed size, extrusion/travel rates, retraction distance & feedrate, and Z-lift. -- **Gcode Block**: Manual text area for entering raw G-code. ### Geometry Processing - **Get Mesh**: Converts a Blender Object into internal `SlicingObject` format. -- **Separate Mesh**: Splits a `SlicingCollection` into two collections (`Selection` and `Inverted`) by comparing a specified mesh attribute against a target value with comparison operators (`==`, `!=`, `>`, `<`, `>=`, `<=`). -- **Separate Mesh by Range**: Splits a `SlicingCollection` into multiple output sockets across an integer range (`Start` to `End`), matching a specified integer attribute. Objects in each output socket are renamed with a zero-indexed padded suffix (`f"{base_mesh_name}{i:0{num_digits}d}"` where `num_digits = max(2, len(str(total_parts)))`). - **Rename Mesh**: Overrides the internal metadata name of a `SlicingObject`, `SlicingCollection`, or `SlicingGroup`. Supports `{AttributeName}` attribute placeholders (e.g., `Rebar-L{Rebar Length}M`) that evaluate to index zero of the target attribute for each object, as well as `{Name}` to include the item's previous name (e.g., `{Name}_T{Thickness}mm`). - **Geometry**: Input geometry data. - **Name**: String input socket or property for the new name template. -- **Assign PDF Properties**: Assigns line width (mm) and line color PDF export properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`). - - **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`). - - **Line Width**: Input socket / float property for line width in mm. - - **Line Color**: Color property for stroke color. -- **Assign Property to Object**: Assigns custom settings/properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) for rendering in PDF properties tables. - - **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`). - - **Settings**: Input socket accepting configuration settings or property items. -- **Assign Modifiers**: Assigns PrusaSlicer part/volume types (`ModelPart`, `ParameterModifier`, `NegativeVolume`, `SupportBlocker`, `SupportEnforcer`, `WipeTower`), extruder assignments (`0` to `5`), and config parameter modifiers directly to geometry (`SlicingObject`, `SlicingCollection`, or `SlicingGroup`) for 3MF export. - - **Geometry**: Generic geometry input socket (`SLICESOCK_Mesh`). - - **Modifiers**: Optional input socket accepting configuration settings dictionary (`SLICESOCK_Settings`). - - **Part Type**: Dropdown property specifying target part/volume type (`Keep Original`, `Model Part`, `Modifier`, `Negative Volume`, `Support Blocker`, `Support Enforcer`, `Wipe Tower`). - - **Extruder**: Dropdown property specifying target extruder assignment (`Keep Original`, `Default Extruder`, `Extruder 1-5`). - **Transform Geometry**: Moves, rotates, and scales geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) relative to the origin. - **Geometry**: Input geometry data. - **Translation**: 3D vector specifying the translation offset. @@ -88,16 +68,20 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Z**: Target minimum Z coordinate (defaults to `0.0`). - **Mesh**: Output dropped mesh data. - **Translation**: Output translation vector (X, Y, Z) applied to the geometry. +- **Separate Mesh**: Splits a `SlicingCollection` into two collections (`Selection` and `Inverted`) by comparing a specified mesh attribute against a target value with comparison operators (`==`, `!=`, `>`, `<`, `>=`, `<=`). +- **Separate Mesh by Range**: Splits a `SlicingCollection` into multiple output sockets across an integer range (`Start` to `End`), matching a specified integer attribute. Objects in each output socket are renamed with a zero-indexed padded suffix (`f"{base_mesh_name}{i:0{num_digits}d}"` where `num_digits = max(2, len(str(total_parts)))`). +- **Arrange Slicing Group**: Arranges the slicing collections in a slicing group on the build plate using the `libnest2d` bin-packing library. It nests their 2D bounding boxes to prevent collisions. + - **Method**: Switch between **NFP** (No-Fit Polygon placer + First-Fit selection), **Bottom-Left** (Bottom-Left placer + First-Fit selection), and **DJD Heuristic** (No-Fit Polygon placer + Dyckhoff's heuristic). + - **Allow Rotations**: Toggle whether to allow rotating objects (by 90-degree steps) during nesting. + - **Bed X / Y**: The build plate dimensions in mm. + - **Spacing**: The minimum distance between arranged objects in mm. - **Get Paths**: Extracts existing edges from a mesh as paths. - **Extrusion Multiplier Attribute**: Mesh attribute to use for per-segment extrusion rates. - **Feedrate Attribute**: Mesh attribute to use for per-segment feedrates. - **Extra Attributes**: Dictionary mapping mesh attribute names to custom G-code prefixes (e.g., `opening_size` -> `S`). -- **Path Offset**: Offsets `SlicePaths` by a 3D vector. -- **Rotate Path**: Rotates `SlicePaths` around the Z-axis. - - **Angle**: Rotation angle in degrees. - - **Center**: Optional center of rotation. If not provided, it defaults to the midpoint of the first layer. - **Slice Shells**: Slices a `SlicingObject` into 2D `SlicePaths` at a given layer height. Automatically aligns path seams to the sharpest concave angle (inner corner) of each layer. Supports rotation for slicing at an angle. - **Buffer Shells**: Offsets/buffers 2D `SlicePaths`. Supports rotated offsetting and multiple miter types. +- **Buffer Shells Multiple**: Offsets 2D `SlicePaths` with multiple buffer passes. - **Buffer Curve**: Buffers open/closed curves using Shapely, allowing open path offsetting. - **Cap Style**: Style of the curve ends (Round, Flat, Square). - **Join Style**: Style of corner joins (Round, Mitre, Bevel). @@ -108,6 +92,11 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Join Style**: Style of corner joins (Round, Mitre, Bevel). - **Distance**: The offset distance (positive for left side, negative for right side). - **Rotation**: Optional Euler rotation for non-planar offsetting. +- **Path Offset**: Offsets `SlicePaths` by a 3D vector. +- **Rotate Path**: Rotates `SlicePaths` around the Z-axis. + - **Angle**: Rotation angle in degrees. + - **Center**: Optional center of rotation. If not provided, it defaults to the midpoint of the first layer. +- **Simplify Paths**: Simplifies `SlicePaths` geometry using the Douglas-Peucker algorithm. - **Infill**: Generates a cross-hatch grid or parallel lines clipped by boundary paths. - **Infill Type**: Switch between **Grid** (cross-hatch), **Rectilinear** (alternating parallel lines), and **Lines** (parallel lines in one direction). - **Density**: Distance between lines in units. @@ -116,25 +105,40 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Get Infill Mask**: Computes the intersection of all closed paths in each group (layer) using Shapely. - **Overhang Mask**: Computes the accumulated overhang mask for each layer, propagating support requirements from top to bottom. - **Boolean Paths**: Performs 2D boolean operations (`UNION`, `INTERSECTION`, `DIFFERENCE`, `XOR`) on multiple slice sets. -- **Arrange Slicing Group**: Arranges the slicing collections in a slicing group on the build plate using the `libnest2d` bin-packing library. It nests their 2D bounding boxes to prevent collisions. - - **Method**: Switch between **NFP** (No-Fit Polygon placer + First-Fit selection), **Bottom-Left** (Bottom-Left placer + First-Fit selection), and **DJD Heuristic** (No-Fit Polygon placer + Dyckhoff's heuristic). - - **Allow Rotations**: Toggle whether to allow rotating objects (by 90-degree steps) during nesting. - - **Bed X / Y**: The build plate dimensions in mm. - - **Spacing**: The minimum distance between arranged objects in mm. - -### Utility & Joiners -- **Join Mesh**: Merges multiple `SlicingObject` instances into one. -- **Join Paths**: Combines multiple `SlicePaths`. Supports Sequential or Interleaved modes. -- **Join Gcode**: Concatenates multiple G-code strings. -- **Join Settings**: Sequentially merges multiple override dictionaries. Later inputs overwrite earlier ones for matching keys. +- **Math**: Float math operations. - **Integer Math**: Simple integer operations (Add, Multiply, etc.). -### Action & I/O -- **Paths to Gcode**: Converts `SlicePaths` into G-code blocks. - - **Header**: Toggles the generation of standard NodeSlicer G-code headers and units setup. -- **Load Gcode**: Reads a G-code file from disk. -- **Gcode to File**: Writes G-code string to a file on disk. - - **Automatic Binary Conversion**: If the G-code contains the comment `; binary_gcode = 1`, the node automatically converts the output to Prusa's binary G-code format (`.bgcode`) using MeatPack encoding and Heatshrink compression. +### PrusaSlicer +- **PrusaSlicer Config**: Configures print, filament, and printer profiles from PrusaSlicer INI bundles. +- **Assign Modifiers**: Assigns PrusaSlicer part/volume types (`ModelPart`, `ParameterModifier`, `NegativeVolume`, `SupportBlocker`, `SupportEnforcer`, `WipeTower`), extruder assignments (`0` to `5`), and config parameter modifiers directly to geometry (`SlicingObject`, `SlicingCollection`, or `SlicingGroup`) for 3MF export. + - **Geometry**: Generic geometry input socket (`SLICESOCK_Mesh`). + - **Modifiers**: Optional input socket accepting configuration settings dictionary (`SLICESOCK_Settings`). + - **Part Type**: Dropdown property specifying target part/volume type (`Keep Original`, `Model Part`, `Modifier`, `Negative Volume`, `Support Blocker`, `Support Enforcer`, `Wipe Tower`). + - **Extruder**: Dropdown property specifying target extruder assignment (`Keep Original`, `Default Extruder`, `Extruder 1-5`). +- **Wipe Tower**: Configures the Wipe Tower printbed position (`wipe_tower_position`) passed to 3MF metadata (`Prusa_Slicer_wipe_tower_information.xml`) without injecting into profile INI settings. + - **Position X**: X coordinate of the wipe tower on the printbed. + - **Position Y**: Y coordinate of the wipe tower on the printbed. +- **Offset Object**: Configures the object placement transform offset (`offset_object`) applied to `` inside 3MF model build data (`3D/3dmodel.model`) without injecting into profile INI settings. + - **Offset X**: X translation offset for the build item. + - **Offset Y**: Y translation offset for the build item. + - **Offset Z**: Z translation offset for the build item. +- **PrusaSlicer Slice**: Exports the current mesh and configuration to a 3MF file and calls PrusaSlicer to generate G-code. + - **Executable**: Path to the PrusaSlicer executable. + - **Object**: The Blender object to slice. + - **Config**: PrusaSlicer configuration (from the **PrusaSlicer Config** node). + - **Gcode**: Output socket providing the generated G-code string. +- **PrusaSlicer Preview**: Launches PrusaSlicer's built-in G-code viewer to visualize a G-code string. + - **Executable**: Path to the PrusaSlicer executable (default: `flatpak run com.prusa3d.PrusaSlicer`). + - **Gcode**: Input socket for the G-code string to preview. + +### PDF Exporter +- **Assign PDF Properties**: Assigns line width (mm) and line color PDF export properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`). + - **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`). + - **Line Width**: Input socket / float property for line width in mm. + - **Line Color**: Color property for stroke color. +- **Assign PDF Object Properties**: Assigns custom settings/properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) for rendering in PDF properties tables and object callout labels. + - **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`). + - **Settings**: Input socket accepting configuration settings or property items. - **Mesh to PDF**: Generates a 1:1 true-scale 2D top-view (+Z orthographic) vector PDF from a `SlicingGroup` or `SlicingCollection` with a custom grid and background color. Subpanel menus are collapsed by default. - **Technical Drawing Header**: Collapsible menu containing string inputs for technical drawing info (Company, Part Name, Drawing No., Author, Revision, Material, Tolerance). If Part Name is left empty, it automatically defaults to the name of the first object being rendered. Automatically computes ISO date and scale length (`1:1`). Renders an ISO/DIN technical drawing header table on the bottom-right of the drawing. - **Geometry**: Input socket accepting a single geometry (`SlicingGroup`, `SlicingCollection`, or `SlicingObject`). Join multiple geometries using Join Geometry nodes. @@ -156,6 +160,28 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Margin (mm)**: Margin around the geometry grid in mm. - **Background**: Color picker for the canvas background. - **Wireframe**: Color picker for the wireframe lines and grid. + +### Gcode Processing +- **Paths to Gcode**: Converts `SlicePaths` into G-code blocks. + - **Header**: Toggles the generation of standard NodeSlicer G-code headers and units setup. +- **Gcode Block**: Manual text area for entering raw G-code. +- **Gcode Regex Replace**: Performs regex search and replace on G-code. +- **Assign Metadata**: Injects custom metadata comments into G-code. +- **Gcode Helper**: Utility helper functions for G-code stream modifications. +- **Load Gcode**: Reads a G-code file from disk. + +### Utility & Joiners +- **Join Mesh**: Merges multiple `SlicingObject` instances into one. +- **Join Paths**: Combines multiple `SlicePaths`. Supports Sequential or Interleaved modes. +- **Join Gcode**: Concatenates multiple G-code strings. +- **Join Settings**: Sequentially merges multiple override dictionaries. Later inputs overwrite earlier ones for matching keys. +- **Join Execution**: Synchronizes sequential execution flows. + +### Action & I/O +- **Gcode to File**: Writes G-code string to a file on disk. + - **Automatic Binary Conversion**: If the G-code contains the comment `; binary_gcode = 1`, the node automatically converts the output to Prusa's binary G-code format (`.bgcode`) using MeatPack encoding and Heatshrink compression. +- **Gcode to Text Block**: Creates a new Blender text block containing the G-code and opens it in any active text editors. +- **Start Print**: Sends G-code directly to a network printer. - **Baserow Submit**: Sends G-code and metadata to a Baserow database. - **Host**: Baserow host URL. - **Token File**: Path to a file containing the Baserow API token. @@ -168,14 +194,6 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Table ID**: The ID or name of the target Airtable table. - **Record ID**: Optional ID of an existing record to update. - **Gcode Field**: The name of the attachment field in Airtable. -- **PrusaSlicer Slice**: Exports the current mesh and configuration to a 3MF file and calls PrusaSlicer to generate G-code. - - **Executable**: Path to the PrusaSlicer executable. - - **Object**: The Blender object to slice. - - **Config**: PrusaSlicer configuration (from the **PrusaSlicer Config** node). - - **Gcode**: Output socket providing the generated G-code string. -- **Gcode to Text Block**: Creates a new Blender text block containing the G-code and opens it in any active text editors. - **Gcode to Preview Mesh**: Decodes G-code and creates a Blender mesh ("Debug" object) for visualization. -- **PrusaSlicer Preview**: Launches PrusaSlicer's built-in G-code viewer to visualize a G-code string. - - **Executable**: Path to the PrusaSlicer executable (default: `flatpak run com.prusa3d.PrusaSlicer`). - - **Gcode**: Input socket for the G-code string to preview. +- **Executor**: Executes sub-tree node chains. - **Test**: Triggers evaluation for debugging purposes. diff --git a/infra/blender/blender_mesh_capture.py b/infra/blender/blender_mesh_capture.py index b1951ca..e391f20 100644 --- a/infra/blender/blender_mesh_capture.py +++ b/infra/blender/blender_mesh_capture.py @@ -112,7 +112,7 @@ class SlicingObject(): modifiers: list[dict[str, Any]] = [] extruder: int = 1 pdf_properties: PdfProperties - object_properties: list[tuple[str, str]] + pdf_object_properties: list[tuple[str, str]] def __init__(self, obj: Object | None = None, parent: str = "") -> None: if not bpy.context.scene: raise Exception('No scene currently open!') @@ -123,7 +123,7 @@ class SlicingObject(): self.modifiers = [] self.transforms = [] self.pdf_properties = PdfProperties() - self.object_properties = [] + self.pdf_object_properties = [] assert bpy.context.scene.unit_settings scene_scale: float = bpy.context.scene.unit_settings.scale_length @@ -150,7 +150,7 @@ class SlicingObject(): merged.object_type = objects[0].object_type merged.extruder = objects[0].extruder merged.pdf_properties = copy.deepcopy(objects[0].pdf_properties) if hasattr(objects[0], "pdf_properties") and objects[0].pdf_properties else PdfProperties() - merged.object_properties = list(getattr(objects[0], "object_properties", [])) + merged.pdf_object_properties = list(getattr(objects[0], "pdf_object_properties", [])) merged.transforms = [] for o in objects: for t in getattr(o, "transforms", []): diff --git a/tests/test_pdf_exporter.py b/tests/test_pdf_exporter.py index 85c95d3..4c50560 100644 --- a/tests/test_pdf_exporter.py +++ b/tests/test_pdf_exporter.py @@ -1119,13 +1119,13 @@ class TestPdfExporter(unittest.TestCase): for coll in updated_sg.collections.values(): for obj in coll.objects: - self.assertEqual(obj.object_properties, props) + self.assertEqual(obj.pdf_object_properties, props) # Also verify SlicePaths support from NodeSlicer.core.slice import SlicePaths sp = SlicePaths([np.array([[0, 0, 0], [10, 10, 0]], dtype=np.float64)]) updated_sp = _assign_properties_to_item(sp, props) - self.assertEqual(updated_sp.object_properties, props) + self.assertEqual(updated_sp.pdf_object_properties, props) # Verify dynamic socket color logic from NodeSlicer.core.nodeslicer.sockets import SLICESOCK_Geometry, SLICESOCK_Paths, SLICESOCK_Mesh @@ -1180,7 +1180,7 @@ class TestPdfExporter(unittest.TestCase): sg.collections = {"Coll1": sc1, "Coll2": sc2} # Assign properties to so1 only - so1.object_properties = [("", "Material: PETG"), ("Tolerance", "ISO 2768-f")] + so1.pdf_object_properties = [("", "Material: PETG"), ("Tolerance", "ISO 2768-f")] edges_2d = extract_unique_top_view_edges(sg) @@ -1236,7 +1236,7 @@ class TestPdfExporter(unittest.TestCase): res_obj = updated_sg.collections["Coll"].objects[0] self.assertEqual( - res_obj.object_properties, + res_obj.pdf_object_properties, [("infill_density", "15.5"), ("material", "ABS"), ("custom_setting", "fixed_value")] ) diff --git a/tests/test_wipe_tower.py b/tests/test_wipe_tower.py index d9d6206..2f54667 100644 --- a/tests/test_wipe_tower.py +++ b/tests/test_wipe_tower.py @@ -109,7 +109,7 @@ class TestAssignModifiersNode(unittest.TestCase): result_so = node.evaluate_output("Geometry") self.assertIsNotNone(result_so) self.assertEqual(result_so.object_type, "ParameterModifier") - self.assertEqual(result_so.extruder, "3") + self.assertEqual(result_so.extruder, 3) mod_dict = {m['param_id']: m['param_value'] for m in result_so.modifiers} self.assertEqual(mod_dict["initial_param"], "123") @@ -129,7 +129,7 @@ class TestAssignModifiersNode(unittest.TestCase): self.assertIsNotNone(result_sg) res_so = result_sg.collections["TestColl"].objects[0] self.assertEqual(res_so.object_type, "NegativeVolume") - self.assertEqual(res_so.extruder, "2") + self.assertEqual(res_so.extruder, 2) mod_dict = {m['param_id']: m['param_value'] for m in res_so.modifiers} self.assertEqual(mod_dict["perimeters"], "4")