Stale sweep identified generated anchors by collection membership and role, so an author's hull-collider in
Generated was deleted. Generated anchors now carry aetheria.generated; the sweep removes only those. New smoke
pass package_asymmetric_pass: an L-shaped render hull pins tractor X and shield centre to the bounds centre,
a Source-only ship pins that generation never reads the Source fallback, and a collider placed in Generated
survives Package. All commits build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AetherDb ship-authoring centre prints Shape.CenterOfMass; Package asks it and moves Ship Root so that point sits
at the world origin, after regenerating the unowned map icon, shield and tractor from the render meshes (Keep
leaves one alone). Package never makes a hull collider (ruling package-frame-defer-collider). smoke_package's
bound-collection rollback cases now rebind to a different path so a rollback that restores keys but not values fails.
C# not yet built when committed; the Blender smokes and Python unit tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_bind_collection restores every aetheria.* key if the ID-property store
refuses a value, so a bad ship id or pending leaves the collection as it
was. smoke_package.py gains default-run cases: relpath raising ValueError
(the cross-drive signal) must store the absolute path and finish; a refused
id or pending must leave bound and unbound collections unchanged.
All commits build (Python only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New Ship places each mesh by the matrix an object can hold (_held: location, rotation, scale), so a sheared parent no
longer makes the drafted cells differ from the cells Rasterise reads from the hull left behind. smoke_frame pins a failed
Save of a saved ship (write failure and stale revision) leaving its ship.cc, New Ship refusing a vertical wall before
anything changes (twice), the sheared-parent equality, and Ctrl+Z past a first Save: the refusals name the way forward
(Bind Ship Collection, Load Layout) and the route works. All commits build.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
New Ship baked rotation and scale into the mesh data after checking the cells on the evaluated mesh, so a Mirror
modifier acted in a changed frame (a half hull, length 10, got 3x5 cells). The meshes now keep their data and carry the
placement as their own matrix; the cells and grid_origin are the ones computed before the bind and are placed by the one
_place_grid writer, which Rasterise shares; no call that can refuse follows the bind. The single-user refusal, which
protected the baking, is deleted. One _triangles_xy reads the evaluated meshes for both New Ship and Rasterise.
smoke_frame: a Mirror half hull gets the cells of the hand-mirrored solid, and the drafted cells equal Rasterise's of the
hull left behind. Every commit builds (Python only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One frame predicate: _loose_meshes no longer exempts Source, so a mesh Rasterise reads is a mesh Flip Nose turns (it
turns Ship Root's descendants) or the operator refuses and selects it. Package: the glTF exporter's RuntimeError is
printed to the console and reported as fixed text; the validator's verdict goes to the console and the panel reports
accepted or refused with the anchor count only (no ship ID, no anchor ID). smoke_frame and smoke_package pin each, five
hand mutants caught. Every commit builds (Python only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One chooser (_ship_collection) and one predicate (_is_ship). _save_layout is the only
replace_layout caller and creates a pending ship's file, removing what it made on failure.
Rasterise writes the layout buffer and aetheria.grid_origin = [x, y, width, height]; Flip Nose
and Package refuse meshes outside Ship Root. New Ship checks first and writes nothing to disk.
Operator reports are fixed text; AetherDb output goes to the system console. smoke_frame covers
undo, hardpoints, Save, cleanup, loose meshes and an empty Game folder preference.
Builds: yes (needs the previous commit's ship_cc).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit's _aetherdb did not parse (unterminated string literal), so 63d16e81 and 6971ff70 cannot import the add-on; this one can.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FindRoot keys on GameData/Aetheria.cc. ShipAuthoringStore.RequireShipId is the one ID rule
(Validate and create), refusing Windows device names and a trailing dot, naming no value.
The add-on's one preference is Game folder; _aetheria_repo, the repo preference and the
dotnet-run invocation are deleted. Smokes take SMOKE_GAME_FOLDER. Not yet built or tested
at this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New Ship creates the ship .cc, binds through the one shared binding, moves the meshes into Source
under a Ship Root, turns them nose to -Y and sizes them. Rasterise is the only writer of
aetheria.grid_origin; Load, Save and Rasterise redraw the Grid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bind called bpy.path.relpath after setting aetheria.asset_kind and aetheria.id, so a .cc on another drive than the
.blend raised and left the collection half-bound, and an unsaved .blend stored a path relative to the working
directory. The stored path is now computed first: absolute for an unsaved .blend, '//'-relative when relpath succeeds,
absolute when it raises ValueError. Every reader already resolves both through bpy.path.abspath.
smoke_package.py gains the bind cases: unsaved stores absolute and survives two Save As moves, a failing bind leaves
no aetheria.* property, same-drive stores '//'-relative, and with SMOKE_OTHER_DRIVE the final bind crosses drives
before the package passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every object tagged aetheria.role becomes a ShipAnchor whose Id and ModelNodeId are its aetheria.id (a muzzle's ParentId
is its parent's). The collection, Grease Pencil excluded, exports to ship.glb with custom properties as node extras;
replace_visual writes the anchors, a lone Grease Pencil object's lines are captured, and AetherDb's verdict shows in the
panel. The add-on preference 'Aetheria repo' names the checkout; by default it is the nearest folder above the .cc.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mod ship file now holds a HullData at mod-hull:<id> and the ShipAuthoring
visual package at mod-ship:<id>. HullData.Visual (key 32) is the typed ref
between them; ShipAuthoring.Hull (key 1) is retired.
- One joint validator over (hull, visual) is the only semantic check; Load
owns the file structure (one hull, one visual, deterministic keys).
- Compose copies the mod store's records verbatim, so the derived catalog
holds one hull per mod ship.
- ship_cc.py edits the hull record's slots directly; the body[HULL_SLOT]
indirection is gone.
- The package fixture is generated from ShipAuthoringTests.Fixture() (glTF
scene included) and exported when AETHERIA_SHIP_FIXTURE_DIR is set.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ship_cc.py edited a ShipAuthoring record by bare slot numbers and hardpoint
positions (hp[3], hp[7:], raw[0..6]), and __init__.py copied the
HardpointType and ItemRotation enums by position. Nothing tested either
file and nothing tied the numbers to the C# [Key] attributes that own them.
- ship_cc.py names its schema facts once: the four record slots (each
commented with the C# member it mirrors), HARDPOINT_MEMBERS (HardpointData
member names in slot order), and the two enum name tuples. Hardpoint rows
are built and read through encode_hardpoint/decode_hardpoint; replace_layout
and the Blender operators no longer index a row by literal. __init__.py
builds its enum items from the tuples. The rotation label
"Counterclockwise" becomes "CounterClockwise", matching the C# member.
- tools/blender/tests/test_ship_cc.py (30 unittest cases, bpy-free, needs
CultLib's cultcache-py and msgpack): read, replace_lines, read_layout,
replace_layout (untouched slots and later-schema hardpoint slots preserved
by mount id, schema catalog preserved, every refusal leaves the file
byte-identical), the row codec, and capture_grease_pencil against stand-in
objects.
- ShipSchemaPinTests (C#, 7 cases) reads ship_cc.py and fails when a slot
constant, a HARDPOINT_MEMBERS name or position, or an enum name/value
disagrees with the C# types, and when a serialized ship no longer has the
nested array shapes the Python indexes (schematic [[w, h, cells]], cells
x-major, hardpoint transform and footprint positions).
Reading the slots from the file's schema catalog instead was rejected: the
catalog lists only document members, and Hull, Shape and HardpointData are
nested types it does not describe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>