- Implemented vtk base for viewing

This commit is contained in:
bklronin 2024-07-04 22:58:52 +02:00
parent fcabe449f8
commit 9daf263aad
6 changed files with 989 additions and 51 deletions

View File

@ -635,8 +635,10 @@ class SketchWidget(QWidget):
return self.width() / self.height() * (1.0 / abs(self.zoom)) return self.width() / self.height() * (1.0 / abs(self.zoom))
def clear_sketch(self): def clear_sketch(self):
self.points = [] self.slv_points_main = []
self.update() self.slv_lines_main = []
self.reset_buffers()
self.solv = SolverSystem()
# Example usage # Example usage

View File

@ -1,15 +1,71 @@
import sys
import numpy as np import numpy as np
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from PySide6.QtOpenGLWidgets import QOpenGLWidget from PySide6.QtOpenGLWidgets import QOpenGLWidget
from PySide6.QtCore import Qt, QPoint from PySide6.QtCore import Qt, QPoint
from OpenGL.GL import * from OpenGL.GL import *
from OpenGL.GLU import * from OpenGL.GLU import *
from stl import mesh
##testing
def create_cube(scale=1):
vertices = np.array([
[0, 0, 0],
[2, 0, 0],
[2, 2, 0],
[0, 2, 0],
[0, 0, 2],
[2, 0, 2],
[2, 2, 2],
[0, 2, 2]
]) * scale
faces = np.array([
[0, 1, 2],
[2, 3, 0],
[4, 5, 6],
[6, 7, 4],
[0, 1, 5],
[5, 4, 0],
[2, 3, 7],
[7, 6, 2],
[0, 3, 7],
[7, 4, 0],
[1, 2, 6],
[6, 5, 1]
])
return vertices, faces
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("OpenGL Cube Viewer")
self.setGeometry(100, 100, 800, 600)
self.opengl_widget = OpenGLWidget()
central_widget = QWidget()
layout = QVBoxLayout()
layout.addWidget(self.opengl_widget)
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
# Load cube data
vertices, faces = create_cube()
self.opengl_widget.load_interactor_mesh((vertices, faces))
class OpenGLWidget(QOpenGLWidget): class OpenGLWidget(QOpenGLWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.scale_factor = 0.001 self.vertices = None
self.faces = None
self.selected_face = -1
self.scale_factor = 1
self.mesh_loaded = None self.mesh_loaded = None
self.interactor_loaded = None
self.centroid = None self.centroid = None
self.stl_file = "out.stl" # Replace with your STL file path self.stl_file = "out.stl" # Replace with your STL file path
self.lastPos = QPoint() self.lastPos = QPoint()
@ -19,13 +75,13 @@ class OpenGLWidget(QOpenGLWidget):
self.yRot = 0 self.yRot = 0
self.zoom = -2 self.zoom = -2
self.sketch = [] self.sketch = []
self.gl_width = self.width() / 100 self.gl_width = self.width()
self.gl_height = self.height() / 100 self.gl_height = self.height()
def map_value_to_range(self, value, value_min=0, value_max=1920, range_min=-1, range_max=1): def map_value_to_range(self, value, value_min=0, value_max=1920, range_min=-1, range_max=1):
value = max(value_min, min(value_max, value)) value = max(value_min, min(value_max, value))
mapped_value = ((value - value_min) / (value_max - value_min)) * (range_max - range_min) + range_min mapped_value = ((value - value_min) / (value_max - value_min)) * (range_max - range_min) + range_min
return mapped_value return mapped_value
def load_stl(self, filename: str) -> object: def load_stl(self, filename: str) -> object:
@ -46,14 +102,23 @@ class OpenGLWidget(QOpenGLWidget):
self.mesh_loaded = stl_mesh.vectors self.mesh_loaded = stl_mesh.vectors
self.centroid = (centroid_x, centroid_y, centroid_z) self.centroid = (centroid_x, centroid_y, centroid_z)
except FileNotFoundError: except FileNotFoundError:
print(f"Error: File {filename} not found.") print(f"Error: File {filename} not found.")
except Exception as e: except Exception as e:
print(f"Error loading {filename}: {e}") print(f"Error loading {filename}: {e}")
return None, (0, 0, 0) return None, (0, 0, 0)
def load_interactor_mesh(self, simp_mesh):
self.interactor_loaded = simp_mesh
# Calculate centroid based on the average position of vertices
centroid = np.mean(simp_mesh[0], axis=0)
self.centroid = tuple(centroid)
print(f"Centroid: {self.centroid}")
self.update()
def load_mesh_direct(self, mesh): def load_mesh_direct(self, mesh):
try: try:
@ -83,40 +148,236 @@ class OpenGLWidget(QOpenGLWidget):
glViewport(0, 0, width, height) glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION) glMatrixMode(GL_PROJECTION)
glLoadIdentity() glLoadIdentity()
aspect = width / float(height) aspect = width / float(height)
self.gl_width = self.width() / 1000 self.gl_width = self.width()
self.gl_height = self.height() / 1000 self.gl_height = self.height()
gluPerspective(45.0, aspect, 0.01, 10.0) gluPerspective(45.0, aspect, 0.01, 1000.0)
glMatrixMode(GL_MODELVIEW) glMatrixMode(GL_MODELVIEW)
def unproject(self, x, y, z, modelview, projection, viewport):
mvp = np.dot(projection, modelview)
mvp_inv = np.linalg.inv(mvp)
ndc = np.array([(x - viewport[0]) / viewport[2] * 2 - 1,
(y - viewport[1]) / viewport[3] * 2 - 1,
2 * z - 1,
1])
world = np.dot(mvp_inv, ndc)
print("world undproj", world)
return world[:3] / world[3]
def draw_ray(self, ray_start, ray_end):
glColor3f(1.0, 0.0, 0.0) # Set the color of the ray (red)
glBegin(GL_LINES)
glVertex3f(*ray_start)
glVertex3f(*ray_end)
glEnd()
def mousePressEvent(self, event):
if event.buttons() & Qt.RightButton:
self.select_face(event)
def select_face(self, event):
x = event.position().x()
y = event.position().y()
modelview = glGetDoublev(GL_MODELVIEW_MATRIX)
projection = glGetDoublev(GL_PROJECTION_MATRIX)
viewport = glGetIntegerv(GL_VIEWPORT)
# Unproject near and far points in world space
ray_start = gluUnProject(x, y, 0.0, modelview, projection, viewport)
ray_end = gluUnProject(x, y, 1.0, modelview, projection, viewport)
ray_start = np.array(ray_start)
ray_end = np.array(ray_end)
ray_direction = ray_end - ray_start
ray_direction /= np.linalg.norm(ray_direction)
print(f"Ray start: {ray_start}")
print(f"Ray end: {ray_end}")
print(f"Ray direction: {ray_direction}")
self.selected_face = self.check_intersection(ray_start, ray_end)
print(f"Selected face: {self.selected_face}")
self.update()
def ray_box_intersection(self, ray_origin, ray_direction, box_min, box_max):
inv_direction = 1 / (ray_direction + 1e-7) # Add small value to avoid division by zero
t1 = (box_min - ray_origin) * inv_direction
t2 = (box_max - ray_origin) * inv_direction
t_min = np.max(np.minimum(t1, t2))
t_max = np.min(np.maximum(t1, t2))
print(f"min: {t_min}, max: {t_max}" )
return t_max >= t_min and t_max > 0
def check_intersection(self, ray_start, ray_end):
# Get the current modelview matrix
modelview = glGetDoublev(GL_MODELVIEW_MATRIX)
# Transform vertices to camera space
vertices_cam = [np.dot(modelview, np.append(v, 1))[:3] for v in self.interactor_loaded[0]]
ray_direction = ray_end - ray_start
ray_direction /= np.linalg.norm(ray_direction)
print(f"Checking intersection with {len(self.interactor_loaded[1])} faces")
for face_idx, face in enumerate(self.interactor_loaded[1]):
v0, v1, v2 = [vertices_cam[i] for i in face]
intersection = self.moller_trumbore(ray_start, ray_direction, v0, v1, v2)
if intersection is not None:
print(f"Intersection found with face {face_idx}")
return face_idx
print("No intersection found")
return None
def moller_trumbore(self, ray_origin, ray_direction, v0, v1, v2):
epsilon = 1e-6
# Find vectors for two edges sharing v0
edge1 = v1 - v0
edge2 = v2 - v0
pvec = np.cross(ray_direction, edge2)
det = np.dot(edge1, pvec)
print(det)
"""if det < epsilon:
return None"""
inv_det = 1.0 / det
tvec = ray_origin - v0
u = np.dot(tvec, pvec) * inv_det
print("u", u )
if u < 0.0 or u > 1.0:
return None
qvec = np.cross(tvec, edge1)
# Calculate v parameter and test bounds
v = np.dot(ray_direction, qvec) * inv_det
print("v", v)
if v < 0.0 or u + v > 1.0:
return None
# Calculate t, ray intersects triangle
t = np.dot(edge2, qvec) * inv_det
print("t",t)
if t > epsilon:
return ray_origin + t * ray_direction
return None
def ray_triangle_intersection(self, ray_origin, ray_direction, v0, v1, v2):
epsilon = 1e-5
edge1 = v1 - v0
edge2 = v2 - v0
h = np.cross(ray_direction, edge2)
a = np.dot(edge1, h)
print(f"Triangle vertices: {v0}, {v1}, {v2}")
print(f"a: {a}")
if abs(a) < epsilon:
print("Ray is parallel to the triangle")
return None # Ray is parallel to the triangle
f = 1.0 / a
s = ray_origin - v0
u = f * np.dot(s, h)
print(f"u: {u}")
if u < 0.0 or u > 1.0:
print("u is out of range")
return None
q = np.cross(s, edge1)
v = f * np.dot(ray_direction, q)
print(f"v: {v}")
if v < 0.0 or u + v > 1.0:
print("v is out of range")
return None
t = f * np.dot(edge2, q)
print(f"t: {t}")
if t > epsilon:
intersection_point = ray_origin + t * ray_direction
print(f"Intersection point: {intersection_point}")
return intersection_point
print("t is too small")
return None
def paintGL(self): def paintGL(self):
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity() glLoadIdentity()
# Apply camera transformation
glTranslatef(0, 0, self.zoom) glTranslatef(0, 0, self.zoom)
glRotatef(self.xRot, 1.0, 0.0, 0.0) glRotatef(self.xRot, 1.0, 0.0, 0.0)
glRotatef(self.yRot, 0.0, 1.0, 0.0) glRotatef(self.yRot, 0.0, 1.0, 0.0)
glColor3f(0.9, 0.8, 0.8) """# Apply model transformation
glTranslatef(self.tx, self.ty, self.tz)
glScalef(self.scale, self.scale, self.scale)
glRotatef(self.model_xRot, 1.0, 0.0, 0.0)
glRotatef(self.model_yRot, 0.0, 1.0, 0.0)
glRotatef(self.model_zRot, 0.0, 0.0, 1.0)"""
glColor3f(0.9, 0.8, 0.8)
self.draw_area() self.draw_area()
if self.mesh_loaded is not None: if self.mesh_loaded is not None:
# Adjust the camera # Adjust the camera for the STL mesh
if self.centroid: if self.centroid:
glPushMatrix() # Save current transformation matrix
glScalef(self.scale_factor, self.scale_factor, self.scale_factor) # Apply scaling glScalef(self.scale_factor, self.scale_factor, self.scale_factor) # Apply scaling
cx, cy, cz = self.centroid cx, cy, cz = self.centroid
gluLookAt(cx, cy, cz + 100, cx, cy, cz, 0, 1, 0) gluLookAt(cx, cy, cz + 100, cx, cy, cz, 0, 1, 0)
self.draw_mesh_direct(self.mesh_loaded) self.draw_mesh_direct(self.mesh_loaded)
else: glPopMatrix() # Restore transformation matrix
glClearColor(0.0, 0.0, 0.0, 1.0) # Set the clear color (black with full opacity)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # Clear the color and depth buffers
if self.interactor_loaded is not None:
# Draw interactor mesh
glPushMatrix() # Save current transformation matrix
glScalef(self.scale_factor, self.scale_factor, self.scale_factor) # Apply scaling
self.draw_interactor(self.interactor_loaded)
glPopMatrix() # Restore transformation matrix
if self.selected_face is not None:
glColor3f(0.0, 1.0, 0.0) # Red color for selected face
glBegin(GL_TRIANGLES)
for vertex_idx in self.interactor_loaded[1][self.selected_face]:
glVertex3fv(self.interactor_loaded[0][vertex_idx])
glEnd()
# Flush the OpenGL pipeline and swap buffers
if hasattr(self, 'ray_start') and hasattr(self, 'ray_end'):
self.draw_ray(self.ray_start, self.ray_end)
glFlush()
def draw_stl(self, vertices): def draw_stl(self, vertices):
glEnable(GL_LIGHTING) glEnable(GL_LIGHTING)
@ -135,6 +396,40 @@ class OpenGLWidget(QOpenGLWidget):
glEnd() glEnd()
self.update() self.update()
def draw_interactor(self, simp_mesh: tuple):
vertices, faces = simp_mesh
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
glEnable(GL_DEPTH_TEST)
glEnable(GL_COLOR_MATERIAL)
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
glLightfv(GL_LIGHT0, GL_POSITION, (0, 0.6, 0.6, 0))
glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.4, 0.4, 0.4, 0.6))
# Draw the faces
glDisable(GL_LIGHTING)
glColor3f(0.2, 0.0, 0.0) # Set face color to red (or any color you prefer)
glBegin(GL_TRIANGLES)
for face in faces:
for vertex_index in face:
glVertex3fv(vertices[vertex_index])
glEnd()
# Draw the lines (edges of the triangles)
glColor3f(0.0, 1.0, 0.0) # Set line color to green (or any color you prefer)
glBegin(GL_LINES)
for face in faces:
for i in range(len(face)):
glVertex3fv(vertices[face[i]])
glVertex3fv(vertices[face[(i + 1) % len(face)]])
glEnd()
glEnable(GL_LIGHTING) # Re-enable lighting if further drawing requires it
def draw_mesh_direct(self, points): def draw_mesh_direct(self, points):
glEnable(GL_LIGHTING) glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0) glEnable(GL_LIGHT0)
@ -169,23 +464,21 @@ class OpenGLWidget(QOpenGLWidget):
glEnable(GL_LIGHTING) # Re-enable lighting if further drawing requires it glEnable(GL_LIGHTING) # Re-enable lighting if further drawing requires it
def draw_area(self): def draw_area(self):
glColor3f(0.5, 0.5, 0.5) # Gray color glColor3f(0.5, 0.5, 0.5) # Gray color
glBegin(GL_LINES) glBegin(GL_LINES)
for x in range(0, self.width(), 20): for x in range(0, self.width(), 1):
x_ndc = self.map_value_to_range(x, 0, value_max=self.width(), range_min=-self.gl_width, range_max=self.gl_width) x_ndc = self.map_value_to_range(x, 0, value_max=self.width(), range_min=-self.gl_width, range_max=self.gl_width)
glVertex2f(x_ndc, -self.gl_height) # Start from y = -1 glVertex2f(x_ndc, -self.gl_height) # Start from y = -1
glVertex2f(x_ndc, self.gl_height) # End at y = 1 glVertex2f(x_ndc, self.gl_height) # End at y = 1
for y in range(0, self.height(), 20): for y in range(0, self.height(), 1):
y_ndc = self.map_value_to_range(y, 0, value_max=self.height(), range_min=-self.gl_height, range_max=self.gl_height) y_ndc = self.map_value_to_range(y, 0, value_max=self.height(), range_min=-self.gl_height, range_max=self.gl_height)
glVertex2f(-self.gl_width, y_ndc) # Start from x = -1 glVertex2f(-self.gl_width, y_ndc) # Start from x = -1
glVertex2f(self.gl_width, y_ndc) # End at x = 1 glVertex2f(self.gl_width, y_ndc) # End at x = 1
glEnd() glEnd()
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
dx = event.x() - self.lastPos.x() dx = event.x() - self.lastPos.x()
dy = event.y() - self.lastPos.y() dy = event.y() - self.lastPos.y()
@ -204,3 +497,8 @@ class OpenGLWidget(QOpenGLWidget):
def aspect_ratio(self): def aspect_ratio(self):
return self.width() / self.height() * (1.0 / abs(self.zoom)) return self.width() / self.height() * (1.0 / abs(self.zoom))
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

