Write every model that moved, and say which ones are waiting

`Write to source` saved the selected row and only that. Nudge three things into
place, click the third and press it, and two of them were still sitting in
memory with nothing on screen to say so - which reads exactly like a button
that does not save, because the file you go and look at is usually not the one
that was selected when you pressed it.

So it writes everything that has moved. A row whose model no longer matches
what its module declares is marked with a `*` until it has been written, so
what is pending is on the list rather than in your memory, and the report says
how many files were written and which. A model no module names - a sword part,
a gem case, an icon of its own - has nowhere to put a transform, and is now
named in that report rather than passed over in silence: it keeps its mark,
because it really is moved and really is unsaved.

Nothing about the writing itself changed. It was tested one model at a time,
which is the one case where the old behaviour was indistinguishable from the
new.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main
Amir Alexander Abdelbaki 2026-08-24 21:31:00 +02:00
parent 8e5178ece6
commit 8f253a848a
2 changed files with 79 additions and 43 deletions

View File

@ -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.

View File

@ -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
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:
self.statusBar().showMessage(f'{path}: not in a directory to write to')
return
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', {})
written = mc.decompose(entry.matrix)
numbers = mc.decompose(entry.matrix)
for group in ('translation', 'rotation'):
transform[group] = {a: _round(written[group][a]) for a in 'xyz'}
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` should come back to.
# `Reset to declared` comes back to and the row stops being marked.
entry.given = entry.matrix.copy()
entry.declared = True
self.statusBar().showMessage(f'wrote the transform to {path}')
written.append(os.path.basename(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)