acdream/src/AcDream.App/Rendering/Wb/GeometryUtils.cs
Erik dc722e70bd feat(O-T7): drop WB project references; complete extraction
End of Phase O extraction. Final cleanup:

- Dropped <ProjectReference> entries to WorldBuilder.Shared and
  Chorizite.OpenGLSDLBackend from both AcDream.App.csproj and
  AcDream.Core.csproj.
- Added Chorizite.Core NuGet PackageReference to AcDream.Core.csproj
  (needed by Core.Rendering.Wb.TextureHelpers for TextureFormat enum;
  previously transitive through the WB project ref).
- Added BCnEncoder.Net.ImageSharp (1.1.2) + SixLabors.ImageSharp (3.1.12)
  as direct PackageReferences to AcDream.App.csproj — previously transitive
  via Chorizite.OpenGLSDLBackend project; used directly by ObjectMeshManager.

Item A (BaseObjectRenderManager static fields):
- Inlined CurrentAtlas/CurrentVAO/CurrentIBO into a new RenderStateCache.cs
  static class (AcDream.App.Rendering.Wb namespace) — the 4 consumers
  (ManagedGLIndexBuffer, ManagedGLTexture, ManagedGLTextureArray, ParticleBatcher)
  all reference RenderStateCache.* instead of BaseObjectRenderManager.*.
- Dropped using Chorizite.OpenGLSDLBackend.Lib from all 4 consumers and from
  WbDrawDispatcher (which had it only as a dead import).

Item B (ActiveParticleEmitter.ObjectLandblock):
- ObjectLandblock? erased to object?; WorldBuilder.Shared.Models.ObjectId? erased
  to ulong? — both fields are stored but never read by any consumer in our codebase.
- Dropped both WB using directives from ActiveParticleEmitter.cs.

Item C (IDatReaderWriter / IDatDatabase):
- Verbatim copy of both interfaces into IDatReaderWriter.cs in
  AcDream.App.Rendering.Wb namespace — DatCollectionAdapter and ObjectMeshManager
  already live in that namespace, so no using changes needed.
- Dropped using WorldBuilder.Shared.Services from DatCollectionAdapter.cs and
  ObjectMeshManager.cs.

Additional extractions required by the reference drop:
- GeometryUtils.cs: verbatim copy of WorldBuilder.Shared.Lib.GeometryUtils
  (float-precision overloads only; Vector3d double-precision overloads omitted —
  ObjectMeshManager uses only the float versions).
- Dropped using WorldBuilder.Shared.Lib from ObjectMeshManager.cs.

WbMeshAdapter.cs cleanup (spec O-D12):
- Deleted _wbDats (DefaultDatReaderWriter) field + ctor init + Dispose call.
- Deleted the [indoor-upload] NULL_RESULT diagnostic block (lines ~205-262) —
  its Phase 2 cell-resolution investigation is complete; its _wbDats.ResolveId
  dependency goes with this commit.
- Deleted _pendingEnvCellRequests field + isPendingEnvCell tracking in Tick().
- Simplified Tick() to a clean drain loop.

Deleted SplitFormulaDivergenceTest.cs — one-time N.5b data-collection sweep;
job done.

Verified acceptance criteria:
- Zero <ProjectReference> to WorldBuilder.* / Chorizite.OpenGLSDLBackend.* in any csproj.
- Zero 'using WorldBuilder.*' / 'using Chorizite.OpenGLSDLBackend.*' in src/.
- DefaultDatReaderWriter referenced in zero places in src/ (comments only).

Build green (0 warnings, 0 errors).
Tests: 1154 total (-1 from deleted SplitFormulaDivergenceTest), 1146 pass,
8 pre-existing failures (unchanged from baseline — physics/input tests
unrelated to this change).

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:17:33 +02:00

147 lines
5 KiB
C#

