Blender ships: hull_grid.grid_size; ship_cc reads HullData.HullType, pinned to C#

grid_size is rasterise's grid arithmetic without its MAX_CELLS refusal, so a
check can judge the bound; rasterise calls the same private filter and sizing.
ship_cc gains HULL_TYPE_SLOT (Key 25), HULL_TYPE_NAMES and read_hull_type;
ShipSchemaPinTests ties both to HullData.HullType and the HullType enum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
soul 2026-10-22 01:00:06 +02:00
commit 0a70e0cf93
5 changed files with 71 additions and 10 deletions

View file

@ -22,6 +22,7 @@ public sealed class ShipSchemaPinTests
[InlineData("SCHEMATIC_LINES_SLOT", typeof(ShipAuthoring), nameof(ShipAuthoring.SchematicLines))]
[InlineData("HULL_SHAPE_SLOT", typeof(HullData), nameof(HullData.Shape))]
[InlineData("HULL_HARDPOINTS_SLOT", typeof(HullData), nameof(HullData.Hardpoints))]
[InlineData("HULL_TYPE_SLOT", typeof(HullData), nameof(HullData.HullType))]
[InlineData("PRODUCT_MANUFACTURER_SLOT", typeof(FactionProductData), nameof(FactionProductData.Manufacturer))]
public void PythonSlotConstantsAreTheKeysOfTheMembersTheyNameInTheirComments(string constant, Type owner, string member)
{
@ -136,6 +137,7 @@ public sealed class ShipSchemaPinTests
{
Assert.Equal(EnumNames<HardpointType>(), Strings("HARDPOINT_TYPE_NAMES"));
Assert.Equal(EnumNames<ItemRotation>(), Strings("ROTATION_NAMES"));
Assert.Equal(EnumNames<HullType>(), Strings("HULL_TYPE_NAMES"));
}
[Fact]

View file

@ -32,6 +32,27 @@ def _inside(triangle, px, py):
return not ((d1 < 0 or d2 < 0 or d3 < 0) and (d1 > 0 or d2 > 0 or d3 > 0))
def _with_area(triangles_xy):
triangles = [tri for tri in triangles_xy if
(tri[1][0] - tri[0][0]) * (tri[2][1] - tri[0][1]) != (tri[1][1] - tri[0][1]) * (tri[2][0] - tri[0][0])]
if not triangles:
raise ValueError("The hull has no triangle with area to rasterise")
return triangles
def _size(triangles, cell_size):
xs = [p[0] for tri in triangles for p in tri]
ys = [p[1] for tri in triangles for p in tri]
return _cells_across(max(xs) - min(xs), cell_size), _cells_across(max(ys) - min(ys), cell_size), xs, ys
def grid_size(triangles_xy, cell_size=CELL_SIZE):
"""(width, height) in cells of the grid rasterise would lay over these triangles, without refusing a grid over
MAX_CELLS: the caller judges the bound. Raises ValueError when no triangle has area."""
width, height, _, _ = _size(_with_area(triangles_xy), cell_size)
return width, height
def rasterise(triangles_xy, cell_size=CELL_SIZE, samples=SAMPLES):
"""Occupied cells of a top-down silhouette given as triangles of (x, y) points in the Ship Root frame.
@ -40,14 +61,8 @@ def rasterise(triangles_xy, cell_size=CELL_SIZE, samples=SAMPLES):
inside several triangles counts once, and each triangle is tested only at the samples inside its bounding box.
Returns (width, height, cells, grid_origin): cells are column-major, cells[x * height + y], and grid_origin is cell
(0, 0)'s centre."""
triangles = [tri for tri in triangles_xy if
(tri[1][0] - tri[0][0]) * (tri[2][1] - tri[0][1]) != (tri[1][1] - tri[0][1]) * (tri[2][0] - tri[0][0])]
if not triangles:
raise ValueError("The hull has no triangle with area to rasterise")
xs = [p[0] for tri in triangles for p in tri]
ys = [p[1] for tri in triangles for p in tri]
width = _cells_across(max(xs) - min(xs), cell_size)
height = _cells_across(max(ys) - min(ys), cell_size)
triangles = _with_area(triangles_xy)
width, height, xs, ys = _size(triangles, cell_size)
for axis, count in (("wide", width), ("long", height)):
if count > MAX_CELLS:
raise ValueError(f"The hull needs {count} cells {axis}; a ship grid holds at most {MAX_CELLS}")

View file

@ -34,15 +34,17 @@ ANCHORS_SLOT = 3 # ShipAuthoring.Anchors
SCHEMATIC_LINES_SLOT = 4 # ShipAuthoring.SchematicLines
HULL_SHAPE_SLOT = 5 # ItemData.Shape
HULL_HARDPOINTS_SLOT = 23 # HullData.Hardpoints
HULL_TYPE_SLOT = 25 # HullData.HullType
# HardpointData member names in slot order; a hardpoint row is one value per name, and any slot past the last
# name belongs to a later schema and is carried through untouched.
HARDPOINT_MEMBERS = ("Type", "Position", "Shape", "Transform", "Rotation", "Armor", "FiringArc")
# ShipAnchor member names in slot order; as for hardpoints, slots past the last name are carried through by anchor Id.
ANCHOR_MEMBERS = ("Id", "Role", "ModelNodeId", "ParentId", "Order")
# Enum member names in value order (HardpointType, ItemRotation).
# Enum member names in value order (HardpointType, ItemRotation, HullType).
HARDPOINT_TYPE_NAMES = ("Hull", "Tool", "Thermal", "Thruster", "WarpDrive", "Reactor", "Radiator",
"Shield", "Sensors", "Energy", "Ballistic", "Launcher", "ControlModule")
ROTATION_NAMES = ("None", "CounterClockwise", "Reversed", "Clockwise")
HULL_TYPE_NAMES = ("Ship", "Station", "Turret")
# ShipAuthoringStore.RequireShipId's rule, mirrored so New Ship refuses before it touches anything; ShipSchemaPinTests
@ -208,6 +210,15 @@ def read_layout(path: str, cultlib_packages: str):
return hull[HULL_SHAPE_SLOT][0], hull[HULL_HARDPOINTS_SLOT], layout_revision(hull[HULL_SHAPE_SLOT][0], hull[HULL_HARDPOINTS_SLOT])
def read_hull_type(path: str, cultlib_packages: str) -> str:
"""The hull's HullData.HullType as its enum member name ("Ship", "Station" or "Turret")."""
hull = _hull_body(read(path, cultlib_packages))
value = hull[HULL_TYPE_SLOT] if len(hull) > HULL_TYPE_SLOT else None
if type(value) is not int or not 0 <= value < len(HULL_TYPE_NAMES):
raise ValueError("The ship file's hull has no hull type this add-on knows")
return HULL_TYPE_NAMES[value]
def layout_revision(shape: list[Any], hardpoints: list[list[Any]]) -> str:
"""The revision of a layout: a hash of its shape ([width, height, cells]) and hardpoint rows as the file stores
them. replace_layout returns it, Load records it, and Package compares the layout buffer's against the file's."""

View file

@ -136,6 +136,16 @@ class RasteriseTests(unittest.TestCase):
hull_grid.rasterise(rect(0, 0, 66, 2))
self.assertEqual(str(caught.exception), "The hull needs 33 cells wide; a ship grid holds at most 32")
def test_grid_size_is_rasterises_size_and_does_not_refuse_an_oversized_grid(self):
self.assertEqual(hull_grid.grid_size(rect(0, 0, 2, 64)), hull_grid.rasterise(rect(0, 0, 2, 64))[:2])
self.assertEqual(hull_grid.grid_size(rect(-3, -1, 3, 4) + CORNER), hull_grid.rasterise(rect(-3, -1, 3, 4) + CORNER)[:2])
self.assertEqual(hull_grid.grid_size(rect(0, 0, 2, 66)), (1, 33))
self.assertEqual(hull_grid.grid_size(rect(0, 0, 66, 2)), (33, 1))
self.assertEqual(hull_grid.grid_size(rect(0, 0, 4, 4), cell_size=1.0), (4, 4))
with self.assertRaises(ValueError) as caught:
hull_grid.grid_size([((0, 0), (1, 1), (2, 2))])
self.assertEqual(str(caught.exception), "The hull has no triangle with area to rasterise")
def test_a_silhouette_with_no_area_is_refused(self):
for triangles in ([], [((0, 0), (1, 1), (2, 2))]):
with self.assertRaises(ValueError) as caught:

View file

@ -44,8 +44,9 @@ def hardpoint_row(mount="thruster.port", tail=("future-hardpoint-slot",)):
def hull_body():
hull = [None] * (ship_cc.HULL_HARDPOINTS_SLOT + 1)
hull = [None] * (ship_cc.HULL_TYPE_SLOT + 1)
hull[1] = "Skiff"
hull[ship_cc.HULL_TYPE_SLOT] = 0
hull[ship_cc.HULL_SHAPE_SLOT] = [shape(2, 2, [True, False, True, False])]
hull[ship_cc.HULL_HARDPOINTS_SLOT] = [hardpoint_row()]
hull.append("future-hull-slot")
@ -128,6 +129,28 @@ class ReadTests(ShipFileCase):
self.assertEqual("Skiff", file.hull.body[1])
self.assertEqual(ship_cc.HULL_SCHEMA, file.hull.envelope.type)
def test_reads_the_hulls_type_by_its_enum_name(self):
for value, name in ((0, "Ship"), (1, "Station"), (2, "Turret")):
with self.subTest(name):
hull = hull_body()
hull[ship_cc.HULL_TYPE_SLOT] = value
self.write(ship_body(), hull)
self.assertEqual(name, ship_cc.read_hull_type(self.path, PACKAGES))
def test_refuses_a_hull_type_it_does_not_know_without_echoing_the_path(self):
short = hull_body()[:ship_cc.HULL_TYPE_SLOT]
for label, value in (("past the enum", 3), ("negative", -1), ("bool", True), ("missing", None),
("text", "Ship"), ("short body", short)):
with self.subTest(label):
hull = value if label == "short body" else hull_body()
if label != "short body":
hull[ship_cc.HULL_TYPE_SLOT] = value
self.write(ship_body(), hull)
with self.assertRaises(ValueError) as caught:
ship_cc.read_hull_type(self.path, PACKAGES)
self.assertEqual("The ship file's hull has no hull type this add-on knows", str(caught.exception))
self.assertNotIn(self.path, str(caught.exception))
def test_reads_a_file_whose_hull_also_carries_products(self):
self.add_products()
file = ship_cc.read(self.path, PACKAGES)