adding prusaslicer base code

This commit is contained in:
Nicolas Predella 2026-05-15 14:37:54 +03:00
parent d19fa0044a
commit 131c676a38
11 changed files with 4619 additions and 1 deletions

24
infra/csv_handling.py Normal file
View file

@ -0,0 +1,24 @@
from functools import lru_cache
import csv
import os
@lru_cache(maxsize=128)
def _load_csv(filename: str, mtime: float, encoding: str | None = None) -> tuple[tuple[str, ...], ...]:
with open(filename, 'r', newline='', encoding=encoding) as f:
reader = csv.reader(f)
return tuple(tuple(row) for row in reader)
def parse_csv_to_tuples(filename: str) -> list[tuple[str, ...]]:
current_mtime = os.path.getmtime(filename)
data: tuple[tuple[str, ...], ...] = _load_csv(filename, current_mtime)
return sorted(data, key=lambda x: x[1])
def parse_csv_to_dict(filename: str) -> dict[str, list[str]]:
current_mtime = os.path.getmtime(filename)
data: tuple[tuple[str, ...], ...] = _load_csv(filename, current_mtime, encoding='utf-8-sig')
result = {}
for row in data:
if row:
result[row[0]] = list(row[1:])
return result

13
infra/json_handling.py Normal file
View file

@ -0,0 +1,13 @@
import os, json
def dict_from_json(path):
with open(path, 'r') as file:
return json.load(file)
def dump_dict_to_json(dictionary, path):
folder = os.path.dirname(path)
if folder and not os.path.exists(folder):
os.makedirs(folder, exist_ok=True)
with open(path, 'w') as file:
json.dump(dictionary, file, indent=2)

View file

@ -216,7 +216,7 @@ class CrealityBackend(PrinterHttpBackend):
@with_api_state('STARTING') @with_api_state('STARTING')
def start_print(self, gcode: Path, name: str) -> None: def start_print(self, gcode: Path, name: str) -> None:
from ..infra.ftp import ftp_upload, ftp_wipe, ftp_get_filesize from ..infra.ftp_handling import ftp_upload, ftp_wipe, ftp_get_filesize
file_size = gcode.stat().st_size file_size = gcode.stat().st_size

View file

@ -0,0 +1,42 @@
# PrusaSlicer Profiles Module
This module handles the parsing, management, and evaluation of PrusaSlicer configuration profiles (`.ini` files). It supports profile inheritance, compatibility evaluation using PrusaSlicer's expression language, and generating merged configurations for slicing.
## Core Components
### 1. `profiles.py`
- **`Profile` Class**: Represents a single PrusaSlicer profile (Printer, Filament, or Print).
- Stores configuration as a dictionary (`conf_dict`).
- Handles inheritance logic via `generate_inherited_confs`.
- Supports compatibility checks via `evaluate_compatibility`.
- **`generate_conf` Function**: Recursively merges inherited profiles into a single configuration dictionary.
### 2. `profile_cache.py`
- **`LocalCache` Class**: Manages a collection of profiles loaded from disk.
- Scans directories for `.ini` files and tracks changes using file metadata.
- Categorizes profiles into `printers`, `print`, and `filament`.
- `evaluate_compatibility`: Checks which filament and print profiles are compatible with specific printer profiles.
- `generate_conf_writer`: Creates a `ConfigWriter` by merging a printer, print, and one or more filament profiles (multi-extruder support).
- **`ConfigWriter` Class**: Helper to write the merged configuration to a file (e.g., for passing to a slicer or embedding in a 3MF).
- **Utility Functions**: `ini_to_dict` and `ini_content_to_dict` use `ConfigParser` to parse PrusaSlicer's INI format.
### 3. `expression_parser.py`
- Implements a recursive descent parser for PrusaSlicer's expression language (used in `compatible_printers_condition` and similar fields).
- **AST Nodes**: `ExprNode`, `LiteralNode`, `VarNode`, `IndexNode`, `UnaryOpNode`, `BinaryOpNode`.
- **`Parser` Class**: Tokenizes and parses expression strings into an AST that can be evaluated against a configuration dictionary.
### 4. `prusaslicer_fields.py`
- Loads metadata about PrusaSlicer configuration fields from `prusaslicer_fields.json` and `prusaslicer_modifier_fields.csv`.
- Provides `search_in_db` and `search_in_mod_db` for looking up field definitions, types, and labels.
## Data Files
- `prusaslicer_fields.json`: Exhaustive database of PrusaSlicer settings, including types, default values, and labels.
- `prusaslicer_modifier_fields.csv`: List of fields that are valid as modifiers.
## Typical Workflow
1. Initialize `LocalCache`.
2. Call `cache.load([paths_to_profiles])`.
3. Select a printer, print, and filament profile(s).
4. (Optional) Evaluate compatibility using `cache.evaluate_compatibility`.
5. Generate a merged config using `cache.generate_conf_writer`.
6. Use `ConfigWriter` to output the final configuration for slicing.

