703 lines
28 KiB
Python
703 lines
28 KiB
Python
from __future__ import annotations
|
|
from typing import Any
|
|
from pathlib import Path
|
|
import os
|
|
import bpy
|
|
import tempfile
|
|
import re
|
|
import json
|
|
import numpy as np
|
|
|
|
from ..bpy_ns_types import NfStringProperty, NfFloatProperty, NfEnumProperty, NfFloatVectorProperty, NfBoolProperty
|
|
from ....registry import register_class
|
|
from .base import ExecSliceNode, SlicePaths, SliceNode, SlicingCollection, SlicingGroup
|
|
|
|
@register_class
|
|
class SLICENODE_GcodeToFile(ExecSliceNode):
|
|
bl_label = "Gcode To File"
|
|
button_label = "Generate File"
|
|
|
|
def init(self, context):
|
|
self._init_exec()
|
|
self.add_input("NodeSocketString", "Folder")
|
|
self.add_input("NodeSocketString", "Name")
|
|
self.add_multi_socket_group("NodeSocketString", name="Gcode")
|
|
|
|
def update(self) -> None:
|
|
super().update()
|
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
|
for inp in self.inputs:
|
|
if getattr(inp, "name", "") == "Gcode" or getattr(inp, "identifier", "").startswith("Gcode_ms_in_"):
|
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
super().draw_buttons(context, layout)
|
|
|
|
def evaluate(self, operator=None):
|
|
from ....infra.networking.gcode_utils import get_gcode_name_from_metadata, prepare_gcode_file
|
|
from ....infra.blender.list_utils import flatten_list
|
|
|
|
raw_gcodes = self.resolve_multi_input('Gcode')
|
|
gcodes: list[str] = [str(gc) for gc in flatten_list(raw_gcodes) if gc]
|
|
|
|
folder: str = self.resolve("Folder")
|
|
if not gcodes:
|
|
if operator: operator.report({'ERROR'}, "No Gcode to save")
|
|
return 'error'
|
|
if not folder:
|
|
if operator: operator.report({'ERROR'}, "No Folder provided")
|
|
return 'error'
|
|
|
|
base_name: str = self.resolve("Name")
|
|
total_parts: int = len(gcodes)
|
|
num_digits: int = max(2, len(str(total_parts)))
|
|
|
|
is_name_list = isinstance(base_name, (list, tuple))
|
|
if is_name_list:
|
|
if len(base_name) == 1:
|
|
base_name = str(base_name[0])
|
|
is_name_list = False
|
|
elif len(base_name) != total_parts:
|
|
msg = f"Input list length mismatch: Gcode has {total_parts} items, but Name has {len(base_name)} items."
|
|
if operator: operator.report({'ERROR'}, msg)
|
|
return 'error'
|
|
|
|
for i, gc in enumerate(gcodes):
|
|
if is_name_list:
|
|
name = str(base_name[i])
|
|
elif base_name:
|
|
if total_parts > 1:
|
|
name = f"{base_name}{i:0{num_digits}d}"
|
|
else:
|
|
name = str(base_name)
|
|
else:
|
|
name = get_gcode_name_from_metadata(gc, default=f'object_{i}')
|
|
|
|
# User's requested logic: .split('.')[0]
|
|
folder_path = Path(bpy.path.abspath(f"//{folder}"))
|
|
filepath_abs = folder_path / name
|
|
dirname = os.path.dirname(filepath_abs)
|
|
basename = os.path.basename(filepath_abs)
|
|
name_part = basename.split('.')[0]
|
|
os.makedirs(dirname, exist_ok=True)
|
|
|
|
try:
|
|
file_bytes, filename = prepare_gcode_file(gc, name_part)
|
|
filepath = os.path.join(dirname, filename)
|
|
with open(filepath, "wb") as f:
|
|
f.write(file_bytes)
|
|
if filename.endswith(".bgcode"):
|
|
if operator: operator.report({'INFO'}, f"Binary G-code generated at {filepath}")
|
|
else:
|
|
if operator: operator.report({'INFO'}, f"G-code saved to {filepath}")
|
|
except Exception as e:
|
|
if operator: operator.report({'ERROR'}, str(e))
|
|
return 'error'
|
|
|
|
return 'ok'
|
|
|
|
@register_class
|
|
class SLICENODE_GcodeToTextBlock(ExecSliceNode):
|
|
bl_label = "Gcode to Text Block"
|
|
button_label = "Update Text Block"
|
|
|
|
text_block_name: str = NfStringProperty(name="Text Block Name", default="NodeSlicer_Gcode.gcode")
|
|
|
|
def init(self, context):
|
|
self._init_exec()
|
|
self.add_input("NodeSocketString", "Gcode")
|
|
|
|
def update(self) -> None:
|
|
super().update()
|
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
|
inp = self.inputs.get("Gcode")
|
|
if inp:
|
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
super().draw_buttons(context, layout)
|
|
layout.prop(self, "text_block_name")
|
|
|
|
def evaluate(self, operator=None):
|
|
from ....infra.blender.list_utils import flatten_list, is_list_data
|
|
raw = self.resolve("Gcode")
|
|
if not raw:
|
|
return 'ok'
|
|
|
|
gcodes = [str(g) for g in flatten_list(raw) if g] if is_list_data(raw) else ([str(raw)] if raw else [])
|
|
if not gcodes:
|
|
return 'ok'
|
|
|
|
base_name = self.text_block_name
|
|
name_root, ext = os.path.splitext(base_name)
|
|
total_parts = len(gcodes)
|
|
num_digits = max(2, len(str(total_parts)))
|
|
|
|
for i, gcode in enumerate(gcodes):
|
|
name = f"{name_root}_{i:0{num_digits}d}{ext}" if total_parts > 1 else base_name
|
|
text_block = bpy.data.texts.get(name)
|
|
if text_block is None:
|
|
text_block = bpy.data.texts.new(name=name)
|
|
text_block.from_string(gcode)
|
|
|
|
# Attempt to show in an open text editor
|
|
assert bpy.context.window_manager
|
|
for window in bpy.context.window_manager.windows:
|
|
for area in window.screen.areas:
|
|
if area.type == 'TEXT_EDITOR':
|
|
for space in area.spaces:
|
|
if space.type == 'TEXT_EDITOR':
|
|
space.text = text_block
|
|
|
|
return 'ok'
|
|
|
|
@register_class
|
|
class SLICENODE_StartPrint(ExecSliceNode):
|
|
bl_label = "Start Print"
|
|
button_label = "Send to Printer"
|
|
|
|
def init(self, context):
|
|
self._init_exec()
|
|
self.add_input("NodeSocketString", "Gcode")
|
|
self.add_input("SLICESOCK_Printer")
|
|
|
|
def evaluate(self, operator=None):
|
|
from ....infra.blender.list_utils import is_list_data
|
|
|
|
raw_gcode = self.resolve("Gcode")
|
|
if is_list_data(raw_gcode):
|
|
if operator: operator.report({'ERROR'}, "Start Print does not accept a list of Gcode")
|
|
return 'error'
|
|
|
|
from ....infra.networking.printer_backends import (
|
|
PrinterConfig,
|
|
PrusaLinkBackend,
|
|
CrealityBackend,
|
|
MoonrakerBackend
|
|
)
|
|
|
|
gcode: str = str(raw_gcode or "")
|
|
config: PrinterConfig | None = self.resolve("Printer")
|
|
|
|
if not gcode:
|
|
if operator: operator.report({'ERROR'}, "No Gcode to send")
|
|
return 'no gcode'
|
|
if not config:
|
|
if operator: operator.report({'ERROR'}, "No Printer configuration")
|
|
return 'no printer'
|
|
|
|
# Instantiate correct backend
|
|
backend = None
|
|
if config.host_type == 'PRUSALINK':
|
|
backend = PrusaLinkBackend(config.host, config.port, config.prefix, api_key=config.password)
|
|
elif config.host_type == 'CREALITY':
|
|
backend = CrealityBackend(config.host, config.port, config.prefix)
|
|
elif config.host_type == 'MOONRAKER':
|
|
backend = MoonrakerBackend(config.host, config.port, config.prefix)
|
|
|
|
if not backend:
|
|
if operator: operator.report({'ERROR'}, f"Unsupported host type: {config.host_type}")
|
|
return 'unsupported'
|
|
|
|
from ....infra.networking.gcode_utils import prepare_gcode_file
|
|
|
|
# Write to temp file to send
|
|
name_part = f"NodeSlicer_{config.label}".replace(" ", "_")
|
|
try:
|
|
file_bytes, filename = prepare_gcode_file(gcode, name_part)
|
|
except Exception as e:
|
|
if operator: operator.report({'ERROR'}, f"Failed to prepare G-code: {str(e)}")
|
|
return 'error'
|
|
|
|
suffix = Path(filename).suffix
|
|
with tempfile.NamedTemporaryFile(mode='wb', suffix=suffix, delete=False) as tf:
|
|
tf.write(file_bytes)
|
|
temp_path = Path(tf.name)
|
|
|
|
try:
|
|
backend.start_print(temp_path, filename)
|
|
if operator: operator.report({'INFO'}, f"Print started on {config.label}")
|
|
except Exception as e:
|
|
if operator: operator.report({'ERROR'}, f"Failed to start print: {str(e)}")
|
|
return 'error'
|
|
finally:
|
|
if temp_path.exists():
|
|
temp_path.unlink()
|
|
|
|
return 'ok'
|
|
|
|
class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
|
|
record_id: str = NfStringProperty(name="Record ID", default="")
|
|
|
|
_db_name = "Database"
|
|
_token_prop_name = "token_path"
|
|
|
|
def init(self, context):
|
|
self._init_exec()
|
|
self.add_input("NodeSocketString", "Gcode")
|
|
self.add_input("SLICESOCK_Settings", "Metadata")
|
|
|
|
def update(self) -> None:
|
|
super().update()
|
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
|
inp = self.inputs.get("Gcode")
|
|
if inp:
|
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
super().draw_buttons(context, layout)
|
|
layout.prop(self, "record_id")
|
|
|
|
def evaluate(self, operator=None):
|
|
from ....infra.networking.bgcode import convert_to_bgcode
|
|
from ....infra.networking.gcode_utils import get_gcode_name_from_metadata, parse_gcode_metadata
|
|
from ....infra.blender.list_utils import flatten_list
|
|
|
|
gcodes: list[str] = [str(gc) for gc in flatten_list(self.resolve_multi_input("Gcode")) if gc]
|
|
|
|
for gc in gcodes:
|
|
if not gc:
|
|
continue
|
|
|
|
# Parse Metadata from Gcode
|
|
part_name = get_gcode_name_from_metadata(gc)
|
|
fields = parse_gcode_metadata(gc)
|
|
fields["Part ID / Name"] = part_name
|
|
|
|
# Detect binary trigger in gcode comments
|
|
is_binary = "; binary_gcode = 1" in gc
|
|
|
|
filename = part_name + (".bgcode" if is_binary else ".gcode")
|
|
|
|
token_path = getattr(self, self._token_prop_name, "")
|
|
token = ""
|
|
if token_path:
|
|
abs_token_path = bpy.path.abspath(token_path)
|
|
if os.path.exists(abs_token_path):
|
|
try:
|
|
with open(abs_token_path, 'r', encoding='utf-8') as f:
|
|
token = f.read().strip()
|
|
except Exception as e:
|
|
if operator: operator.report({'ERROR'}, f"Failed to read token file: {str(e)}")
|
|
return 'error'
|
|
|
|
if not self._check_config(token):
|
|
if operator: operator.report({'ERROR'}, f"{self._db_name} configuration incomplete")
|
|
return 'incomplete config'
|
|
|
|
temp_dir = tempfile.TemporaryDirectory()
|
|
temp_path = Path(temp_dir.name) / filename
|
|
|
|
try:
|
|
if is_binary:
|
|
content = convert_to_bgcode(gc, part_name, True)
|
|
with open(temp_path, "wb") as f:
|
|
f.write(content)
|
|
else:
|
|
with open(temp_path, 'w', encoding='utf-8') as f:
|
|
f.write(gc)
|
|
|
|
record_id = self._submit_to_db(token, fields, temp_path)
|
|
if operator: operator.report({'INFO'}, f"Submitted to {self._db_name} (Record: {record_id})")
|
|
except Exception as e:
|
|
if operator: operator.report({'ERROR'}, f"{self._db_name} Error: {str(e)}")
|
|
finally:
|
|
if temp_path.exists():
|
|
temp_path.unlink()
|
|
temp_dir.cleanup()
|
|
|
|
def _check_config(self, token: str) -> bool:
|
|
return False
|
|
|
|
def _submit_to_db(self, token: str, fields: dict[str, Any], temp_path: Path) -> str:
|
|
return ""
|
|
|
|
@register_class
|
|
class SLICENODE_BaserowSubmit(ExecSliceNode_DatabaseSubmit):
|
|
bl_label = "Baserow Submit"
|
|
button_label = "Submit to Baserow"
|
|
|
|
host: str = NfStringProperty(name="Host", default="https://baserow.io")
|
|
token_path: str = NfStringProperty(name="Token File", subtype='FILE_PATH', default="")
|
|
table_id: str = NfStringProperty(name="Table ID", default="")
|
|
|
|
_db_name = "Baserow"
|
|
_token_prop_name = "token_path"
|
|
|
|
def draw_buttons(self, context, layout):
|
|
layout.prop(self, "host")
|
|
layout.prop(self, "token_path")
|
|
layout.prop(self, "table_id")
|
|
super().draw_buttons(context, layout)
|
|
|
|
def _check_config(self, token):
|
|
return bool(token and self.table_id and self.host)
|
|
|
|
def _submit_to_db(self, token, fields, temp_path):
|
|
from ....infra.networking.baserow_backend import BaserowBackend
|
|
db = BaserowBackend(self.host, token)
|
|
|
|
if self.record_id:
|
|
try:
|
|
rid = int(self.record_id)
|
|
res = db.upsert_record(self.table_id, rid, fields)
|
|
except ValueError:
|
|
raise ValueError("Record ID must be an integer")
|
|
else:
|
|
res = db.create_record(self.table_id, fields)
|
|
|
|
record_id = int(res['id'])
|
|
db.upload_attachment(self.table_id, record_id, 'GCODE', temp_path)
|
|
return str(record_id)
|
|
|
|
@register_class
|
|
class SLICENODE_AirtableSubmit(ExecSliceNode_DatabaseSubmit):
|
|
bl_label = "Airtable Submit"
|
|
button_label = "Submit to Airtable"
|
|
|
|
pat_path: str = NfStringProperty(name="PAT File", subtype='FILE_PATH', default="")
|
|
base_id: str = NfStringProperty(name="Base ID", default="")
|
|
table_id: str = NfStringProperty(name="Table ID", default="")
|
|
|
|
_db_name = "Airtable"
|
|
_token_prop_name = "pat_path"
|
|
|
|
def draw_buttons(self, context, layout):
|
|
layout.prop(self, "pat_path")
|
|
layout.prop(self, "base_id")
|
|
layout.prop(self, "table_id")
|
|
super().draw_buttons(context, layout)
|
|
|
|
def _check_config(self, token):
|
|
return bool(token and self.base_id and self.table_id)
|
|
|
|
def _submit_to_db(self, token, fields, temp_path):
|
|
from ....infra.networking.airtable_backend import AirtableInterface, AirtableRecord
|
|
db = AirtableInterface(token)
|
|
|
|
if self.record_id:
|
|
record: AirtableRecord = {"id": self.record_id, "fields": fields}
|
|
res = db.update_records(self.base_id, self.table_id, [record])
|
|
record_id = res[0]['id']
|
|
else:
|
|
res = db.create_record(self.base_id, self.table_id, fields)
|
|
record_id = res['id']
|
|
|
|
db.upload_attachment(self.base_id, record_id, 'GCODE', temp_path)
|
|
return str(record_id)
|
|
|
|
|
|
@register_class
|
|
class SLICENODE_MeshToPdf(ExecSliceNode):
|
|
bl_label = "Mesh to PDF"
|
|
button_label = "Generate PDF"
|
|
|
|
template: str = NfStringProperty(
|
|
name="Template",
|
|
default="{object_name}-{size_x_mm}x{size_y_mm}-{today_date_iso}.pdf",
|
|
)
|
|
|
|
@property
|
|
def filename_template(self) -> str:
|
|
return getattr(self, "template", "{object_name}-{size_x_mm}x{size_y_mm}-{today_date_iso}.pdf")
|
|
|
|
@filename_template.setter
|
|
def filename_template(self, value: str) -> None:
|
|
self.template = value
|
|
|
|
draw_wireframe: bool = NfBoolProperty(name="Wireframe", default=True)
|
|
draw_boundary: bool = NfBoolProperty(name="Boundary (1 Face)", default=False)
|
|
draw_contour: bool = NfBoolProperty(name="Contour", default=False)
|
|
draw_crease: bool = NfBoolProperty(name="Crease Angle", default=False)
|
|
crease_angle: float = NfFloatProperty(name="Angle (°)", default=30.0, min=0.0, max=180.0)
|
|
draw_bbox: bool = NfBoolProperty(name="Bounding Box", default=False)
|
|
draw_sa2_holes: bool = NfBoolProperty(name="Holes (Ø)", default=False)
|
|
show_lineart: bool = NfBoolProperty(name="Lineart Visibility", default=False)
|
|
show_sa2: bool = NfBoolProperty(name="SA2 Settings", default=False)
|
|
draw_grid_xy: bool = NfBoolProperty(name="XY Grid (Top)", default=True)
|
|
draw_grid_xz: bool = NfBoolProperty(name="XZ Grid (Front)", default=False)
|
|
draw_grid_yz: bool = NfBoolProperty(name="YZ Grid (Side)", default=False)
|
|
show_grids: bool = NfBoolProperty(name="Grids", default=False)
|
|
|
|
draw_header: bool = NfBoolProperty(name="Technical Header", default=True)
|
|
show_header: bool = NfBoolProperty(name="Technical Drawing Header", default=False)
|
|
header_company: str = NfStringProperty(name="Company", default="")
|
|
header_part_name: str = NfStringProperty(name="Part Name", default="")
|
|
header_drawing_number: str = NfStringProperty(name="Drawing No.", default="")
|
|
header_author: str = NfStringProperty(name="Author", default="")
|
|
header_revision: str = NfStringProperty(name="Revision", default="A")
|
|
header_material: str = NfStringProperty(name="Material", default="")
|
|
header_tolerance: str = NfStringProperty(name="Tolerance", default="")
|
|
|
|
font_size: float = NfFloatProperty(name="Font Size (mm)", default=0.0, min=0.0, max=100.0)
|
|
margin: float = NfFloatProperty(name="Margin (mm)", default=10.0, min=0.0, max=500.0)
|
|
|
|
bg_color: tuple[float, float, float] = NfFloatVectorProperty(
|
|
name="Background",
|
|
subtype='COLOR',
|
|
default=(1.0, 1.0, 1.0),
|
|
min=0.0,
|
|
max=1.0,
|
|
size=3
|
|
)
|
|
|
|
wire_color: tuple[float, float, float] = NfFloatVectorProperty(
|
|
name="Wireframe",
|
|
subtype='COLOR',
|
|
default=(0.0, 0.0, 0.0),
|
|
min=0.0,
|
|
max=1.0,
|
|
size=3
|
|
)
|
|
|
|
bbox_color: tuple[float, float, float] = NfFloatVectorProperty(
|
|
name="BBox Color",
|
|
subtype='COLOR',
|
|
default=(0.0, 0.0, 1.0),
|
|
min=0.0,
|
|
max=1.0,
|
|
size=3
|
|
)
|
|
|
|
def init(self, context):
|
|
self._init_exec()
|
|
self.add_input("SLICESOCK_DirPath", "Folder")
|
|
self.add_input("SLICESOCK_Settings", "Settings")
|
|
self.add_input("SLICESOCK_Mesh", "Geometry")
|
|
self.add_input("SLICESOCK_Paths", "Paths")
|
|
|
|
def update(self) -> None:
|
|
super().update()
|
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
|
for name in ("Geometry", "Paths"):
|
|
inp = self.inputs.get(name)
|
|
if inp:
|
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
super().draw_buttons(context, layout)
|
|
|
|
layout.prop(self, "template")
|
|
|
|
# Technical Drawing Header subpanel (collapsible, collapsed by default)
|
|
layout.prop(self, "show_header", text="Technical Drawing Header", icon='TRIA_DOWN' if self.show_header else 'TRIA_RIGHT')
|
|
if self.show_header:
|
|
box = layout.box()
|
|
box.prop(self, "draw_header")
|
|
box.prop(self, "header_company")
|
|
box.prop(self, "header_part_name")
|
|
box.prop(self, "header_drawing_number")
|
|
box.prop(self, "header_author")
|
|
box.prop(self, "header_revision")
|
|
box.prop(self, "header_material")
|
|
box.prop(self, "header_tolerance")
|
|
|
|
# Lineart Visibility subpanel (collapsible, collapsed by default)
|
|
layout.prop(self, "show_lineart", text="Lineart Visibility", icon='TRIA_DOWN' if self.show_lineart else 'TRIA_RIGHT')
|
|
if self.show_lineart:
|
|
box = layout.box()
|
|
box.prop(self, "draw_wireframe")
|
|
box.prop(self, "draw_boundary")
|
|
box.prop(self, "draw_contour")
|
|
box.prop(self, "draw_crease")
|
|
if self.draw_crease:
|
|
box.prop(self, "crease_angle")
|
|
|
|
# SA2 Settings subpanel (collapsible, collapsed by default)
|
|
layout.prop(self, "show_sa2", text="SA2 Settings", icon='TRIA_DOWN' if self.show_sa2 else 'TRIA_RIGHT')
|
|
if self.show_sa2:
|
|
box = layout.box()
|
|
box.prop(self, "draw_sa2_holes")
|
|
|
|
# Grid Display subpanel
|
|
layout.prop(self, "show_grids", text="Grids", icon='TRIA_DOWN' if self.show_grids else 'TRIA_RIGHT')
|
|
if self.show_grids:
|
|
box = layout.box()
|
|
box.prop(self, "draw_grid_xy")
|
|
box.prop(self, "draw_grid_xz")
|
|
box.prop(self, "draw_grid_yz")
|
|
|
|
# Display settings
|
|
layout.prop(self, "draw_bbox")
|
|
if self.draw_bbox:
|
|
layout.prop(self, "bbox_color")
|
|
|
|
# Styling settings
|
|
layout.prop(self, "font_size")
|
|
layout.prop(self, "margin")
|
|
layout.prop(self, "bg_color")
|
|
layout.prop(self, "wire_color")
|
|
|
|
def evaluate(self, operator=None):
|
|
import datetime
|
|
from ...pdf_exporter import (
|
|
extract_unique_top_view_edges,
|
|
export_mesh_to_pdf,
|
|
format_pdf_filename_template,
|
|
get_first_object_name,
|
|
)
|
|
|
|
geom = self.resolve("Geometry")
|
|
slice_paths = self.resolve("Paths")
|
|
folder: str = self.resolve("Folder") or ""
|
|
|
|
# Collect raw geometry items
|
|
raw_geom_items: list[Any] = []
|
|
if geom is not None:
|
|
raw_list = geom if isinstance(geom, list) else [geom]
|
|
raw_geom_items = [g for g in raw_list if g is not None]
|
|
|
|
# Collect slice path items
|
|
slice_paths_items: list[Any] = []
|
|
if slice_paths is not None:
|
|
is_sp_inst = isinstance(slice_paths, SlicePaths)
|
|
raw_list = slice_paths if (isinstance(slice_paths, list) and not is_sp_inst) else [slice_paths]
|
|
slice_paths_items = [p for p in raw_list if p is not None]
|
|
|
|
if not raw_geom_items and not slice_paths_items:
|
|
if operator: operator.report({'ERROR'}, "No Geometry or Paths connected")
|
|
return 'error'
|
|
|
|
if not folder:
|
|
if operator: operator.report({'ERROR'}, "No Folder provided")
|
|
return 'error'
|
|
|
|
abs_folder = folder
|
|
if hasattr(bpy, "path") and hasattr(bpy.path, "abspath"):
|
|
try:
|
|
res = bpy.path.abspath(f"//{folder}" if not folder.startswith(("/", "\\")) and ":" not in folder else folder)
|
|
if isinstance(res, str) and not type(res).__module__.startswith("unittest.mock"):
|
|
abs_folder = res
|
|
except Exception:
|
|
pass
|
|
folder_path = Path(abs_folder)
|
|
|
|
has_raw_geom = len(raw_geom_items) > 0
|
|
primary_items = raw_geom_items if has_raw_geom else slice_paths_items
|
|
|
|
is_single_pdf_file = len(primary_items) == 1 and folder_path.suffix.lower() == ".pdf"
|
|
|
|
# Automatic scene scale determination
|
|
scale_str = "1:1"
|
|
try:
|
|
if hasattr(bpy, "context") and hasattr(bpy.context, "scene") and bpy.context.scene:
|
|
scale_len = getattr(bpy.context.scene.unit_settings, "scale_length", 1.0)
|
|
if scale_len == 1.0 or scale_len == 0.001:
|
|
scale_str = "1:1"
|
|
elif scale_len > 1.0:
|
|
scale_str = f"{scale_len:g}:1"
|
|
elif scale_len > 0.0:
|
|
scale_str = f"1:{1.0 / scale_len:g}"
|
|
except Exception:
|
|
scale_str = "1:1"
|
|
|
|
header_info = {
|
|
"company": self.header_company,
|
|
"part_name": self.header_part_name,
|
|
"drawing_number": self.header_drawing_number,
|
|
"author": self.header_author,
|
|
"revision": self.header_revision,
|
|
"material": self.header_material,
|
|
"tolerance": self.header_tolerance,
|
|
"date": datetime.date.today().isoformat(),
|
|
"scale": scale_str,
|
|
}
|
|
|
|
template = getattr(self, "template", None) or getattr(self, "filename_template", None) or "{object_name}-{size_x_mm}x{size_y_mm}-{today_date_iso}.pdf"
|
|
|
|
for i, target_item in enumerate(primary_items):
|
|
if has_raw_geom:
|
|
current_raw_geom = target_item
|
|
current_slice_paths = slice_paths
|
|
else:
|
|
current_raw_geom = None
|
|
current_slice_paths = target_item
|
|
|
|
current_header_info = dict(header_info)
|
|
part_name_prop = (self.header_part_name or "").strip()
|
|
if not part_name_prop:
|
|
default_part_name = (
|
|
get_first_object_name(target_item)
|
|
or get_first_object_name(current_raw_geom)
|
|
or get_first_object_name(current_slice_paths)
|
|
)
|
|
current_header_info["part_name"] = default_part_name
|
|
|
|
edges_2d = np.empty((0, 2, 2), dtype=np.float64)
|
|
if current_raw_geom is not None:
|
|
raw_e = extract_unique_top_view_edges(current_raw_geom)
|
|
if len(raw_e) > 0:
|
|
edges_2d = raw_e
|
|
if current_slice_paths is not None:
|
|
sp_e = extract_unique_top_view_edges(current_slice_paths)
|
|
if len(sp_e) > 0:
|
|
edges_2d = np.concatenate([edges_2d, sp_e], axis=0) if len(edges_2d) > 0 else sp_e
|
|
|
|
try:
|
|
exporter = export_mesh_to_pdf(
|
|
edges_2d=edges_2d,
|
|
filepath=None,
|
|
draw_wireframe=self.draw_wireframe,
|
|
draw_boundary=self.draw_boundary,
|
|
draw_contour=self.draw_contour,
|
|
draw_crease=self.draw_crease,
|
|
draw_bbox=self.draw_bbox,
|
|
draw_sa2_holes=self.draw_sa2_holes,
|
|
draw_header=self.draw_header,
|
|
header_info=current_header_info,
|
|
crease_angle=self.crease_angle,
|
|
margin_mm=self.margin,
|
|
font_size_mm=self.font_size * 2.834645669291339,
|
|
stroke_color=(float(self.wire_color[0]), float(self.wire_color[1]), float(self.wire_color[2])),
|
|
bg_color=(float(self.bg_color[0]), float(self.bg_color[1]), float(self.bg_color[2])),
|
|
bbox_color=(float(self.bbox_color[0]), float(self.bbox_color[1]), float(self.bbox_color[2])),
|
|
raw_geometry=current_raw_geom,
|
|
slice_paths=current_slice_paths,
|
|
draw_grid_xy=getattr(self, "draw_grid_xy", True),
|
|
draw_grid_xz=getattr(self, "draw_grid_xz", False),
|
|
draw_grid_yz=getattr(self, "draw_grid_yz", False),
|
|
)
|
|
|
|
if is_single_pdf_file:
|
|
os.makedirs(folder_path.parent, exist_ok=True)
|
|
filepath = str(folder_path)
|
|
else:
|
|
out_dir = folder_path if folder_path.suffix == "" else folder_path.parent
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
if isinstance(target_item, SlicingGroup) and target_item.collections:
|
|
raw_name = next(iter(target_item.collections.keys()))
|
|
elif hasattr(target_item, 'name') and getattr(target_item, 'name'):
|
|
raw_name = str(getattr(target_item, 'name'))
|
|
else:
|
|
raw_name = f"object_{i}"
|
|
|
|
object_name = raw_name.split('.')[0] if raw_name else f"object_{i}"
|
|
filename = format_pdf_filename_template(
|
|
template=template,
|
|
object_name=object_name,
|
|
size_x_mm=round(exporter.page_w_mm),
|
|
size_y_mm=round(exporter.page_h_mm),
|
|
today_date_iso=datetime.date.today().isoformat(),
|
|
target_item=target_item,
|
|
header_info=current_header_info,
|
|
)
|
|
filepath = str(out_dir / filename)
|
|
|
|
if len(edges_2d) == 0:
|
|
if operator: operator.report({'WARNING'}, f"Geometry for {os.path.basename(filepath)} has no valid 2D top-view wireframe edges")
|
|
|
|
exporter.export(filepath)
|
|
if operator: operator.report({'INFO'}, f"Mesh PDF saved to {filepath}")
|
|
except Exception as e:
|
|
if operator: operator.report({'ERROR'}, f"PDF Generation Error: {str(e)}")
|
|
return 'error'
|
|
|
|
return 'ok'
|
|
|
|
|
|
# Alias for backward compatibility
|
|
SLICENODE_WireframeToPdf = SLICENODE_MeshToPdf
|
|
|
|
|
|
|
|
|