View File

@ -0,0 +1,447 @@
import sys
import numpy as np
import vtk
from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import Signal
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
class VTKWidget(QtWidgets.QWidget):
face_data = Signal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.vtk_widget = QVTKRenderWindowInteractor(self)
# Create layout and add VTK widget
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.vtk_widget)
self.setLayout(layout)
# Create VTK pipeline
self.renderer = vtk.vtkRenderer()
self.vtk_widget.GetRenderWindow().AddRenderer(self.renderer)
self.interactor = self.vtk_widget.GetRenderWindow().GetInteractor()
# Set up the camera
self.camera = self.renderer.GetActiveCamera()
self.camera.SetPosition(5, 5, 5)
self.camera.SetFocalPoint(0, 0, 0)
# Set up picking
self.picker = vtk.vtkCellPicker()
self.picker.SetTolerance(0.0005)
# Create a mapper and actor for picked cells
self.picked_mapper = vtk.vtkDataSetMapper()
self.picked_actor = vtk.vtkActor()
self.picked_actor.SetMapper(self.picked_mapper)
self.picked_actor.GetProperty().SetColor(1.0, 0.0, 0.0) # Red color for picked faces
self.renderer.AddActor(self.picked_actor)
# Set up interactor style
self.style = vtk.vtkInteractorStyleTrackballCamera()
self.interactor.SetInteractorStyle(self.style)
# Add observer for mouse clicks
self.interactor.AddObserver("RightButtonPressEvent", self.on_click)
def create_cube_mesh(self):
cube_source = vtk.vtkCubeSource()
print(cube_source)
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(cube_source.GetOutputPort())
actor = vtk.vtkActor()
actor.SetMapper(mapper)
self.renderer.AddActor(actor)
def simplify_mesh(self, input_mesh, target_reduction):
# Create the quadric decimation filter
decimate = vtk.vtkDecimatePro()
decimate.SetInputData(input_mesh)
# Set the reduction factor (0 to 1, where 1 means maximum reduction)
decimate.SetTargetReduction(target_reduction)
# Optional: Preserve topology (if needed)
decimate.PreserveTopologyOn()
# Perform the decimation
decimate.Update()
return decimate.GetOutput()
def combine_coplanar_faces(self, input_polydata, tolerance=0.001):
# Clean the polydata to merge duplicate points
clean = vtk.vtkCleanPolyData()
clean.SetInputData(input_polydata)
clean.SetTolerance(tolerance)
clean.Update()
# Generate normals and merge coplanar polygons
normals = vtk.vtkPolyDataNormals()
normals.SetInputConnection(clean.GetOutputPort())
normals.SplittingOff() # Disable splitting of sharp edges
normals.ConsistencyOn() # Ensure consistent polygon ordering
normals.AutoOrientNormalsOn() # Automatically orient normals
normals.ComputePointNormalsOff() # We only need face normals
normals.ComputeCellNormalsOn() # Compute cell normals
normals.Update()
return normals.GetOutput()
def poisson_reconstruction(self, points):
# Create a polydata object from points
point_polydata = vtk.vtkPolyData()
point_polydata.SetPoints(points)
# Create a surface reconstruction filter
surf = vtk.vtkSurfaceReconstructionFilter()
surf.SetInputData(point_polydata)
surf.Update()
# Create a contour filter to extract the surface
cf = vtk.vtkContourFilter()
cf.SetInputConnection(surf.GetOutputPort())
cf.SetValue(0, 0.0)
cf.Update()
# Reverse normals
reverse = vtk.vtkReverseSense()
reverse.SetInputConnection(cf.GetOutputPort())
reverse.ReverseCellsOn()
reverse.ReverseNormalsOn()
reverse.Update()
return reverse.GetOutput()
def load_interactor_mesh(self, simp_mesh):
vertices, faces = simp_mesh
self.load_custom_mesh(vertices, faces)
def load_custom_mesh(self, vertices, faces):
### Load meshes by own module
# Create a vtkPoints object and store the points in it
points = vtk.vtkPoints()
for vertex in vertices:
points.InsertNextPoint(vertex)
# Create a vtkCellArray to store the faces
cells = vtk.vtkCellArray()
for face in faces:
triangle = vtk.vtkTriangle()
triangle.GetPointIds().SetId(0, face[0])
triangle.GetPointIds().SetId(1, face[1])
triangle.GetPointIds().SetId(2, face[2])
cells.InsertNextCell(triangle)
# Create a polydata object
polydata = vtk.vtkPolyData()
polydata.SetPoints(points)
polydata.SetPolys(cells)
# Create mapper and actor
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(polydata) # Make sure this line is present
actor = vtk.vtkActor()
actor.SetMapper(mapper)
# Add to renderer
self.renderer.AddActor(actor)
# Force an update of the pipeline
mapper.Update()
self.vtk_widget.GetRenderWindow().Render()
def create_simplified_outline(self, polydata):
# 1. Extract the outer surface
surface_filter = vtk.vtkDataSetSurfaceFilter()
surface_filter.SetInputData(polydata)
surface_filter.Update()
# 2. Extract feature edges (only boundary edges)
feature_edges = vtk.vtkFeatureEdges()
feature_edges.SetInputConnection(surface_filter.GetOutputPort())
feature_edges.BoundaryEdgesOn()
feature_edges.FeatureEdgesOff()
feature_edges.NonManifoldEdgesOff()
feature_edges.ManifoldEdgesOff()
feature_edges.Update()
# 3. Clean the edges to merge duplicate points
cleaner = vtk.vtkCleanPolyData()
cleaner.SetInputConnection(feature_edges.GetOutputPort())
cleaner.Update()
# 4. Optional: Smooth the outline
smooth = vtk.vtkSmoothPolyDataFilter()
smooth.SetInputConnection(cleaner.GetOutputPort())
smooth.SetNumberOfIterations(15)
smooth.SetRelaxationFactor(0.1)
smooth.FeatureEdgeSmoothingOff()
smooth.BoundarySmoothingOn()
smooth.Update()
return smooth
def render_from_points_direct_with_faces(self, points):
# Create a vtkPoints object and store the points in it
vtk_points = vtk.vtkPoints()
for point in points:
vtk_points.InsertNextPoint(point)
# Create a vtkCellArray to store the triangles
triangles = vtk.vtkCellArray()
# Assuming points are organized as triplets forming triangles
for i in range(0, len(points), 3):
triangle = vtk.vtkTriangle()
triangle.GetPointIds().SetId(0, i)
triangle.GetPointIds().SetId(1, i + 1)
triangle.GetPointIds().SetId(2, i + 2)
triangles.InsertNextCell(triangle)
# Create a polydata object
polydata = vtk.vtkPolyData()
polydata.SetPoints(vtk_points)
polydata.SetPolys(triangles)
# Optional: Merge duplicate points
cleaner = vtk.vtkCleanPolyData()
cleaner.SetInputData(polydata)
cleaner.Update()
# Optional: Combine coplanar faces
normals = vtk.vtkPolyDataNormals()
normals.SetInputConnection(cleaner.GetOutputPort())
normals.SplittingOff()
normals.ConsistencyOn()
normals.AutoOrientNormalsOn()
normals.ComputePointNormalsOff()
normals.ComputeCellNormalsOn()
normals.Update()
# Create a mapper and actor
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(normals.GetOutputPort())
actor = vtk.vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetColor(1, 1, 1) # Set color (white in this case)
actor.GetProperty().EdgeVisibilityOn() # Show edges
actor.GetProperty().SetLineWidth(2) # Set line width
feature_edges = self.create_simplified_outline(polydata)
# Create a mapper for the feature edges
edge_mapper = vtk.vtkPolyDataMapper()
# Already wiht output
edge_mapper.SetInputConnection(feature_edges.GetOutputPort())
# Create an actor for the feature edges
edge_actor = vtk.vtkActor()
edge_actor.SetMapper(edge_mapper)
# Set the properties of the edge actor
edge_actor.GetProperty().SetColor(1, 0, 0) # Set color (red in this case)
edge_actor.GetProperty().SetLineWidth(2) # Set line width
# Optionally, if you want to keep the original mesh visible:
# (assuming you have the original mesh mapper and actor set up)
self.renderer.AddActor(actor) # Add the original mesh actor
# Add the edge actor to the renderer
self.renderer.AddActor(edge_actor)
# Force an update of the pipeline
mapper.Update()
self.vtk_widget.GetRenderWindow().Render()
# Print statistics
print(f"Original points: {len(points)}")
print(f"Number of triangles: {triangles.GetNumberOfCells()}")
print(f"Final number of points: {normals.GetOutput().GetNumberOfPoints()}")
print(f"Final number of cells: {normals.GetOutput().GetNumberOfCells()}")
def render_from_points_direct(self, points):
### Rendermethod for SDF mesh (output)
# Create a vtkPoints object and store the points in it
vtk_points = vtk.vtkPoints()
for point in points:
vtk_points.InsertNextPoint(point)
# Create a polydata object
point_polydata = vtk.vtkPolyData()
point_polydata.SetPoints(vtk_points)
# Surface reconstruction
surf = vtk.vtkSurfaceReconstructionFilter()
surf.SetInputData(point_polydata)
surf.Update()
# Create a contour filter to extract the surface
cf = vtk.vtkContourFilter()
cf.SetInputConnection(surf.GetOutputPort())
cf.SetValue(0, 0.0)
cf.Update()
# Reverse the normals
reverse = vtk.vtkReverseSense()
reverse.SetInputConnection(cf.GetOutputPort())
reverse.ReverseCellsOn()
reverse.ReverseNormalsOn()
reverse.Update()
# Get the reconstructed mesh
reconstructed_mesh = reverse.GetOutput()
"""# Simplify the mesh
target_reduction = 1 # Adjust this value as needed
simplified_mesh = self.simplify_mesh(reconstructed_mesh, target_reduction)
combinded_faces = self.combine_coplanar_faces(simplified_mesh, 0.001)"""
# Create a mapper and actor for the simplified mesh
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(reconstructed_mesh)
actor = vtk.vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetColor(1, 1, 1) # Set color (white in this case)
actor.GetProperty().EdgeVisibilityOn() # Show edges
actor.GetProperty().SetLineWidth(2) # Set line width
# Add the actor to the renderer
self.renderer.AddActor(actor)
# Force an update of the pipeline
#mapper.Update()
self.vtk_widget.GetRenderWindow().Render()
# Print statistics
print(f"Original points: {len(points)}")
print(
f"Reconstructed mesh: {reconstructed_mesh.GetNumberOfPoints()} points, {reconstructed_mesh.GetNumberOfCells()} cells")
"""print(
f"Simplified mesh: {simplified_mesh.GetNumberOfPoints()} points, {simplified_mesh.GetNumberOfCells()} cells")"""
def on_click(self, obj, event):
click_pos = self.interactor.GetEventPosition()
# Perform pick
self.picker.Pick(click_pos[0], click_pos[1], 0, self.renderer)
# Get picked cell
cell_id = self.picker.GetCellId()
if cell_id != -1:
print(f"Picked face ID: {cell_id}")
# Get the polydata and the picked cell
polydata = self.picker.GetActor().GetMapper().GetInput()
cell = polydata.GetCell(cell_id)
# Project2D
renderer = self.vtk_widget.GetRenderWindow().GetRenderers().GetFirstRenderer()
camera = renderer.GetActiveCamera()
# Get cell type
cell_type = cell.GetCellType()
print(f"Cell type: {cell_type}")
# Get points of the cell
points = cell.GetPoints()
num_points = points.GetNumberOfPoints()
print(f"Number of points in the cell: {num_points}")
vec_points = []
# Get coordinates of each point
for i in range(num_points):
point = points.GetPoint(i)
print(f"Point {i}: {point}")
vec_points.append(point)
# Get normal of the cell (if it's a polygon)
if cell_type == vtk.VTK_TRIANGLE:
normal = [0, 0, 0]
vtk.vtkPolygon.ComputeNormal(points, normal)
print(f"Face normal: {normal}")
# Get cell data
cell_data = polydata.GetCellData()
if cell_data:
num_arrays = cell_data.GetNumberOfArrays()
print(f"Number of cell data arrays: {num_arrays}")
for i in range(num_arrays):
array = cell_data.GetArray(i)
array_name = array.GetName()
num_components = array.GetNumberOfComponents()
value = [0] * num_components
array.GetTuple(cell_id, value)
print(f"Cell data '{array_name}': {value}")
# Get point data (average of all points in the cell)
point_data = polydata.GetPointData()
if point_data:
num_arrays = point_data.GetNumberOfArrays()
print(f"Number of point data arrays: {num_arrays}")
for i in range(num_arrays):
array = point_data.GetArray(i)
array_name = array.GetName()
num_components = array.GetNumberOfComponents()
avg_value = np.zeros(num_components)
for j in range(num_points):
point_id = cell.GetPointId(j)
value = [0] * num_components
array.GetTuple(point_id, value)
avg_value += np.array(value)
avg_value /= num_points
print(f"Average point data '{array_name}': {avg_value}")
if num_points and cell_data:
face_orient = {'cell_data': cell_data, 'points': vec_points }
print(face_orient)
self.face_data.emit(face_orient)
# Highlight picked face (your existing code)
ids = vtk.vtkIdTypeArray()
ids.SetNumberOfComponents(1)
ids.InsertNextValue(cell_id)
selection_node = vtk.vtkSelectionNode()
selection_node.SetFieldType(vtk.vtkSelectionNode.CELL)
selection_node.SetContentType(vtk.vtkSelectionNode.INDICES)
selection_node.SetSelectionList(ids)
selection = vtk.vtkSelection()
selection.AddNode(selection_node)
extract_selection = vtk.vtkExtractSelection()
extract_selection.SetInputData(0, polydata)
extract_selection.SetInputData(1, selection)
extract_selection.Update()
self.picked_mapper.SetInputData(extract_selection.GetOutput())
self.vtk_widget.GetRenderWindow().Render()
def start(self):
self.interactor.Initialize()
self.interactor.Start()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.vtk_widget = VTKWidget()
self.setCentralWidget(self.vtk_widget)
self.setWindowTitle("VTK Mesh Viewer")
self.vtk_widget.create_cube_mesh()
self.show()
self.vtk_widget.start()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