View file

@ -0,0 +1,335 @@
from __future__ import annotations
import operator
from typing import TYPE_CHECKING, TypeAlias
if TYPE_CHECKING:
Token: TypeAlias = tuple[str, str]
import re
# === AST Node Definitions ===
_ops = {
"==": (str, operator.eq),
"!=": (str, operator.ne),
"<": (float, operator.lt),
">": (float, operator.gt),
"<=": (float, operator.le),
">=": (float, operator.ge),
}
class ExprNode:
def eval(self, context: dict[str, str]) -> str | float | re.Pattern[str] | bool:
raise NotImplementedError("Must implement eval in subclass")
class LiteralNode(ExprNode):
def __init__(self, value: str | re.Pattern[str]) -> None:
self.value: str | re.Pattern[str] = value
def eval(self, context: dict[str, str]) -> str | float | re.Pattern[str] | bool:
return self.value
class VarNode(ExprNode):
def __init__(self, name: str) -> None:
self.name: str = name
def eval(self, context: dict[str, str]) -> str | float | re.Pattern[str] | bool:
return context.get(self.name, '0')
class IndexNode(ExprNode):
def __init__(self, name: str, index: int) -> None:
self.name: str = name
self.index: int = index
def eval(self, context: dict[str, str]) -> str | float | re.Pattern[str] | bool:
val = context.get(self.name, '0')
val = val.split(',')
if self.index > len(val) -1: return '0'
return val[self.index]
class UnaryOpNode(ExprNode):
def __init__(self, op: str, child: ExprNode) -> None:
self.op = op
self.child = child
def eval(self, context: dict[str, str]) -> str | float | re.Pattern[str] | bool:
cval = self.child.eval(context)
if isinstance(cval, re.Pattern):
raise TypeError("Unary Operator cannot evaluate type re.Pattern")
if self.op == "!":
return not float(cval)
raise RuntimeError(f"Unknown unary operator {self.op}")
class BinaryOpNode(ExprNode):
def __init__(self, left: ExprNode, op: str, right: ExprNode) -> None:
self.left = left
self.op = op
self.right = right
def eval(self, context: dict[str, str]) -> str | float | re.Pattern[str] | bool:
if self.op in ["and", "&&"]:
if not self.left.eval(context): return False
return self.right.eval(context)
if self.op == "or":
if self.left.eval(context): return True
return self.right.eval(context)
# For everything else, we evaluate both sides
lv: str | float | re.Pattern[str] | bool = self.left.eval(context)
rv: str | float | re.Pattern[str] | bool = self.right.eval(context)
caster, func = _ops.get(self.op, (None, None))
if caster and func:
if isinstance(lv, re.Pattern) or isinstance(rv, re.Pattern):
raise RuntimeError(f"Operands of {self.op} must not be regex patterns")
return func(caster(lv), caster(rv))
if self.op == "=~":
if not isinstance(rv, re.Pattern):
raise TypeError("Right operand of =~ must be a regex pattern")
return rv.search(str(lv)) is not None
if self.op == "!~":
if not isinstance(rv, re.Pattern):
raise TypeError("Right operand of !~ must be a regex pattern")
return rv.search(str(lv)) is None
raise RuntimeError(f"Unknown binary operator {self.op}")
# === Parser & Tokenizer ===
class Parser:
def __init__(self, text: str):
self.text = text
self.tokens: list[Token] = self._tokenize(text)
self.i = 0 # index into tokens
def _tokenize(self, text: str) -> list[Token]:
tokens: list[Token] = []
i = 0
length: int = len(text)
while i < length:
c = text[i]
# Skip whitespace
if c.isspace():
i += 1
continue
# Parentheses / Brackets
if c == "(":
tokens.append(("LPAREN", c)); i += 1; continue
if c == ")":
tokens.append(("RPAREN", c)); i += 1; continue
if c == "[":
tokens.append(("LBRACK", c)); i += 1; continue
if c == "]":
tokens.append(("RBRACK", c)); i += 1; continue
# Multi‐char operators: check longest first
# Order matters: check 3‐character operators? We only have 2‐char: ==, !=, >=, <=, =~, !~, and the single‐char '!'
if text.startswith("==", i):
tokens.append(("OP", "==")); i += 2; continue
if text.startswith("!=", i):
tokens.append(("OP", "!=")); i += 2; continue
if text.startswith(">=", i):
tokens.append(("OP", ">=")); i += 2; continue
if text.startswith("<=", i):
tokens.append(("OP", "<=")); i += 2; continue
if text.startswith("=~", i):
tokens.append(("OP", "=~")); i += 2; continue
if text.startswith("!~", i):
tokens.append(("OP", "!~")); i += 2; continue
if text.startswith("&&", i):
tokens.append(("OP", "and")); i += 2; continue
if text.startswith("||", i):
tokens.append(("OP", "or")); i += 2; continue
# Single‐char '>' or '<'
if c == ">":
tokens.append(("OP", ">")); i += 1; continue
if c == "<":
tokens.append(("OP", "<")); i += 1; continue
# Single‐char '!'
if c == "!":
tokens.append(("OP", "!")); i += 1; continue
# Regex literal: starts with '/'
if c == "/":
# Read until the next unescaped '/'
j = i + 1
while j < length:
if text[j] == "/" and text[j-1] != "\\":
break
j += 1
if j >= length:
raise SyntaxError("Unterminated regex literal")
pattern_body = text[i+1:j]
tokens.append(("REGEX", pattern_body))
i = j + 1
continue
# Quoted string literal: " … " or ' … '
if c in ("'", '"'):
quote_char = c
j = i + 1
while j < length:
if text[j] == quote_char and text[j-1] != "\\":
break
j += 1
if j >= length:
raise SyntaxError("Unterminated string literal")
str_body = text[i+1: j]
# We do not interpret escape sequences here; we store raw content
tokens.append(("STRING", str_body))
i = j + 1
continue
# Number: [0-9]+(\.[0-9]+)?
if c.isdigit():
j = i
while j < length and (text[j].isdigit() or text[j] == "."):
j += 1
num_text = text[i:j]
tokens.append(("NUMBER", num_text))
i = j
continue
# Identifier or keyword (and/or)
if c.isalpha() or c == "_":
j = i
while j < length and (text[j].isalnum() or text[j] == "_"):
j += 1
ident = text[i:j]
if ident == "and":
tokens.append(("OP", "and"))
elif ident == "or":
tokens.append(("OP", "or"))
else:
tokens.append(("IDENT", ident))
i = j
continue
# If we get here, it's an unexpected character
raise SyntaxError(f"Unexpected character: {c}")
return tokens
def _peek(self) -> Token | None:
if self.i < len(self.tokens):
return self.tokens[self.i]
return None
def _advance(self) -> Token:
if self.i >= len(self.tokens):
raise SyntaxError("Unexpected end of input")
tok = self.tokens[self.i]
self.i += 1
return tok
def _expect(self, ttype: str, value: str | None = None) -> Token:
tok = self._peek()
if tok is None:
raise SyntaxError(f"Expected {ttype} but found end of input")
if tok[0] != ttype or (value is not None and tok[1] != value):
raise SyntaxError(f"Expected {ttype} {value!r} but found {tok}")
return self._advance()
def parse(self) -> ExprNode:
node = self._parse_or()
if self._peek() is not None:
raise SyntaxError(f"Unexpected token after end: {self._peek()}")
return node
def _parse_or(self) -> ExprNode:
left = self._parse_and()
while True:
tok = self._peek()
if tok is not None and tok[0] == "OP" and tok[1] == "or":
self._advance()
right = self._parse_and()
left = BinaryOpNode(left, "or", right)
else:
break
return left
def _parse_and(self) -> ExprNode:
left = self._parse_not()
while True:
tok = self._peek()
if tok is not None and tok[0] == "OP" and tok[1] == "and":
self._advance()
right = self._parse_not()
left = BinaryOpNode(left, "and", right)
else:
break
return left
def _parse_not(self) -> ExprNode:
tok = self._peek()
# If we see a lone '!' (not followed by '=' or '~'), it is unary NOT
if tok is not None and tok[0] == "OP" and tok[1] == "!":
# Make sure it's not part of '!=' or '!~' (those would have been tokenized as OP("!=") or OP("!~"))
self._advance()
child = self._parse_not()
return UnaryOpNode("!", child)
else:
return self._parse_comp()
def _parse_comp(self) -> ExprNode:
left = self._parse_atom()
tok = self._peek()
if tok is not None and tok[0] == "OP" and tok[1] in ("==", "!=", "<", ">", "<=", ">=", "=~", "!~"):
op = tok[1]
self._advance()
right = self._parse_atom()
return BinaryOpNode(left, op, right)
return left
def _parse_atom(self) -> ExprNode:
tok = self._peek()
if tok is None:
raise SyntaxError("Unexpected end of input in atom")
ttype, val = tok
if ttype in ["STRING", "NUMBER"]:
self._advance()
return LiteralNode(val)
if ttype == "REGEX":
self._advance()
try:
pat = re.compile(val)
except re.error as e:
raise SyntaxError(f"Invalid regex /{val}/: {e}")
return LiteralNode(pat)
if ttype == "IDENT":
# Could be a variable or keyword—but "and"/"or" handled earlier
name = val
self._advance()
# Check for indexing: IDENT '[' NUMBER ']'
nxt = self._peek()
if nxt is not None and nxt[0] == "LBRACK":
self._advance() # consume '['
idx_tok = self._expect("NUMBER")
idx = int(idx_tok[1])
self._expect("RBRACK")
return IndexNode(name, idx)
else:
return VarNode(name)
if ttype == "LPAREN":
self._advance()
node = self._parse_or()
self._expect("RPAREN")
return node
raise SyntaxError(f"Unexpected token in atom: {tok}")

