diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/BlockEditor.cs b/game/addons/tools/Code/Scene/Mesh/Tools/BlockEditor.cs
new file mode 100644
index 00000000..c7b53fe3
--- /dev/null
+++ b/game/addons/tools/Code/Scene/Mesh/Tools/BlockEditor.cs
@@ -0,0 +1,350 @@
+namespace Editor.MeshEditor;
+
+///
+/// Create stuff as long as it fits in a box, woah crazy.
+///
+[Title( "Block" ), Icon( "view_in_ar" )]
+public sealed class BlockEditor( PrimitiveTool tool ) : PrimitiveEditor( tool )
+{
+ PrimitiveBuilder _primitive = EditorTypeLibrary.Create( nameof( BlockPrimitive ) );
+ Material _activeMaterial = tool.ActiveMaterial;
+
+ BBox? _box;
+ BBox _startBox;
+ BBox _deltaBox;
+ Vector3 _dragStartPos;
+ bool _dragStarted;
+ Model _previewModel;
+
+ static float TextSize => 22 * Gizmo.Settings.GizmoScale * Application.DpiScale;
+
+ private static float s_lastHeight = 128;
+
+ public override bool CanBuild => _primitive is not null && _box.HasValue;
+ public override bool InProgress => _dragStarted || CanBuild;
+
+ public override PolygonMesh Build()
+ {
+ if ( !CanBuild ) return null;
+
+ var box = _box.Value;
+ if ( _primitive.Is2D )
+ {
+ box.Maxs.z = box.Mins.z;
+ }
+
+ var material = Tool.ActiveMaterial;
+ var primitive = new PrimitiveBuilder.PolygonMesh();
+ _primitive.Material = material;
+ _primitive.SetFromBox( box );
+ _primitive.Build( primitive );
+
+ var mesh = new PolygonMesh();
+ var vertices = mesh.AddVertices( [.. primitive.Vertices] );
+
+ foreach ( var face in primitive.Faces )
+ {
+ var index = mesh.AddFace( [.. face.Indices.Select( x => vertices[x] )] );
+ mesh.SetFaceMaterial( index, material );
+ }
+
+ mesh.TextureAlignToGrid( Transform.Zero );
+ mesh.SetSmoothingAngle( 40.0f );
+
+ return mesh;
+ }
+
+ public override void OnCreated( MeshComponent component )
+ {
+ var selection = SceneEditorSession.Active.Selection;
+ selection.Set( component.GameObject );
+
+ if ( !_dragStarted )
+ {
+ EditorToolManager.SetSubTool( nameof( MeshSelection ) );
+ }
+
+ _box = null;
+ _dragStarted = false;
+ }
+
+ void StartStage( SceneTrace trace )
+ {
+ var tr = trace.Run();
+
+ if ( !tr.Hit )
+ {
+ var plane = new Plane( Vector3.Up, 0.0f );
+ if ( plane.TryTrace( Gizmo.CurrentRay, out var point, true ) )
+ {
+ tr.Hit = true;
+ tr.Normal = plane.Normal;
+ tr.EndPosition = point;
+ }
+ }
+
+ if ( !tr.Hit ) return;
+
+ tr.EndPosition = GridSnap( tr.EndPosition, tr.Normal );
+
+ if ( Gizmo.WasLeftMousePressed )
+ {
+ _dragStartPos = tr.EndPosition;
+ _dragStarted = true;
+
+ if ( _box.HasValue )
+ {
+ Tool.Create();
+ }
+
+ _box = null;
+ _dragStarted = true;
+ }
+ else
+ {
+ var size = 3.0f * Gizmo.Camera.Position.Distance( tr.EndPosition ) / 1000.0f;
+ Gizmo.Draw.Color = Color.White;
+ Gizmo.Draw.SolidSphere( tr.EndPosition, size );
+ }
+ }
+
+ void DraggingStage()
+ {
+ var plane = new Plane( _dragStartPos, Vector3.Up );
+ if ( !plane.TryTrace( Gizmo.CurrentRay, out var point, true ) ) return;
+
+ point = GridSnap( point, Vector3.Up );
+
+ if ( !Gizmo.IsLeftMouseDown )
+ {
+ var delta = point - _dragStartPos;
+
+ if ( delta.x.AlmostEqual( 0.0f ) || delta.y.AlmostEqual( 0.0f ) )
+ {
+ _box = null;
+ _dragStarted = false;
+
+ return;
+ }
+
+ _box = new BBox( _dragStartPos, point + Vector3.Up * s_lastHeight );
+ _dragStarted = false;
+
+ BuildPreview();
+ }
+ else
+ {
+ var box = new BBox( _dragStartPos, point );
+ Gizmo.Draw.IgnoreDepth = true;
+ Gizmo.Draw.LineThickness = 2;
+ Gizmo.Draw.Color = Gizmo.Colors.Active.WithAlpha( 0.5f );
+ Gizmo.Draw.LineBBox( box );
+ Gizmo.Draw.Color = Gizmo.Colors.Left;
+ Gizmo.Draw.ScreenText( $"L: {box.Size.y:0.#}", box.Mins.WithY( box.Center.y ), Vector2.Up * 32, size: TextSize );
+ Gizmo.Draw.Color = Gizmo.Colors.Forward;
+ Gizmo.Draw.ScreenText( $"W: {box.Size.x:0.#}", box.Mins.WithX( box.Center.x ), Vector2.Up * 32, size: TextSize );
+ }
+ }
+
+ public override void OnUpdate( SceneTrace trace )
+ {
+ if ( Application.IsKeyDown( KeyCode.Escape ) ||
+ Application.IsKeyDown( KeyCode.Delete ) )
+ {
+ Cancel();
+ }
+
+ if ( _activeMaterial != Tool.ActiveMaterial )
+ {
+ BuildPreview();
+ _activeMaterial = Tool.ActiveMaterial;
+ }
+
+ if ( !Gizmo.Pressed.Any )
+ {
+ if ( _dragStarted )
+ {
+ DraggingStage();
+ }
+ else
+ {
+ StartStage( trace );
+ }
+ }
+
+ DrawBox();
+ }
+
+ void Cancel()
+ {
+ _box = null;
+ _dragStarted = false;
+ }
+
+ public override void OnCancel()
+ {
+ Cancel();
+ }
+
+ void DrawBox()
+ {
+ if ( !_box.HasValue ) return;
+
+ var box = _box.Value;
+
+ if ( _primitive.Is2D )
+ {
+ box.Maxs.z = box.Mins.z;
+ }
+
+ using ( Gizmo.Scope( "box" ) )
+ {
+ Gizmo.Hitbox.DepthBias = 0.01f;
+
+ if ( !Gizmo.Pressed.Any )
+ {
+ _startBox = box;
+ _deltaBox = default;
+ }
+
+ if ( Gizmo.Control.BoundingBox( "Resize", box, out var outBox ) )
+ {
+ _deltaBox.Maxs += outBox.Maxs - box.Maxs;
+ _deltaBox.Mins += outBox.Mins - box.Mins;
+
+ box = Gizmo.Snap( _startBox, _deltaBox );
+
+ if ( _primitive.Is2D )
+ {
+ var b = box;
+ b.Mins.z = _box.Value.Mins.z;
+ b.Maxs.z = _box.Value.Maxs.z;
+ _box = b;
+
+ box.Mins.z = b.Mins.z;
+ box.Maxs.z = b.Mins.z;
+ }
+ else
+ {
+ s_lastHeight = MathF.Abs( box.Size.z );
+
+ _box = box;
+ }
+
+ BuildPreview();
+ }
+
+ Gizmo.Draw.IgnoreDepth = true;
+ Gizmo.Draw.LineThickness = 2;
+ Gizmo.Draw.Color = Gizmo.Colors.Active.WithAlpha( 0.5f );
+ Gizmo.Draw.LineBBox( box );
+ Gizmo.Draw.LineThickness = 3;
+ Gizmo.Draw.Color = Gizmo.Colors.Left;
+ Gizmo.Draw.ScreenText( $"L: {box.Size.y:0.#}", box.Maxs.WithY( box.Center.y ), Vector2.Up * 32, size: TextSize );
+ Gizmo.Draw.Line( box.Maxs.WithY( box.Mins.y ), box.Maxs.WithY( box.Maxs.y ) );
+ Gizmo.Draw.Color = Gizmo.Colors.Forward;
+ Gizmo.Draw.ScreenText( $"W: {box.Size.x:0.#}", box.Maxs.WithX( box.Center.x ), Vector2.Up * 32, size: TextSize );
+ Gizmo.Draw.Line( box.Maxs.WithX( box.Mins.x ), box.Maxs.WithX( box.Maxs.x ) );
+ Gizmo.Draw.Color = Gizmo.Colors.Up;
+ Gizmo.Draw.ScreenText( $"H: {box.Size.z:0.#}", box.Maxs.WithZ( box.Center.z ), Vector2.Up * 32, size: TextSize );
+ Gizmo.Draw.Line( box.Maxs.WithZ( box.Mins.z ), box.Maxs.WithZ( box.Maxs.z ) );
+ }
+
+ if ( _previewModel.IsValid() && !_previewModel.IsError )
+ {
+ Gizmo.Draw.Model( _previewModel );
+ }
+ }
+
+ static Vector3 GridSnap( Vector3 point, Vector3 normal )
+ {
+ var basis = Rotation.LookAt( normal );
+ return Gizmo.Snap( point * basis.Inverse, new Vector3( 0, 1, 1 ) ) * basis;
+ }
+
+ public override Widget CreateWidget()
+ {
+ return new BlockEditorWidget( this, BuildPreview );
+ }
+
+ void BuildPreview()
+ {
+ var mesh = Build();
+ _previewModel = mesh?.Rebuild();
+ }
+
+ private static IEnumerable GetBuilderTypes()
+ {
+ return EditorTypeLibrary.GetTypes()
+ .Where( x => !x.IsAbstract )
+ .OrderBy( x => x.Name );
+ }
+
+ class BlockEditorWidget : ToolSidebarWidget
+ {
+ readonly BlockEditor _editor;
+ readonly Layout _controlLayout;
+ PrimitiveBuilder _primitive;
+ readonly Action _onEdited;
+
+ public BlockEditorWidget( BlockEditor editor, Action onEdited )
+ {
+ _editor = editor;
+ _primitive = _editor._primitive;
+ _onEdited = onEdited;
+
+ Layout.Margin = 0;
+
+ {
+ var group = AddGroup( "Shape Type" );
+ var list = group.Add( new PrimitiveListView( this ) );
+ list.FixedWidth = 200;
+ list.SetItems( GetBuilderTypes() );
+ list.SelectItem( list.Items.FirstOrDefault( x => (x as TypeDescription).TargetType == _primitive?.GetType() ) );
+ list.ItemSelected = ( e ) => OnPrimitiveSelected( (e as TypeDescription).TargetType );
+ list.BuildLayout();
+ }
+
+ _controlLayout = Layout.AddColumn();
+ BuildControlSheet();
+
+ Layout.AddStretchCell();
+ }
+
+ void OnPrimitiveSelected( Type type )
+ {
+ _editor._primitive = EditorTypeLibrary.Create( type );
+ _editor.BuildPreview();
+ }
+
+ void BuildControlSheet()
+ {
+ using var x = SuspendUpdates.For( this );
+
+ _controlLayout.Clear( true );
+
+ if ( _primitive is null ) return;
+
+ var title = EditorTypeLibrary.GetType( _primitive.GetType() ).Title;
+ var w = new ToolSidebarWidget( this );
+ w.Layout.Margin = 0;
+ _controlLayout.Add( w );
+
+ var group = w.AddGroup( $"{title} Properties" );
+ var so = _primitive.GetSerialized();
+ so.OnPropertyChanged += ( e ) => _onEdited?.Invoke();
+ var sheet = new ControlSheet();
+ sheet.AddObject( so );
+ group.Add( sheet );
+ }
+
+ [EditorEvent.Frame]
+ public void Frame()
+ {
+ if ( _primitive == _editor._primitive ) return;
+
+ _primitive = _editor._primitive;
+ BuildControlSheet();
+ }
+ }
+}
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/BlockTool.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/BlockTool.UI.cs
deleted file mode 100644
index 7480d8c6..00000000
--- a/game/addons/tools/Code/Scene/Mesh/Tools/BlockTool.UI.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-
-namespace Editor.MeshEditor;
-
-public partial class BlockTool
-{
- private Layout ControlLayout { get; set; }
-
- public override Widget CreateToolSidebar()
- {
- var widget = new ToolSidebarWidget();
-
- widget.AddTitle( "Create Primitive" );
-
- var list = new PrimitiveListView( widget );
- list.SetItems( GetBuilderTypes() );
-
- {
- var group = widget.AddGroup( "Shape Type" );
-
- group.Add( list );
- list.SelectItem( list.Items.FirstOrDefault() );
- list.ItemSelected = ( e ) => Current = _primitives.FirstOrDefault( x => x.GetType() == (e as TypeDescription).TargetType );
- }
-
- {
- var group = widget.AddGroup( "Shape Settings" );
-
- ControlLayout = group;
- BuildControlSheet();
- }
-
- widget.Layout.AddStretchCell();
-
- return widget;
- }
-
- public void OnEdited( SerializedProperty property )
- {
- RebuildMesh();
- }
-
- private void BuildControlSheet()
- {
- if ( !ControlLayout.IsValid() )
- return;
-
- ControlLayout.Clear( true );
-
- if ( Current is null )
- return;
-
- var so = Current.GetSerialized();
- so.OnPropertyChanged += OnEdited;
- var sheet = new ControlSheet();
- sheet.AddObject( so );
- ControlLayout.Add( sheet );
- }
-}
-
-file class PrimitiveListView : ListView
-{
- public PrimitiveListView( Widget parent ) : base( parent )
- {
- ItemSpacing = 0;
- ItemSize = 24;
-
- HorizontalScrollbarMode = ScrollbarMode.Off;
- VerticalScrollbarMode = ScrollbarMode.Off;
- }
-
- protected override void DoLayout()
- {
- base.DoLayout();
-
- var rect = CanvasRect;
- var itemSize = ItemSize;
- var itemSpacing = ItemSpacing;
- var itemsPerRow = 1;
- var itemCount = Items.Count();
-
- if ( itemSize.x > 0 ) itemsPerRow = ((rect.Width + itemSpacing.x) / (itemSize.x + itemSpacing.x)).FloorToInt();
- itemsPerRow = Math.Max( 1, itemsPerRow );
-
- var rowCount = MathX.CeilToInt( itemCount / (float)itemsPerRow );
- FixedHeight = rowCount * (itemSize.y + itemSpacing.y) + Margin.EdgeSize.y;
- }
-
- protected override string GetTooltip( object obj )
- {
- var builder = obj as TypeDescription;
- var displayInfo = DisplayInfo.ForType( builder.TargetType );
- return displayInfo.Name;
- }
-
- protected override void PaintItem( VirtualWidget item )
- {
- if ( item.Selected )
- {
- Paint.ClearPen();
- Paint.SetBrush( Theme.Blue );
- Paint.DrawRect( item.Rect, 4 );
- }
-
- var builder = item.Object as TypeDescription;
- var displayInfo = DisplayInfo.ForType( builder.TargetType );
-
- Paint.SetPen( item.Selected || item.Hovered ? Color.White : Color.Gray );
- Paint.DrawIcon( item.Rect, displayInfo.Icon ?? "square", HeaderBarStyle.IconSize );
- }
-}
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/BlockTool.cs b/game/addons/tools/Code/Scene/Mesh/Tools/BlockTool.cs
deleted file mode 100644
index 0b72cc00..00000000
--- a/game/addons/tools/Code/Scene/Mesh/Tools/BlockTool.cs
+++ /dev/null
@@ -1,426 +0,0 @@
-
-namespace Editor.MeshEditor;
-
-///
-/// Create new shapes by dragging out a block
-///
-[Title( "Block Tool" )]
-[Icon( "view_in_ar" )]
-[Group( "4" )]
-[Alias( "tools.block-tool" )]
-public partial class BlockTool( MeshTool meshTool ) : EditorTool
-{
- private BBox _box;
- private BBox _startBox;
- private BBox _deltaBox;
- private bool _resizing;
- private bool _resizePressed;
- private bool _inProgress;
- private bool _dragging;
- private Vector3 _dragStartPos;
-
- private readonly HashSet _primitives = new();
- private PrimitiveBuilder _primitive;
- private SceneObject _sceneObject;
-
- private PrimitiveBuilder Current
- {
- get => _primitive;
- set
- {
- if ( _primitive == value )
- return;
-
- _primitive = value;
- _primitive.Material = meshTool.ActiveMaterial;
-
- BuildControlSheet();
- RebuildMesh();
- }
- }
-
- private bool InProgress
- {
- get => _inProgress;
- set
- {
- if ( _inProgress == value )
- return;
-
- _inProgress = value;
- }
- }
-
- private static float LastHeight = 128;
-
- public override void OnEnabled()
- {
- base.OnEnabled();
-
- AllowGameObjectSelection = false;
- Selection.Clear();
-
- CreatePrimitiveBuilders();
- }
-
- public override void OnDisabled()
- {
- base.OnDisabled();
-
- if ( _sceneObject.IsValid() )
- {
- _sceneObject.RenderingEnabled = false;
- _sceneObject.Delete();
- _sceneObject = null;
- }
-
- if ( InProgress )
- {
- using ( Scene.Push() )
- {
- var go = CreateFromBox( _box );
- Selection.Set( go );
- }
- InProgress = false;
- }
- else
- {
- var selectedObjects = Selection.OfType().ToArray();
- Selection.Clear();
- foreach ( var o in selectedObjects )
- Selection.Add( o );
- }
-
- _resizing = false;
- _resizePressed = false;
- _inProgress = false;
- _dragging = false;
- }
-
- private PolygonMesh Build( BBox box )
- {
- var primitive = new PrimitiveBuilder.PolygonMesh();
- _primitive.SetFromBox( box );
- _primitive.Build( primitive );
-
- var mesh = new PolygonMesh();
- var hVertices = mesh.AddVertices( primitive.Vertices.ToArray() );
-
- foreach ( var face in primitive.Faces )
- {
- var index = mesh.AddFace( face.Indices.Select( x => hVertices[x] ).ToArray() );
- mesh.SetFaceMaterial( index, face.Material );
- }
-
- return mesh;
- }
-
- private void RebuildMesh()
- {
- if ( !InProgress )
- return;
-
- if ( Current.Is2D )
- {
- _box.Maxs.z = _box.Mins.z;
- }
- else
- {
- _box.Maxs.z = _box.Mins.z + LastHeight;
- }
-
- var box = _box;
- var position = box.Center;
- box = BBox.FromPositionAndSize( 0, box.Size );
-
- var mesh = Build( box );
-
- foreach ( var hFace in mesh.FaceHandles )
- mesh.SetFaceMaterial( hFace, _primitive.Material );
-
- mesh.TextureAlignToGrid( Transform.Zero.WithPosition( position ) );
- mesh.SetSmoothingAngle( 40.0f );
-
- var model = mesh.Rebuild();
- var transform = new Transform( position );
-
- if ( !_sceneObject.IsValid() )
- {
- _sceneObject = new SceneObject( Scene.SceneWorld, model, transform );
- }
- else
- {
- _sceneObject.Model = model;
- _sceneObject.Transform = transform;
- }
- }
-
- private void CreatePrimitiveBuilders()
- {
- _primitives.Clear();
-
- foreach ( var type in GetBuilderTypes() )
- {
- _primitives.Add( type.Create() );
- }
-
- _primitive = _primitives.FirstOrDefault();
- _primitive.Material = meshTool.ActiveMaterial;
- }
-
- private static IEnumerable GetBuilderTypes()
- {
- return EditorTypeLibrary.GetTypes()
- .Where( x => !x.IsAbstract ).OrderBy( x => x.Name );
- }
-
- private GameObject CreateFromBox( BBox box )
- {
- if ( _primitive is null )
- return null;
-
- using ( SceneEditorSession.Active.UndoScope( "Create Block" ).WithGameObjectCreations().Push() )
- {
- if ( _sceneObject.IsValid() )
- {
- _sceneObject.RenderingEnabled = false;
- _sceneObject.Delete();
- _sceneObject = null;
- }
-
- var go = new GameObject( true, "Box" );
- var mc = go.Components.Create( false );
-
- var position = box.Center;
- box = BBox.FromPositionAndSize( 0, box.Size );
-
- var polygonMesh = Build( box );
-
- foreach ( var hFace in polygonMesh.FaceHandles )
- polygonMesh.SetFaceMaterial( hFace, _primitive.Material );
-
- polygonMesh.TextureAlignToGrid( Transform.Zero.WithPosition( position ) );
-
- mc.WorldPosition = position;
- mc.Mesh = polygonMesh;
- mc.SmoothingAngle = 40.0f;
- mc.Enabled = true;
-
- return go;
- }
- }
-
- public override void OnSelectionChanged()
- {
- base.OnSelectionChanged();
-
- if ( !Selection.OfType().Any() )
- {
- return;
- }
-
- EditorToolManager.SetSubTool( nameof( PositionMode ) );
- }
-
- public override void OnUpdate()
- {
- if ( Selection.OfType().Any() )
- return;
-
- if ( InProgress && Application.FocusWidget.IsValid() )
- {
- if ( Application.IsKeyDown( KeyCode.Escape ) ||
- Application.IsKeyDown( KeyCode.Delete ) )
- {
- _resizing = false;
- _dragging = false;
- InProgress = false;
-
- if ( _sceneObject.IsValid() )
- {
- _sceneObject.RenderingEnabled = false;
- _sceneObject.Delete();
- _sceneObject = null;
- }
- }
- }
-
- if ( Current is null )
- return;
-
- var textSize = 22 * Gizmo.Settings.GizmoScale * Application.DpiScale;
-
- if ( InProgress )
- {
- using ( Gizmo.Scope( "Tool" ) )
- {
- Gizmo.Hitbox.DepthBias = 0.01f;
-
- if ( !Gizmo.Pressed.Any && Gizmo.HasMouseFocus )
- {
- _resizing = false;
- _deltaBox = default;
- _startBox = default;
-
- if ( Current.Is2D )
- {
- _box.Maxs.z = _box.Mins.z;
- }
- else
- {
- _box.Maxs.z = _box.Mins.z + LastHeight;
- }
- }
-
- if ( Gizmo.Control.BoundingBox( "Resize", _box, out var outBox, out _resizePressed ) )
- {
- if ( !_resizing )
- {
- _startBox = _box;
- _resizing = true;
- _deltaBox = new BBox( Vector3.Zero, Vector3.Zero );
- }
-
- _deltaBox.Maxs += outBox.Maxs - _box.Maxs;
- _deltaBox.Mins += outBox.Mins - _box.Mins;
-
- _box = Gizmo.Snap( _startBox, _deltaBox );
-
- if ( Current.Is2D )
- {
- _box.Mins.z = _startBox.Mins.z;
- _box.Maxs.z = _startBox.Mins.z;
- }
- else
- {
- LastHeight = System.MathF.Abs( _box.Size.z );
- }
-
- RebuildMesh();
- }
-
- Gizmo.Draw.Color = Color.Red.WithAlpha( 0.5f );
- Gizmo.Draw.LineBBox( _startBox );
- }
-
- using ( Gizmo.Scope( "box" ) )
- {
- Gizmo.Draw.IgnoreDepth = true;
- Gizmo.Draw.LineThickness = 2;
- Gizmo.Draw.Color = Gizmo.Colors.Active.WithAlpha( 0.5f );
- Gizmo.Draw.LineBBox( _box );
- Gizmo.Draw.LineThickness = 3;
- Gizmo.Draw.Color = Gizmo.Colors.Left;
- Gizmo.Draw.ScreenText( $"L: {_box.Size.y:0.#}", _box.Maxs.WithY( _box.Center.y ), Vector2.Up * 32, size: textSize );
- Gizmo.Draw.Line( _box.Maxs.WithY( _box.Mins.y ), _box.Maxs.WithY( _box.Maxs.y ) );
- Gizmo.Draw.Color = Gizmo.Colors.Forward;
- Gizmo.Draw.ScreenText( $"W: {_box.Size.x:0.#}", _box.Maxs.WithX( _box.Center.x ), Vector2.Up * 32, size: textSize );
- Gizmo.Draw.Line( _box.Maxs.WithX( _box.Mins.x ), _box.Maxs.WithX( _box.Maxs.x ) );
- Gizmo.Draw.Color = Gizmo.Colors.Up;
- Gizmo.Draw.ScreenText( $"H: {_box.Size.z:0.#}", _box.Maxs.WithZ( _box.Center.z ), Vector2.Up * 32, size: textSize );
- Gizmo.Draw.Line( _box.Maxs.WithZ( _box.Mins.z ), _box.Maxs.WithZ( _box.Maxs.z ) );
- }
-
- if ( Application.FocusWidget.IsValid() && Application.IsKeyDown( KeyCode.Enter ) )
- {
- var go = CreateFromBox( _box );
- Selection.Set( go );
-
- InProgress = false;
-
- EditorToolManager.SetSubTool( nameof( PositionMode ) );
- }
- }
- else
- {
- _resizePressed = false;
- }
-
- if ( _resizePressed )
- return;
-
- var tr = Trace.UseRenderMeshes( true )
- .UsePhysicsWorld( true )
- .Run();
-
- if ( !tr.Hit || _dragging )
- {
- var plane = _dragging ? new Plane( _dragStartPos, Vector3.Up ) : new Plane( Vector3.Up, 0.0f );
- if ( plane.TryTrace( new Ray( tr.StartPosition, tr.Direction ), out tr.EndPosition, true ) )
- {
- tr.Hit = true;
- tr.Normal = plane.Normal;
- }
- }
-
- if ( !tr.Hit )
- return;
-
- var r = Rotation.LookAt( tr.Normal );
- var localPosition = tr.EndPosition * r.Inverse;
- localPosition = Gizmo.Snap( localPosition, new Vector3( 0, 1, 1 ) );
- tr.EndPosition = localPosition * r;
-
- if ( !_dragging )
- {
- using ( Gizmo.Scope( "Aim Handle", new Transform( tr.EndPosition, Rotation.LookAt( tr.Normal ) ) ) )
- {
- Gizmo.Draw.Color = Color.White;
- var size = 3.0f * Gizmo.Camera.Position.Distance( tr.EndPosition ) / 1000.0f;
- Gizmo.Draw.SolidSphere( 0, size );
- }
- }
-
- if ( Gizmo.WasLeftMousePressed )
- {
- if ( InProgress )
- CreateFromBox( _box );
-
- _dragging = true;
- _dragStartPos = tr.EndPosition;
- InProgress = false;
- }
- else if ( Gizmo.WasLeftMouseReleased && _dragging )
- {
- var spacing = Gizmo.Settings.SnapToGrid ? Gizmo.Settings.GridSpacing : 1.0f;
- var box = new BBox( _dragStartPos, tr.EndPosition );
-
- if ( box.Size.x >= spacing || box.Size.y >= spacing )
- {
- if ( Gizmo.Settings.SnapToGrid )
- {
- if ( box.Size.x < spacing ) box.Maxs.x += spacing;
- if ( box.Size.y < spacing ) box.Maxs.y += spacing;
- }
-
- float height = Current.Is2D ? 0 : LastHeight;
- var size = box.Size.WithZ( height );
- var position = box.Center.WithZ( box.Center.z + (height * 0.5f) );
- _box = BBox.FromPositionAndSize( position, size );
- InProgress = true;
-
- RebuildMesh();
- }
-
- _dragging = false;
- _dragStartPos = default;
- }
-
- if ( _dragging )
- {
- using ( Gizmo.Scope( "Rect", 0 ) )
- {
- var box = new BBox( _dragStartPos, tr.EndPosition );
-
- Gizmo.Draw.IgnoreDepth = true;
- Gizmo.Draw.LineThickness = 2;
- Gizmo.Draw.Color = Gizmo.Colors.Active.WithAlpha( 0.5f );
- Gizmo.Draw.LineBBox( box );
- Gizmo.Draw.Color = Gizmo.Colors.Left;
- Gizmo.Draw.ScreenText( $"L: {box.Size.y:0.#}", box.Mins.WithY( box.Center.y ), Vector2.Up * 32, size: textSize );
- Gizmo.Draw.Color = Gizmo.Colors.Forward;
- Gizmo.Draw.ScreenText( $"W: {box.Size.x:0.#}", box.Mins.WithX( box.Center.x ), Vector2.Up * 32, size: textSize );
- }
- }
- }
-}
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/EdgeTool.cs b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeTool.cs
index e4c6489e..b95eb3e5 100644
--- a/game/addons/tools/Code/Scene/Mesh/Tools/EdgeTool.cs
+++ b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeTool.cs
@@ -17,7 +17,7 @@ public sealed partial class EdgeTool( MeshTool tool ) : SelectionTool(
using var scope = Gizmo.Scope( "EdgeTool" );
- var closestEdge = GetClosestEdge( 8 );
+ var closestEdge = MeshTrace.GetClosestEdge( 8 );
if ( closestEdge.IsValid() )
Gizmo.Hitbox.TrySetHovered( closestEdge.Transform.PointToWorld( closestEdge.Line.Center ) );
@@ -167,7 +167,7 @@ public sealed partial class EdgeTool( MeshTool tool ) : SelectionTool(
private void SelectEdgeLoop()
{
- var edge = GetClosestEdge( 8 );
+ var edge = MeshTrace.GetClosestEdge( 8 );
if ( !edge.IsValid() )
return;
@@ -184,7 +184,7 @@ public sealed partial class EdgeTool( MeshTool tool ) : SelectionTool(
private void SelectEdge()
{
- var edge = GetClosestEdge( 8 );
+ var edge = MeshTrace.GetClosestEdge( 8 );
if ( edge.IsValid() )
{
using ( Gizmo.Scope( "Edge Hover" ) )
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/MeshSelection.cs b/game/addons/tools/Code/Scene/Mesh/Tools/MeshSelection.cs
index 72beb55b..0e21e319 100644
--- a/game/addons/tools/Code/Scene/Mesh/Tools/MeshSelection.cs
+++ b/game/addons/tools/Code/Scene/Mesh/Tools/MeshSelection.cs
@@ -110,7 +110,6 @@ public sealed partial class MeshSelection( MeshTool tool ) : SelectionTool
public override void OnEnabled()
{
- Selection.Clear();
OnSelectionChanged();
var undo = SceneEditorSession.Active.UndoSystem;
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.UI.cs
index f1c5b621..81df49a0 100644
--- a/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.UI.cs
+++ b/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.UI.cs
@@ -21,8 +21,8 @@ partial class MeshTool
file class MeshToolShortcutsWidget : Widget
{
- [Shortcut( "tools.block-tool", "Shift+B", typeof( SceneDock ) )]
- public void ActivateBlockTool() => EditorToolManager.SetSubTool( nameof( BlockTool ) );
+ [Shortcut( "tools.primitive-tool", "Shift+B", typeof( SceneDock ) )]
+ public void ActivatePrimitiveTool() => EditorToolManager.SetSubTool( nameof( PrimitiveTool ) );
[Shortcut( "tools.vertex-tool", "1", typeof( SceneDock ) )]
public void ActivateVertexTool() => EditorToolManager.SetSubTool( nameof( VertexTool ) );
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.cs b/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.cs
index 657b8378..59ea2737 100644
--- a/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.cs
+++ b/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.cs
@@ -16,7 +16,7 @@ public partial class MeshTool : EditorTool
public override IEnumerable GetSubtools()
{
- yield return new BlockTool( this );
+ yield return new PrimitiveTool( this );
yield return new MeshSelection( this );
yield return new VertexTool( this );
yield return new EdgeTool( this );
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/PolygonEditor.cs b/game/addons/tools/Code/Scene/Mesh/Tools/PolygonEditor.cs
new file mode 100644
index 00000000..fecb528c
--- /dev/null
+++ b/game/addons/tools/Code/Scene/Mesh/Tools/PolygonEditor.cs
@@ -0,0 +1,344 @@
+using System.Runtime.InteropServices;
+
+namespace Editor.MeshEditor;
+
+///
+/// Draw a polygon mesh.
+///
+[Title( "Polygon" ), Icon( "pentagon" )]
+public sealed class PolygonEditor( PrimitiveTool tool ) : PrimitiveEditor( tool )
+{
+ Plane _plane;
+ readonly List _points = [];
+ HalfEdgeMesh.FaceHandle _face;
+ Model _previewModel;
+ bool _valid;
+ Material _activeMaterial = tool.ActiveMaterial;
+
+ public override bool CanBuild => _points.Count >= 3 && _valid;
+ public override bool InProgress => _points.Count > 0;
+
+ public override PolygonMesh Build()
+ {
+ if ( !CanBuild ) return default;
+
+ var mesh = new PolygonMesh();
+ var count = _points.Count;
+
+ PlaneEquation( _points, out var faceNormal );
+
+ var hasHeight = !Height.AlmostEqual( 0.0f );
+ var flip = faceNormal.Dot( _plane.Normal ) < 0.0f;
+ if ( hasHeight && !Hollow ) flip = !flip;
+
+ var basePoints = new Vector3[count];
+ if ( flip ) for ( var i = 0; i < count; i++ ) basePoints[i] = _points[count - 1 - i];
+ else _points.CopyTo( basePoints );
+
+ var bottomRing = mesh.AddVertices( basePoints );
+ var bottomFace = mesh.AddFace( bottomRing );
+ if ( !bottomFace.IsValid ) return default;
+
+ mesh.SetFaceMaterial( bottomFace, Tool.ActiveMaterial );
+
+ if ( !hasHeight )
+ {
+ mesh.SetSmoothingAngle( 40.0f );
+ mesh.TextureAlignToGrid( Transform.Zero );
+ _face = bottomFace;
+ return mesh;
+ }
+
+ var offset = Vector3.Up * Height;
+ var topPoints = new Vector3[count];
+ for ( var i = 0; i < count; i++ ) topPoints[i] = basePoints[i] + offset;
+
+ var topRing = mesh.AddVertices( topPoints );
+ var topCap = new HalfEdgeMesh.VertexHandle[count];
+ for ( var i = 0; i < count; i++ ) topCap[i] = topRing[count - 1 - i];
+
+ var topFace = mesh.AddFace( topCap );
+ mesh.SetFaceMaterial( topFace, Tool.ActiveMaterial );
+
+ for ( var i = 0; i < count; i++ )
+ {
+ mesh.SetFaceMaterial( mesh.AddFace( [bottomRing[i], topRing[i], topRing[(i + 1) % count], bottomRing[(i + 1) % count]] ),
+ Tool.ActiveMaterial );
+ }
+
+ mesh.SetSmoothingAngle( 40.0f );
+ mesh.TextureAlignToGrid( Transform.Zero );
+
+ _face = topFace;
+ return mesh;
+ }
+
+
+ void BuildPreview()
+ {
+ var mesh = Build();
+ _previewModel = mesh?.Rebuild();
+ }
+
+ public override void OnCreated( MeshComponent component )
+ {
+ _points.Clear();
+ _valid = false;
+
+ var selection = SceneEditorSession.Active.Selection;
+ selection.Clear();
+ selection.Add( component.GameObject );
+ selection.Add( new MeshFace( component, _face ) );
+
+ EditorToolManager.SetSubTool( nameof( FaceTool ) );
+ }
+
+ public override void OnUpdate( SceneTrace trace )
+ {
+ if ( Application.IsKeyDown( KeyCode.Escape ) )
+ {
+ Cancel();
+ }
+
+ if ( _activeMaterial != Tool.ActiveMaterial )
+ {
+ BuildPreview();
+ _activeMaterial = Tool.ActiveMaterial;
+ }
+
+ if ( !Gizmo.Pressed.Any )
+ {
+ if ( _points.Count > 0 )
+ {
+ DrawingStage();
+ }
+ else
+ {
+ StartStage( trace );
+ }
+ }
+
+ DrawGizmos();
+ }
+
+ public override void OnCancel()
+ {
+ Cancel();
+ }
+
+ void Cancel()
+ {
+ _points.Clear();
+ _valid = false;
+ }
+
+ void DrawGizmos()
+ {
+ Gizmo.Draw.IgnoreDepth = true;
+ Gizmo.Draw.LineThickness = 2;
+
+ var valid = _previewModel.IsValid() && !_previewModel.IsError;
+
+ if ( valid )
+ {
+ Gizmo.Draw.Model( _previewModel );
+ }
+
+ if ( _points.Count < 3 ) valid = true;
+
+ Gizmo.Draw.Color = valid ? Color.Yellow : Color.Red;
+
+ for ( int i = 0; i < _points.Count; i++ )
+ {
+ var a = _points[i];
+ var b = _points[(i + 1) % _points.Count];
+
+ Gizmo.Draw.Line( a, b );
+ }
+
+ for ( int i = 0; i < _points.Count; i++ )
+ {
+ var point = _points[i];
+ var size = 3.0f * Gizmo.Camera.Position.Distance( point ) / 1000.0f;
+
+ using ( Gizmo.Scope( $"point {i}" ) )
+ {
+ Gizmo.Hitbox.DepthBias = 0.01f;
+ Gizmo.Hitbox.Sphere( new Sphere( point, size * 2 ) );
+
+ if ( Gizmo.Pressed.This )
+ {
+ if ( _plane.TryTrace( Gizmo.CurrentRay, out var newPoint, true ) )
+ {
+ newPoint = GridSnap( newPoint, _plane.Normal );
+ if ( !point.AlmostEqual( newPoint ) )
+ {
+ _points[i] = newPoint;
+ point = newPoint;
+
+ OnPointsChanged();
+ }
+ }
+ }
+
+ Gizmo.Draw.Color = Gizmo.IsHovered ? Color.Yellow : Color.White;
+ Gizmo.Draw.SolidSphere( point, Gizmo.IsHovered ? size * 2 : size );
+ }
+ }
+ }
+
+ void AddPoint( Vector3 point )
+ {
+ _points.Add( point );
+ OnPointsChanged();
+ }
+
+ void RemovePoint()
+ {
+ if ( _points.Count == 0 ) return;
+ _points.RemoveAt( _points.Count - 1 );
+ OnPointsChanged();
+ }
+
+ void OnPointsChanged()
+ {
+ _valid = _points.Count < 3 || Mesh.TriangulatePolygon( CollectionsMarshal.AsSpan( _points ) ).Length >= 3;
+ BuildPreview();
+ }
+
+ void DrawingStage()
+ {
+ if ( !_plane.TryTrace( Gizmo.CurrentRay, out var point, true ) ) return;
+
+ point = GridSnap( point, _plane.Normal );
+
+ if ( Gizmo.WasLeftMousePressed )
+ {
+ AddPoint( point );
+ }
+
+ if ( !Gizmo.HasHovered )
+ {
+ var size = 3.0f * Gizmo.Camera.Position.Distance( point ) / 1000.0f;
+ Gizmo.Draw.Color = Color.White;
+ Gizmo.Draw.SolidSphere( point, size );
+ Gizmo.Draw.Line( _points.Last(), point );
+ }
+ }
+
+ void StartStage( SceneTrace trace )
+ {
+ _previewModel = null;
+
+ var tr = trace.Run();
+
+ if ( !tr.Hit )
+ {
+ var plane = new Plane( Vector3.Up, 0.0f );
+ if ( plane.TryTrace( Gizmo.CurrentRay, out var point, true ) )
+ {
+ tr.Hit = true;
+ tr.Normal = plane.Normal;
+ tr.EndPosition = point;
+ }
+ }
+
+ if ( !tr.Hit ) return;
+
+ tr.EndPosition = GridSnap( tr.EndPosition, tr.Normal );
+
+ if ( Gizmo.WasLeftMousePressed )
+ {
+ _plane = new Plane( tr.EndPosition, tr.Normal );
+ _points.Add( tr.EndPosition );
+ }
+
+ if ( !Gizmo.HasHovered )
+ {
+ var size = 3.0f * Gizmo.Camera.Position.Distance( tr.EndPosition ) / 1000.0f;
+ Gizmo.Draw.Color = Color.White;
+ Gizmo.Draw.SolidSphere( tr.EndPosition, size );
+ }
+ }
+
+ public override Widget CreateWidget()
+ {
+ return new PolygonEditorWidget( this );
+ }
+
+ [WideMode]
+ public float Height
+ {
+ get;
+ set
+ {
+ if ( field == value ) return;
+
+ field = value;
+
+ BuildPreview();
+ }
+ }
+
+ [WideMode]
+ public bool Hollow
+ {
+ get;
+ set
+ {
+ if ( field == value ) return;
+
+ field = value;
+
+ BuildPreview();
+ }
+ }
+
+ class PolygonEditorWidget : ToolSidebarWidget
+ {
+ readonly PolygonEditor _editor;
+
+ public PolygonEditorWidget( PolygonEditor editor )
+ {
+ _editor = editor;
+
+ Layout.Margin = 0;
+
+ {
+ var group = AddGroup( "Polygon Properties" );
+ var row = group.AddRow();
+ var so = editor.GetSerialized();
+ row.Add( ControlSheetRow.Create( so.GetProperty( nameof( editor.Height ) ) ) );
+ row.Add( ControlSheetRow.Create( so.GetProperty( nameof( editor.Hollow ) ) ) ).FixedWidth = 60;
+ }
+
+ Layout.AddStretchCell();
+ }
+
+ [Shortcut( "editor.delete", "DEL", typeof( SceneDock ) )]
+ public void DeletePoint() => _editor.RemovePoint();
+ }
+
+ static Vector3 GridSnap( Vector3 point, Vector3 normal )
+ {
+ var basis = Rotation.LookAt( normal );
+ return Gizmo.Snap( point * basis.Inverse, new Vector3( 0, 1, 1 ) ) * basis;
+ }
+
+ static void PlaneEquation( IReadOnlyList vertices, out Vector3 outNormal )
+ {
+ var normal = Vector3.Zero;
+ var count = vertices.Count;
+
+ for ( var i = 0; i < count; i++ )
+ {
+ var u = vertices[i];
+ var v = vertices[(i + 1) % count];
+ normal.x += (u.y - v.y) * (u.z + v.z);
+ normal.y += (u.z - v.z) * (u.x + v.x);
+ normal.z += (u.x - v.x) * (u.y + v.y);
+ }
+
+ outNormal = normal.Normal;
+ }
+}
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/PrimitiveEditor.cs b/game/addons/tools/Code/Scene/Mesh/Tools/PrimitiveEditor.cs
new file mode 100644
index 00000000..76fd116c
--- /dev/null
+++ b/game/addons/tools/Code/Scene/Mesh/Tools/PrimitiveEditor.cs
@@ -0,0 +1,30 @@
+namespace Editor.MeshEditor;
+
+public abstract class PrimitiveEditor
+{
+ private readonly TypeDescription _type;
+
+ protected PrimitiveTool Tool { get; private init; }
+
+ public string Title => _type.Title;
+ public string Icon => _type.Icon;
+
+ public virtual bool CanBuild => false;
+ public virtual bool InProgress => false;
+
+ protected PrimitiveEditor( PrimitiveTool tool )
+ {
+ Tool = tool;
+ _type = EditorTypeLibrary.GetType( GetType() );
+ }
+
+ public abstract void OnUpdate( SceneTrace trace );
+ public abstract void OnCancel();
+ public abstract PolygonMesh Build();
+
+ public virtual void OnCreated( MeshComponent component )
+ {
+ }
+
+ public virtual Widget CreateWidget() => null;
+}
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/PrimitiveTool.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/PrimitiveTool.UI.cs
new file mode 100644
index 00000000..9276837f
--- /dev/null
+++ b/game/addons/tools/Code/Scene/Mesh/Tools/PrimitiveTool.UI.cs
@@ -0,0 +1,202 @@
+using Sandbox.UI;
+
+namespace Editor.MeshEditor;
+
+partial class PrimitiveTool
+{
+ public override Widget CreateToolSidebar()
+ {
+ return new PrimitiveToolWidget( this );
+ }
+
+ public class PrimitiveToolWidget : ToolSidebarWidget
+ {
+ readonly PrimitiveTool _tool;
+ readonly Widget _settingsWidget;
+ readonly Button _createButton;
+ readonly Button _cancelButton;
+ readonly IconButton _iconLabel;
+ readonly Label.Header _titleLabel;
+
+ void OnEditorSelected( Type type )
+ {
+ _tool.Editor = EditorTypeLibrary.Create( type, [_tool] );
+
+ UpdateTitle();
+ BuildSettings();
+ }
+
+ void BuildSettings()
+ {
+ using var x = SuspendUpdates.For( this );
+
+ var widget = _tool.Editor?.CreateWidget();
+ if ( widget is null )
+ {
+ _settingsWidget.Hide();
+ }
+ else
+ {
+ _settingsWidget.Layout.Clear( true );
+ _settingsWidget.Layout.Add( widget );
+ _settingsWidget.Show();
+ }
+ }
+
+ [EditorEvent.Frame]
+ public void Frame()
+ {
+ UpdateButtons();
+ }
+
+ void UpdateButtons()
+ {
+ _createButton?.Enabled = _tool.Editor is not null && _tool.Editor.CanBuild;
+ _cancelButton?.Enabled = _tool.Editor is not null && _tool.Editor.InProgress;
+ }
+
+ void UpdateTitle()
+ {
+ var type = EditorTypeLibrary.GetType( _tool.Editor?.GetType() );
+ if ( type is null ) return;
+
+ _iconLabel?.Icon = type.Icon;
+ _titleLabel?.Text = $"Create {type.Title}";
+ }
+
+ public PrimitiveToolWidget( PrimitiveTool tool ) : base()
+ {
+ _tool = tool;
+
+ {
+ var titleRow = Layout.AddRow();
+ titleRow.Margin = new Margin( 0, 0, 0, 8 );
+ titleRow.Spacing = 4;
+
+ _iconLabel = titleRow.Add( new IconButton( null ), 0 );
+ _iconLabel.IconSize = 18;
+ _iconLabel.Background = Color.Transparent;
+ _iconLabel.Foreground = Theme.Blue;
+ _titleLabel = titleRow.Add( new Label.Header( null ), 1 );
+
+ UpdateTitle();
+ }
+
+ {
+ var list = new PrimitiveListView( this );
+ list.FixedWidth = 200;
+ list.SetItems( GetBuilderTypes() );
+ list.SelectItem( list.Items.FirstOrDefault( x => (x as TypeDescription).TargetType == tool.Editor?.GetType() ) );
+ list.ItemSelected = ( e ) => OnEditorSelected( (e as TypeDescription).TargetType );
+ list.BuildLayout();
+
+ var group = AddGroup( "Primitive Type" );
+ group.Add( list );
+ }
+
+ {
+ Layout.AddSpacingCell( 4 );
+
+ var row = Layout.AddRow();
+ row.Spacing = 4;
+
+ _createButton = row.Add( new Button( "Create", "done" )
+ {
+ Clicked = Create,
+ ToolTip = "[Create " + EditorShortcuts.GetKeys( "mesh.primitive-tool-create" ) + "]",
+ } );
+
+ _cancelButton = row.Add( new Button( "Cancel", "close" )
+ {
+ Clicked = Cancel,
+ ToolTip = "[Cancel " + EditorShortcuts.GetKeys( "mesh.primitive-tool-cancel" ) + "]"
+ } );
+
+ UpdateButtons();
+
+ Layout.AddSpacingCell( 4 );
+ }
+
+ {
+ _settingsWidget = new ToolSidebarWidget( this );
+ _settingsWidget.Layout.Margin = 0;
+ Layout.Add( _settingsWidget );
+
+ BuildSettings();
+ }
+
+ Layout.AddStretchCell();
+ }
+
+ [Shortcut( "mesh.primitive-tool-create", "enter", ShortcutType.Application )]
+ void Create() => _tool.Create();
+
+ [Shortcut( "mesh.primitive-tool-cancel", "ESC", ShortcutType.Application )]
+ void Cancel() => _tool.Cancel();
+
+ static IEnumerable GetBuilderTypes()
+ {
+ return EditorTypeLibrary.GetTypes()
+ .Where( x => !x.IsAbstract )
+ .OrderBy( x => x.Name );
+ }
+ }
+}
+
+public class PrimitiveListView : ListView
+{
+ public PrimitiveListView( Widget parent ) : base( parent )
+ {
+ ItemSpacing = 0;
+ ItemSize = 32;
+ Margin = 0;
+
+ HorizontalScrollbarMode = ScrollbarMode.Off;
+ VerticalScrollbarMode = ScrollbarMode.Off;
+ }
+
+ protected override void DoLayout()
+ {
+ base.DoLayout();
+
+ BuildLayout();
+ }
+
+ public void BuildLayout()
+ {
+ var rect = CanvasRect;
+ var itemSize = ItemSize;
+ var itemSpacing = ItemSpacing;
+ var itemsPerRow = 1;
+ var itemCount = Items.Count();
+
+ if ( itemSize.x > 0 ) itemsPerRow = ((rect.Width + itemSpacing.x) / (itemSize.x + itemSpacing.x)).FloorToInt();
+ itemsPerRow = Math.Max( 1, itemsPerRow );
+
+ var rowCount = MathX.CeilToInt( itemCount / (float)itemsPerRow );
+ FixedHeight = rowCount * (itemSize.y + itemSpacing.y) + Margin.EdgeSize.y;
+ }
+
+ protected override string GetTooltip( object obj )
+ {
+ var builder = obj as TypeDescription;
+ var displayInfo = DisplayInfo.ForType( builder.TargetType );
+ return displayInfo.Name;
+ }
+
+ protected override void PaintItem( VirtualWidget item )
+ {
+ if ( item.Selected )
+ {
+ Paint.ClearPen();
+ Paint.SetBrush( Theme.Blue );
+ Paint.DrawRect( item.Rect, 4 );
+ }
+
+ var builder = item.Object as TypeDescription;
+ var displayInfo = DisplayInfo.ForType( builder.TargetType );
+
+ Paint.SetPen( item.Selected || item.Hovered ? Color.White : Color.Gray );
+ Paint.DrawIcon( item.Rect, displayInfo.Icon ?? "square", HeaderBarStyle.IconSize );
+ }
+}
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/PrimitiveTool.cs b/game/addons/tools/Code/Scene/Mesh/Tools/PrimitiveTool.cs
new file mode 100644
index 00000000..785b3c3d
--- /dev/null
+++ b/game/addons/tools/Code/Scene/Mesh/Tools/PrimitiveTool.cs
@@ -0,0 +1,66 @@
+namespace Editor.MeshEditor;
+
+///
+/// Create different types of primitive meshes.
+///
+[Title( "Primitive Tool" )]
+[Icon( "view_in_ar" )]
+[Alias( "tools.primitive-tool" )]
+public partial class PrimitiveTool( MeshTool tool ) : EditorTool
+{
+ public PrimitiveEditor Editor { get; private set; }
+
+ public Material ActiveMaterial => tool.ActiveMaterial;
+
+ public override void OnEnabled()
+ {
+ Editor = EditorTypeLibrary.Create( typeof( BlockEditor ), [this] );
+ }
+
+ public override void OnDisabled()
+ {
+ Create();
+
+ Editor = null;
+ }
+
+ public void Create()
+ {
+ if ( Editor is null ) return;
+ if ( !Editor.CanBuild ) return;
+
+ var mesh = Editor.Build();
+ if ( mesh is null ) return;
+
+ var name = Editor.Title;
+
+ using var scope = SceneEditorSession.Scope();
+ using ( SceneEditorSession.Active.UndoScope( $"Create {name}" )
+ .WithGameObjectCreations()
+ .Push() )
+ {
+ var bounds = mesh.CalculateBounds();
+ mesh.ApplyTransform( new Transform( -bounds.Center ) );
+
+ var go = new GameObject( true, name );
+ go.WorldPosition = bounds.Center;
+ var c = go.Components.Create( false );
+ c.Mesh = mesh;
+ c.SmoothingAngle = 40.0f;
+
+ Editor.OnCreated( c );
+
+ c.Enabled = true;
+ }
+ }
+
+ public override void OnUpdate()
+ {
+ Editor?.OnUpdate( MeshTrace );
+ }
+
+ public void Cancel()
+ {
+ Editor?.OnCancel();
+ }
+}
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/SceneTraceExtensions.cs b/game/addons/tools/Code/Scene/Mesh/Tools/SceneTraceExtensions.cs
new file mode 100644
index 00000000..24386d26
--- /dev/null
+++ b/game/addons/tools/Code/Scene/Mesh/Tools/SceneTraceExtensions.cs
@@ -0,0 +1,118 @@
+
+namespace Editor.MeshEditor;
+
+static class SceneTraceMeshExtensions
+{
+ static Vector2 RayScreenPosition => SceneViewportWidget.MousePosition;
+
+ public static MeshVertex GetClosestVertex( this SceneTrace trace, int radius )
+ {
+ var point = RayScreenPosition;
+ var bestFace = TraceFace( trace, out var bestHitDistance );
+ var bestVertex = bestFace.GetClosestVertex( point, radius );
+
+ if ( bestFace.IsValid() && bestVertex.IsValid() )
+ return bestVertex;
+
+ var results = TraceFaces( trace, radius, point );
+ foreach ( var result in results )
+ {
+ var face = result.MeshFace;
+ var hitDistance = result.Distance;
+ var vertex = face.GetClosestVertex( point, radius );
+ if ( !vertex.IsValid() )
+ continue;
+
+ if ( hitDistance < bestHitDistance || !bestFace.IsValid() )
+ {
+ bestHitDistance = hitDistance;
+ bestVertex = vertex;
+ bestFace = face;
+ }
+ }
+
+ return bestVertex;
+ }
+
+ public static MeshEdge GetClosestEdge( this SceneTrace trace, int radius )
+ {
+ var point = RayScreenPosition;
+ var bestFace = TraceFace( trace, out var bestHitDistance );
+ var hitPosition = Gizmo.CurrentRay.Project( bestHitDistance );
+ var bestEdge = bestFace.GetClosestEdge( hitPosition, point, radius );
+
+ if ( bestFace.IsValid() && bestEdge.IsValid() )
+ return bestEdge;
+
+ var results = TraceFaces( trace, radius, point );
+ foreach ( var result in results )
+ {
+ var face = result.MeshFace;
+ var hitDistance = result.Distance;
+ hitPosition = Gizmo.CurrentRay.Project( hitDistance );
+
+ var edge = face.GetClosestEdge( hitPosition, point, radius );
+ if ( !edge.IsValid() )
+ continue;
+
+ if ( hitDistance < bestHitDistance || !bestFace.IsValid() )
+ {
+ bestHitDistance = hitDistance;
+ bestEdge = edge;
+ bestFace = face;
+ }
+ }
+
+ return bestEdge;
+ }
+
+ static MeshFace TraceFace( this SceneTrace trace, out float distance )
+ {
+ distance = default;
+
+ var result = trace.Run();
+ if ( !result.Hit || result.Component is not MeshComponent component )
+ return default;
+
+ distance = result.Distance;
+ var face = component.Mesh.TriangleToFace( result.Triangle );
+ return new MeshFace( component, face );
+ }
+
+ struct MeshFaceTraceResult
+ {
+ public MeshFace MeshFace;
+ public float Distance;
+ }
+
+ static List TraceFaces( this SceneTrace trace, int radius, Vector2 point )
+ {
+ var rays = new List { Gizmo.CurrentRay };
+ for ( var ring = 1; ring < radius; ring++ )
+ {
+ rays.Add( Gizmo.Camera.GetRay( point + new Vector2( 0, ring ) ) );
+ rays.Add( Gizmo.Camera.GetRay( point + new Vector2( ring, 0 ) ) );
+ rays.Add( Gizmo.Camera.GetRay( point + new Vector2( 0, -ring ) ) );
+ rays.Add( Gizmo.Camera.GetRay( point + new Vector2( -ring, 0 ) ) );
+ }
+
+ var faces = new List();
+ var faceHash = new HashSet();
+ foreach ( var ray in rays )
+ {
+ var result = trace.Ray( ray, Gizmo.RayDepth ).Run();
+ if ( !result.Hit )
+ continue;
+
+ if ( result.Component is not MeshComponent component )
+ continue;
+
+ var face = component.Mesh.TriangleToFace( result.Triangle );
+ var faceElement = new MeshFace( component, face );
+ if ( faceHash.Add( faceElement ) )
+ faces.Add( new MeshFaceTraceResult { MeshFace = faceElement, Distance = result.Distance } );
+ }
+
+ return faces;
+ }
+}
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/SelectionTool.cs b/game/addons/tools/Code/Scene/Mesh/Tools/SelectionTool.cs
index 4653b1d9..f5bbf817 100644
--- a/game/addons/tools/Code/Scene/Mesh/Tools/SelectionTool.cs
+++ b/game/addons/tools/Code/Scene/Mesh/Tools/SelectionTool.cs
@@ -51,8 +51,6 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool where T
protected virtual bool HasMoveMode => true;
- public static Vector2 RayScreenPosition => SceneViewportWidget.MousePosition;
-
public static bool IsMultiSelecting => Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Ctrl ) ||
Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Shift );
@@ -510,80 +508,6 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool where T
_undoScope = null;
}
- public MeshVertex GetClosestVertex( int radius )
- {
- var point = RayScreenPosition;
- var bestFace = TraceFace( out var bestHitDistance );
- var bestVertex = bestFace.GetClosestVertex( point, radius );
-
- if ( bestFace.IsValid() && bestVertex.IsValid() )
- return bestVertex;
-
- var results = TraceFaces( radius, point );
- foreach ( var result in results )
- {
- var face = result.MeshFace;
- var hitDistance = result.Distance;
- var vertex = face.GetClosestVertex( point, radius );
- if ( !vertex.IsValid() )
- continue;
-
- if ( hitDistance < bestHitDistance || !bestFace.IsValid() )
- {
- bestHitDistance = hitDistance;
- bestVertex = vertex;
- bestFace = face;
- }
- }
-
- return bestVertex;
- }
-
- public MeshEdge GetClosestEdge( int radius )
- {
- var point = RayScreenPosition;
- var bestFace = TraceFace( out var bestHitDistance );
- var hitPosition = Gizmo.CurrentRay.Project( bestHitDistance );
- var bestEdge = bestFace.GetClosestEdge( hitPosition, point, radius );
-
- if ( bestFace.IsValid() && bestEdge.IsValid() )
- return bestEdge;
-
- var results = TraceFaces( radius, point );
- foreach ( var result in results )
- {
- var face = result.MeshFace;
- var hitDistance = result.Distance;
- hitPosition = Gizmo.CurrentRay.Project( hitDistance );
-
- var edge = face.GetClosestEdge( hitPosition, point, radius );
- if ( !edge.IsValid() )
- continue;
-
- if ( hitDistance < bestHitDistance || !bestFace.IsValid() )
- {
- bestHitDistance = hitDistance;
- bestEdge = edge;
- bestFace = face;
- }
- }
-
- return bestEdge;
- }
-
- private MeshFace TraceFace( out float distance )
- {
- distance = default;
-
- var result = MeshTrace.Run();
- if ( !result.Hit || result.Component is not MeshComponent component )
- return default;
-
- distance = result.Distance;
- var face = component.Mesh.TriangleToFace( result.Triangle );
- return new MeshFace( component, face );
- }
-
public MeshFace TraceFace()
{
if ( IsBoxSelecting )
@@ -597,43 +521,6 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool where T
return new MeshFace( component, face );
}
- private struct MeshFaceTraceResult
- {
- public MeshFace MeshFace;
- public float Distance;
- }
-
- private List TraceFaces( int radius, Vector2 point )
- {
- var rays = new List { Gizmo.CurrentRay };
- for ( var ring = 1; ring < radius; ring++ )
- {
- rays.Add( Gizmo.Camera.GetRay( point + new Vector2( 0, ring ) ) );
- rays.Add( Gizmo.Camera.GetRay( point + new Vector2( ring, 0 ) ) );
- rays.Add( Gizmo.Camera.GetRay( point + new Vector2( 0, -ring ) ) );
- rays.Add( Gizmo.Camera.GetRay( point + new Vector2( -ring, 0 ) ) );
- }
-
- var faces = new List();
- var faceHash = new HashSet();
- foreach ( var ray in rays )
- {
- var result = MeshTrace.Ray( ray, Gizmo.RayDepth ).Run();
- if ( !result.Hit )
- continue;
-
- if ( result.Component is not MeshComponent component )
- continue;
-
- var face = component.Mesh.TriangleToFace( result.Triangle );
- var faceElement = new MeshFace( component, face );
- if ( faceHash.Add( faceElement ) )
- faces.Add( new MeshFaceTraceResult { MeshFace = faceElement, Distance = result.Distance } );
- }
-
- return faces;
- }
-
public static Vector3 ComputeTextureVAxis( Vector3 normal ) => FaceDownVectors[GetOrientationForPlane( normal )];
private static int GetOrientationForPlane( Vector3 plane )
diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/VertexTool.cs b/game/addons/tools/Code/Scene/Mesh/Tools/VertexTool.cs
index a9bc30cb..9e0b5571 100644
--- a/game/addons/tools/Code/Scene/Mesh/Tools/VertexTool.cs
+++ b/game/addons/tools/Code/Scene/Mesh/Tools/VertexTool.cs
@@ -69,7 +69,7 @@ public sealed partial class VertexTool( MeshTool tool ) : SelectionTool