conversor3mf 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Johnny Welton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: conversor3mf
3
+ Version: 0.1.0
4
+ Summary: Convert Bambu Lab / MakerWorld multicolor 3MF projects to the Anycubic Kobra X, in the slot order of your ACE, with an MCP server for Claude
5
+ Author: Johnny Welton
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/JohnnyTrindade/conversor-3mf
8
+ Project-URL: Issues, https://github.com/JohnnyTrindade/conversor-3mf/issues
9
+ Keywords: 3mf,3d-printing,anycubic,kobra-x,ace,bambu-studio,makerworld,mcp,multicolor
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Natural Language :: Portuguese (Brazilian)
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: mcp<3,>=2.3
21
+ Requires-Dist: numpy>=2
22
+ Requires-Dist: pillow>=11
23
+ Dynamic: license-file
24
+
25
+ # conversor3mf
26
+
27
+ [![testes](https://github.com/JohnnyTrindade/conversor-3mf/actions/workflows/testes.yml/badge.svg)](https://github.com/JohnnyTrindade/conversor-3mf/actions/workflows/testes.yml)
28
+
29
+ **Print MakerWorld multicolor models on an Anycubic Kobra X without recoloring anything.**
30
+ [Português](README.pt-BR.md)
31
+
32
+ Most multicolor 3MF files on MakerWorld are made for Bambu Lab printers. When you open
33
+ one in Anycubic Slicer Next, the colors come in the file's filament order, not the order
34
+ of the spools in your ACE. After syncing the ACE they end up swapped. Painted models are
35
+ the worst case: the per-object color menu only shows one color, so you end up repainting
36
+ by hand.
37
+
38
+ conversor3mf rewrites the project so that **each color of the model goes to the ACE slot
39
+ where that spool already is**. It also includes an MCP server, so you can ask Claude to do
40
+ it for you.
41
+
42
+ > Not affiliated with Anycubic or Bambu Lab. All trademarks belong to their owners.
43
+ > Respect each model's license: the tool converts files you downloaded for your own use
44
+ > and never redistributes anything.
45
+
46
+ ## What it does
47
+
48
+ - **Remaps colors.** Rewrites per-triangle paint, the filament of each part and
49
+ per-layer filament changes to your ACE slot order. It picks the closest color
50
+ (CIEDE2000) and merges identical colors.
51
+ - **Opens as a native Kobra X project.** No "Printer parameter switching" dialog and no
52
+ geometry-only import.
53
+ - **Keeps the designer's choices.** Layer height, walls, infill, supports, brim, ironing
54
+ and fuzzy skin carry over. Speeds and temperatures come from the Kobra X system profile.
55
+ - **Recenters plates.** Objects on every plate are centered on the 260 mm Kobra X bed.
56
+ - **Warns you.** It flags colors with no similar spool, more colors than ACE slots, and
57
+ PLA/PETG mixes.
58
+ - **Recolors whole regions ("islands").** Turns all holly leaves green or all stars red,
59
+ including the side walls of raised details, without touching the paint brush.
60
+ - **Never overwrites anything.** It writes `<name>_KobraX.3mf` next to the original.
61
+
62
+ ## Requirements
63
+
64
+ - Windows with **Anycubic Slicer Next** installed. It is detected automatically; if not,
65
+ set `CONVERSOR3MF_SLICER` to its folder.
66
+ - **Python 3.10+**.
67
+ - An Anycubic **Kobra X** with ACE. Other Anycubic printers may work but are untested;
68
+ see [Limitations](#limitations).
69
+
70
+ ## Install
71
+
72
+ ```bash
73
+ pip install git+https://github.com/JohnnyTrindade/conversor-3mf
74
+ ```
75
+
76
+ ### Use with Claude (MCP)
77
+
78
+ ```bash
79
+ claude mcp add conversor3mf --scope user -- conversor3mf-mcp
80
+ ```
81
+
82
+ Then just ask, in any language:
83
+
84
+ > "Convert C:\Downloads\coaster.3mf for my Kobra X"
85
+ >
86
+ > "I swapped slot 4 for a yellow PLA"
87
+ >
88
+ > "In the coaster, make the holly leaves green and the stars red"
89
+
90
+ Claude shows where each color will go before converting. When recoloring, it looks at a
91
+ numbered map of the model.
92
+
93
+ ## First-time setup: tell it what is in your ACE
94
+
95
+ 1. In Anycubic Slicer Next, select the Kobra X, sync the ACE and save any project.
96
+ 2. Run this command, or ask Claude to "set up my ACE from that project":
97
+
98
+ ```bash
99
+ conversor3mf ace definir "C:\path\to\saved_project.3mf"
100
+ ```
101
+
102
+ This saves the printer profile and your slot colors and materials. When you only change a
103
+ spool's color, update it like this:
104
+
105
+ ```bash
106
+ conversor3mf ace slot 4 --cor "#FFD700" --tipo PLA
107
+ ```
108
+
109
+ For a material that is not in any slot yet (ABS, TPU…), save a new project with it and run
110
+ `ace definir` again.
111
+
112
+ Your data lives in `%APPDATA%\conversor3mf`. Change it with `CONVERSOR3MF_DADOS`.
113
+
114
+ ## Command line
115
+
116
+ ```bash
117
+ conversor3mf planejar model.3mf # show which slot each color goes to
118
+ conversor3mf converter model.3mf --mapa 1=3 # convert; force filament 1 into slot 3
119
+ conversor3mf ilhas model_KobraX.3mf --vista cima # numbered map of color regions
120
+ conversor3mf repintar model_KobraX.3mf 32=4,3=3 # recolor islands 32 and 3
121
+ conversor3mf verificar model_KobraX.3mf # reopen in Anycubic Slicer Next and compare
122
+ ```
123
+
124
+ Command and option names are in Portuguese. Run `conversor3mf --help` for the full list.
125
+
126
+ ## How it works
127
+
128
+ - **Color format.** A Bambu/Orca 3MF stores filament numbers, not colors. The paint of each
129
+ triangle is a hex-encoded subdivision tree with a filament number on every leaf. The
130
+ converter decodes that tree, swaps the numbers and encodes it back. Every other part of
131
+ the file is left untouched.
132
+ - **Printer settings.** They come from a project saved by your own Anycubic Slicer Next,
133
+ so no Anycubic profiles are shipped with this package.
134
+ - **Islands.** Connected triangles of the same color, after welding duplicate vertices.
135
+ When a triangle has several colors, recoloring only changes the leaves of that island's
136
+ color, including on the island's border.
137
+
138
+ ## Limitations
139
+
140
+ - **Verification of multi-plate projects.** Anycubic Slicer Next 2.0.0.3 crashes from the
141
+ command line on any project with more than one plate. Those projects are only checked
142
+ internally, so open them in the slicer before slicing.
143
+ - **Plate grid spacing.** It is measured only for the Kobra X (314.4 × 328.8 mm). Other
144
+ printers fall back to 1.2 × the bed size.
145
+ - **More colors than slots.** Similar colors get merged. For multi-plate projects you can
146
+ print plate by plate and use `--mapa`.
147
+ - **Mesh-only 3MF files.** Files without a slicer project inside have no colors to
148
+ convert.
149
+
150
+ ## Development
151
+
152
+ ```bash
153
+ python -m venv .venv
154
+ .venv\Scripts\pip install -e .
155
+ .venv\Scripts\python -m unittest discover -s tests
156
+ ```
157
+
158
+ The tests use synthetic 3MF files built in `tests/fixtures.py` and do not need the slicer.
159
+ `ferramentas/testar_na_interface.ps1` opens a file in the slicer GUI, takes a screenshot
160
+ and closes it.
161
+
162
+ ## License
163
+
164
+ MIT
@@ -0,0 +1,140 @@
1
+ # conversor3mf
2
+
3
+ [![testes](https://github.com/JohnnyTrindade/conversor-3mf/actions/workflows/testes.yml/badge.svg)](https://github.com/JohnnyTrindade/conversor-3mf/actions/workflows/testes.yml)
4
+
5
+ **Print MakerWorld multicolor models on an Anycubic Kobra X without recoloring anything.**
6
+ [Português](README.pt-BR.md)
7
+
8
+ Most multicolor 3MF files on MakerWorld are made for Bambu Lab printers. When you open
9
+ one in Anycubic Slicer Next, the colors come in the file's filament order, not the order
10
+ of the spools in your ACE. After syncing the ACE they end up swapped. Painted models are
11
+ the worst case: the per-object color menu only shows one color, so you end up repainting
12
+ by hand.
13
+
14
+ conversor3mf rewrites the project so that **each color of the model goes to the ACE slot
15
+ where that spool already is**. It also includes an MCP server, so you can ask Claude to do
16
+ it for you.
17
+
18
+ > Not affiliated with Anycubic or Bambu Lab. All trademarks belong to their owners.
19
+ > Respect each model's license: the tool converts files you downloaded for your own use
20
+ > and never redistributes anything.
21
+
22
+ ## What it does
23
+
24
+ - **Remaps colors.** Rewrites per-triangle paint, the filament of each part and
25
+ per-layer filament changes to your ACE slot order. It picks the closest color
26
+ (CIEDE2000) and merges identical colors.
27
+ - **Opens as a native Kobra X project.** No "Printer parameter switching" dialog and no
28
+ geometry-only import.
29
+ - **Keeps the designer's choices.** Layer height, walls, infill, supports, brim, ironing
30
+ and fuzzy skin carry over. Speeds and temperatures come from the Kobra X system profile.
31
+ - **Recenters plates.** Objects on every plate are centered on the 260 mm Kobra X bed.
32
+ - **Warns you.** It flags colors with no similar spool, more colors than ACE slots, and
33
+ PLA/PETG mixes.
34
+ - **Recolors whole regions ("islands").** Turns all holly leaves green or all stars red,
35
+ including the side walls of raised details, without touching the paint brush.
36
+ - **Never overwrites anything.** It writes `<name>_KobraX.3mf` next to the original.
37
+
38
+ ## Requirements
39
+
40
+ - Windows with **Anycubic Slicer Next** installed. It is detected automatically; if not,
41
+ set `CONVERSOR3MF_SLICER` to its folder.
42
+ - **Python 3.10+**.
43
+ - An Anycubic **Kobra X** with ACE. Other Anycubic printers may work but are untested;
44
+ see [Limitations](#limitations).
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install git+https://github.com/JohnnyTrindade/conversor-3mf
50
+ ```
51
+
52
+ ### Use with Claude (MCP)
53
+
54
+ ```bash
55
+ claude mcp add conversor3mf --scope user -- conversor3mf-mcp
56
+ ```
57
+
58
+ Then just ask, in any language:
59
+
60
+ > "Convert C:\Downloads\coaster.3mf for my Kobra X"
61
+ >
62
+ > "I swapped slot 4 for a yellow PLA"
63
+ >
64
+ > "In the coaster, make the holly leaves green and the stars red"
65
+
66
+ Claude shows where each color will go before converting. When recoloring, it looks at a
67
+ numbered map of the model.
68
+
69
+ ## First-time setup: tell it what is in your ACE
70
+
71
+ 1. In Anycubic Slicer Next, select the Kobra X, sync the ACE and save any project.
72
+ 2. Run this command, or ask Claude to "set up my ACE from that project":
73
+
74
+ ```bash
75
+ conversor3mf ace definir "C:\path\to\saved_project.3mf"
76
+ ```
77
+
78
+ This saves the printer profile and your slot colors and materials. When you only change a
79
+ spool's color, update it like this:
80
+
81
+ ```bash
82
+ conversor3mf ace slot 4 --cor "#FFD700" --tipo PLA
83
+ ```
84
+
85
+ For a material that is not in any slot yet (ABS, TPU…), save a new project with it and run
86
+ `ace definir` again.
87
+
88
+ Your data lives in `%APPDATA%\conversor3mf`. Change it with `CONVERSOR3MF_DADOS`.
89
+
90
+ ## Command line
91
+
92
+ ```bash
93
+ conversor3mf planejar model.3mf # show which slot each color goes to
94
+ conversor3mf converter model.3mf --mapa 1=3 # convert; force filament 1 into slot 3
95
+ conversor3mf ilhas model_KobraX.3mf --vista cima # numbered map of color regions
96
+ conversor3mf repintar model_KobraX.3mf 32=4,3=3 # recolor islands 32 and 3
97
+ conversor3mf verificar model_KobraX.3mf # reopen in Anycubic Slicer Next and compare
98
+ ```
99
+
100
+ Command and option names are in Portuguese. Run `conversor3mf --help` for the full list.
101
+
102
+ ## How it works
103
+
104
+ - **Color format.** A Bambu/Orca 3MF stores filament numbers, not colors. The paint of each
105
+ triangle is a hex-encoded subdivision tree with a filament number on every leaf. The
106
+ converter decodes that tree, swaps the numbers and encodes it back. Every other part of
107
+ the file is left untouched.
108
+ - **Printer settings.** They come from a project saved by your own Anycubic Slicer Next,
109
+ so no Anycubic profiles are shipped with this package.
110
+ - **Islands.** Connected triangles of the same color, after welding duplicate vertices.
111
+ When a triangle has several colors, recoloring only changes the leaves of that island's
112
+ color, including on the island's border.
113
+
114
+ ## Limitations
115
+
116
+ - **Verification of multi-plate projects.** Anycubic Slicer Next 2.0.0.3 crashes from the
117
+ command line on any project with more than one plate. Those projects are only checked
118
+ internally, so open them in the slicer before slicing.
119
+ - **Plate grid spacing.** It is measured only for the Kobra X (314.4 × 328.8 mm). Other
120
+ printers fall back to 1.2 × the bed size.
121
+ - **More colors than slots.** Similar colors get merged. For multi-plate projects you can
122
+ print plate by plate and use `--mapa`.
123
+ - **Mesh-only 3MF files.** Files without a slicer project inside have no colors to
124
+ convert.
125
+
126
+ ## Development
127
+
128
+ ```bash
129
+ python -m venv .venv
130
+ .venv\Scripts\pip install -e .
131
+ .venv\Scripts\python -m unittest discover -s tests
132
+ ```
133
+
134
+ The tests use synthetic 3MF files built in `tests/fixtures.py` and do not need the slicer.
135
+ `ferramentas/testar_na_interface.ps1` opens a file in the slicer GUI, takes a screenshot
136
+ and closes it.
137
+
138
+ ## License
139
+
140
+ MIT
File without changes
@@ -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())
@@ -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