search menus and refactor

This commit is contained in:
Nicolas Predella 2026-05-20 14:43:59 +03:00
parent 95911e6805
commit 0c4d4398e1
4 changed files with 69 additions and 136 deletions

View file

@ -10,7 +10,7 @@ from typing import Any, TypeAlias
import bpy import bpy
import numpy as np import numpy as np
from mathutils import Euler from mathutils import Euler
from .bpy_ns_types import NfIntProperty, NfStringProperty, NfEnumProperty, NfBoolProperty from .bpy_ns_types import NfIntProperty, NfStringProperty, NfEnumProperty, NfBoolProperty, notify_tree_changed
from ...registry import register_class from ...registry import register_class
from .. import slice from .. import slice
@ -18,7 +18,8 @@ from ..slice import Pather, slice_at_range, SlicePaths
from ...infra.blender.blender_mesh_capture import SlicingCollection, SlicingObject, capture_mesh_edges, SlicingGroup from ...infra.blender.blender_mesh_capture import SlicingCollection, SlicingObject, capture_mesh_edges, SlicingGroup
from ...infra.blender.blender_bridge import get_all_children from ...infra.blender.blender_bridge import get_all_children
from ...infra.blender.blender_slice_nodes_generic import SliceNode, resolve_input from ...infra.blender.blender_slice_nodes_generic import SliceNode, resolve_input
from .bpy_ns_types import notify_tree_changed from ...infra.prusaslicer_profiles.cache import LocalCache
from ...infra.prusaslicer_profiles.models import Profile
from ...infra._3mf_writer._3mf import prepare_3mf from ...infra._3mf_writer._3mf import prepare_3mf
class SliceIntrinsics: class SliceIntrinsics:
@ -108,101 +109,33 @@ class SLICENODE_PrusaslicerConf(SliceNode):
notify_tree_changed(self, context) notify_tree_changed(self, context)
def get_printers(self, context): def search_printers(self, _context, edit_text):
self.ensure_profiles_loaded()
cache: LocalCache = self.get_cache()
items: list[str] = [p.id for k, p in cache.printer_profiles.items() if edit_text.lower() in p.id.lower()]
return items
def search_conf(self, edit_text: str, conf_type: str) -> list[str]:
if not (printer_id := self.printer_id): return []
self.ensure_profiles_loaded() self.ensure_profiles_loaded()
cache = self.get_cache() cache = self.get_cache()
items = [('NONE', "None", "No printer selected", 'NONE', 0)] if not (printer := cache.printer_profiles.get("printer:" + printer_id)): return []
for i, (k, p) in enumerate(cache.printer_profiles.items(), 1):
items.append((k, p.id, p.path.name, 'PRINTER', i)) profiles: list[Profile] = printer.compatible_filament_profiles if conf_type == "filaments" else printer.compatible_print_profiles
items: list[str] = [p.id for p in profiles if edit_text.lower() in p.id.lower()]
return items return items
def get_filaments(self, context): def search_filaments(self, _context, edit_text) -> list[str]:
self.ensure_profiles_loaded() return self.search_conf(edit_text, conf_type="filaments")
cache = self.get_cache()
items = [('NONE', "None", "No filament selected", 'NONE', 0)]
printer_id = self.printer_id def search_prints(self, _context, edit_text) -> list[str]:
if printer_id == 'NONE': return self.search_conf(edit_text, conf_type="prints")
return items
printer = cache.printer_profiles.get(printer_id) printer_id: NfStringProperty(name="Printer", default='NONE', update=update_profiles, search=search_printers) # pyright: ignore[reportInvalidTypeForm]
if not printer: filament_id: NfStringProperty(name="Filament", default='NONE', search=search_filaments) # pyright: ignore[reportInvalidTypeForm]
return items print_id: NfStringProperty(name="Print", default='NONE', search=search_prints) # pyright: ignore[reportInvalidTypeForm]
idx = 1
for k in printer.compatible_profiles:
if k.startswith("filament:"):
p = cache.profiles[k]
items.append((k, p.id, p.path.name, 'FILAMENT', idx))
idx += 1
return items
def get_prints(self, context):
self.ensure_profiles_loaded()
cache = self.get_cache()
items = [('NONE', "None", "No print selected", 'NONE', 0)]
printer_id = self.printer_id
if printer_id == 'NONE':
return items
printer = cache.printer_profiles.get(printer_id)
if not printer:
return items
idx = 1
for k in printer.compatible_profiles:
if k.startswith("print:"):
p = cache.profiles[k]
items.append((k, p.id, p.path.name, 'PRINT', idx))
idx += 1
return items
printer_id: NfStringProperty(default='NONE', update=update_profiles) # pyright: ignore[reportInvalidTypeForm]
filament_id: NfStringProperty(default='NONE') # pyright: ignore[reportInvalidTypeForm]
print_id: NfStringProperty(default='NONE') # pyright: ignore[reportInvalidTypeForm]
def _get_enum_idx(self, items, value):
for item in items:
if item[0] == value:
return item[4]
return 0
def _set_enum_idx(self, items, index, prop_name):
for item in items:
if item[4] == index:
setattr(self, prop_name, item[0])
break
def get_printer_enum(self): return self._get_enum_idx(self.get_printers(None), self.printer_id)
def set_printer_enum(self, value): self._set_enum_idx(self.get_printers(None), value, 'printer_id')
def get_filament_enum(self): return self._get_enum_idx(self.get_filaments(None), self.filament_id)
def set_filament_enum(self, value): self._set_enum_idx(self.get_filaments(None), value, 'filament_id')
def get_print_enum(self): return self._get_enum_idx(self.get_prints(None), self.print_id)
def set_print_enum(self, value): self._set_enum_idx(self.get_prints(None), value, 'print_id')
printer_enum: NfEnumProperty(
name="Printer",
items=get_printers,
get=get_printer_enum,
set=set_printer_enum
) # pyright: ignore[reportInvalidTypeForm]
filament_enum: NfEnumProperty(
name="Filament",
items=get_filaments,
get=get_filament_enum,
set=set_filament_enum
) # pyright: ignore[reportInvalidTypeForm]
print_enum: NfEnumProperty(
name="Print",
items=get_prints,
get=get_print_enum,
set=set_print_enum
) # pyright: ignore[reportInvalidTypeForm]
def init(self, context): def init(self, context):
self.add_output("SLICESOCK_PrusaslicerConf", "Config") self.add_output("SLICESOCK_PrusaslicerConf", "Config")
@ -210,10 +143,10 @@ class SLICENODE_PrusaslicerConf(SliceNode):
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
assert layout assert layout
self.ensure_profiles_loaded() self.ensure_profiles_loaded()
layout.prop(self, "printer_enum") layout.prop(self, "printer_id")
if self.printer_id != 'NONE': if self.printer_id != 'NONE':
layout.prop(self, "filament_enum") layout.prop(self, "filament_id")
layout.prop(self, "print_enum") layout.prop(self, "print_id")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
self.ensure_profiles_loaded() self.ensure_profiles_loaded()
@ -222,9 +155,9 @@ class SLICENODE_PrusaslicerConf(SliceNode):
cache = self.get_cache() cache = self.get_cache()
return { return {
'printer': self.printer_id, 'printer': "printer:" + self.printer_id,
'filament': [self.filament_id], 'filament': ["filament:" + self.filament_id],
'print': self.print_id, 'print': "print:" + self.print_id,
'cache': cache 'cache': cache
} }

