Editor/Pipe/ArchPipePart.cs
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 );
}
}