using System.Numerics;
// Phase O-T7: verbatim copy of WorldBuilder.Shared.Lib.GeometryUtils into
// the AcDream.App.Rendering.Wb namespace so the WorldBuilder.Shared project
// reference can be dropped. Only the float-precision overloads are used by
// ObjectMeshManager (RayIntersectsSphere + RayIntersectsTriangle). The
// double-precision overloads are kept verbatim for completeness.
namespace AcDream.App.Rendering.Wb;
public static class GeometryUtils {
public static bool RayIntersectsBox(Vector3 rayOrigin, Vector3 rayDirection, Vector3 min, Vector3 max, out float distance) {
distance = 0;
float tmin = 0.0f;
float tmax = float.MaxValue;
if (Math.Abs(rayDirection.X) < 1e-7f) {
if (rayOrigin.X < min.X || rayOrigin.X > max.X) return false;
}
else {
float invD = 1.0f / rayDirection.X;
float t0 = (min.X - rayOrigin.X) * invD;
float t1 = (max.X - rayOrigin.X) * invD;
if (t0 > t1) (t0, t1) = (t1, t0);
tmin = Math.Max(tmin, t0);
tmax = Math.Min(tmax, t1);
if (tmin > tmax) return false;
}
if (Math.Abs(rayDirection.Y) < 1e-7f) {
if (rayOrigin.Y < min.Y || rayOrigin.Y > max.Y) return false;
}
else {
float invD = 1.0f / rayDirection.Y;
float t0 = (min.Y - rayOrigin.Y) * invD;
float t1 = (max.Y - rayOrigin.Y) * invD;
if (t0 > t1) (t0, t1) = (t1, t0);
tmin = Math.Max(tmin, t0);
tmax = Math.Min(tmax, t1);
if (tmin > tmax) return false;
}
if (Math.Abs(rayDirection.Z) < 1e-7f) {
if (rayOrigin.Z < min.Z || rayOrigin.Z > max.Z) return false;
}
else {
float invD = 1.0f / rayDirection.Z;
float t0 = (min.Z - rayOrigin.Z) * invD;
float t1 = (max.Z - rayOrigin.Z) * invD;
if (t0 > t1) (t0, t1) = (t1, t0);
tmin = Math.Max(tmin, t0);
tmax = Math.Min(tmax, t1);
if (tmin > tmax) return false;
}
distance = tmin;
return true;
}
public static bool RayIntersectsTriangle(Vector3 origin, Vector3 direction, Vector3 v0, Vector3 v1, Vector3 v2, out float t) {
t = 0;
Vector3 edge1 = v1 - v0;
Vector3 edge2 = v2 - v0;
Vector3 h = Vector3.Cross(direction, edge2);
float a = Vector3.Dot(edge1, h);
if (a > -0.00001f && a < 0.00001f) return false;
float f = 1.0f / a;
Vector3 s = origin - v0;
float u = f * Vector3.Dot(s, h);
if (u < 0.0f || u > 1.0f) return false;
Vector3 q = Vector3.Cross(s, edge1);
float v = f * Vector3.Dot(direction, q);
if (v < 0.0f || u + v > 1.0f) return false;
t = f * Vector3.Dot(edge2, q);
return t > 0.00001f;
}
public static bool RayIntersectsSphere(Vector3 rayOrigin, Vector3 rayDirection, Vector3 sphereOrigin, float sphereRadius, out float distance) {
distance = 0;
Vector3 l = sphereOrigin - rayOrigin;
float tca = Vector3.Dot(l, rayDirection);
if (tca < 0) return false;
float d2 = Vector3.Dot(l, l) - tca * tca;
float r2 = sphereRadius * sphereRadius;
if (d2 > r2) return false;
float thc = MathF.Sqrt(r2 - d2);
distance = tca - thc;
return true;
}
public static ushort PackKey(int x, int y) => (ushort)((x << 8) | y);
/// <summary>
/// Converts a quaternion to Euler angles (in degrees) using the ZYX convention.
/// </summary>
public static Vector3 QuaternionToEuler(Quaternion q) {
float x = q.X, y = q.Y, z = q.Z, w = q.W;
float roll, pitch, yaw;
float sinr_cosp = 2 * (w * x + y * z);
float cosr_cosp = 1 - 2 * (x * x + y * y);
roll = (float)Math.Atan2(sinr_cosp, cosr_cosp);
float sinp = 2 * (w * y - z * x);
if (Math.Abs(sinp) >= 1)
pitch = (float)Math.CopySign(Math.PI / 2, sinp);
else
pitch = (float)Math.Asin(sinp);
float siny_cosp = 2 * (w * z + x * y);
float cosy_cosp = 1 - 2 * (y * y + z * z);
yaw = (float)Math.Atan2(siny_cosp, cosy_cosp);
return new Vector3(roll, pitch, yaw) * (180.0f / (float)Math.PI);
}
/// <summary>
/// Converts Euler angles (in degrees) to a quaternion using the ZYX convention.
/// </summary>
public static Quaternion EulerToQuaternion(Vector3 euler) {
Vector3 rads = euler * (MathF.PI / 180.0f);
float roll = rads.X;
float pitch = rads.Y;
float yaw = rads.Z;
float cr = MathF.Cos(roll * 0.5f);
float sr = MathF.Sin(roll * 0.5f);
float cp = MathF.Cos(pitch * 0.5f);
float sp = MathF.Sin(pitch * 0.5f);
float cy = MathF.Cos(yaw * 0.5f);
float sy = MathF.Sin(yaw * 0.5f);
return new Quaternion(
sr * cp * cy - cr * sp * sy,
cr * sp * cy + sr * cp * sy,
cr * cp * sy - sr * sp * cy,
cr * cp * cy + sr * sp * sy
);
}
}