View file

@ -0,0 +1,251 @@
from __future__ import annotations
import os
import tempfile
from pathlib import Path
from typing import Any
from .profiles import Profile
class LocalCache:
profiles: dict[str, Profile] = {}
files_metadata: dict[str, Any] = {}
@property
def display_profiles(self) -> dict[str, Profile]:
return {k: profile for k, profile in self.profiles.items() if '*' not in profile.id}
@property
def printers_profiles(self) -> dict[str, Profile]:
return {k: profile for k, profile in self.display_profiles.items() if profile.category == 'printer'}
@property
def print_profiles(self) -> dict[str, Profile]:
return {k: profile for k, profile in self.display_profiles.items() if profile.category == 'print'}
@property
def filament_profiles(self) -> dict[str, Profile]:
return {k: profile for k, profile in self.display_profiles.items() if profile.category == 'filament'}
def load(self, dirs: list[Path | str]) -> tuple[dict[str, tuple[Any, Any]], dict[str, Any], dict[str, Any]]:
old = self.files_metadata.copy()
self._fetch_files_metadata(dirs)
new = self.files_metadata
changed = {k: (old[k], new[k]) for k in old.keys() & new.keys() if old[k] != new[k]}
added = {k: new[k] for k in new.keys() - old.keys()}
deleted = {k: old[k] for k in old.keys() - new.keys()}
if len(changed | added | deleted) == 0: return {}, {}, {}
for deleted_file in deleted:
keys_to_remove = [key for key, val in self.profiles.items() if str(val.path) == deleted_file]
for key in keys_to_remove:
self.profiles.pop(key, None)
for file_path in (changed | added):
self._process_ini_to_cache_dict(file_path)
self.files_metadata = new
for k, profile in self.display_profiles.items():
profile.generate_inherited_confs(self.profiles)
return changed, added, deleted
@property
def vendors(self):
return sorted({ p.vendor for p in self.filament_profiles.values() })
def evaluate_compatibility(self, enabled_printers: set[str], enabled_vendors: set[str]):
for k, profile in self.printers_profiles.items():
if k not in enabled_printers: continue
# if profile.compatible_profiles: continue
enabled_vendors.add("")
enabled_filament_profiles = {k: p for k, p in self.filament_profiles.items() if p.vendor in enabled_vendors | {''}}
profile.evaluate_compatibility({k: pp.compatibility_expression for k, pp in (enabled_filament_profiles | self.print_profiles).items()})
def _fetch_files_metadata(self, dirs: list[Path | str]):
self.files_metadata = {}
# Iterate over all provided directories
for directory in dirs:
sanitized_path = self._sanitize_directory(directory)
if not sanitized_path:
continue
# Use os.walk with followlinks=True to ensure linked folders are processed
for root, _, files in os.walk(sanitized_path, followlinks=True):
for file in files:
if file.endswith('.ini'):
file_path = Path(root) / file
try:
last_modified = file_path.stat().st_mtime
self.files_metadata[str(file_path)] = last_modified
except OSError as e:
print(f"Error reading file {file_path}: {e}")
continue
def _sanitize_directory(self, dir_str: Path | str) -> Path | None:
if not dir_str:
return None
sanitized = Path(os.path.expanduser(dir_str)).resolve()
if not sanitized.is_dir():
print(f"Path is not a valid directory: {dir_str}")
return None
return sanitized
def _process_ini_to_cache_dict(self, path: str):
# Convert ConfigParser content into a dictionary
has_header, ini_dict = ini_to_dict(path)
# Flatten the dictionary for profiles and add to self.config_headers
for key, conf_dict in ini_dict.items():
if ":" in key:
self.profiles[key] = Profile(
key,
key.split(':')[0] if len(key.split(':')) > 1 else '',
Path(path),
has_header,
conf_dict
)
return
def generate_conf_writer(self, printer_profile: str, filament_profile: list[str], print_profile: str, overrides: dict[str, dict[str, str]]) -> 'ConfigWriter':
from .prusaslicer_fields import search_db
conf = {}
# add printer and print profile
conf.update(self.profiles[printer_profile].all_conf_dict)
conf.update(self.profiles[print_profile].all_conf_dict)
# add filament profiles per extruder
filament_merged_conf: dict[str, str] = {}
filament_confs: list[dict[str, str]] = [self.profiles[profile].all_conf_dict for profile in filament_profile]
common_keys: set[str] = set().union(*filament_confs)
common_keys.discard('cooling_slowdown_logic')
for key in common_keys:
key_props: dict[str, Any] | None = search_db.get(key, None)
if not key_props: print(f'Key {key} unrecognized! Check your configuration'); continue
default_value: str = key_props.get('default', '')
key_type: str = key_props['type']
separator = ',' if key_type in ['coPercents', 'coFloats', 'coFloatsOrPercents', 'coInts', 'coIntsNullable', 'coBools', 'coPoints'] else ';'
values: list[str] = [
d.get(key, default_value)
for d in filament_confs
]
joined: str = separator.join(values)
filament_merged_conf[key] = joined
conf.update(filament_merged_conf)
# overwrite with overrides
conf.update({k: o['value'] for k, o in overrides.items()})
# add profile names
conf.update({
'printer_settings_id': printer_profile.split(":")[1],
'filament_settings_id': ";".join(p.split(":")[1] for p in filament_profile),
'print_settings_id': print_profile.split(":")[1],
})
return ConfigWriter(conf)
class ConfigWriter:
def __init__(self, conf: dict[str, str | list[str]]) -> None:
self.config_dict: dict[str, str | list[str]] = conf
self.temp_dir = tempfile.gettempdir()
def write_ini_3mf(self, config_local_path: Path):
with open(config_local_path, 'w') as file:
for key, val in dict(sorted(self.config_dict.items())).items():
file.write(f"; {key} = {val}\n")
def get(self, key: str, default: Any = None) -> str | list[str]:
return self.config_dict[key]
@property
def checksum(self) -> int:
import json, zlib
data = json.dumps(self.config_dict, sort_keys=True).encode("utf-8")
return zlib.crc32(data) & 0xFFFFFFFF
def generate_conf(profiles: dict[str, Any], id: str) -> dict[str, str]:
if not (profile := profiles.get(id)): return {}
if not profile.conf_dict: return {}
conf_current = profiles[id].conf_dict # Copy to avoid modifying the original config
if conf_current.get('inherits', False):
curr_category = id.split(":")[0]
inherited_ids = [curr_category + ":" + inherit_id.strip() for inherit_id in conf_current['inherits'].split(';')] # Split on semicolon for multiple inheritance
merged_conf = {}
for inherit_id in inherited_ids:
if inherit_id in profiles:
inherited_conf = generate_conf(profiles, inherit_id) # Recursive call for each inherited config
merged_conf.update(inherited_conf) # Merge each inherited config
merged_conf.update(conf_current) # Update with current config values (overriding inherited)
conf_current = merged_conf
conf_current.pop('inherits', None)
conf_current.pop('renamed_from', None)
return conf_current
import re
from configparser import ConfigParser, MissingSectionHeaderError
def ini_to_dict(path: str) -> tuple[bool, dict[str, dict[str, str]]]:
# Read the file content from the path
with open(path, 'r') as file:
content = file.read()
config = ConfigParser(interpolation=None)
try:
# Attempt to parse the content
config.read_string(content)
has_header = True
except MissingSectionHeaderError:
# Determine category based on specific IDs in the content
if re.search(r'^filament_settings_id', content, re.MULTILINE):
cat = 'filament'
elif re.search(r'^print_settings_id', content, re.MULTILINE):
cat = 'print'
elif re.search(r'^printer_settings_id', content, re.MULTILINE):
cat = 'printer'
else:
raise ValueError(f"Unable to determine category for the INI file: {path}")
# Extract the filename without extension
name = os.path.splitext(os.path.basename(path))[0]
# Create a default section with the determined category and name
default_section = f"[{cat}:{name}]\n" + content
config.read_string(default_section)
has_header = False
# Convert ConfigParser content into a dictionary
return has_header, {
section: dict(sorted(config.items(section)))
for section in sorted(config.sections())
}
def ini_content_to_dict(path: str) -> dict[str, str]:
# Read the file content from the path
with open(path, 'r') as file:
content = file.read()
config = ConfigParser(interpolation=None)
default_section = f"[default:default]\n" + content
config.read_string(default_section)
return dict(sorted(config.items(config.sections()[0])))