View file

@ -61,25 +61,19 @@ class LocalCache:
# Recalculate inheritance for all profiles # Recalculate inheritance for all profiles
for profile in self.display_profiles.values(): for profile in self.display_profiles.values():
profile.generate_inherited_confs(self.profiles) profile.generate_inherited_conf(self.profiles)
return changed, added, deleted return changed, added, deleted
def evaluate_compatibility(self, enabled_printers: set[str], enabled_vendors: set[str]): def evaluate_compatibility(self, enabled_printers: set[str], enabled_vendors: set[str]):
"""Evaluates compatibility expressions for all active profiles.""" """Evaluates compatibility expressions for all active profiles."""
for k, printer in self.printer_profiles.items(): for k in enabled_printers:
if k not in enabled_printers: printer = self.printer_profiles["printer:" + k]
continue
# Combine vendors with empty string for default/generic profiles
active_vendors = enabled_vendors | {''} active_vendors = enabled_vendors | {''}
active_filaments = {k: p for k, p in self.filament_profiles.items() if p.vendor in active_vendors} active_filaments = {k: p for k, p in self.filament_profiles.items() if p.vendor in active_vendors}
compats = { printer.evaluate_compatibility(active_filaments | self.print_profiles)
k: p.compatibility_expression
for k, p in (active_filaments | self.print_profiles).items()
}
printer.evaluate_compatibility(compats)
def generate_conf_writer( def generate_conf_writer(
self, self,
@ -103,9 +97,9 @@ class LocalCache:
all_keys.discard('cooling_slowdown_logic') all_keys.discard('cooling_slowdown_logic')
for key in all_keys: for key in all_keys:
from .fields import search_db
key_props = search_db.get(key) key_props = search_db.get(key)
if not key_props: if not key_props:
print(f"Warning: Key '{key}' unrecognized in fields database.")
continue continue
default_val = key_props.get('default', '') default_val = key_props.get('default', '')

View file

@ -1,61 +1,67 @@
from __future__ import annotations from __future__ import annotations
import tempfile import tempfile
from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any, TYPE_CHECKING from typing import Any, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from .parser import ExprNode from .parser import ExprNode
@dataclass
class Profile: class Profile:
key: str def __init__(
category: str self,
path: Path key: str,
has_header: bool category: str,
path: Path,
has_header: bool,
conf_dict: dict[str, Any] conf_dict: dict[str, Any]
):
self.key = key
self.category = category
self.path = path
self.has_header = has_header
self.conf_dict = conf_dict
id: str = field(init=False)
vendor: str = ""
all_conf_dict: dict[str, Any] = field(default_factory=dict)
compatible_profiles: list[str] = field(default_factory=list)
compatibility_expression: ExprNode | None = None
def __post_init__(self):
self.id = self.key.split(':')[1] if len(self.key.split(':')) > 1 else self.key self.id = self.key.split(':')[1] if len(self.key.split(':')) > 1 else self.key
self.vendor = ""
self.all_conf_dict: dict[str, Any] = {}
self.compatibility_expression: ExprNode | None = None
def evaluate_compatibility(self, compats: dict[str, ExprNode | None]): self.compatible_profiles: list[Profile] = []
self.compatible_profiles = []
if self.category.startswith('printer:'):
return
for key, compat in compats.items(): @property
if not compat: def compatible_print_profiles(self) -> list[Profile]:
self.compatible_profiles.append(key) return [k for k in self.compatible_profiles if k.category == 'print']
continue
if compat.eval(self.all_conf_dict):
self.compatible_profiles.append(key)
print(f'Evaled {self.id}') @property
def compatible_filament_profiles(self) -> list[Profile]:
return [k for k in self.compatible_profiles if k.category == 'filament']
def generate_inherited_confs(self, all_profiles: dict[str, Profile]): def generate_inherited_conf(self, all_profiles: dict[str, Profile]):
"""Calculates the full configuration dictionary by resolving inheritance."""
from .utils import generate_conf from .utils import generate_conf
from .parser import Parser from .parser import Parser
self.all_conf_dict = generate_conf(all_profiles, self.key) self.all_conf_dict = generate_conf(all_profiles, self.key)
self.vendor = self.all_conf_dict.get('filament_vendor', '')
if self.category == 'printer': if self.category == 'printer':
nozzle_diameters = self.all_conf_dict.get('nozzle_diameter', '0.4') nozzle_diameters = self.all_conf_dict.get('nozzle_diameter', '0.4')
self.all_conf_dict['num_extruders'] = str(len(nozzle_diameters.split(','))) self.all_conf_dict['num_extruders'] = str(len(nozzle_diameters.split(',')))
self.vendor = self.all_conf_dict.get('filament_vendor', '')
if exp := self.all_conf_dict.get('compatible_printers_condition'): if exp := self.all_conf_dict.get('compatible_printers_condition'):
try: try:
self.compatibility_expression = Parser(exp).parse() self.compatibility_expression = Parser(exp).parse()
except Exception as e: except Exception as e:
print(f'Expression parsing failed for {self.id}: {exp} - {e}') print(f'Expression parsing failed for {self.id}: {exp} - {e}')
def evaluate_compatibility(self, profiles: dict[str, Profile]):
"""Checks which of the provided profiles are compatible with this one."""
self.compatible_profiles = []
for key, p in profiles.items():
compat = p.compatibility_expression
if not compat or compat.eval(self.all_conf_dict):
self.compatible_profiles += [p]
class ConfigWriter: class ConfigWriter:
def __init__(self, conf: dict[str, str | list[str]]) -> None: def __init__(self, conf: dict[str, str | list[str]]) -> None:
self.config_dict: dict[str, str | list[str]] = conf self.config_dict: dict[str, str | list[str]] = conf

View file

@ -37,7 +37,7 @@ class TestPrusaSlicerProfiles(unittest.TestCase):
profile = self.cache.profiles[filament_keys[0]] profile = self.cache.profiles[filament_keys[0]]
self.assertTrue(len(profile.all_conf_dict) > 0) self.assertTrue(len(profile.all_conf_dict) > 0)
self.assertTrue(len(profile.vendor) > 0 or profile.all_conf_dict.get('filament_vendor') is None) self.assertTrue(profile.vendor or profile.all_conf_dict.get('filament_vendor') is None)
def test_fields_database(self): def test_fields_database(self):
"""Verify that the fields database is accessible and searchable.""" """Verify that the fields database is accessible and searchable."""