diff --git a/README.md b/README.md index 7cb9239..e852718 100644 --- a/README.md +++ b/README.md @@ -465,8 +465,10 @@ model's origin move it along an axis and the rotate toggle swaps them for three rings; the position and rotation boxes read what the drag did, in the units a module writes, and take typed numbers back. `Copy as JSON` puts the `transform` block on the clipboard, and `Write to source` puts it back in the module entry -it was read from - scale and origin untouched, and nothing to write for a model -no module names. `Unwrap UVs` re-cuts the selected model's texture so every +it was read from - scale and origin untouched. It writes every model that has +moved rather than just the selected one, and marks a moved row with a `*` until +it has; a model no module names has nowhere to write to and is named in the +report instead. `Unwrap UVs` re-cuts the selected model's texture so every face has a patch of its own, writes the atlas size beside the boxes, and paints a template to match where there is no texture yet - never over one there is. diff --git a/tools/ARMOUR_QUICKSTART.py b/tools/ARMOUR_QUICKSTART.py index 836ab5b..1a757e6 100755 --- a/tools/ARMOUR_QUICKSTART.py +++ b/tools/ARMOUR_QUICKSTART.py @@ -34,12 +34,15 @@ toggle swaps them for three rings. Dragging one and typing a number are the same edit seen from two sides, so the panel always reads what the model is doing, and `Copy as JSON` hands over the block to paste into the module. -`Write to source` is the one thing here that changes a file: it puts those -numbers back into the module entry they were read from, leaving its scale and -its origin alone. Placement lives in the module, so a model no module names - -a sword part, an icon of its own - has nowhere to write to and says so. When it -is the geometry itself that wants moving, ARMOUR_GUI edits the boxes and -ARMOUR_EDITOR poses the result in a scene. +`Write to source` is the one thing here that changes a module file. It writes +every model that has moved, not only the one in front of you - a row that has +been moved and not yet written is marked with a `*`, so what is pending is on +the list rather than in your memory - putting each model's numbers back into +the module entry they were read from, and leaving its scale and its origin +alone. Placement lives in the module, so a model no module names - a sword +part, an icon of its own - has nowhere to write to and is named in the report +instead. When it is the geometry itself that wants moving, ARMOUR_GUI edits the +boxes and ARMOUR_EDITOR poses the result in a scene. """ from __future__ import annotations @@ -384,6 +387,7 @@ class Quickstart(QtWidgets.QMainWindow): entry.matrix = mc.transform_matrix( {group: {axis: self.spins[(group, axis)].value() for axis in 'xyz'} for group in ('translation', 'rotation')}) + self._mark_moved() self.rebuild() def reset_transform(self): @@ -393,50 +397,65 @@ class Quickstart(QtWidgets.QMainWindow): return entry.matrix = entry.given.copy() self._sync_transform() + self._mark_moved() self.rebuild() + def moved(self): + """Every model whose transform is no longer what its module declares.""" + return [e for e in self.entries if not np.allclose(e.matrix, e.given)] + def write_to_source(self): - """Put the panel's numbers back into the module that declares the model. + """Put the moved models' numbers back into the modules that declare them. - Placement lives in the module rather than in the geometry, so that is - what is written: the `translation` and `rotation` of the model entry - the tool read this placement from, with its scale and its origin left - as the module had them. A model no module names has nowhere to write - to - its geometry is its position - and says so rather than guessing. + Everything that has moved, not just the row in front of you: a session + is spent nudging several things into place, and a button that saved one + of them would leave the rest to be noticed later or lost. Placement + lives in the module, so what is written is the `translation` and the + `rotation` of the model entry each placement was read from, with its + scale and its origin left as the module had them. - This is the one thing here that changes a file. + A model no module names has nowhere to write to - its geometry is its + position - and is named in the report rather than passed over. + + This is the one thing here that changes a module file. """ - entry = self._current() - if entry is None: - return - if entry.source is None or not entry.loose: - self.statusBar().showMessage( - f'{entry.label}: no module in this tree names it, so there is no' - ' transform to write - move the boxes in ARMOUR_GUI instead') + moved = self.moved() + if not moved: + self.statusBar().showMessage('nothing has moved - nothing to write') return - path, section, index = entry.source - full = self._on_disk(path) - if full is None: - self.statusBar().showMessage(f'{path}: not in a directory to write to') - return - with open(full) as fh: - doc = json.load(fh) - model = doc['data'][section]['model'] - target = (model[index] if isinstance(model, list) else model) - transform = target.setdefault('transform', {}) - written = mc.decompose(entry.matrix) - for group in ('translation', 'rotation'): - transform[group] = {a: _round(written[group][a]) for a in 'xyz'} - with open(full, 'w') as fh: - fh.write(ae._dumps(doc)) - fh.write('\n') + written, refused = [], [] + for entry in moved: + if entry.source is None or not entry.loose: + refused.append(entry.label) + continue + path, section, index = entry.source + full = self._on_disk(path) + if full is None: + refused.append(entry.label) + continue + with open(full) as fh: + doc = json.load(fh) + model = doc['data'][section]['model'] + target = (model[index] if isinstance(model, list) else model) + transform = target.setdefault('transform', {}) + numbers = mc.decompose(entry.matrix) + for group in ('translation', 'rotation'): + transform[group] = {a: _round(numbers[group][a]) for a in 'xyz'} + with open(full, 'w') as fh: + fh.write(ae._dumps(doc)) + fh.write('\n') + # What the file says is what is declared now, so this is the state + # `Reset to declared` comes back to and the row stops being marked. + entry.given = entry.matrix.copy() + entry.declared = True + written.append(os.path.basename(path)) - # What the file says is what is declared now, so this is the state - # `Reset to declared` should come back to. - entry.given = entry.matrix.copy() - entry.declared = True - self.statusBar().showMessage(f'wrote the transform to {path}') + self._mark_moved() + said = f'wrote {len(written)}: ' + ', '.join(written) if written else '' + if refused: + said += ('; ' if said else '') + 'no module names ' + ', '.join(refused) + self.statusBar().showMessage(said) def unwrap_uvs(self, _checked=False): """Re-cut the selected model's texture so every face has its own patch. @@ -575,6 +594,7 @@ class Quickstart(QtWidgets.QMainWindow): for actor in self._actors_of(entry): actor.user_matrix = np.eye(4) self._sync_transform() + self._mark_moved() self.rebuild() def _folded(self, entry, user_matrix): @@ -679,6 +699,20 @@ class Quickstart(QtWidgets.QMainWindow): def _count(self): self.count_label.setText(f'{len(self.entries)} armour models') + def _mark_moved(self): + """Mark the rows whose model has been moved but not yet written. + + Editing the text is what makes a row say it; doing that quietly is + what keeps it from being read back as a tick and redrawing the scene + on every drag. + """ + moved = {id(e) for e in self.moved()} + self.list.blockSignals(True) + for item in self._rows(): + entry = self.entries[item.data(QtCore.Qt.UserRole)] + item.setText(entry.label + (' *' if id(entry) in moved else '')) + self.list.blockSignals(False) + def _rows(self): for i in range(self.list.count()): yield self.list.item(i)