View File

@ -0,0 +1,111 @@
import sys
import numpy as np
import pyvista as pv
from pyvista.plotting.opts import ElementType
from pyvistaqt import QtInteractor
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
class PyVistaWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# Create the PyVista plotter
self.plotter = QtInteractor(self)
self.plotter.background_color = "darkgray"
# Create a layout and add the PyVista widget
layout = QVBoxLayout()
layout.addWidget(self.plotter.interactor)
self.setLayout(layout)
# Set up the picker
#self.plotter.enable_cell_picking(callback=self.on_cell_pick, show=True)
self.plotter.enable_element_picking(callback=self.on_cell_pick, show=True, mode="face", left_clicking=True)
def on_cell_pick(self, element):
if element is not None:
mesh = self.plotter.mesh # Get the current mesh
print(mesh)
print(element)
"""# Get the face data
face = mesh.extract_cells(element)
# Compute face normal
face.compute_normals(cell_normals=True, inplace=True)
normal = face.cell_data['Normals'][0]
# Get the points of the face
points = face.points
print(f"Picked face ID: {face_id}")
print(f"Face normal: {normal}")
print("Face points:")
for point in points:
print(point)"""
else:
print("No face was picked or the picked element is not a face.")
def create_simplified_outline(self, mesh, camera):
# Project 3D to 2D
points_2d = self.plotter.map_to_2d(mesh.points)
# Detect silhouette edges (simplified approach)
edges = mesh.extract_feature_edges(feature_angle=90, boundary_edges=False, non_manifold_edges=False)
# Project edges to 2D
edge_points_2d = self.plotter.map_to_2d(edges.points)
# Create 2D outline
self.plotter.add_lines(edge_points_2d, color='black', width=2)
self.plotter.render()
def mesh_from_points(self, points):
# Convert points to numpy array if not already
points = np.array(points)
# Create faces array
num_triangles = len(points) // 3
faces = np.arange(len(points)).reshape(num_triangles, 3)
faces = np.column_stack((np.full(num_triangles, 3), faces)) # Add 3 as first column
# Create PyVista PolyData
mesh = pv.PolyData(points, faces)
# Optional: Merge duplicate points
mesh = mesh.clean()
# Optional: Compute normals
mesh = mesh.compute_normals(point_normals=False, cell_normals=True, consistent_normals=True)
edges = mesh.extract_feature_edges(30, non_manifold_edges=False)
# Clear any existing meshes
self.plotter.clear()
# Add the mesh to the plotter
self.plotter.add_mesh(mesh, pickable=True, color='white', show_edges=True, line_width=2, pbr=True, metallic=0.8, roughness=0.1, diffuse=1)
self.plotter.add_mesh(edges, color="red", line_width=10)
# Reset the camera to fit the new mesh
self.plotter.reset_camera()
# Update the render window
self.plotter.update()
# Print statistics
print(f"Original points: {len(points)}")
print(f"Number of triangles: {num_triangles}")
print(f"Final number of points: {mesh.n_points}")
print(f"Final number of cells: {mesh.n_cells}")
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PyVista in PySide6")
self.setGeometry(100, 100, 800, 600)

