extapps 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.
- extapps/__init__.py +57 -0
- extapps/map_compositor/__init__.py +22 -0
- extapps/map_compositor/launcher.py +63 -0
- extapps/map_compositor/map_compositor.ui +248 -0
- extapps/map_compositor/map_compositor_ui.py +133 -0
- extapps/map_compositor/slots.py +443 -0
- extapps/map_converter/__init__.py +23 -0
- extapps/map_converter/launcher.py +39 -0
- extapps/map_converter/map_converter.ui +794 -0
- extapps/map_converter/map_converter_ui.py +374 -0
- extapps/map_converter/slots.py +1146 -0
- extapps/map_packer/__init__.py +23 -0
- extapps/map_packer/launcher.py +28 -0
- extapps/map_packer/map_packer.ui +547 -0
- extapps/map_packer/map_packer_ui.py +260 -0
- extapps/map_packer/slots.py +250 -0
- extapps/mesh_convert/__init__.py +22 -0
- extapps/mesh_convert/launcher.py +29 -0
- extapps/mesh_convert/mesh_convert.ui +226 -0
- extapps/mesh_convert/mesh_convert_ui.py +124 -0
- extapps/mesh_convert/slots.py +288 -0
- extapps/metashape_workflow/__init__.py +38 -0
- extapps/metashape_workflow/_metashape_workflow.py +360 -0
- extapps/metashape_workflow/launcher.py +45 -0
- extapps/metashape_workflow/metashape_workflow.ui +387 -0
- extapps/metashape_workflow/metashape_workflow_ui.py +199 -0
- extapps/metashape_workflow/slots.py +524 -0
- extapps-0.1.0.dist-info/METADATA +67 -0
- extapps-0.1.0.dist-info/RECORD +33 -0
- extapps-0.1.0.dist-info/WHEEL +5 -0
- extapps-0.1.0.dist-info/entry_points.txt +6 -0
- extapps-0.1.0.dist-info/licenses/LICENSE +21 -0
- extapps-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
# !/usr/bin/python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
"""UI slot bindings for the map_compositor window.
|
|
4
|
+
|
|
5
|
+
Slots own the UI state and compose a :class:`MapCompositor` engine.
|
|
6
|
+
Engine status messages flow through ``self.engine.logger`` (a LoggingMixin
|
|
7
|
+
logger) which has a default ``StreamHandler`` for console output; we
|
|
8
|
+
attach uitk's :class:`TextEditLogHandler` alongside it so the UI message
|
|
9
|
+
panel auto-scrolls. Progress-bar updates use a thin callback.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import pythontk as ptk
|
|
17
|
+
from pythontk.core_utils.logging_mixin import LevelAwareFormatter
|
|
18
|
+
from qtpy.QtWidgets import QPushButton
|
|
19
|
+
from uitk.widgets.textEditLogHandler import TextEditLogHandler
|
|
20
|
+
|
|
21
|
+
from pythontk import BatchResult, MapCompositor, NormalOutputMode
|
|
22
|
+
|
|
23
|
+
_DOCS_URL = "https://github.com/m3trik/extapps#readme"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_intro() -> str:
|
|
27
|
+
"""One-time intro panel: minimal quickstart + link to full docs.
|
|
28
|
+
|
|
29
|
+
The full filename-suffix table used to live here but dwarfed the
|
|
30
|
+
actual instructions — readers had to scroll past ~30 rows of alias
|
|
31
|
+
lists to find the basic export settings. Moved to the GitHub README;
|
|
32
|
+
this panel now stays a single screen.
|
|
33
|
+
"""
|
|
34
|
+
return (
|
|
35
|
+
"<u>Quickstart</u><br>"
|
|
36
|
+
" 1. Set the <b>source</b> and <b>destination</b> directories.<br>"
|
|
37
|
+
" 2. (Optional) Set a <b>map name</b> prefix.<br>"
|
|
38
|
+
" 3. Click <b>Combine Maps</b>.<br><br>"
|
|
39
|
+
"<u>Required Substance Painter Export Settings</u><br>"
|
|
40
|
+
" Output Template: <b>Document channels</b><br>"
|
|
41
|
+
" Padding: <b>Dilation + transparent</b> or "
|
|
42
|
+
"<b>Dilation + default background color</b><br><br>"
|
|
43
|
+
f'<span style="color:#888888">'
|
|
44
|
+
f"Full filename-suffix table and detailed docs: "
|
|
45
|
+
f'<a href="{_DOCS_URL}" style="color:#88AACC">{_DOCS_URL}</a>'
|
|
46
|
+
"</span>"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MapCompositorSlots:
|
|
51
|
+
"""UI slot handler. Composes a :class:`MapCompositor` via ``self.engine``."""
|
|
52
|
+
|
|
53
|
+
msg_intro = _build_intro()
|
|
54
|
+
|
|
55
|
+
# (display label, NormalOutputMode) — order shown in the header combo.
|
|
56
|
+
NORMAL_MODE_CHOICES = (
|
|
57
|
+
("Both (auto-convert)", NormalOutputMode.BOTH),
|
|
58
|
+
("OpenGL only", NormalOutputMode.OPENGL_ONLY),
|
|
59
|
+
("DirectX only", NormalOutputMode.DIRECTX_ONLY),
|
|
60
|
+
("No conversion", NormalOutputMode.NONE),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# "None" disables the post-composite workflow pass; the remaining entries
|
|
64
|
+
# are populated from pythontk's MapRegistry at runtime so the menu stays
|
|
65
|
+
# in sync with the registry's WF.* workflow keys.
|
|
66
|
+
_NO_TEMPLATE_LABEL = "None (composite only)"
|
|
67
|
+
|
|
68
|
+
def __init__(self, switchboard) -> None:
|
|
69
|
+
self.sb = switchboard
|
|
70
|
+
self.ui = self.sb.loaded_ui.map_compositor
|
|
71
|
+
|
|
72
|
+
self.engine = MapCompositor(progress_callback=self._on_progress)
|
|
73
|
+
logger = self.engine.logger
|
|
74
|
+
# Class-level logger — sweep stale widget handlers from prior sessions.
|
|
75
|
+
for h in list(logger.handlers):
|
|
76
|
+
if hasattr(h, "widget"):
|
|
77
|
+
logger.removeHandler(h)
|
|
78
|
+
# Attach directly; ``_set_text_handler`` would force this handler
|
|
79
|
+
# process-wide. monospace=False keeps the intro's rich-text font;
|
|
80
|
+
# log records still render monospace via TextEditLogHandler's <span>.
|
|
81
|
+
logger.setLevel(logging.INFO)
|
|
82
|
+
handler = TextEditLogHandler(self.ui.txt003, monospace=False)
|
|
83
|
+
handler.setLevel(logging.INFO)
|
|
84
|
+
handler.setFormatter(LevelAwareFormatter(logger=logger, strip_html=False))
|
|
85
|
+
logger.addHandler(handler)
|
|
86
|
+
|
|
87
|
+
self.default_toolTip_txt000 = self.ui.txt000.toolTip()
|
|
88
|
+
self.default_toolTip_txt001 = self.ui.txt001.toolTip()
|
|
89
|
+
|
|
90
|
+
self.ui.txt003.setText(self.msg_intro)
|
|
91
|
+
self.ui.footer.setDefaultStatusText("Ready.")
|
|
92
|
+
|
|
93
|
+
# The primary action ("Combine Maps") lives in the footer with a
|
|
94
|
+
# styled background so it stands out from the status text. Connect
|
|
95
|
+
# to the existing slot method directly — Switchboard won't auto-wire
|
|
96
|
+
# it because the button isn't part of the .ui tree.
|
|
97
|
+
self.combine_btn = QPushButton("Combine Maps")
|
|
98
|
+
self.combine_btn.setToolTip("Start the compositing process.")
|
|
99
|
+
self.combine_btn.clicked.connect(self.b002)
|
|
100
|
+
self.ui.footer.add_widget(
|
|
101
|
+
self.combine_btn, side="right", background=True, rounded=False
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# --- engine pass-through (back-compat with previous public API) ---
|
|
105
|
+
@property
|
|
106
|
+
def removeNormalMap(self) -> bool:
|
|
107
|
+
return self.engine.remove_normal_map
|
|
108
|
+
|
|
109
|
+
@removeNormalMap.setter
|
|
110
|
+
def removeNormalMap(self, value: bool) -> None:
|
|
111
|
+
self.engine.remove_normal_map = value
|
|
112
|
+
|
|
113
|
+
# --- input/output text-field properties ---
|
|
114
|
+
@property
|
|
115
|
+
def input_dir(self) -> str:
|
|
116
|
+
return self.ui.txt000.text()
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def output_dir(self) -> str:
|
|
120
|
+
return self.ui.txt001.text()
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def map_name(self) -> str:
|
|
124
|
+
return self.ui.txt002.text()
|
|
125
|
+
|
|
126
|
+
# --- shared helpers ---
|
|
127
|
+
def _bind_recent_values(
|
|
128
|
+
self,
|
|
129
|
+
widget,
|
|
130
|
+
settings_key: str,
|
|
131
|
+
legacy_key: str,
|
|
132
|
+
*,
|
|
133
|
+
auto_record: bool = False,
|
|
134
|
+
):
|
|
135
|
+
"""Attach a RecentValuesOption and seed it from legacy QSettings."""
|
|
136
|
+
from uitk.widgets.optionBox.options.recent_values import RecentValuesOption
|
|
137
|
+
|
|
138
|
+
opt = RecentValuesOption(
|
|
139
|
+
wrapped_widget=widget,
|
|
140
|
+
settings_key=settings_key,
|
|
141
|
+
max_recent=10,
|
|
142
|
+
auto_record=auto_record,
|
|
143
|
+
)
|
|
144
|
+
widget.option_box.add_option(opt)
|
|
145
|
+
if not opt.recent_values:
|
|
146
|
+
for v in self.ui.settings.value(legacy_key, []):
|
|
147
|
+
if v != "/":
|
|
148
|
+
opt.add_recent_value(v)
|
|
149
|
+
return opt
|
|
150
|
+
|
|
151
|
+
def _bind_dir_actions(self, widget, browse_title: str):
|
|
152
|
+
"""Attach Set (browse-directory) and Open (reveal-in-explorer) buttons.
|
|
153
|
+
|
|
154
|
+
Returns the Open ActionOption so the caller can toggle its enabled
|
|
155
|
+
state from the text-change handler.
|
|
156
|
+
"""
|
|
157
|
+
from uitk.widgets.optionBox.options.action import ActionOption
|
|
158
|
+
from uitk.widgets.optionBox.options.browse import BrowseOption
|
|
159
|
+
|
|
160
|
+
open_opt = ActionOption(
|
|
161
|
+
wrapped_widget=widget,
|
|
162
|
+
callback=lambda: self._open_dir(widget.text()),
|
|
163
|
+
icon="open_external",
|
|
164
|
+
tooltip="Open this directory in the file explorer.",
|
|
165
|
+
)
|
|
166
|
+
widget.option_box.add_option(open_opt)
|
|
167
|
+
|
|
168
|
+
browse_opt = BrowseOption(
|
|
169
|
+
wrapped_widget=widget,
|
|
170
|
+
mode="directory",
|
|
171
|
+
title=browse_title,
|
|
172
|
+
tooltip="Browse for a directory.",
|
|
173
|
+
)
|
|
174
|
+
widget.option_box.add_option(browse_opt)
|
|
175
|
+
|
|
176
|
+
return open_opt
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _open_dir(path: Optional[str]) -> None:
|
|
180
|
+
try:
|
|
181
|
+
os.startfile(path)
|
|
182
|
+
except (FileNotFoundError, TypeError):
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
def _on_dir_validated(self, ok: bool, text: str, open_opt):
|
|
186
|
+
"""Toggle the Open button in response to validated dir text."""
|
|
187
|
+
open_opt.widget.setEnabled(bool(text and ok))
|
|
188
|
+
|
|
189
|
+
def _on_progress(self, percent: float) -> None:
|
|
190
|
+
"""Engine→UI progress bar bridge (routes through the footer).
|
|
191
|
+
|
|
192
|
+
Goes through ``Footer.update_progress`` rather than poking
|
|
193
|
+
``progress_bar.setValue`` directly — the bar is created with
|
|
194
|
+
``auto_hide=True`` and only becomes visible after the matching
|
|
195
|
+
``start_progress`` call in ``process()``. ``ProgressBar.update_progress``
|
|
196
|
+
already pumps the event loop, so no extra ``processEvents`` here.
|
|
197
|
+
"""
|
|
198
|
+
self.ui.footer.update_progress(int(percent))
|
|
199
|
+
|
|
200
|
+
# --- widget init handlers ---
|
|
201
|
+
def header_init(self, widget):
|
|
202
|
+
"""Populate the header menu with global options."""
|
|
203
|
+
widget.menu.add(
|
|
204
|
+
"QCheckBox",
|
|
205
|
+
setText="Optimize output",
|
|
206
|
+
setObjectName="chk_optimize",
|
|
207
|
+
setChecked=self.engine.optimize_output,
|
|
208
|
+
setToolTip=(
|
|
209
|
+
"Run ImgUtils.optimize_texture on each saved map "
|
|
210
|
+
"(enforces map-type bit depth and mode)."
|
|
211
|
+
),
|
|
212
|
+
stateChanged=self._on_optimize_toggled,
|
|
213
|
+
)
|
|
214
|
+
widget.menu.add(
|
|
215
|
+
"QComboBox",
|
|
216
|
+
setObjectName="cmb_normal_mode",
|
|
217
|
+
setToolTip=(
|
|
218
|
+
"Choose which DirectX/OpenGL normal map variant(s) to output. "
|
|
219
|
+
"Note: when an output template is selected, the template's "
|
|
220
|
+
"required normal format may add a sibling file alongside the "
|
|
221
|
+
"compositor's output."
|
|
222
|
+
),
|
|
223
|
+
addItems=[label for label, _mode in self.NORMAL_MODE_CHOICES],
|
|
224
|
+
)
|
|
225
|
+
# Sync the combo to the engine's current value before connecting the
|
|
226
|
+
# signal, so the initial setCurrentIndex doesn't re-trigger the slot.
|
|
227
|
+
modes = [m for _label, m in self.NORMAL_MODE_CHOICES]
|
|
228
|
+
widget.menu.cmb_normal_mode.setCurrentIndex(
|
|
229
|
+
modes.index(self.engine.normal_output_mode)
|
|
230
|
+
)
|
|
231
|
+
widget.menu.cmb_normal_mode.currentIndexChanged.connect(
|
|
232
|
+
self._on_normal_mode_changed
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Output template — names sourced from pythontk's MapRegistry so the
|
|
236
|
+
# menu mirrors the registry's WF.* workflow keys. None = composite
|
|
237
|
+
# only (no post-pass). When set, after compositing finishes the
|
|
238
|
+
# engine runs MapFactory.prepare_maps with the matching workflow
|
|
239
|
+
# preset to pack/rename files for the target engine.
|
|
240
|
+
presets = ptk.MapRegistry().get_workflow_presets()
|
|
241
|
+
self._template_choices = (self._NO_TEMPLATE_LABEL, *presets.keys())
|
|
242
|
+
widget.menu.add(
|
|
243
|
+
"QComboBox",
|
|
244
|
+
setObjectName="cmb_output_template",
|
|
245
|
+
setToolTip=(
|
|
246
|
+
"Post-process composited output for a target workflow. "
|
|
247
|
+
"Packs/renames the files for the chosen engine (e.g. Unity "
|
|
248
|
+
"HDRP packs Metallic/AO/Smoothness into an MSAO MaskMap). "
|
|
249
|
+
"Original composited files stay on disk alongside the "
|
|
250
|
+
"workflow output."
|
|
251
|
+
),
|
|
252
|
+
addItems=list(self._template_choices),
|
|
253
|
+
)
|
|
254
|
+
# Pre-select to match the engine field (default: None).
|
|
255
|
+
current = self.engine.output_template or self._NO_TEMPLATE_LABEL
|
|
256
|
+
try:
|
|
257
|
+
widget.menu.cmb_output_template.setCurrentIndex(
|
|
258
|
+
self._template_choices.index(current)
|
|
259
|
+
)
|
|
260
|
+
except ValueError:
|
|
261
|
+
widget.menu.cmb_output_template.setCurrentIndex(0)
|
|
262
|
+
widget.menu.cmb_output_template.currentIndexChanged.connect(
|
|
263
|
+
self._on_output_template_changed
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def _on_optimize_toggled(self, state) -> None:
|
|
267
|
+
# Qt.Checked is 2 in PySide6, 2 in PySide2 — robust check via bool.
|
|
268
|
+
self.engine.optimize_output = bool(state)
|
|
269
|
+
|
|
270
|
+
def _on_normal_mode_changed(self, index: int) -> None:
|
|
271
|
+
_label, mode = self.NORMAL_MODE_CHOICES[index]
|
|
272
|
+
self.engine.normal_output_mode = mode
|
|
273
|
+
|
|
274
|
+
def _on_output_template_changed(self, index: int) -> None:
|
|
275
|
+
choice = self._template_choices[index]
|
|
276
|
+
self.engine.output_template = (
|
|
277
|
+
None if choice == self._NO_TEMPLATE_LABEL else choice
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def txt000_init(self, widget):
|
|
281
|
+
"""Init Source Directory"""
|
|
282
|
+
self._recent_input_dirs = self._bind_recent_values(
|
|
283
|
+
widget,
|
|
284
|
+
"map_compositor_input_dirs",
|
|
285
|
+
"prev_input_dirs",
|
|
286
|
+
auto_record=True,
|
|
287
|
+
)
|
|
288
|
+
self._open_input_dir = self._bind_dir_actions(
|
|
289
|
+
widget, browse_title="Select a directory containing image files."
|
|
290
|
+
)
|
|
291
|
+
widget.set_validator(
|
|
292
|
+
"dir",
|
|
293
|
+
invalid_tooltip="Invalid directory",
|
|
294
|
+
empty_tooltip=self.default_toolTip_txt000,
|
|
295
|
+
)
|
|
296
|
+
widget.validated.connect(
|
|
297
|
+
lambda ok, text: self._on_dir_validated(ok, text, self._open_input_dir)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def txt001_init(self, widget):
|
|
301
|
+
"""Init Destination Directory"""
|
|
302
|
+
self._recent_output_dirs = self._bind_recent_values(
|
|
303
|
+
widget,
|
|
304
|
+
"map_compositor_output_dirs",
|
|
305
|
+
"prev_output_dirs",
|
|
306
|
+
auto_record=True,
|
|
307
|
+
)
|
|
308
|
+
self._open_output_dir = self._bind_dir_actions(
|
|
309
|
+
widget, browse_title="Select an output directory."
|
|
310
|
+
)
|
|
311
|
+
widget.set_validator(
|
|
312
|
+
"dir",
|
|
313
|
+
invalid_tooltip="Invalid directory",
|
|
314
|
+
empty_tooltip=self.default_toolTip_txt001,
|
|
315
|
+
)
|
|
316
|
+
widget.validated.connect(
|
|
317
|
+
lambda ok, text: self._on_dir_validated(ok, text, self._open_output_dir)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def txt002_init(self, widget):
|
|
321
|
+
"""Init Map Name"""
|
|
322
|
+
self._recent_map_names = self._bind_recent_values(
|
|
323
|
+
widget,
|
|
324
|
+
"map_compositor_map_names",
|
|
325
|
+
"prev_map_names",
|
|
326
|
+
auto_record=True,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# --- button handlers ---
|
|
330
|
+
def b002(self):
|
|
331
|
+
"""Combine Maps"""
|
|
332
|
+
self.ui.txt003.clear()
|
|
333
|
+
self.ui.footer.setStatusText("Loading maps …")
|
|
334
|
+
self.engine.logger.info("Loading maps ..", preset="italic")
|
|
335
|
+
self.sb.app.processEvents()
|
|
336
|
+
images = ptk.get_images(self.input_dir)
|
|
337
|
+
self.process(images, self.input_dir, self.output_dir, self.map_name)
|
|
338
|
+
|
|
339
|
+
# --- orchestration ---
|
|
340
|
+
def process(self, images, input_dir, output_dir, map_name=None):
|
|
341
|
+
"""Validate dirs, prepare sorted-image groups, and drive the engine."""
|
|
342
|
+
if not (input_dir and output_dir):
|
|
343
|
+
self.engine.logger.error(
|
|
344
|
+
"You must specify a source and destination directory."
|
|
345
|
+
)
|
|
346
|
+
self.ui.footer.setStatusText("Source and destination directories required.")
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
invalid_dir = next(
|
|
350
|
+
(d for d in (input_dir, output_dir) if not ptk.is_valid(d, "dir")),
|
|
351
|
+
None,
|
|
352
|
+
)
|
|
353
|
+
if invalid_dir:
|
|
354
|
+
self.engine.logger.error(f"Directory is invalid: <b>{invalid_dir}</b>.")
|
|
355
|
+
self.ui.footer.setStatusText(f"Invalid directory: {invalid_dir}")
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
if not map_name:
|
|
359
|
+
map_name = ptk.format_path(input_dir, "dir")
|
|
360
|
+
|
|
361
|
+
sorted_images = ptk.MapFactory.sort_images_by_type(images)
|
|
362
|
+
has_normal_pair = ptk.MapFactory.contains_map_types(
|
|
363
|
+
sorted_images, ["Normal_DirectX", "Normal_OpenGL"]
|
|
364
|
+
)
|
|
365
|
+
# +1 for the auto-generated complementary normal map.
|
|
366
|
+
total_maps_extra = 1 if has_normal_pair else 0
|
|
367
|
+
|
|
368
|
+
if self.engine.remove_normal_map and has_normal_pair:
|
|
369
|
+
normal = next(
|
|
370
|
+
(
|
|
371
|
+
k
|
|
372
|
+
for k in sorted_images
|
|
373
|
+
if ptk.MapFactory.resolve_map_type(k) == "Normal"
|
|
374
|
+
),
|
|
375
|
+
None,
|
|
376
|
+
)
|
|
377
|
+
if normal:
|
|
378
|
+
del sorted_images[normal]
|
|
379
|
+
|
|
380
|
+
# When the user has both DX *and* GL sources but only wants one
|
|
381
|
+
# format, drop the redundant one — otherwise the engine would
|
|
382
|
+
# process each independently and the iteration order would decide
|
|
383
|
+
# which content survives (the second-processed format overwrites
|
|
384
|
+
# the first via the auto-invert path).
|
|
385
|
+
mode = self.engine.normal_output_mode
|
|
386
|
+
if (
|
|
387
|
+
mode is NormalOutputMode.OPENGL_ONLY
|
|
388
|
+
and "Normal_OpenGL" in sorted_images
|
|
389
|
+
and "Normal_DirectX" in sorted_images
|
|
390
|
+
):
|
|
391
|
+
del sorted_images["Normal_DirectX"]
|
|
392
|
+
elif (
|
|
393
|
+
mode is NormalOutputMode.DIRECTX_ONLY
|
|
394
|
+
and "Normal_OpenGL" in sorted_images
|
|
395
|
+
and "Normal_DirectX" in sorted_images
|
|
396
|
+
):
|
|
397
|
+
del sorted_images["Normal_OpenGL"]
|
|
398
|
+
|
|
399
|
+
# Drop maps superseded by present packed maps (e.g. ORM → drops
|
|
400
|
+
# Metallic/Roughness/AO; MSAO → drops more). In-place mutation.
|
|
401
|
+
# NB: pythontk's sort_images_by_type already aliases legacy variants
|
|
402
|
+
# like Mixed_AO into Ambient_Occlusion, so no manual rename is needed.
|
|
403
|
+
ptk.MapFactory.filter_redundant_maps(sorted_images)
|
|
404
|
+
|
|
405
|
+
total_layers = sum(len(v) for v in sorted_images.values())
|
|
406
|
+
total_maps = len(sorted_images) + total_maps_extra
|
|
407
|
+
|
|
408
|
+
self.engine.logger.info(
|
|
409
|
+
f"Sorting <b>{total_layers}</b> images, into "
|
|
410
|
+
f"<b>{total_maps}</b> maps ..",
|
|
411
|
+
preset="italic",
|
|
412
|
+
)
|
|
413
|
+
# Reveal the footer's slim progress bar; engine ticks flow into
|
|
414
|
+
# update_progress via _on_progress. finish_progress() in every
|
|
415
|
+
# exit path auto-hides the bar after a short delay.
|
|
416
|
+
self.ui.footer.start_progress(
|
|
417
|
+
total=100,
|
|
418
|
+
text=f"Compositing {total_maps} maps from {total_layers} layers …",
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
result = self.engine.process_batch(sorted_images, output_dir, map_name)
|
|
423
|
+
except Exception as e:
|
|
424
|
+
self.engine.logger.error(
|
|
425
|
+
f"Operation encountered the following error:<br>{e}"
|
|
426
|
+
)
|
|
427
|
+
self.ui.footer.finish_progress(f"Failed: {e}")
|
|
428
|
+
raise
|
|
429
|
+
|
|
430
|
+
if result is BatchResult.MASK_FAILURE:
|
|
431
|
+
self.engine.logger.error(
|
|
432
|
+
"Unable to create masks from the source images.<br>"
|
|
433
|
+
"To create a mask, at least one set of source maps need a "
|
|
434
|
+
"transparent or single color background,<br>alternatively a "
|
|
435
|
+
"set of mask maps can be added to the source folder. "
|
|
436
|
+
"ex. <map_name>_mask.png"
|
|
437
|
+
)
|
|
438
|
+
self.ui.footer.finish_progress(
|
|
439
|
+
"Mask creation failed — see message panel for details."
|
|
440
|
+
)
|
|
441
|
+
else:
|
|
442
|
+
self.engine.logger.success("COMPLETED.")
|
|
443
|
+
self.ui.footer.finish_progress(f"Wrote {total_maps} map(s) to {output_dir}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# !/usr/bin/python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
"""Map Converter — texture conversion, channel packing, PBR-workflow prep.
|
|
4
|
+
|
|
5
|
+
Engine logic lives in :class:`pythontk.ImgUtils` and
|
|
6
|
+
:class:`pythontk.MapFactory`; this package holds only the Switchboard
|
|
7
|
+
panel and launcher.
|
|
8
|
+
"""
|
|
9
|
+
from pythontk.core_utils.module_resolver import bootstrap_package
|
|
10
|
+
|
|
11
|
+
__package__ = "extapps.map_converter"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_INCLUDE = {
|
|
15
|
+
"launcher": ["MapConverterUI"],
|
|
16
|
+
"slots": ["MapConverterSlots"],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
bootstrap_package(globals(), include=DEFAULT_INCLUDE)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = ["MapConverterUI", "MapConverterSlots"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# !/usr/bin/python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
"""Application shell for the Map Converter UI.
|
|
4
|
+
|
|
5
|
+
Engine logic lives in :class:`pythontk.ImgUtils` / :class:`pythontk.MapFactory`
|
|
6
|
+
and slot bindings in :mod:`extapps.map_converter.slots`; this module
|
|
7
|
+
only assembles the Switchboard-driven UI and provides the script
|
|
8
|
+
entry point.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MapConverterUI:
|
|
13
|
+
"""Standalone launcher. Constructing the class returns a configured UI.
|
|
14
|
+
|
|
15
|
+
``__new__`` is overridden to return the wired Switchboard UI directly,
|
|
16
|
+
so ``MapConverterUI()`` yields the UI (not an instance). Use this when
|
|
17
|
+
running outside a host DCC. Hosts that need to inject a
|
|
18
|
+
``texture_provider`` should register :class:`MapConverterSlots`
|
|
19
|
+
themselves rather than going through this launcher.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __new__(cls):
|
|
23
|
+
from uitk import Switchboard
|
|
24
|
+
from extapps.map_converter.slots import MapConverterSlots
|
|
25
|
+
|
|
26
|
+
sb = Switchboard(ui_source="map_converter.ui", slot_source=MapConverterSlots)
|
|
27
|
+
ui = sb.loaded_ui.map_converter
|
|
28
|
+
|
|
29
|
+
ui.set_attributes(WA_TranslucentBackground=True)
|
|
30
|
+
ui.set_flags(FramelessWindowHint=True)
|
|
31
|
+
ui.style.set(theme="dark", style_class="translucentBgWithBorder")
|
|
32
|
+
ui.header.config_buttons("menu", "minimize", "hide")
|
|
33
|
+
return ui
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# -----------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
MapConverterUI().show(pos="screen", app_exec=True)
|