View file

@ -0,0 +1,55 @@
from pathlib import Path
from typing import Any
from .expression_parser import Parser, ExprNode
class Profile():
def __init__(self, key: str, category: str, path: Path, has_header: bool, conf_dict: dict[str, Any]):
self.id: str = key.split(':')[1] if len(key.split(':')) > 1 else key
self.key: str = key
self.category: str = category
self.vendor: str = ''
self.path: Path = path
self.has_header: bool = has_header
self.conf_dict: dict[str, Any] = conf_dict
self.all_conf_dict: dict[str, Any] = {}
self.compatible_profiles: list[str] = []
self.compatibility_expression: ExprNode | None = None
def evaluate_compatibility(self, compats: dict[str, ExprNode | None]):
self.compatible_profiles = []
if self.category.startswith('printer:'): return
for key, compat in compats.items():
if not compat: self.compatible_profiles.append(key); continue
if compat.eval(self.all_conf_dict): self.compatible_profiles.append(key)
pass
print(f'Evaled {self.id}')
def generate_inherited_confs(self, all_confs_dict: dict[str, Any] = {}):
self.all_conf_dict = generate_conf(all_confs_dict, self.key)
if self.category == 'printer': self.all_conf_dict['num_extruders'] = str(len(self.all_conf_dict['nozzle_diameter'].split(',')))
self.vendor = self.all_conf_dict.get('filament_vendor', '')
if exp := self.all_conf_dict.get('compatible_printers_condition'):
try:
self.compatibility_expression = Parser(exp).parse()
except:
print(f'Expression parsing failed: {exp}')
def generate_conf(profiles: dict[str, Any], id: str) -> dict[str, str]:
if not (profile := profiles.get(id)): return {}
if not profile.conf_dict: return {}
conf_current = profiles[id].conf_dict # Copy to avoid modifying the original config
if conf_current.get('inherits', False):
curr_category = id.split(":")[0]
inherited_ids = [curr_category + ":" + inherit_id.strip() for inherit_id in conf_current['inherits'].split(';')] # Split on semicolon for multiple inheritance
merged_conf = {}
for inherit_id in inherited_ids:
if inherit_id in profiles:
inherited_conf = generate_conf(profiles, inherit_id) # Recursive call for each inherited config
merged_conf.update(inherited_conf) # Merge each inherited config
merged_conf.update(conf_current) # Update with current config values (overriding inherited)
conf_current = merged_conf
conf_current.pop('inherits', None)
conf_current.pop('renamed_from', None)
return conf_current

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any
import os
from functools import lru_cache
from ..json_handling import dict_from_json
from ..csv_handling import parse_csv_to_dict
search_db_path: str = os.path.join('prusaslicer_fields.json')
search_db: dict[str, dict[str, Any]] = dict_from_json(search_db_path)
search_db_mod_path: str = os.path.join('prusaslicer_modifier_fields.csv')
search_db_mod: dict[str, list[str]] = parse_csv_to_dict(search_db_mod_path)
@lru_cache(maxsize=128)
def search_in_db(term) -> dict[str, dict[str, Any]]:
words: Any = term.lower().split()
return {k: v for k, v in search_db.items() if all([word in k+" "+v.get('label','') for word in words])}
@lru_cache(maxsize=128)
def search_in_mod_db(term) -> dict[str, dict[str, Any]]:
words = term.lower().split()
filtered_search_db = {k: v for k, v in search_db.items() if k in search_db_mod}
return {k: v for k, v in filtered_search_db.items() if all([word in k+" "+v.get('label','') for word in words])}
pass

