mattercontrol/PartPreviewWindow/View3D/MeshViewerWidget.cs
2017-10-21 08:56:08 -07:00

669 lines
No EOL
21 KiB
C#

/*
Copyright (c) 2014, Lars Brubaker
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those
of the authors and should not be interpreted as representing official policies,
either expressed or implied, of the FreeBSD Project.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MatterHackers.Agg;
using MatterHackers.Agg.OpenGlGui;
using MatterHackers.Agg.UI;
using MatterHackers.DataConverters3D;
using MatterHackers.MatterControl;
using MatterHackers.MatterControl.PartPreviewWindow;
using MatterHackers.PolygonMesh;
using MatterHackers.RenderOpenGl;
using MatterHackers.RenderOpenGl.OpenGl;
using MatterHackers.VectorMath;
namespace MatterHackers.MeshVisualizer
{
public enum BedShape { Rectangular, Circular };
public class DrawGlContentEventArgs : DrawEventArgs
{
public bool ZBuffered { get; }
public DrawGlContentEventArgs(bool zBuffered, DrawEventArgs e)
: base(e.graphics2D)
{
ZBuffered = zBuffered;
}
}
public static class MatterialRendering
{
public static RGBA_Bytes Color(int materialIndex)
{
return RGBA_Floats.FromHSL(Math.Max(materialIndex, 0) / 10.0, .99, .49).GetAsRGBA_Bytes();
}
}
public class MeshViewerWidget : GuiWidget
{
// TODO: Need to be instance based for multi-printer
public GuiWidget ParentSurface { get; set; }
private RenderTypes renderType = RenderTypes.Shaded;
private InteractionLayer interactionLayer;
private BedConfig sceneContext;
private double selectionHighlightWidth = 5;
public MeshViewerWidget(BedConfig sceneContext, InteractionLayer interactionLayer, string startingTextMessage = "", EditorType editorType = EditorType.Part)
{
this.EditorMode = editorType;
this.scene = sceneContext.Scene;
this.sceneContext = sceneContext;
this.interactionLayer = interactionLayer;
this.World = interactionLayer.World;
scene.SelectionChanged += (sender, e) =>
{
Invalidate();
};
RenderType = RenderTypes.Shaded;
RenderBed = true;
RenderBuildVolume = false;
BedColor = new RGBA_Floats(.8, .8, .8, .7).GetAsRGBA_Bytes();
BuildVolumeColor = new RGBA_Floats(.2, .8, .3, .2).GetAsRGBA_Bytes();
this.interactionLayer.DrawGlOpaqueContent += Draw_GlOpaqueContent;
this.interactionLayer.DrawGlTransparentContent += Draw_GlTransparentContent;
}
public override void OnParentChanged(EventArgs e)
{
this.ParentSurface = this.Parent;
base.OnParentChanged(e);
}
public WorldView World { get; }
public event EventHandler LoadDone;
public bool AllowBedRenderingWhenEmpty { get; set; }
public RGBA_Bytes BedColor { get; set; }
public RGBA_Bytes BuildVolumeColor { get; set; }
public static AxisAlignedBoundingBox GetAxisAlignedBoundingBox(List<MeshGroup> meshGroups)
{
AxisAlignedBoundingBox totalMeshBounds = AxisAlignedBoundingBox.Empty;
bool first = true;
foreach (MeshGroup meshGroup in meshGroups)
{
AxisAlignedBoundingBox meshBounds = meshGroup.GetAxisAlignedBoundingBox();
if (first)
{
totalMeshBounds = meshBounds;
first = false;
}
else
{
totalMeshBounds = AxisAlignedBoundingBox.Union(totalMeshBounds, meshBounds);
}
}
return totalMeshBounds;
}
public override void OnLoad(EventArgs args)
{
// some debug code to be able to click on parts
if (false)
{
AfterDraw += (sender, e) =>
{
foreach (var child in scene.Children)
{
this.World.RenderDebugAABB(e.graphics2D, child.TraceData().GetAxisAlignedBoundingBox());
this.World.RenderDebugAABB(e.graphics2D, child.GetAxisAlignedBoundingBox(Matrix4X4.Identity));
}
};
}
base.OnLoad(args);
}
public override void FindNamedChildrenRecursive(string nameToSearchFor, List<WidgetAndPosition> foundChildren, RectangleDouble touchingBounds, SearchType seachType, bool allowInvalidItems = true)
{
foreach (var child in scene.Children)
{
string object3DName = child.Name;
if (object3DName == null && child.MeshPath != null)
{
object3DName = Path.GetFileName(child.MeshPath);
}
bool nameFound = false;
if (seachType == SearchType.Exact)
{
if (object3DName == nameToSearchFor)
{
nameFound = true;
}
}
else
{
if (nameToSearchFor == ""
|| object3DName.Contains(nameToSearchFor))
{
nameFound = true;
}
}
if (nameFound)
{
AxisAlignedBoundingBox bounds = child.TraceData().GetAxisAlignedBoundingBox();
RectangleDouble screenBoundsOfObject3D = RectangleDouble.ZeroIntersection;
for(int i=0; i<4; i++)
{
screenBoundsOfObject3D.ExpandToInclude(this.World.GetScreenPosition(bounds.GetTopCorner(i)));
screenBoundsOfObject3D.ExpandToInclude(this.World.GetScreenPosition(bounds.GetBottomCorner(i)));
}
if (touchingBounds.IsTouching(screenBoundsOfObject3D))
{
Vector3 renderPosition = bounds.Center;
Vector2 objectCenterScreenSpace = this.World.GetScreenPosition(renderPosition);
Point2D screenPositionOfObject3D = new Point2D((int)objectCenterScreenSpace.x, (int)objectCenterScreenSpace.y);
foundChildren.Add(new WidgetAndPosition(this, screenPositionOfObject3D, object3DName));
}
}
}
base.FindNamedChildrenRecursive(nameToSearchFor, foundChildren, touchingBounds, seachType, allowInvalidItems);
}
protected InteractiveScene scene { get; }
public bool RenderBed { get; set; }
public bool RenderBuildVolume { get; set; }
public RenderTypes RenderType
{
get => this.IsActive ? renderType : RenderTypes.Wireframe;
set
{
if (renderType != value)
{
renderType = value;
foreach(var renderTransfrom in scene.VisibleMeshes())
{
renderTransfrom.Mesh.MarkAsChanged();
}
}
}
}
public static void AssertDebugNotDefined()
{
#if DEBUG
throw new Exception("DEBUG is defined and should not be!");
#endif
}
public static RGBA_Bytes GetExtruderColor(int extruderIndex)
{
return MatterialRendering.Color(extruderIndex);
}
public void CreateGlDataObject(IObject3D item)
{
if(item.Mesh != null)
{
GLMeshTrianglePlugin.Get(item.Mesh);
}
foreach (IObject3D child in item.Children.Where(o => o.Mesh != null))
{
GLMeshTrianglePlugin.Get(child.Mesh);
}
}
public bool SuppressUiVolumes { get; set; } = false;
private CancellationTokenSource fileLoadCancellationTokenSource;
public async Task LoadItemIntoScene(string itemPath, Vector2 bedCenter = new Vector2(), string itemName = null)
{
if (File.Exists(itemPath))
{
fileLoadCancellationTokenSource = new CancellationTokenSource();
// TODO: How to we handle mesh load errors? How do we report success?
IObject3D loadedItem = await Task.Run(() => Object3D.Load(itemPath, fileLoadCancellationTokenSource.Token));
if (loadedItem != null)
{
if (itemName != null)
{
loadedItem.Name = itemName;
}
// SetMeshAfterLoad
scene.Children.Modify(list =>
{
if (loadedItem.Mesh != null)
{
// STLs currently load directly into the mesh rather than as a group like AMF
list.Add(loadedItem);
}
else
{
list.AddRange(loadedItem.Children);
}
});
CreateGlDataObject(loadedItem);
}
else
{
// TODO: Error message container moved to Interaction Layer - how could we support this type of error for a loaded scene item?
//partProcessingInfo.centeredInfoText.Text = string.Format("Sorry! No 3D view available\nfor this file.");
}
// Invoke LoadDone event
LoadDone?.Invoke(this, null);
}
else
{
// TODO: Error message container moved to Interaction Layer - how could we support this type of error for a loaded scene item?
//partProcessingInfo.centeredInfoText.Text = string.Format("{0}\n'{1}'", "File not found on disk.", Path.GetFileName(itemPath));
}
fileLoadCancellationTokenSource = null;
}
public override void OnClosed(ClosedEventArgs e)
{
fileLoadCancellationTokenSource?.Cancel();
base.OnClosed(e);
}
public bool IsActive { get; set; } = true;
private void DrawObject(IObject3D object3D, List<IObject3D> transparentMeshes, bool parentSelected, DrawEventArgs e)
{
var totalVertices = 0;
var debugBorderColor = RGBA_Bytes.Green;
var debugNotSelectedFillColor = new RGBA_Bytes(RGBA_Bytes.White, 120);
foreach (var renderData in object3D.VisibleMeshes())
{
totalVertices += renderData.Mesh.Vertices.Count;
if (totalVertices > 1000)
{
break;
}
}
var frustum = World.GetClippingFrustum();
bool tooBigForComplexSelection = totalVertices > 1000;
if (tooBigForComplexSelection
&& scene.DebugItem == null
&& scene.HasSelection
&& (object3D == scene.SelectedItem || scene.SelectedItem.Children.Contains(object3D)))
{
GLHelper.PrepareFor3DLineRender(true);
RenderAABB(frustum, object3D.GetAxisAlignedBoundingBox(Matrix4X4.Identity), Matrix4X4.Identity, RGBA_Bytes.White, selectionHighlightWidth);
GL.Enable(EnableCap.Lighting);
}
foreach (var renderData in object3D.VisibleMeshes())
{
bool isSelected = parentSelected ||
scene.HasSelection && (object3D == scene.SelectedItem || scene.SelectedItem.Children.Contains(object3D));
RGBA_Bytes drawColor = renderData.WorldColor();
if (renderData.WorldOutputType() == PrintOutputTypes.Support)
{
drawColor = new RGBA_Bytes(RGBA_Bytes.Yellow, 120);
}
else if (renderData.WorldOutputType() == PrintOutputTypes.Hole)
{
drawColor = new RGBA_Bytes(RGBA_Bytes.Gray, 120);
}
// check if we should be rendering materials (this overrides the other colors)
if (this.RenderType == RenderTypes.Materials)
{
drawColor = MatterialRendering.Color(renderData.WorldMaterialIndex());
}
bool isDebugItem = false;
bool renderAsSolid = drawColor.alpha == 255;
#if DEBUG
isDebugItem = scene.DebugItem == renderData;
renderAsSolid = (renderAsSolid && scene.DebugItem == null)
|| isDebugItem;
#endif
if (renderAsSolid)
{
GLHelper.Render(renderData.Mesh, drawColor, renderData.WorldMatrix(), RenderType, renderData.WorldMatrix() * World.ModelviewMatrix);
}
else
{
transparentMeshes.Add(new Object3D()
{
Mesh = renderData.Mesh,
Matrix = renderData.WorldMatrix(),
Color = (scene.DebugItem == null) ? drawColor : debugNotSelectedFillColor,
MaterialIndex = renderData.WorldMaterialIndex(),
OutputType = renderData.WorldOutputType()
});
}
if (isSelected && !tooBigForComplexSelection)
{
RenderSelection(renderData, frustum);
}
#if DEBUG
if (isDebugItem)
{
var aabb = object3D.GetAxisAlignedBoundingBox(Matrix4X4.Identity);
GLHelper.PrepareFor3DLineRender(true);
RenderAABB(frustum, aabb, Matrix4X4.Identity, debugBorderColor, 1);
if (renderData.Mesh != null)
{
GLHelper.Render(renderData.Mesh, debugBorderColor, renderData.WorldMatrix(), RenderTypes.Wireframe, renderData.WorldMatrix() * World.ModelviewMatrix);
}
}
#endif
// RenderNormals(renderData);
// turn lighting back on after rendering selection outlines
GL.Enable(EnableCap.Lighting);
}
}
private void RenderNormals(IObject3D renderData)
{
var frustum = World.GetClippingFrustum();
foreach (var face in renderData.Mesh.Faces)
{
int vertexCount = 0;
Vector3 faceCenter = Vector3.Zero;
foreach (var vertex in face.Vertices())
{
faceCenter += vertex.Position;
vertexCount++;
}
faceCenter /= vertexCount;
var transformed1 = Vector3.Transform(faceCenter, renderData.Matrix);
var normal = Vector3.TransformNormal(face.Normal, renderData.Matrix).GetNormal();
GLHelper.Render3DLineNoPrep(frustum, World, transformed1, transformed1 + normal, RGBA_Bytes.Red, 2);
}
}
private void RenderSelection(IObject3D renderData, Frustum frustum)
{
var screenPosition = new Vector3[3];
GLHelper.PrepareFor3DLineRender(true);
if (renderData.Mesh.Vertices.Count < 1000)
{
foreach (MeshEdge meshEdge in renderData.Mesh.MeshEdges)
{
if (meshEdge.GetNumFacesSharingEdge() == 2)
{
var meshToView = renderData.WorldMatrix() * World.ModelviewMatrix;
FaceEdge firstFaceEdge = meshEdge.firstFaceEdge;
FaceEdge nextFaceEdge = meshEdge.firstFaceEdge.radialNextFaceEdge;
// find out if one face is facing the camera and one is facing away
var viewVertexPosition = Vector3.Transform(firstFaceEdge.FirstVertex.Position, meshToView);
var viewFirstNormal = Vector3.TransformNormal(firstFaceEdge.ContainingFace.Normal, meshToView).GetNormal();
var viewNextNormal = Vector3.TransformNormal(nextFaceEdge.ContainingFace.Normal, meshToView).GetNormal();
// Is the plane facing the camera (0, 0, 0). Finding the distance from the orign to the plane along the normal.
var firstTowards = Vector3.Dot(viewFirstNormal, viewVertexPosition) < 0;
var nextTowards = Vector3.Dot(viewNextNormal, viewVertexPosition) < 0;
if (firstTowards != nextTowards)
{
var transformed1 = Vector3.Transform(meshEdge.VertexOnEnd[0].Position, renderData.WorldMatrix());
var transformed2 = Vector3.Transform(meshEdge.VertexOnEnd[1].Position, renderData.WorldMatrix());
GLHelper.Render3DLineNoPrep(frustum, World, transformed1, transformed2, RGBA_Bytes.White, selectionHighlightWidth);
}
}
}
}
else // just render the bounding box
{
RenderAABB(frustum, renderData.Mesh.GetAxisAlignedBoundingBox(), renderData.WorldMatrix(), RGBA_Bytes.White, selectionHighlightWidth);
}
}
void RenderAABB(Frustum frustum, AxisAlignedBoundingBox bounds, Matrix4X4 matrix, RGBA_Bytes color, double width)
{
for (int i = 0; i < 4; i++)
{
Vector3 bottomStartPosition = Vector3.Transform(bounds.GetBottomCorner(i), matrix);
Vector3 bottomEndPosition = Vector3.Transform(bounds.GetBottomCorner((i + 1) % 4), matrix);
Vector3 topStartPosition = Vector3.Transform(bounds.GetTopCorner(i), matrix);
Vector3 topEndPosition = Vector3.Transform(bounds.GetTopCorner((i + 1) % 4), matrix);
GLHelper.Render3DLineNoPrep(frustum, World, bottomStartPosition, bottomEndPosition, color, width);
GLHelper.Render3DLineNoPrep(frustum, World, topStartPosition, topEndPosition, color, width);
GLHelper.Render3DLineNoPrep(frustum, World, topStartPosition, bottomStartPosition, color, width);
}
}
public enum EditorType { Printer, Part }
public EditorType EditorMode { get; set; } = EditorType.Part;
private int BackToFrontXY(IObject3D a, IObject3D b)
{
var aCenterWorld = Vector3.Transform(a.Mesh.GetAxisAlignedBoundingBox().Center, a.Matrix);
aCenterWorld.z = 0; // we only want to look at the distance on xy in world space
var aCenterInViewSpace = Vector3.Transform(aCenterWorld, World.ModelviewMatrix);
var bCenterWorld = Vector3.Transform(b.Mesh.GetAxisAlignedBoundingBox().Center, b.Matrix);
bCenterWorld.z = 0; // we only want to look at the distance on xy in world space
var bCenterInViewSpace = Vector3.Transform(bCenterWorld, World.ModelviewMatrix);
return bCenterInViewSpace.LengthSquared.CompareTo(aCenterInViewSpace.LengthSquared);
}
private void Draw_GlOpaqueContent(object sender, DrawEventArgs e)
{
List<IObject3D> transparentMeshes = new List<IObject3D>();
foreach (var object3D in scene.Children)
{
DrawObject(object3D, transparentMeshes, false, e);
}
}
private void Draw_GlTransparentContent(object sender, DrawEventArgs e)
{
List<IObject3D> transparentMeshes = new List<IObject3D>();
foreach (var object3D in scene.Children)
{
if (object3D.Visible)
{
DrawObject(object3D, transparentMeshes, false, e);
}
}
transparentMeshes.Sort(BackToFrontXY);
var bedNormalInViewSpace = Vector3.TransformNormal(Vector3.UnitZ, World.ModelviewMatrix).GetNormal();
var pointOnBedInViewSpace = Vector3.Transform(new Vector3(10, 10, 0), World.ModelviewMatrix);
var lookingDownOnBed = Vector3.Dot(bedNormalInViewSpace, pointOnBedInViewSpace) < 0;
if (lookingDownOnBed)
{
// render the bed
RenderBedMesh(lookingDownOnBed);
// than the transparent stuff
//int colorIndex = 0; // helps debug the sorting order
foreach (var transparentRenderData in transparentMeshes)
{
var color = transparentRenderData.Color;
//color = RGBA_Floats.FromHSL(Math.Max(colorIndex++, 0) / 10.0, .99, .49).GetAsRGBA_Bytes();
GLHelper.Render(transparentRenderData.Mesh, color, transparentRenderData.Matrix, RenderTypes.Outlines, transparentRenderData.Matrix * World.ModelviewMatrix);
}
}
else
{
// render the transparent stuff
foreach (var transparentRenderData in transparentMeshes)
{
GLHelper.Render(transparentRenderData.Mesh, transparentRenderData.Color, transparentRenderData.Matrix, RenderTypes.Outlines, transparentRenderData.Matrix * World.ModelviewMatrix);
}
// than render the bed
RenderBedMesh(lookingDownOnBed);
}
// we don't want to render the bed or build volume before we load a model.
if (scene.HasChildren() || AllowBedRenderingWhenEmpty)
{
if (false) // this is code to draw a small axis indicator
{
double big = 10;
double small = 1;
Mesh xAxis = PlatonicSolids.CreateCube(big, small, small);
GLHelper.Render(xAxis, RGBA_Bytes.Red);
Mesh yAxis = PlatonicSolids.CreateCube(small, big, small);
GLHelper.Render(yAxis, RGBA_Bytes.Green);
Mesh zAxis = PlatonicSolids.CreateCube(small, small, big);
GLHelper.Render(zAxis, RGBA_Bytes.Blue);
}
}
DrawInteractionVolumes(e);
}
private void RenderBedMesh(bool lookingDownOnBed)
{
if (this.EditorMode == EditorType.Printer)
{
// only render if we are above the bed
if (RenderBed)
{
var bedColor = this.BedColor;
if (!lookingDownOnBed)
{
bedColor = new RGBA_Bytes(this.BedColor, this.BedColor.alpha / 4);
}
GLHelper.Render(sceneContext.Mesh, bedColor, RenderTypes.Shaded, World.ModelviewMatrix);
if (sceneContext.PrinterShape != null)
{
GLHelper.Render(sceneContext.PrinterShape, bedColor, RenderTypes.Shaded, World.ModelviewMatrix);
}
}
if (sceneContext.BuildVolumeMesh != null && RenderBuildVolume)
{
GLHelper.Render(sceneContext.BuildVolumeMesh, this.BuildVolumeColor, RenderTypes.Shaded, World.ModelviewMatrix);
}
}
else
{
GL.Disable(EnableCap.Texture2D);
GL.Disable(EnableCap.Blend);
int width = 600;
GL.Begin(BeginMode.Lines);
{
for (int i = -width; i <= width; i += 50)
{
GL.Color4(240, 240, 240, 255);
GL.Vertex3(i, width, 0);
GL.Vertex3(i, -width, 0);
GL.Vertex3(width, i, 0);
GL.Vertex3(-width, i, 0);
}
GL.Color4(255, 0, 0, 255);
GL.Vertex3(width, 0, 0);
GL.Vertex3(-width, 0, 0);
GL.Color4(0, 255, 0, 255);
GL.Vertex3(0, width, 0);
GL.Vertex3(0, -width, 0);
GL.Color4(0, 0, 255, 255);
GL.Vertex3(0, 0, 10);
GL.Vertex3(0, 0, -10);
}
GL.End();
}
}
private void DrawInteractionVolumes(DrawEventArgs e)
{
if(SuppressUiVolumes)
{
return;
}
// draw on top of anything that is already drawn
foreach (InteractionVolume interactionVolume in interactionLayer.InteractionVolumes)
{
if (interactionVolume.DrawOnTop)
{
GL.Disable(EnableCap.DepthTest);
interactionVolume.DrawGlContent(new DrawGlContentEventArgs(false, e));
GL.Enable(EnableCap.DepthTest);
}
}
// Draw again setting the depth buffer and ensuring that all the interaction objects are sorted as well as we can
foreach (InteractionVolume interactionVolume in interactionLayer.InteractionVolumes)
{
interactionVolume.DrawGlContent(new DrawGlContentEventArgs(true, e));
}
}
}
}