88
main.py
View File

@ -1,13 +1,14 @@
import uuid import uuid
import names import names
from PySide6.QtCore import Qt, QPoint from PySide6.QtCore import Qt, QPoint
from PySide6.QtWidgets import QApplication, QMainWindow, QSizePolicy, QInputDialog from PySide6.QtWidgets import QApplication, QMainWindow, QSizePolicy, QInputDialog
from Gui import Ui_fluencyCAD # Import the generated GUI module from Gui import Ui_fluencyCAD # Import the generated GUI module
from drawing_modules.gl_widget import OpenGLWidget from drawing_modules.vtk_widget import VTKWidget
from drawing_modules.vysta_widget import PyVistaWidget
from drawing_modules.draw_widget2d import SketchWidget from drawing_modules.draw_widget2d import SketchWidget
from sdf import * from sdf import *
from python_solvespace import SolverSystem, ResultFlag from python_solvespace import SolverSystem, ResultFlag
from mesh_modules import simple_mesh
# main, draw_widget, gl_widget # main, draw_widget, gl_widget
@ -19,11 +20,11 @@ class MainWindow(QMainWindow):
self.ui = Ui_fluencyCAD() self.ui = Ui_fluencyCAD()
self.ui.setupUi(self) self.ui.setupUi(self)
self.openGLWidget = OpenGLWidget() self.custom_3D_Widget = VTKWidget()
layout = self.ui.gl_box.layout() layout = self.ui.gl_box.layout()
layout.addWidget(self.openGLWidget) layout.addWidget(self.custom_3D_Widget)
size_policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) size_policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
#self.openGLWidget.setSizePolicy(size_policy) #self.custom_3D_Widget.setSizePolicy(size_policy)
self.sketchWidget = SketchWidget() self.sketchWidget = SketchWidget()
layout2 = self.ui.sketch_tab.layout() # Get the layout of self.ui.gl_canvas layout2 = self.ui.sketch_tab.layout() # Get the layout of self.ui.gl_canvas
@ -40,10 +41,10 @@ class MainWindow(QMainWindow):
#self.ui.pb_apply_code.pressed.connect(self.check_current_tab) #self.ui.pb_apply_code.pressed.connect(self.check_current_tab)
self.ui.sketch_list.currentItemChanged.connect(self.on_item_changed) self.ui.sketch_list.currentItemChanged.connect(self.on_item_changed)
self.ui.sketch_list.itemChanged.connect(self.view_update) self.ui.sketch_list.itemChanged.connect(self.draw_mesh)
### Sketches ### Sketches
self.ui.pb_origin_wp.pressed.connect(self.add_wp_origin) self.ui.pb_origin_wp.pressed.connect(self.add_new_sketch)
self.ui.pb_nw_sktch.pressed.connect(self.add_sketch) self.ui.pb_nw_sktch.pressed.connect(self.add_sketch)
self.ui.pb_del_sketch.pressed.connect(self.del_sketch) self.ui.pb_del_sketch.pressed.connect(self.del_sketch)
@ -65,7 +66,8 @@ class MainWindow(QMainWindow):
self.sketchWidget.constrain_done.connect(self.draw_op_complete) self.sketchWidget.constrain_done.connect(self.draw_op_complete)
def add_wp_origin(self): def add_new_sketch(self):
self.sketchWidget.clear_sketch()
self.sketchWidget.create_workplane() self.sketchWidget.create_workplane()
def act_line_mode(self): def act_line_mode(self):
@ -124,13 +126,24 @@ class MainWindow(QMainWindow):
self.sketchWidget.mouse_mode = None self.sketchWidget.mouse_mode = None
self.sketchWidget.reset_buffers() self.sketchWidget.reset_buffers()
def view_update(self): def calc_sketch_projection_3d(self, depth):
print("Update") name = self.ui.sketch_list.currentItem().text()
#print("selected_for disp", name)
model = self.model['sketch'][name]['sketch_points']
#print("sketch points from model", model)
simp_mesh = simple_mesh.generate_mesh(model, depth)
print("Generated model", simp_mesh)
self.custom_3D_Widget. load_interactor_mesh(simp_mesh) #draw_interactor(simp_mesh)
def draw_mesh(self):
#print("Update")
name = self.ui.body_list.currentItem().text() name = self.ui.body_list.currentItem().text()
print("selected_for disp", name) #print("selected_for disp", name)
model = self.model['operation'][name]['sdf_object'] model = self.model['operation'][name]['sdf_object']
mesh = model.generate(samples=2**12) mesh = model.generate()
self.openGLWidget.load_mesh_direct(mesh) #print("Mesh sdf", mesh)
#self.custom_3D_Widget.render_from_points_direct_with_faces(mesh)
self.custom_3D_Widget.render_from_points_direct_with_faces(mesh)
def on_item_changed(self, current_item, previous_item): def on_item_changed(self, current_item, previous_item):
if current_item: if current_item:
@ -138,16 +151,25 @@ class MainWindow(QMainWindow):
#self.view_update() #self.view_update()
print(f"Selected item: {name}") print(f"Selected item: {name}")
def add_sketch(self): def convert_points_for_sdf(self):
points_for_poly = [] points_for_sdf = []
name = f"sketch-{str(names.get_first_name())}"
for point_to_poly in self.sketchWidget.slv_points_main: for point_to_poly in self.sketchWidget.slv_points_main:
points_for_poly.append(self.translate_points_tup(point_to_poly['ui_point'])) points_for_sdf.append(self.translate_points_tup(point_to_poly['ui_point']))
return points_for_sdf
def add_sketch(self):
name = f"sketch-{str(names.get_first_name())}"
points_for_sdf = self.convert_points_for_sdf()
element = { element = {
'id': name, 'id': name,
'type': 'polygon', 'type': 'sketch',
'sketch_points': points_for_poly, 'point_list': self.sketchWidget.slv_points_main,
'line_list': self.sketchWidget.slv_lines_main,
'sketch_points': points_for_sdf,
'solver': self.sketchWidget.solv
} }
self.model['sketch'][element['id']] = element self.model['sketch'][element['id']] = element
@ -162,10 +184,15 @@ class MainWindow(QMainWindow):
def edit_sketch(self): def edit_sketch(self):
name = self.ui.sketch_list.currentItem().text() name = self.ui.sketch_list.currentItem().text()
self.sketchWidget.clear_sketch() #self.sketchWidget.clear_sketch()
points = self.model['sketch'][name]['sketch_points']
print("points", points) self.sketchWidget.slv_points_main = self.model['sketch'][name]['point_list']
self.sketchWidget.set_points(points) self.sketchWidget.slv_lines_main = self.model['sketch'][name]['line_list']
self.sketchWidget.solv = self.model['sketch'][name]['solver']
self.sketchWidget.update()
print("model",self.model)
print("widget", self.sketchWidget.slv_points_main)
def del_sketch(self): def del_sketch(self):
print("Deleting") print("Deleting")
@ -217,7 +244,7 @@ class MainWindow(QMainWindow):
self.ui.body_list.takeItem(row) # Remove the item from the list widget self.ui.body_list.takeItem(row) # Remove the item from the list widget
self.model['operation'].pop(item_name) # Remove the item from the operation dictionary self.model['operation'].pop(item_name) # Remove the item from the operation dictionary
print(f"Removed operation: {item_name}") print(f"Removed operation: {item_name}")
self.openGLWidget.clear_mesh() self.custom_3D_Widget.clear_mesh()
def translate_points_tup(self, point: QPoint): def translate_points_tup(self, point: QPoint):
"""QPoints from Display to mesh data """QPoints from Display to mesh data
@ -240,8 +267,11 @@ class MainWindow(QMainWindow):
length, ok = QInputDialog.getDouble(self, 'Extrude Length', 'Enter a mm value:', decimals=2) length, ok = QInputDialog.getDouble(self, 'Extrude Length', 'Enter a mm value:', decimals=2)
#TODO : Implement cancel #TODO : Implement cancel
#Create and draw Interactor
geo = Geometry() geo = Geometry()
f = geo.extrude_shape(points, length) f = geo.extrude_shape(points, length)
name_op = f"extrd-{name}" name_op = f"extrd-{name}"
element = { element = {
'id': name_op, 'id': name_op,
@ -251,11 +281,11 @@ class MainWindow(QMainWindow):
print(element) print(element)
self.model['operation'][name_op] = element self.model['operation'][name_op] = element
self.ui.body_list.addItem(name_op) self.ui.body_list.addItem(name_op)
items = self.ui.body_list.findItems(name_op, Qt.MatchExactly)[0] items = self.ui.body_list.findItems(name_op, Qt.MatchExactly)[0]
self.ui.body_list.setCurrentItem(items) self.ui.body_list.setCurrentItem(items)
self.view_update() self.draw_mesh()
#self.calc_sketch_projection_3d(length)
def send_cut(self): def send_cut(self):
name = self.ui.body_list.currentItem().text() name = self.ui.body_list.currentItem().text()
@ -278,13 +308,13 @@ class MainWindow(QMainWindow):
self.ui.body_list.addItem(name_op) self.ui.body_list.addItem(name_op)
items = self.ui.sketch_list.findItems(name_op, Qt.MatchExactly) items = self.ui.sketch_list.findItems(name_op, Qt.MatchExactly)
self.ui.body_list.setCurrentItem(items[-1]) self.ui.body_list.setCurrentItem(items[-1])
self.view_update() self.draw_mesh()
else: else:
print("mindestens 2!") print("mindestens 2!")
def load_and_render(self, file): def load_and_render(self, file):
self.openGLWidget.load_stl(file) self.custom_3D_Widget.load_stl(file)
self.openGLWidget.update() self.custom_3D_Widget.update()
class Geometry: class Geometry:
def distance(self, p1, p2): def distance(self, p1, p2):

View File

@ -0,0 +1,50 @@
import numpy as np
from scipy.spatial import ConvexHull
from stl import mesh
def generate_mesh(points, depth):
"""
Generate a mesh by extruding a 2D shape along the Z-axis.
:param points: List of (x, y) tuples representing the 2D shape.
:param depth: Extrusion depth along the Z-axis.
:return: Tuple of vertices and faces.
"""
# Convert points to a numpy array
points_2d = np.array(points)
# Get the convex hull of the points to ensure they form a proper polygon
hull = ConvexHull(points_2d)
hull_points = points_2d[hull.vertices]
# Generate the top and bottom faces
bottom_face = np.hstack((hull_points, np.zeros((hull_points.shape[0], 1))))
top_face = np.hstack((hull_points, np.ones((hull_points.shape[0], 1)) * depth))
# Combine top and bottom vertices
vertices_array = np.vstack((bottom_face, top_face))
# Create faces
faces = []
# Bottom face triangulation (counter-clockwise)
for i in range(len(hull_points) - 2):
faces.append([0, i + 2, i + 1])
# Top face triangulation (counter-clockwise, with an offset)
top_offset = len(hull_points)
for i in range(len(hull_points) - 2):
faces.append([top_offset, top_offset + i + 1, top_offset + i + 2])
# Side faces (ensure counter-clockwise order)
for i in range(len(hull_points)):
next_i = (i + 1) % len(hull_points)
faces.append([i, top_offset + i, top_offset + next_i])
faces.append([i, top_offset + next_i, next_i])
# Convert vertices to the desired format: list of tuples
vertices = [tuple(vertex) for vertex in vertices_array]
return vertices, faces