View file

@ -0,0 +1,52 @@
bottom_fill_pattern
bottom_solid_layers
bottom_solid_min_thickness
bridge_angle
bridge_flow_ratio
bridge_speed
enable_dynamic_overhang_speeds
external_perimeter_extrusion_width
external_perimeter_speed
external_perimeters_first
extra_perimeters
extra_perimeters_on_overhangs
fill_angle
fill_density
fill_pattern
fuzzy_skin
fuzzy_skin_point_dist
fuzzy_skin_thickness
gap_fill_enabled
gap_fill_speed
infill_anchor
infill_anchor_max
infill_every_layers
infill_extrusion_width
infill_overlap
infill_speed
ironing
ironing_flowrate
ironing_spacing
ironing_speed
ironing_type
only_one_perimeter_first_layer
overhang_speed_0
overhang_speed_1
overhang_speed_2
overhang_speed_3
overhangs
perimeter_extrusion_width
perimeter_speed
perimeters
small_perimeter_speed
solid_infill_below_area
solid_infill_every_layers
solid_infill_extrusion_width
solid_infill_speed
thin_walls
top_fill_pattern
top_infill_extrusion_width
top_one_perimeter_type
top_solid_infill_speed
top_solid_layers
top_solid_min_thickness
1 bottom_fill_pattern
2 bottom_solid_layers
3 bottom_solid_min_thickness
4 bridge_angle
5 bridge_flow_ratio
6 bridge_speed
7 enable_dynamic_overhang_speeds
8 external_perimeter_extrusion_width
9 external_perimeter_speed
10 external_perimeters_first
11 extra_perimeters
12 extra_perimeters_on_overhangs
13 fill_angle
14 fill_density
15 fill_pattern
16 fuzzy_skin
17 fuzzy_skin_point_dist
18 fuzzy_skin_thickness
19 gap_fill_enabled
20 gap_fill_speed
21 infill_anchor
22 infill_anchor_max
23 infill_every_layers
24 infill_extrusion_width
25 infill_overlap
26 infill_speed
27 ironing
28 ironing_flowrate
29 ironing_spacing
30 ironing_speed
31 ironing_type
32 only_one_perimeter_first_layer
33 overhang_speed_0
34 overhang_speed_1
35 overhang_speed_2
36 overhang_speed_3
37 overhangs
38 perimeter_extrusion_width
39 perimeter_speed
40 perimeters
41 small_perimeter_speed
42 solid_infill_below_area
43 solid_infill_every_layers
44 solid_infill_extrusion_width
45 solid_infill_speed
46 thin_walls
47 top_fill_pattern
48 top_infill_extrusion_width
49 top_one_perimeter_type
50 top_solid_infill_speed
51 top_solid_layers
52 top_solid_min_thickness