feat(hero): v0.1.2 — scale calibration, façade detail, export preset refactor (#28)

Merged PR #28: v0.1.2 — scale calibration, façade detail, export preset

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
This commit was merged in pull request #28.
This commit is contained in:
zwitschi authored and zwitschi committed 2026-11-08 18:48:33 +01:00
1 parent 640bff9631
commit ba68dcfdbc
60 files changed
+1114 -291

No files matched your search

+108
View File
@@ -0,0 +1,108 @@
"""Tests for Tools/facade_detail_concepts.py — the pure parts.
No network calls: the OpenRouter request and the image bytes are represented as
the small structures the API uses. Generation itself is manual and paid.
"""
import base64
import json
import os
import sys
import unittest
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
sys.path.insert(0, os.path.join(ROOT, "Tools"))
import facade_detail_concepts as fc # noqa: E402
class StudyTableTest(unittest.TestCase):
def test_every_study_has_a_reference_and_a_prompt(self):
self.assertTrue(fc.STUDIES)
for name, (ref, prompt) in fc.STUDIES.items():
self.assertTrue(ref, f"{name} has no reference")
self.assertTrue(prompt.strip(), f"{name} has no prompt")
# References are relative to Art/reference and never escape it.
self.assertFalse(os.path.isabs(ref), f"{name} reference is absolute")
self.assertNotIn("..", ref, f"{name} reference escapes the directory")
def test_prompt_always_carries_the_anchor_suffix(self):
for name in fc.STUDIES:
prompt = fc.build_prompt(name)
self.assertIn(fc.ANCHOR_SUFFIX, prompt)
self.assertIn(fc.STUDIES[name][1], prompt)
def test_unknown_study_raises(self):
with self.assertRaises(KeyError):
fc.build_prompt("no-such-study")
class NextFreePathTest(unittest.TestCase):
def test_uses_the_plain_name_when_free(self):
import tempfile
with tempfile.TemporaryDirectory() as tmp:
self.assertEqual(os.path.basename(fc.next_free_path("x", tmp)), "x.png")
def test_never_overwrites(self):
import tempfile
with tempfile.TemporaryDirectory() as tmp:
open(os.path.join(tmp, "x.png"), "w").close()
self.assertEqual(os.path.basename(fc.next_free_path("x", tmp)), "x-v2.png")
open(os.path.join(tmp, "x-v2.png"), "w").close()
self.assertEqual(os.path.basename(fc.next_free_path("x", tmp)), "x-v3.png")
class DecodeImageTest(unittest.TestCase):
PAYLOAD_BYTES = b"\x89PNG\r\n\x1a\nfake"
def _data_url(self):
return "data:image/png;base64," + base64.b64encode(self.PAYLOAD_BYTES).decode()
def test_reads_message_images(self):
payload = {"choices": [{"message": {
"images": [{"image_url": {"url": self._data_url()}}]}}]}
self.assertEqual(fc.decode_image(payload), self.PAYLOAD_BYTES)
def test_reads_a_data_url_embedded_in_content(self):
payload = {"choices": [{"message": {"content": f"here: {self._data_url()}"}}]}
self.assertEqual(fc.decode_image(payload), self.PAYLOAD_BYTES)
def test_refusal_raises_with_the_text(self):
payload = {"choices": [{"message": {"content": "I cannot do that."}}]}
with self.assertRaises(SystemExit):
fc.decode_image(payload)
class DataUrlTest(unittest.TestCase):
def test_encodes_a_jpeg(self):
import tempfile
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as handle:
handle.write(b"jpegbytes")
path = handle.name
try:
url = fc.data_url(path)
self.assertTrue(url.startswith("data:image/jpeg;base64,"))
finally:
os.unlink(path)
def test_rejects_an_unsupported_type(self):
import tempfile
with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as handle:
path = handle.name
try:
with self.assertRaises(SystemExit):
fc.data_url(path)
finally:
os.unlink(path)
class SidecarShapeTest(unittest.TestCase):
def test_studies_and_outputs_are_local_only(self):
# The output directory lives under the gitignored Art/reference tree, so
# neither the images nor the sidecars can be committed.
self.assertIn(os.path.join("Art", "reference"), fc.OUT_DIR)
self.assertIn("gitignored", fc.__doc__)
if __name__ == "__main__":
unittest.main()
+70
View File
@@ -0,0 +1,70 @@
"""Tests for Tools/fbx_export_preset.py.
The preset module keeps its argument set in plain dicts and builds it in
`preset_kwargs()`, so the export contract can be checked without starting
Blender. Only `export_static`/`export_character` import `bpy`.
"""
import os
import sys
import unittest
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
sys.path.insert(0, os.path.join(ROOT, "Tools"))
import fbx_export_preset as preset # noqa: E402
class MetricPresetTest(unittest.TestCase):
def test_scale_and_axis_contract(self):
# These three are the project's recorded conversion contract: metres ->
# centimetres by a factor of exactly 100, east +X, north -Y, up +Z.
kwargs = preset.preset_kwargs()
self.assertTrue(kwargs["apply_unit_scale"])
self.assertEqual(kwargs["apply_scale_options"], "FBX_SCALE_NONE")
self.assertEqual(kwargs["axis_forward"], "-Y")
self.assertEqual(kwargs["axis_up"], "Z")
def test_smoothing_groups_are_exported(self):
# "OFF" (the pre-refactor value) drops smoothing groups and every Unreal
# import warns about it, with round parts faceted.
self.assertEqual(preset.preset_kwargs()["mesh_smooth_type"], "FACE")
def test_selection_flag(self):
self.assertFalse(preset.preset_kwargs()["use_selection"])
self.assertTrue(preset.preset_kwargs(selection=True)["use_selection"])
def test_object_types_are_meshes_only(self):
self.assertEqual(preset.preset_kwargs()["object_types"], {"MESH"})
class CharacterPresetTest(unittest.TestCase):
def test_character_exports_armature_and_mesh(self):
kwargs = preset.preset_kwargs("ue_character_v1")
self.assertEqual(kwargs["object_types"], {"ARMATURE", "MESH"})
self.assertFalse(kwargs["add_leaf_bones"])
self.assertEqual(kwargs["path_mode"], "COPY")
self.assertFalse(kwargs["embed_textures"])
def test_character_keeps_the_scale_contract(self):
kwargs = preset.preset_kwargs("ue_character_v1")
self.assertEqual(kwargs["apply_scale_options"], "FBX_SCALE_NONE")
self.assertEqual(kwargs["mesh_smooth_type"], "FACE")
class OverrideTest(unittest.TestCase):
def test_overrides_win(self):
kwargs = preset.preset_kwargs(mesh_smooth_type="OFF")
self.assertEqual(kwargs["mesh_smooth_type"], "OFF")
def test_unknown_preset_raises(self):
with self.assertRaises(KeyError):
preset.preset_kwargs("ue_imperial_v9")
def test_filepath_must_not_be_an_override(self):
with self.assertRaises(ValueError):
preset.preset_kwargs(filepath="/tmp/x.fbx")
if __name__ == "__main__":
unittest.main()