Merge eureka/aetheria-release-ships-studio-footprint (ships-studio-footprint, head 13499535)
Spec aetheria-release:cut_spec:cut-ships-studio-footprint.r1; reports h1, h2; Soul s2 holds. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
e1ec1e89a2
6 changed files with 323 additions and 12 deletions
|
|
@ -132,8 +132,7 @@ public sealed class InspectableSchematicShapeDrawer : ICultInspectorDrawer
|
|||
using (new EditorGUILayout.HorizontalScope())
|
||||
for (var x = 0; x < shape.Width; x++)
|
||||
{
|
||||
var hardpoint = hull?.Hardpoints.FirstOrDefault(hp => hp.Shape.Coordinates
|
||||
.Any(v => v.x + hp.Position.x == x && v.y + hp.Position.y == y));
|
||||
var hardpoint = hull?.Hardpoints.FirstOrDefault(hp => HullFootprint.Cells(hp).Contains(int2(x, y)));
|
||||
var previous = GUI.backgroundColor;
|
||||
if (hardpoint != null) GUI.backgroundColor = hardpoint.TintColor.ToColor();
|
||||
shape[int2(x, y)] = GUILayout.Toggle(shape[int2(x, y)], GUIContent.none, GUILayout.Width(16));
|
||||
|
|
@ -145,3 +144,47 @@ public sealed class InspectableSchematicShapeDrawer : ICultInspectorDrawer
|
|||
return shape;
|
||||
}
|
||||
}
|
||||
|
||||
// A hardpoint's schematic footprint over its hull's cells: the hull's occupied cells shaded, this hardpoint's cells lit
|
||||
// in its tint, other hardpoints' cells tinted and disabled. A click edits Position and Shape together through
|
||||
// HullFootprint.Toggle, which owns the rule so a Thing host reads the same one. The footprint is authored for gameplay,
|
||||
// never derived from the model. Read-only on inspector.Record; only the HardpointData it is given changes.
|
||||
[CultInspectorDrawer(typeof(HardpointData))]
|
||||
public sealed class HardpointFootprintDrawer : ICultInspectorDrawer
|
||||
{
|
||||
public object Draw(CultInspector inspector, string label, Type type, object value, MemberInfo member)
|
||||
{
|
||||
var hardpoint = value as HardpointData;
|
||||
if (type != typeof(HardpointData) || hardpoint == null) return inspector.DrawDefault(label, type, value, member);
|
||||
var hull = inspector.Record as HullData;
|
||||
var own = hull?.Hardpoints.FirstOrDefault(hp => hp.Type == hardpoint.Type && hp.Position.Equals(hardpoint.Position)
|
||||
&& HullFootprint.Cells(hp).SequenceEqual(HullFootprint.Cells(hardpoint)));
|
||||
var drawn = (HardpointData) inspector.DrawDefault(label, type, value, member);
|
||||
if (hull?.Shape == null) return drawn;
|
||||
|
||||
var others = hull.Hardpoints.Where(hp => hp != own).ToArray();
|
||||
EditorGUILayout.LabelField("Footprint");
|
||||
for (var y = hull.Shape.Height - 1; y >= 0; y--)
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
for (var x = 0; x < hull.Shape.Width; x++)
|
||||
{
|
||||
var cell = int2(x, y);
|
||||
var other = others.FirstOrDefault(hp => HullFootprint.Cells(hp).Contains(cell));
|
||||
var on = HullFootprint.Cells(drawn).Contains(cell);
|
||||
var previous = GUI.backgroundColor;
|
||||
GUI.backgroundColor = on ? drawn.TintColor.ToColor() : other != null ? other.TintColor.ToColor()
|
||||
: hull.Shape[cell] ? Color.gray : previous;
|
||||
EditorGUI.BeginChangeCheck();
|
||||
using (new EditorGUI.DisabledScope(other != null && !on))
|
||||
GUILayout.Toggle(on || other != null, GUIContent.none, GUILayout.Width(16));
|
||||
if (EditorGUI.EndChangeCheck() && HullFootprint.Toggle(drawn.Shape, drawn.Position, cell, out var shape, out var position))
|
||||
{
|
||||
drawn.Shape = shape;
|
||||
drawn.Position = position;
|
||||
GUI.changed = true;
|
||||
}
|
||||
GUI.backgroundColor = previous;
|
||||
}
|
||||
return drawn;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
Assets/Scripts/ServerShared/HullFootprint.cs
Normal file
43
Assets/Scripts/ServerShared/HullFootprint.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
|
||||
// A hardpoint's schematic footprint: its Shape laid over the hull's cells at its Position, in hull orientation.
|
||||
// The footprint is authored for gameplay and never derived from the model or the Blender add-on
|
||||
// (schematic-and-model-are-separate-spaces). This is the one reading of footprint cells in hull space and the one
|
||||
// edit rule, so any host (the Studio drawer now, a Thing host later) edits footprints the same way.
|
||||
public static class HullFootprint
|
||||
{
|
||||
public static IEnumerable<int2> Cells(HardpointData hardpoint) =>
|
||||
hardpoint.Shape.Coordinates.Select(c => c + hardpoint.Position);
|
||||
|
||||
// Toggles one hull cell of the footprint at `position`. A cell outside the footprint grows it toward any edge;
|
||||
// a cell inside is removed and empty border rows and columns are trimmed; every other cell keeps its hull
|
||||
// coordinate. Removing the last cell is refused (false, outputs are the inputs' values). Builds a new Shape and
|
||||
// never edits the one it is given.
|
||||
public static bool Toggle(Shape footprint, int2 position, int2 hullCell, out Shape shape, out int2 newPosition)
|
||||
{
|
||||
var cells = Cells(footprint, position);
|
||||
if (!cells.Remove(hullCell)) cells.Add(hullCell);
|
||||
else if (cells.Count == 0)
|
||||
{
|
||||
shape = footprint;
|
||||
newPosition = position;
|
||||
return false;
|
||||
}
|
||||
|
||||
var min = cells.Aggregate((a, b) => new int2(math.min(a.x, b.x), math.min(a.y, b.y)));
|
||||
var max = cells.Aggregate((a, b) => new int2(math.max(a.x, b.x), math.max(a.y, b.y)));
|
||||
shape = new Shape(max.x - min.x + 1, max.y - min.y + 1);
|
||||
foreach (var cell in cells) shape[cell - min] = true;
|
||||
newPosition = min;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static HashSet<int2> Cells(Shape footprint, int2 position) =>
|
||||
new HashSet<int2>(footprint.Coordinates.Select(c => c + position));
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/HullFootprint.cs.meta
Normal file
11
Assets/Scripts/ServerShared/HullFootprint.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5e2b8c4a7d1f4a39b6c0e83f91d7a254
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -60,6 +60,7 @@ public class Shape
|
|||
}
|
||||
|
||||
Cells = newCells;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
private static int mod(int x, int m) {
|
||||
|
|
@ -85,11 +86,7 @@ public class Shape
|
|||
{
|
||||
get
|
||||
{
|
||||
if (_dirty)
|
||||
{
|
||||
_cachedShapeCoordinates = EnumerateShapeCoordinates().ToArray();
|
||||
_dirty = false;
|
||||
}
|
||||
Refresh();
|
||||
return _cachedShapeCoordinates;
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +105,14 @@ public class Shape
|
|||
private int2[] _cachedAllShapeCoordinates;
|
||||
|
||||
[IgnoreMember]
|
||||
public int2[] AllCoordinates => _cachedAllShapeCoordinates ?? (_cachedAllShapeCoordinates = EnumerateAllShapeCoordinates().ToArray());
|
||||
public int2[] AllCoordinates
|
||||
{
|
||||
get
|
||||
{
|
||||
Refresh();
|
||||
return _cachedAllShapeCoordinates;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<int2> EnumerateAllShapeCoordinates()
|
||||
{
|
||||
|
|
@ -121,11 +125,27 @@ public class Shape
|
|||
}
|
||||
}
|
||||
|
||||
private float2? _centerOfMass;
|
||||
private float2 _centerOfMass;
|
||||
|
||||
[IgnoreMember]
|
||||
public float2 CenterOfMass => _centerOfMass ?? (_centerOfMass = Coordinates
|
||||
.Aggregate(float2.zero, (total, coord) => total + coord) / Coordinates.Length).Value;
|
||||
public float2 CenterOfMass
|
||||
{
|
||||
get
|
||||
{
|
||||
Refresh();
|
||||
return _centerOfMass;
|
||||
}
|
||||
}
|
||||
|
||||
// One dirty flag: Coordinates, AllCoordinates and CenterOfMass are caches of Cells and recompute together.
|
||||
private void Refresh()
|
||||
{
|
||||
if (!_dirty) return;
|
||||
_cachedShapeCoordinates = EnumerateShapeCoordinates().ToArray();
|
||||
_cachedAllShapeCoordinates = EnumerateAllShapeCoordinates().ToArray();
|
||||
_centerOfMass = _cachedShapeCoordinates.Aggregate(float2.zero, (total, coord) => total + coord) / _cachedShapeCoordinates.Length;
|
||||
_dirty = false;
|
||||
}
|
||||
|
||||
public bool this[int2 pos] {
|
||||
get { return pos.x >= 0 && pos.y >= 0 && pos.x < Width && pos.y < Height && Cells[pos.x, pos.y]; }
|
||||
|
|
@ -256,7 +276,7 @@ public class Shape
|
|||
{
|
||||
if (x >= 0 && x < Width && y >= 0 && y < Height)
|
||||
{
|
||||
Cells[steep ? y : x, steep ? x : y] = true;
|
||||
this[int2(steep ? y : x, steep ? x : y)] = true;
|
||||
}
|
||||
error += derr;
|
||||
if (error >= 0.5f)
|
||||
|
|
|
|||
126
tests/Aetheria.Shared.Tests/HullFootprintTests.cs
Normal file
126
tests/Aetheria.Shared.Tests/HullFootprintTests.cs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
using System.Linq;
|
||||
using CultMath;
|
||||
using Xunit;
|
||||
|
||||
// HullFootprint: the one rule for editing a hardpoint's schematic footprint (Position + Shape together) and the
|
||||
// one reading of its cells in hull space.
|
||||
public sealed class HullFootprintTests
|
||||
{
|
||||
private static HardpointData Hardpoint(int2 position, params int2[] cellsInShapeSpace)
|
||||
{
|
||||
var max = cellsInShapeSpace.Aggregate(new int2(0, 0), (a, c) => new int2(CultMath.math.max(a.x, c.x), CultMath.math.max(a.y, c.y)));
|
||||
var shape = new Shape(max.x + 1, max.y + 1);
|
||||
foreach (var c in cellsInShapeSpace) shape[c] = true;
|
||||
return new HardpointData { Position = position, Shape = shape };
|
||||
}
|
||||
|
||||
private static int2[] Hull(HardpointData hp) => HullFootprint.Cells(hp).OrderBy(c => c.y).ThenBy(c => c.x).ToArray();
|
||||
|
||||
private static HardpointData Apply(HardpointData hp, int2 hullCell)
|
||||
{
|
||||
Assert.True(HullFootprint.Toggle(hp.Shape, hp.Position, hullCell, out var shape, out var position));
|
||||
return new HardpointData { Position = position, Shape = shape };
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellsAreTheShapeLaidAtPosition()
|
||||
{
|
||||
var hp = Hardpoint(new int2(3, 3), new int2(0, 0), new int2(1, 0));
|
||||
Assert.Equal(new[] { new int2(3, 3), new int2(4, 3) }, Hull(hp));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FootprintGrowsTowardAnyEdgeAndKeepsItsCells()
|
||||
{
|
||||
var hp = Hardpoint(new int2(3, 3), new int2(0, 0));
|
||||
|
||||
var grown = Apply(Apply(hp, new int2(2, 3)), new int2(3, 2));
|
||||
|
||||
Assert.Equal(new int2(2, 2), grown.Position);
|
||||
Assert.Equal(2, grown.Shape.Width);
|
||||
Assert.Equal(2, grown.Shape.Height);
|
||||
Assert.Equal(new[] { new int2(3, 2), new int2(2, 3), new int2(3, 3) }, Hull(grown));
|
||||
|
||||
var right = Apply(Apply(hp, new int2(4, 3)), new int2(3, 4));
|
||||
Assert.Equal(new int2(3, 3), right.Position);
|
||||
Assert.Equal(new[] { new int2(3, 3), new int2(4, 3), new int2(3, 4) }, Hull(right));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FootprintTrimsBackToItsOriginal()
|
||||
{
|
||||
var hp = Hardpoint(new int2(3, 3), new int2(0, 0));
|
||||
var grown = Apply(Apply(hp, new int2(2, 3)), new int2(3, 2));
|
||||
|
||||
var trimmed = Apply(Apply(grown, new int2(2, 3)), new int2(3, 2));
|
||||
|
||||
Assert.Equal(hp.Position, trimmed.Position);
|
||||
Assert.Equal(1, trimmed.Shape.Width);
|
||||
Assert.Equal(1, trimmed.Shape.Height);
|
||||
Assert.Equal(Hull(hp), Hull(trimmed));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FootprintRefusesItsLastCell()
|
||||
{
|
||||
var hp = Hardpoint(new int2(3, 3), new int2(0, 0));
|
||||
|
||||
Assert.False(HullFootprint.Toggle(hp.Shape, hp.Position, new int2(3, 3), out var shape, out var position));
|
||||
|
||||
Assert.Equal(hp.Position, position);
|
||||
Assert.Same(hp.Shape, shape);
|
||||
Assert.Equal(new[] { new int2(3, 3) }, Hull(hp));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RefusedClickLeavesPositionWhereItWasWhenTheLastCellIsNotAtPosition()
|
||||
{
|
||||
var hp = Hardpoint(new int2(3, 3), new int2(1, 0));
|
||||
Assert.Equal(new[] { new int2(4, 3) }, Hull(hp));
|
||||
|
||||
Assert.False(HullFootprint.Toggle(hp.Shape, hp.Position, new int2(4, 3), out var shape, out var position));
|
||||
|
||||
Assert.Equal(new int2(3, 3), position);
|
||||
Assert.Same(hp.Shape, shape);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WideFootprintLaysEveryCellAtPositionAndEditsAtItsFarEnd()
|
||||
{
|
||||
var hp = Hardpoint(new int2(2, 2), new int2(0, 0), new int2(1, 0), new int2(2, 0), new int2(3, 0), new int2(4, 0));
|
||||
Assert.Equal(new[] { new int2(2, 2), new int2(3, 2), new int2(4, 2), new int2(5, 2), new int2(6, 2) }, Hull(hp));
|
||||
|
||||
var grown = Apply(hp, new int2(7, 2));
|
||||
Assert.Equal(new int2(2, 2), grown.Position);
|
||||
Assert.Equal(new[] { new int2(2, 2), new int2(3, 2), new int2(4, 2), new int2(5, 2), new int2(6, 2), new int2(7, 2) }, Hull(grown));
|
||||
|
||||
var trimmed = Apply(hp, new int2(6, 2));
|
||||
Assert.Equal(new int2(2, 2), trimmed.Position);
|
||||
Assert.Equal(new[] { new int2(2, 2), new int2(3, 2), new int2(4, 2), new int2(5, 2) }, Hull(trimmed));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FootprintToggleLeavesItsInputUnchanged()
|
||||
{
|
||||
var hp = Hardpoint(new int2(3, 3), new int2(0, 0), new int2(1, 0));
|
||||
|
||||
HullFootprint.Toggle(hp.Shape, hp.Position, new int2(2, 3), out _, out _);
|
||||
HullFootprint.Toggle(hp.Shape, hp.Position, new int2(4, 3), out _, out _);
|
||||
|
||||
Assert.Equal(new int2(3, 3), hp.Position);
|
||||
Assert.Equal(2, hp.Shape.Width);
|
||||
Assert.Equal(1, hp.Shape.Height);
|
||||
Assert.Equal(new[] { new int2(3, 3), new int2(4, 3) }, Hull(hp));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimmingAnInteriorBorderMovesPositionWithoutMovingCells()
|
||||
{
|
||||
var hp = Hardpoint(new int2(5, 5), new int2(0, 0), new int2(1, 0), new int2(2, 0));
|
||||
|
||||
var trimmed = Apply(hp, new int2(5, 5));
|
||||
|
||||
Assert.Equal(new int2(6, 5), trimmed.Position);
|
||||
Assert.Equal(new[] { new int2(6, 5), new int2(7, 5) }, Hull(trimmed));
|
||||
}
|
||||
}
|
||||
68
tests/Aetheria.Shared.Tests/ShapeCacheTests.cs
Normal file
68
tests/Aetheria.Shared.Tests/ShapeCacheTests.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using System.Linq;
|
||||
using CultMath;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
|
||||
// Shape's derived reads (Coordinates, AllCoordinates, CenterOfMass) are caches of Cells and follow every edit.
|
||||
public sealed class ShapeCacheTests
|
||||
{
|
||||
[Fact]
|
||||
public void CenterOfMassFollowsACellCleared()
|
||||
{
|
||||
var shape = new Shape(2, 1);
|
||||
shape[int2(0, 0)] = true;
|
||||
shape[int2(1, 0)] = true;
|
||||
Assert.Equal(0.5f, shape.CenterOfMass.x);
|
||||
|
||||
shape[int2(0, 0)] = false;
|
||||
|
||||
Assert.Equal(1f, shape.CenterOfMass.x);
|
||||
Assert.Equal(0f, shape.CenterOfMass.y);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResizeRecountsEveryCoordinateRead()
|
||||
{
|
||||
var shape = new Shape(2, 1);
|
||||
shape[int2(0, 0)] = true;
|
||||
shape[int2(1, 0)] = true;
|
||||
Assert.Equal(2, shape.AllCoordinates.Length);
|
||||
Assert.Equal(2, shape.Coordinates.Length);
|
||||
var before = shape.CenterOfMass;
|
||||
|
||||
shape.Resize(3, 2);
|
||||
|
||||
Assert.Equal(6, shape.AllCoordinates.Length);
|
||||
Assert.Equal(shape.Cells.Cast<bool>().Count(c => c), shape.Coordinates.Length);
|
||||
Assert.NotEqual(before, shape.CenterOfMass);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResizeKeepsExistingCellsAndFillsNewOnes()
|
||||
{
|
||||
// The hull drawer's width grow relies on the fill (ships-studio-footprint r1 keeps it).
|
||||
var shape = new Shape(2, 1);
|
||||
shape[int2(0, 0)] = true;
|
||||
|
||||
shape.Resize(3, 1);
|
||||
|
||||
Assert.True(shape[int2(0, 0)]);
|
||||
Assert.False(shape[int2(1, 0)]);
|
||||
Assert.True(shape[int2(2, 0)]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetLineRecountsEveryCoordinateRead()
|
||||
{
|
||||
var shape = new Shape(4, 1);
|
||||
shape[int2(0, 0)] = true;
|
||||
Assert.Single(shape.Coordinates);
|
||||
Assert.Equal(0f, shape.CenterOfMass.x);
|
||||
|
||||
shape.SetLine(float2(0f, 0f), float2(3f, 0f));
|
||||
|
||||
Assert.Equal(4, shape.Coordinates.Length);
|
||||
Assert.Equal(4, shape.AllCoordinates.Length);
|
||||
Assert.Equal(1.5f, shape.CenterOfMass.x);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue