Editor/Pipe/ArchPipePart.cs

Editor data model for architectural pipe runs and their brackets. Defines enums for pipe content, mount, dodge and bracket kinds, classes ArchPipeDetail, ArchPipePart and ArchPipeBracketPart with geometry, materials, detail settings and simple helper properties and methods used by the editor.

File Access
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// What runs inside the corridor. The gesture is one dragged volume whatever this says; the content only
// decides the section, how tightly it may bend and what it wears. APPEND ONLY - a kind inserted mid-list
// renumbers every authored run after it.
public enum PipeContent
{
	Pipe,
	Conduit,
	Cable,
	Tray,
	Duct
}

// Which plane of the volume the lanes seat against, and therefore which way "away" is. One box, three
// answers: a ceiling run hangs under the top, a floor run lies on the bottom, a wall run stacks up the face.
public enum PipeMount
{
	Ceiling,
	Floor,
	Wall
}

// How the volume fills across. Fill holds the spacing and takes as many lanes as the box allows; Count holds
// the number and spreads them over whatever the box is - so widening the box adds pipes one way and spaces
// them out the other.
// Which way a run steps to clear what is standing in it. Away is out of the surface it is mounted on, which
// is the hump you see where one bundle crosses another.
public enum PipeDodge
{
	Away,
	Toward,
	Side,
	None
}

// The geometry budget, per run and per bracket. A blockout wants six-sided pipes and mitred elbows; a hero
// interior wants sixteen and a swept bend. NOTHING here moves anything - every knob buys triangles only, so
// a run detailed down still crosses and clears exactly where the detailed one did.
public sealed class ArchPipeDetail
{
	public int Sides { get; set; } = 8;
	// 0 mitres the elbow into one edge, which is all a distant run needs; higher sweeps it through that many
	// segments at a radius taken from the section, so a big dodge stays a proper bend rather than a wide chamfer.
	public int BendSegments { get; set; } = 3;
	public bool Couplings { get; set; } = true;
	public float CouplingSpacing { get; set; } = 96f;
	// A collar where the run actually turns, which is where a real one is jointed.
	public bool BendCollars { get; set; } = true;

	public ArchPipeDetail Copy() => new()
	{
		Sides = Sides,
		BendSegments = BendSegments,
		Couplings = Couplings,
		CouplingSpacing = CouplingSpacing,
		BendCollars = BendCollars
	};
}

// A corridor of services: the box says where they may run, the content says what they are, and the stack
// order says which of two crossing corridors gives way. Nothing about the crossing is stored - it is
// re-derived from where both volumes stand now, so moving either one re-thinks the dodge.
public sealed class ArchPipePart : IArchCollides, IArchPainted, IArchNamed
{
	public ArchCollisionMode? Collision { get; set; }

	public int Id { get; set; }
	public string Name { get; set; } = "Pipe Run";
	public int Level { get; set; }
	public PipeContent Content { get; set; } = PipeContent.Pipe;
	public PipeMount Mount { get; set; } = PipeMount.Ceiling;
	public List<ArchCurveNode> Nodes { get; set; } = new();
	public Vector2 Outward { get; set; } = new( 1f, 0f );
	public float Diameter { get; set; } = 4f;
	public List<string> Materials { get; set; } = new();
	public int RunMaterialIndex { get; set; }
	public int FittingMaterialIndex { get; set; }
	public int SupportMaterialIndex { get; set; }
	public PipeDodge Dodge { get; set; } = PipeDodge.Away;
	public float Clearance { get; set; } = 2f;
	public bool Supports { get; set; } = true;
	public float SupportSpacing { get; set; } = 96f;
	public BracketKind SupportKind { get; set; } = BracketKind.Trapeze;
	public ArchPipeDetail Detail { get; set; } = new();
	public ArchPalette Palette { get; set; } = new();

	public float Radius => MathF.Max( 0.25f, Diameter * 0.5f );

	public bool IsCable => Content == PipeContent.Cable;

	public bool IsTray => Content == PipeContent.Tray;

	public Vector2 Facing => Outward.Length < 0.01f ? new Vector2( 1f, 0f ) : Outward.Normal;

	public string MaterialAt( int index )
	{
		return index >= 0 && index < Materials.Count ? Materials[index] : null;
	}
}

// What holds a run up. Every kind is sized off the runs it OVERLAPS rather than typed: the cross-member
// spans what is actually crossing the volume and the drops reach from the volume's own seat down to it, so
// adding a pipe to a bundle widens its hangers with no second edit.
public enum BracketKind
{
	Trapeze,
	Rod,
	Clamp,
	Strap
}

public sealed class ArchPipeBracketPart : IArchCollides, IArchPainted, IArchNamed
{
	public ArchCollisionMode? Collision { get; set; }

	public int Id { get; set; }
	public string Name { get; set; } = "Brackets";
	public int Level { get; set; }
	public BracketKind Kind { get; set; } = BracketKind.Trapeze;
	public PipeMount Mount { get; set; } = PipeMount.Ceiling;
	public Vector2 Min { get; set; }
	public Vector2 Max { get; set; }
	public float BaseHeight { get; set; }
	public float TopHeight { get; set; }
	public bool AlongX { get; set; } = true;
	public Vector2 Outward { get; set; } = new( 1f, 0f );
	public float Spacing { get; set; } = 96f;
	public float MemberWidth { get; set; } = 2f;
	// Clear of what it holds, so a strap never reads as sunk into the pipe.
	public float Clearance { get; set; } = 0.5f;
	// How far past the outermost lane the cross-member runs.
	public float Overhang { get; set; } = 3f;
	public ArchPipeDetail Detail { get; set; } = new();
	public ArchPalette Palette { get; set; } = new();

	public Vector2 Size => Max - Min;

	public float Rise => MathF.Max( 1f, TopHeight - BaseHeight );

	public List<Vector2> Outline() => ArchFootprint.Rect( Min, Max );

	public Vector2 Facing => Outward.Length < 0.01f ? new Vector2( 1f, 0f ) : Outward.Normal;

	public void Reshape( BBox pulled )
	{
		Min = new Vector2( pulled.Mins.x, pulled.Mins.y );
		Max = new Vector2( pulled.Maxs.x, pulled.Maxs.y );
		BaseHeight = pulled.Mins.z;
		TopHeight = MathF.Max( pulled.Mins.z + 1f, pulled.Maxs.z );
	}
}