conversor3mf 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conversor3mf/__init__.py +0 -0
- conversor3mf/__main__.py +111 -0
- conversor3mf/ace.py +203 -0
- conversor3mf/config.py +106 -0
- conversor3mf/conversor.py +438 -0
- conversor3mf/cores.py +133 -0
- conversor3mf/ilhas.py +447 -0
- conversor3mf/pintura.py +96 -0
- conversor3mf/projeto.py +220 -0
- conversor3mf/servidor_mcp.py +151 -0
- conversor3mf/verificar.py +75 -0
- conversor3mf-0.1.0.dist-info/METADATA +164 -0
- conversor3mf-0.1.0.dist-info/RECORD +17 -0
- conversor3mf-0.1.0.dist-info/WHEEL +5 -0
- conversor3mf-0.1.0.dist-info/entry_points.txt +3 -0
- conversor3mf-0.1.0.dist-info/licenses/LICENSE +21 -0
- conversor3mf-0.1.0.dist-info/top_level.txt +1 -0
conversor3mf/__init__.py
ADDED
|
File without changes
|
conversor3mf/__main__.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Linha de comando: python -m conversor3mf <comando> ..."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import ace as modulo_ace
|
|
11
|
+
from . import ilhas
|
|
12
|
+
from .conversor import converter, planejar
|
|
13
|
+
from .projeto import ler_projeto
|
|
14
|
+
from .verificar import verificar
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _mapa(texto: str | None) -> dict[int, int]:
|
|
18
|
+
"""'1=3,2=1' -> {1: 3, 2: 1}"""
|
|
19
|
+
if not texto:
|
|
20
|
+
return {}
|
|
21
|
+
mapa = {}
|
|
22
|
+
for par in texto.split(","):
|
|
23
|
+
origem, _, slot = par.partition("=")
|
|
24
|
+
mapa[int(origem)] = int(slot)
|
|
25
|
+
return mapa
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _mostrar(dados) -> None:
|
|
29
|
+
print(json.dumps(dados, indent=2, ensure_ascii=False))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv: list[str] | None = None) -> int:
|
|
33
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
34
|
+
p = argparse.ArgumentParser(prog="conversor3mf", description="Converte 3MF da Bambu para a Kobra X na ordem do seu ACE.")
|
|
35
|
+
sub = p.add_subparsers(dest="comando", required=True)
|
|
36
|
+
|
|
37
|
+
s = sub.add_parser("inspecionar", help="mostra filamentos, cores e onde cada um é usado")
|
|
38
|
+
s.add_argument("arquivo")
|
|
39
|
+
|
|
40
|
+
s = sub.add_parser("planejar", help="mostra em qual slot cada cor vai sair, sem gravar nada")
|
|
41
|
+
s.add_argument("arquivo")
|
|
42
|
+
s.add_argument("--mapa", help="força slots, ex.: 1=3,2=1 (filamento do modelo = slot do ACE)")
|
|
43
|
+
|
|
44
|
+
s = sub.add_parser("converter", help="grava o 3MF convertido")
|
|
45
|
+
s.add_argument("arquivo")
|
|
46
|
+
s.add_argument("--mapa", help="força slots, ex.: 1=3,2=1")
|
|
47
|
+
s.add_argument("--saida", help="caminho do arquivo convertido (padrão: <nome>_KobraX.3mf)")
|
|
48
|
+
s.add_argument("--sem-verificar", action="store_true", help="não confere no slicer depois de gravar")
|
|
49
|
+
|
|
50
|
+
s = sub.add_parser("verificar", help="abre o arquivo no Anycubic Slicer Next e confere")
|
|
51
|
+
s.add_argument("arquivo")
|
|
52
|
+
|
|
53
|
+
s = sub.add_parser("ilhas", help="desenha o objeto com o número de cada região de mesma cor")
|
|
54
|
+
s.add_argument("arquivo")
|
|
55
|
+
s.add_argument("--vista", default="cima", choices=["cima", "baixo", "frente", "tras", "esquerda", "direita"])
|
|
56
|
+
s.add_argument("--objeto")
|
|
57
|
+
s.add_argument("--regiao", help="zoom em mm: x0,y0,x1,y1")
|
|
58
|
+
s.add_argument("--png", help="onde salvar a imagem (padrão: <nome>_ilhas_<vista>.png ao lado)")
|
|
59
|
+
|
|
60
|
+
s = sub.add_parser("repintar", help="grava um 3MF novo com ilhas em outro slot")
|
|
61
|
+
s.add_argument("arquivo")
|
|
62
|
+
s.add_argument("mudancas", help="ilha=slot separados por vírgula, ex.: 32=4,3=3")
|
|
63
|
+
s.add_argument("--objeto")
|
|
64
|
+
s.add_argument("--saida")
|
|
65
|
+
|
|
66
|
+
s = sub.add_parser("ace", help="mostra ou altera os slots do ACE")
|
|
67
|
+
acoes = s.add_subparsers(dest="acao", required=True)
|
|
68
|
+
acoes.add_parser("mostrar")
|
|
69
|
+
d = acoes.add_parser("definir", help="usa um projeto salvo no slicer (com o ACE sincronizado) como molde")
|
|
70
|
+
d.add_argument("projeto")
|
|
71
|
+
t = acoes.add_parser("slot", help="troca cor/tipo/nome de um slot")
|
|
72
|
+
t.add_argument("numero", type=int)
|
|
73
|
+
t.add_argument("--cor")
|
|
74
|
+
t.add_argument("--tipo")
|
|
75
|
+
t.add_argument("--nome")
|
|
76
|
+
|
|
77
|
+
a = p.parse_args(argv)
|
|
78
|
+
if a.comando == "inspecionar":
|
|
79
|
+
_mostrar(ler_projeto(a.arquivo).resumo())
|
|
80
|
+
elif a.comando == "planejar":
|
|
81
|
+
_mostrar(planejar(ler_projeto(a.arquivo), modulo_ace.carregar(), _mapa(a.mapa)).resumo())
|
|
82
|
+
elif a.comando == "converter":
|
|
83
|
+
resultado = converter(a.arquivo, a.saida, _mapa(a.mapa))
|
|
84
|
+
dados = resultado.resumo()
|
|
85
|
+
if not a.sem_verificar:
|
|
86
|
+
dados["verificacao"] = verificar(resultado.destino)
|
|
87
|
+
_mostrar(dados)
|
|
88
|
+
elif a.comando == "verificar":
|
|
89
|
+
_mostrar(verificar(a.arquivo))
|
|
90
|
+
elif a.comando == "ilhas":
|
|
91
|
+
regiao = [float(x) for x in a.regiao.split(",")] if a.regiao else None
|
|
92
|
+
png, info = ilhas.mapa_de_ilhas(a.arquivo, a.objeto, a.vista, regiao)
|
|
93
|
+
destino = Path(a.png) if a.png else Path(a.arquivo).with_name(f"{Path(a.arquivo).stem}_ilhas_{a.vista}.png")
|
|
94
|
+
destino.write_bytes(png)
|
|
95
|
+
_mostrar({"imagem": str(destino), **info})
|
|
96
|
+
elif a.comando == "repintar":
|
|
97
|
+
resultado = ilhas.repintar_ilhas(a.arquivo, _mapa(a.mudancas), a.objeto, a.saida)
|
|
98
|
+
resultado["verificacao"] = verificar(resultado["arquivo_repintado"])
|
|
99
|
+
_mostrar(resultado)
|
|
100
|
+
elif a.comando == "ace":
|
|
101
|
+
if a.acao == "mostrar":
|
|
102
|
+
_mostrar(modulo_ace.carregar().resumo())
|
|
103
|
+
elif a.acao == "definir":
|
|
104
|
+
_mostrar(modulo_ace.definir_molde(a.projeto).resumo())
|
|
105
|
+
else:
|
|
106
|
+
_mostrar(modulo_ace.alterar_slot(a.numero, a.cor, a.tipo, a.nome).resumo())
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
raise SystemExit(main())
|
conversor3mf/ace.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Molde de configuração da impressora e slots do ACE.
|
|
2
|
+
|
|
3
|
+
O molde é tirado de um projeto salvo pelo próprio Anycubic Slicer Next depois de
|
|
4
|
+
sincronizar o ACE: ele já traz impressora, perfis de filamento e a ordem real
|
|
5
|
+
dos carretéis. O arquivo `dados/meu_ace.json` guarda os slots e pode ser
|
|
6
|
+
editado à mão (por exemplo, para trocar a cor de um slot sem salvar outro molde).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import zipfile
|
|
14
|
+
from dataclasses import asdict, dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from . import cores
|
|
18
|
+
from .config import pasta_dados, pasta_perfis
|
|
19
|
+
|
|
20
|
+
__all__ = ["Slot", "Ace", "definir_molde", "carregar", "alterar_slot", "aplicar_slots"]
|
|
21
|
+
|
|
22
|
+
ARQUIVO_ACE = "meu_ace.json"
|
|
23
|
+
ARQUIVO_MOLDE = "molde_project_settings.json"
|
|
24
|
+
|
|
25
|
+
# Espaçamento entre placas medido em projetos salvos pelo Anycubic Slicer Next
|
|
26
|
+
ESPACAMENTO_PLACAS = {"Anycubic Kobra X": (314.4, 328.8)}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Slot:
|
|
31
|
+
slot: int
|
|
32
|
+
cor: str
|
|
33
|
+
tipo: str
|
|
34
|
+
nome: str
|
|
35
|
+
perfil: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Ace:
|
|
40
|
+
slots: list[Slot]
|
|
41
|
+
molde: dict
|
|
42
|
+
origem: str
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def impressora(self) -> str:
|
|
46
|
+
return self.molde.get("printer_model", "?")
|
|
47
|
+
|
|
48
|
+
def resumo(self) -> dict:
|
|
49
|
+
return {
|
|
50
|
+
"impressora": self.impressora,
|
|
51
|
+
"molde_tirado_de": self.origem,
|
|
52
|
+
"slots": [asdict(s) for s in self.slots],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _chaves_de_filamento() -> set[str]:
|
|
57
|
+
"""Todas as chaves que aparecem em perfis de filamento do slicer."""
|
|
58
|
+
chaves = {"filament_colour", "filament_colour_info", "filament_settings_id", "default_filament_colour"}
|
|
59
|
+
perfis = pasta_perfis()
|
|
60
|
+
for arquivo in (perfis / "filament").glob("*.json") if perfis else []:
|
|
61
|
+
try:
|
|
62
|
+
chaves |= set(json.loads(arquivo.read_text(encoding="utf8")))
|
|
63
|
+
except (OSError, ValueError):
|
|
64
|
+
continue
|
|
65
|
+
return chaves - {"name", "inherits", "from", "type", "setting_id", "instantiation", "version",
|
|
66
|
+
"compatible_printers", "compatible_printers_condition", "is_custom_defined",
|
|
67
|
+
"filament_id"}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def perfil_de_processo(nome: str) -> dict | None:
|
|
71
|
+
perfis = pasta_perfis()
|
|
72
|
+
caminho = perfis / "process" / f"{nome}.json" if perfis else None
|
|
73
|
+
if caminho is None or not caminho.exists():
|
|
74
|
+
return None
|
|
75
|
+
perfil = json.loads(caminho.read_text(encoding="utf8"))
|
|
76
|
+
if perfil.get("inherits"):
|
|
77
|
+
base = perfil_de_processo(perfil["inherits"]) or {}
|
|
78
|
+
perfil = {**base, **perfil}
|
|
79
|
+
return perfil
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
CHAVES_META = {"type", "from", "setting_id", "is_custom_defined", "instantiation", "version",
|
|
83
|
+
"inherits", "name", "compatible_printers", "compatible_printers_condition"}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def definir_molde(projeto_3mf: str | Path) -> Ace:
|
|
87
|
+
"""Lê um projeto salvo pelo Anycubic Slicer Next e grava molde + slots em dados/."""
|
|
88
|
+
projeto_3mf = Path(projeto_3mf)
|
|
89
|
+
with zipfile.ZipFile(projeto_3mf) as arquivo:
|
|
90
|
+
config = json.loads(arquivo.read("Metadata/project_settings.config"))
|
|
91
|
+
principal = arquivo.read("3D/3dmodel.model").decode("utf8")
|
|
92
|
+
embutidos = [n for n in arquivo.namelist()
|
|
93
|
+
if re.match(r"Metadata/(machine|filament|process)_settings_\d+\.config$", n)]
|
|
94
|
+
if not str(config.get("printer_model", "")).startswith("Anycubic"):
|
|
95
|
+
raise ValueError(f"o projeto é de {config.get('printer_model')}, preciso de um salvo para a impressora Anycubic")
|
|
96
|
+
diferencas = config.get("different_settings_to_system") or []
|
|
97
|
+
if diferencas and diferencas[-1]:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"o perfil de impressora desse projeto foi modificado "
|
|
100
|
+
f"({diferencas[-1][:120]}). Salve um projeto usando o perfil de sistema da impressora."
|
|
101
|
+
)
|
|
102
|
+
if any("machine_settings" in n for n in embutidos):
|
|
103
|
+
# projetos com perfil de impressora embutido podem carregar G-code de outra máquina
|
|
104
|
+
raise ValueError("o projeto traz um perfil de impressora personalizado embutido; use outro projeto")
|
|
105
|
+
|
|
106
|
+
# processo volta ao perfil de fábrica; as escolhas do modelo entram na conversão
|
|
107
|
+
perfil = perfil_de_processo(config.get("print_settings_id", ""))
|
|
108
|
+
if perfil:
|
|
109
|
+
for chave, valor in perfil.items():
|
|
110
|
+
if chave not in CHAVES_META and chave in config:
|
|
111
|
+
config[chave] = valor
|
|
112
|
+
if diferencas:
|
|
113
|
+
diferencas[0] = ""
|
|
114
|
+
app = re.search(r'<metadata name="Application">([^<]*)<', principal)
|
|
115
|
+
config["_aplicativo"] = app.group(1) if app else f"BambuStudio-{config.get('version', '2.0.0.3')}"
|
|
116
|
+
|
|
117
|
+
n = len(config.get("filament_colour") or [])
|
|
118
|
+
slots = [
|
|
119
|
+
Slot(
|
|
120
|
+
slot=i + 1,
|
|
121
|
+
cor=cores.normalizar(config["filament_colour"][i]),
|
|
122
|
+
tipo=config["filament_type"][i],
|
|
123
|
+
nome=cores.nome_da_cor(config["filament_colour"][i]),
|
|
124
|
+
perfil=config["filament_settings_id"][i],
|
|
125
|
+
)
|
|
126
|
+
for i in range(n)
|
|
127
|
+
]
|
|
128
|
+
pasta_dados().mkdir(parents=True, exist_ok=True)
|
|
129
|
+
(pasta_dados() / ARQUIVO_MOLDE).write_text(json.dumps(config, indent=1, ensure_ascii=False), encoding="utf8")
|
|
130
|
+
ace = Ace(slots=slots, molde=config, origem=str(projeto_3mf))
|
|
131
|
+
salvar_slots(ace)
|
|
132
|
+
return ace
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def salvar_slots(ace: Ace) -> None:
|
|
136
|
+
dados = {
|
|
137
|
+
"_leia_me": "Slots do ACE na ordem da impressora. Pode editar cor e nome; "
|
|
138
|
+
"para mudar o tipo use um tipo que já exista em algum slot do molde.",
|
|
139
|
+
"molde_tirado_de": ace.origem,
|
|
140
|
+
"slots": [asdict(s) for s in ace.slots],
|
|
141
|
+
}
|
|
142
|
+
pasta_dados().mkdir(parents=True, exist_ok=True)
|
|
143
|
+
(pasta_dados() / ARQUIVO_ACE).write_text(json.dumps(dados, indent=2, ensure_ascii=False), encoding="utf8")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def carregar() -> Ace:
|
|
147
|
+
caminho_molde = pasta_dados() / ARQUIVO_MOLDE
|
|
148
|
+
caminho_ace = pasta_dados() / ARQUIVO_ACE
|
|
149
|
+
if not caminho_molde.exists() or not caminho_ace.exists():
|
|
150
|
+
raise FileNotFoundError(
|
|
151
|
+
"ainda não há molde do ACE. Salve um projeto no Anycubic Slicer Next com o ACE "
|
|
152
|
+
"sincronizado e rode: python -m conversor3mf ace definir <projeto.3mf>"
|
|
153
|
+
)
|
|
154
|
+
molde = json.loads(caminho_molde.read_text(encoding="utf8"))
|
|
155
|
+
dados = json.loads(caminho_ace.read_text(encoding="utf8"))
|
|
156
|
+
slots = [Slot(**{**s, "cor": cores.normalizar(s["cor"])}) for s in dados["slots"]]
|
|
157
|
+
tipos_no_molde = set(molde.get("filament_type") or [])
|
|
158
|
+
for s in slots:
|
|
159
|
+
if s.tipo not in tipos_no_molde:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"slot {s.slot} está como {s.tipo}, mas o molde só tem {sorted(tipos_no_molde)}. "
|
|
162
|
+
"Salve um projeto no slicer com esse filamento e defina o molde de novo."
|
|
163
|
+
)
|
|
164
|
+
return Ace(slots=slots, molde=molde, origem=dados.get("molde_tirado_de", "?"))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def alterar_slot(numero: int, cor: str | None = None, tipo: str | None = None, nome: str | None = None) -> Ace:
|
|
168
|
+
"""Atualiza um slot depois de trocar o carretel; o tipo precisa existir no molde."""
|
|
169
|
+
ace = carregar()
|
|
170
|
+
slot = next((s for s in ace.slots if s.slot == numero), None)
|
|
171
|
+
if slot is None:
|
|
172
|
+
raise ValueError(f"slot {numero} não existe")
|
|
173
|
+
if tipo:
|
|
174
|
+
tipos = sorted(set(ace.molde.get("filament_type") or []))
|
|
175
|
+
if tipo not in tipos:
|
|
176
|
+
raise ValueError(
|
|
177
|
+
f"o molde só tem {tipos}. Para usar {tipo}, salve um projeto no slicer com esse "
|
|
178
|
+
"filamento em algum slot e defina o molde de novo."
|
|
179
|
+
)
|
|
180
|
+
slot.tipo = tipo
|
|
181
|
+
if cor:
|
|
182
|
+
slot.cor = cores.normalizar(cor)
|
|
183
|
+
slot.nome = cores.nome_da_cor(slot.cor)
|
|
184
|
+
if nome:
|
|
185
|
+
slot.nome = nome
|
|
186
|
+
salvar_slots(ace)
|
|
187
|
+
return ace
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def aplicar_slots(config: dict, ace: Ace) -> None:
|
|
191
|
+
"""Ajusta as listas por filamento do config para refletir os slots (cor e tipo)."""
|
|
192
|
+
chaves = _chaves_de_filamento()
|
|
193
|
+
n = len(ace.slots)
|
|
194
|
+
tipos_molde = list(config.get("filament_type") or [])
|
|
195
|
+
original = {k: list(v) for k, v in config.items() if k in chaves and isinstance(v, list) and len(v) == n}
|
|
196
|
+
for i, slot in enumerate(ace.slots):
|
|
197
|
+
if i < len(tipos_molde) and tipos_molde[i] != slot.tipo:
|
|
198
|
+
fonte = tipos_molde.index(slot.tipo)
|
|
199
|
+
for chave, valores in original.items():
|
|
200
|
+
config[chave][i] = valores[fonte]
|
|
201
|
+
config["filament_colour"][i] = slot.cor
|
|
202
|
+
if "filament_colour_info" in config and i < len(config["filament_colour_info"]):
|
|
203
|
+
config["filament_colour_info"][i] = slot.cor
|
conversor3mf/config.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Onde ficam os dados do usuário e o Anycubic Slicer Next.
|
|
2
|
+
|
|
3
|
+
Variáveis de ambiente (opcionais):
|
|
4
|
+
- CONVERSOR3MF_DADOS: pasta do molde e dos slots do ACE
|
|
5
|
+
- CONVERSOR3MF_SLICER: pasta de instalação do Anycubic Slicer Next (ou o executável)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
__all__ = ["pasta_dados", "pasta_slicer", "executavel_slicer", "pasta_perfis"]
|
|
15
|
+
|
|
16
|
+
NOME_APP = "conversor3mf"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def pasta_dados() -> Path:
|
|
20
|
+
if os.environ.get("CONVERSOR3MF_DADOS"):
|
|
21
|
+
return Path(os.environ["CONVERSOR3MF_DADOS"])
|
|
22
|
+
if sys.platform == "win32":
|
|
23
|
+
return Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming")) / NOME_APP
|
|
24
|
+
if sys.platform == "darwin":
|
|
25
|
+
return Path.home() / "Library" / "Application Support" / NOME_APP
|
|
26
|
+
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / NOME_APP
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _do_registro() -> list[Path]:
|
|
30
|
+
if sys.platform != "win32":
|
|
31
|
+
return []
|
|
32
|
+
import winreg
|
|
33
|
+
|
|
34
|
+
achados = []
|
|
35
|
+
chaves = [
|
|
36
|
+
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
|
|
37
|
+
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"),
|
|
38
|
+
(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
|
|
39
|
+
]
|
|
40
|
+
for raiz, caminho in chaves:
|
|
41
|
+
try:
|
|
42
|
+
pai = winreg.OpenKey(raiz, caminho)
|
|
43
|
+
except OSError:
|
|
44
|
+
continue
|
|
45
|
+
with pai:
|
|
46
|
+
for i in range(winreg.QueryInfoKey(pai)[0]):
|
|
47
|
+
try:
|
|
48
|
+
with winreg.OpenKey(pai, winreg.EnumKey(pai, i)) as filho:
|
|
49
|
+
nome = str(winreg.QueryValueEx(filho, "DisplayName")[0])
|
|
50
|
+
if "anycubicslicernext" not in nome.replace(" ", "").lower():
|
|
51
|
+
continue
|
|
52
|
+
for valor in ("DisplayIcon", "UninstallString", "InstallLocation"):
|
|
53
|
+
try:
|
|
54
|
+
texto = str(winreg.QueryValueEx(filho, valor)[0]).strip('" ').split(",")[0]
|
|
55
|
+
except OSError:
|
|
56
|
+
continue
|
|
57
|
+
if texto:
|
|
58
|
+
p = Path(texto)
|
|
59
|
+
achados.append(p.parent if p.suffix.lower() == ".exe" else p)
|
|
60
|
+
except OSError:
|
|
61
|
+
continue
|
|
62
|
+
return achados
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def pasta_slicer() -> Path | None:
|
|
66
|
+
"""Pasta de instalação do Anycubic Slicer Next, ou None se não encontrar."""
|
|
67
|
+
candidatos: list[Path] = []
|
|
68
|
+
escolhido = os.environ.get("CONVERSOR3MF_SLICER")
|
|
69
|
+
if escolhido is not None and escolhido.strip().lower() in ("", "nenhum", "none"):
|
|
70
|
+
return None # usado nos testes: finge que o slicer não está instalado
|
|
71
|
+
if escolhido:
|
|
72
|
+
p = Path(escolhido)
|
|
73
|
+
candidatos.append(p.parent if p.is_file() else p)
|
|
74
|
+
candidatos += _do_registro()
|
|
75
|
+
if sys.platform == "win32":
|
|
76
|
+
for base in (os.environ.get("ProgramFiles"), os.environ.get("ProgramFiles(x86)"), os.environ.get("LOCALAPPDATA")):
|
|
77
|
+
if base:
|
|
78
|
+
candidatos += [Path(base) / "AnycubicSlicerNext", Path(base) / "Programs" / "AnycubicSlicerNext"]
|
|
79
|
+
elif sys.platform == "darwin":
|
|
80
|
+
candidatos.append(Path("/Applications/AnycubicSlicerNext.app"))
|
|
81
|
+
for c in candidatos:
|
|
82
|
+
if executavel_slicer(c) is not None:
|
|
83
|
+
return c
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def executavel_slicer(pasta: Path | None = None) -> Path | None:
|
|
88
|
+
pasta = pasta if pasta is not None else pasta_slicer()
|
|
89
|
+
if pasta is None:
|
|
90
|
+
return None
|
|
91
|
+
for p in (pasta / "AnycubicSlicerNext.exe", pasta / "Contents" / "MacOS" / "AnycubicSlicerNext"):
|
|
92
|
+
if p.is_file():
|
|
93
|
+
return p
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def pasta_perfis() -> Path | None:
|
|
98
|
+
"""Pasta com os perfis de sistema da Anycubic (processo, filamento, impressora)."""
|
|
99
|
+
pasta = pasta_slicer()
|
|
100
|
+
if pasta is None:
|
|
101
|
+
return None
|
|
102
|
+
for p in (pasta / "resources" / "profiles" / "Anycubic",
|
|
103
|
+
pasta / "Contents" / "Resources" / "profiles" / "Anycubic"):
|
|
104
|
+
if p.is_dir():
|
|
105
|
+
return p
|
|
106
|
+
return None
|