- Basic 2D projection

This commit is contained in:
bklronin 2024-07-08 22:14:25 +02:00
parent 9daf263aad
commit 5ff48c0f5e
16 changed files with 967 additions and 826 deletions

View File

@ -1,81 +0,0 @@
import sys
from OpenGL.raw.GL.VERSION.GL_1_0 import glClearColor
from PySide6.QtCore import Qt
from PySide6.QtOpenGLWidgets import QOpenGLWidget
from PySide6.QtWidgets import QDoubleSpinBox, QPushButton, QVBoxLayout, QApplication, QWidget
from OpenGL.GL import *
from OpenGL.GLUT import *
class GLWidget(QOpenGLWidget):
def __init__(self, parent=None):
super(GLWidget, self).__init__(parent)
self.shapes = []
self.extruded_shapes = []
def initializeGL(self):
glClearColor(1, 1, 1, 1)
def paintGL(self):
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
glColor3f(1, 0, 0)
for shape in self.shapes:
glBegin(GL_LINE_LOOP)
glVertex2f(shape[0], shape[1]) # Access x and y coordinates of the point
glEnd()
def resizeGL(self, w, h):
glViewport(0, 0, w, h)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0, w, h, 0, -1, 1)
glMatrixMode(GL_MODELVIEW)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
pos = event.pos()
x = int(pos.x())
y = int(pos.y())
self.shapes.append((x, y)) # Append coordinate tuple (x, y)
self.update()
def extrude_shapes(self, height):
self.extruded_shapes = []
for shape in self.shapes:
extruded_shape = []
for point in shape:
extruded_shape.append([point[0], point[1]])
for point in shape:
extruded_shape.append([point[0], point[1] + height])
self.extruded_shapes.append(extruded_shape)
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.gl_widget = GLWidget()
self.height_spin_box = QDoubleSpinBox()
self.height_spin_box.setRange(0, 100)
self.height_spin_box.setValue(10)
self.extrude_button = QPushButton("Extrude")
self.extrude_button.clicked.connect(self.extrude_shapes)
layout = QVBoxLayout()
layout.addWidget(self.gl_widget)
layout.addWidget(self.height_spin_box)
layout.addWidget(self.extrude_button)
self.setLayout(layout)
def extrude_shapes(self):
height = self.height_spin_box.value()
self.gl_widget.extrude_shapes(height)
self.gl_widget.update()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.setGeometry(100, 100, 800, 600)
window.setWindowTitle("Extrude Shapes")
window.show()
sys.exit(app.exec_())

View File

@ -1,62 +0,0 @@
import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton
from PySide6.QtGui import QPainter, QPen
from PySide6.QtCore import Qt
class CADWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("CAD")
self.resize(800, 600)
# Create a label for the dimensions input
self.dimension_label = QLabel("Dimensions:", self)
self.dimension_label.move(10, 10)
# Create a line edit for the user to enter the dimensions
self.dimension_input = QLineEdit()
self.dimension_input.setPlaceholderText("Enter dimensions...")
self.dimension_input.setFixedWidth(300)
self.dimension_input.move(10, 40)
# Create a button for the user to confirm the dimensions
self.confirm_button = QPushButton("Confirm", self)
self.confirm_button.clicked.connect(self.confirm_dimensions)
self.confirm_button.move(10, 70)
# Create a label for the canvas
self.canvas_label = QLabel("Canvas:", self)
self.canvas_label.move(400, 10)
# Create a canvas widget for the user to draw on
self.canvas = QWidget()
self.canvas.setFixedSize(300, 300)
self.canvas.move(400, 40)
# Set the default dimensions of the canvas
self.dimensions = (300, 300)
def confirm_dimensions(self):
# Get the dimensions from the line edit and convert to a tuple
dimensions = self.dimension_input.text().split(",")
dimensions = [int(d) for d in dimensions]
self.dimensions = dimensions
# Resize the canvas widget to match the new dimensions
self.canvas.setFixedSize(*self.dimensions)
def paintEvent(self, event):
# Get a painter object for painting on the canvas
painter = QPainter(self.canvas)
# Set the pen color and width
painter.pen = QPen(Qt.black, 2)
# Draw a line across the canvas using the dimensions from the user input
painter.drawLine(0, 0, *self.dimensions)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = CADWindow()
window.show()
sys.exit(app.exec_())

View File

@ -1,38 +0,0 @@
from PySide6 import QtGui
from PySide6.Qt3DCore import Qt3DCore
from PySide6.Qt3DExtras import Qt3DExtras
from PySide6.QtWidgets import QApplication, QMainWindow
import sys
class My3DWindow(QMainWindow):
def __init__(self):
super(My3DWindow, self).__init__()
# Create the 3D scene
scene = Qt3DCore.QEntity()
# Create a 3D view container
view = Qt3DExtras.Qt3DWindow()
view.defaultFrameGraph().setClearColor(QtGui.QColor(0, 0, 0, 0))
# Set root entity for the scene
view.setRootEntity(scene)
# Create a 3D transform
transform = Qt3DCore.QTransform()
transform.setScale3D(QtGui.QVector3D(1.0, 1.0, 1.0))
# Set components for the mesh entity
mesh_entity.addComponent(material)
mesh_entity.addComponent(transform)
# Add the mesh entity to the scene
scene.addComponent(mesh_entity)
self.setCentralWidget(view)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = My3DWindow()
window.show()
sys.exit(app.exec_())

Binary file not shown.

View File

@ -2,6 +2,7 @@ import math
import re
from copy import copy
import numpy as np
from PySide6.QtWidgets import QApplication, QWidget, QMessageBox, QInputDialog
from PySide6.QtGui import QPainter, QPen, QColor
from PySide6.QtCore import Qt, QPoint, QPointF, Signal
@ -47,6 +48,81 @@ class SketchWidget(QWidget):
def create_workplane(self):
self.wp = self.solv.create_2d_base()
def create_workplane_projected(self):
self.wp = self.solv.create_2d_base()
def create_proj_lines(self, lines):
"""Lines as orientation projected from the sketch"""
for line in lines:
for point in line:
x, y, z = point
point = self.solv.add_point_2d(x, y, self.wp)
relation_point = {} # Reinitialize the dictionary
handle_nr = self.get_handle_nr(str(point))
relation_point['handle_nr'] = handle_nr
relation_point['solv_handle'] = point
relation_point['ui_point'] = QPoint(x, y)
self.slv_points_main.append(relation_point)
print("points", self.slv_points_main)
print("lines", self.slv_lines_main)
print("lines", lines)
def find_duplicate_points_2d(self, edges):
points = []
seen = set()
duplicates = []
for edge in edges:
for point in edge:
# Extract only x and y coordinates
point_2d = (point[0], point[1])
if point_2d in seen:
if point_2d not in duplicates:
duplicates.append(point_2d)
else:
seen.add(point_2d)
points.append(point_2d)
return duplicates
def normal_to_quaternion(self, normal):
normal = np.array(normal)
#normal = normal / np.linalg.norm(normal)
axis = np.cross([0, 0, 1], normal)
if np.allclose(axis, 0):
axis = np.array([1, 0, 0])
else:
axis = axis / np.linalg.norm(axis) # Normalize the axis
angle = np.arccos(np.dot([0, 0, 1], normal))
qw = np.cos(angle / 2)
sin_half_angle = np.sin(angle / 2)
qx, qy, qz = axis * sin_half_angle # This will now work correctly
return qw, qx, qy, qz
def create_workplane_space(self, points, normal):
print("edges", points)
origin = self.find_duplicate_points_2d(points)
print(origin)
x, y = origin[0]
origin = QPoint(x, y)
origin_handle = self.get_handle_from_ui_point(origin)
qw, qx, qy, qz = self.normal_to_quaternion(normal)
slv_normal = self.solv.add_normal_3d(qw, qx, qy, qz)
self.wp = self.solv.add_work_plane(origin_handle, slv_normal)
print(self.wp)
def get_handle_nr(self, input_str: str) -> int:
# Define the regex pattern to extract the handle number
pattern = r"handle=(\d+)"

View File

@ -5,6 +5,7 @@ import vtk
from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import Signal
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from vtkmodules.util.numpy_support import vtk_to_numpy, numpy_to_vtk
class VTKWidget(QtWidgets.QWidget):
@ -12,6 +13,10 @@ class VTKWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.access_selected_points = []
self.selected_normal = None
self.selected_edges = []
self.cell_normals = None
self.vtk_widget = QVTKRenderWindowInteractor(self)
# Create layout and add VTK widget
@ -26,12 +31,12 @@ class VTKWidget(QtWidgets.QWidget):
# Set up the camera
self.camera = self.renderer.GetActiveCamera()
self.camera.SetPosition(5, 5, 5)
self.camera.SetPosition(5, 5, 100)
self.camera.SetFocalPoint(0, 0, 0)
# Set up picking
self.picker = vtk.vtkCellPicker()
self.picker.SetTolerance(0.0005)
self.picker.SetTolerance(0.005)
# Create a mapper and actor for picked cells
self.picked_mapper = vtk.vtkDataSetMapper()
@ -47,78 +52,109 @@ class VTKWidget(QtWidgets.QWidget):
# Add observer for mouse clicks
self.interactor.AddObserver("RightButtonPressEvent", self.on_click)
def create_cube_mesh(self):
cube_source = vtk.vtkCubeSource()
print(cube_source)
@staticmethod
def compute_normal_from_lines(line1, line2):
vec1 = line1[1] - line1[0]
vec2 = line2[1] - line2[0]
normal = np.cross(vec1, vec2)
print(normal)
normal = normal / np.linalg.norm(normal)
return normal
def load_interactor_mesh(self, edges):
# Create vtkPoints to store all points
points = vtk.vtkPoints()
# Create vtkCellArray to store the lines
lines = vtk.vtkCellArray()
for edge in edges:
# Add points for this edge
point_id1 = points.InsertNextPoint(edge[0])
point_id2 = points.InsertNextPoint(edge[1])
# Create a line using the point IDs
line = vtk.vtkLine()
line.GetPointIds().SetId(0, point_id1)
line.GetPointIds().SetId(1, point_id2)
# Add the line to the cell array
lines.InsertNextCell(line)
# Create vtkPolyData to store the geometry
polydata = vtk.vtkPolyData()
polydata.SetPoints(points)
polydata.SetLines(lines)
# Verify that the polydata is not empty
if polydata.GetNumberOfPoints() == 0 or polydata.GetNumberOfCells() == 0:
print("Error: PolyData is empty")
sys.exit(1)
# Create a mapper and actor
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(cube_source.GetOutputPort())
mapper.SetInputData(polydata)
actor = vtk.vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetColor(0.0, 0.0, 1.0) # Set color to red
actor.GetProperty().SetLineWidth(2) # Set line width
# Add the actor to the scene
self.renderer.AddActor(actor)
#self.renderer.SetBackground(0.1, 0.2, 0.4) # Set background color
def simplify_mesh(self, input_mesh, target_reduction):
# Create the quadric decimation filter
decimate = vtk.vtkDecimatePro()
decimate.SetInputData(input_mesh)
# Render and interact
# Force an update of the pipeline
# mapper.Update()
self.vtk_widget.GetRenderWindow().Render()
# Set the reduction factor (0 to 1, where 1 means maximum reduction)
decimate.SetTargetReduction(target_reduction)
def render_from_points_direct_with_faces(self, vertices, faces):
points = vtk.vtkPoints()
for i in range(vertices.shape[0]):
points.InsertNextPoint(vertices[i])
# Optional: Preserve topology (if needed)
decimate.PreserveTopologyOn()
# Create a vtkCellArray to store the triangles
triangles = vtk.vtkCellArray()
for i in range(faces.shape[0]):
triangle = vtk.vtkTriangle()
triangle.GetPointIds().SetId(0, faces[i, 0])
triangle.GetPointIds().SetId(1, faces[i, 1])
triangle.GetPointIds().SetId(2, faces[i, 2])
triangles.InsertNextCell(triangle)
# Perform the decimation
decimate.Update()
# Create a polydata object
polydata = vtk.vtkPolyData()
polydata.SetPoints(points)
polydata.SetPolys(triangles)
return decimate.GetOutput()
# Calculate normals
normalGenerator = vtk.vtkPolyDataNormals()
normalGenerator.SetInputData(polydata)
normalGenerator.ComputePointNormalsOn()
normalGenerator.ComputeCellNormalsOn()
normalGenerator.Update()
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()
self.cell_normals = vtk_to_numpy(normalGenerator.GetOutput().GetCellData().GetNormals())
# 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()
# Create a mapper and actor
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(polydata)
return normals.GetOutput()
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
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()
# (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
# 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)
# Force an update of the pipeline
#mapper.Update()
self.vtk_widget.GetRenderWindow().Render()
def load_custom_mesh(self, vertices, faces):
### Load meshes by own module
@ -154,277 +190,114 @@ class VTKWidget(QtWidgets.QWidget):
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
# Get picked cell ID
cell_id = self.picker.GetCellId()
if cell_id != -1:
print(f"Picked face ID: {cell_id}")
print(f"Picked cell 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()
# Ensure it's a line
if cell.GetCellType() == vtk.VTK_LINE:
# Get the two points of the line
point_id1 = cell.GetPointId(0)
point_id2 = cell.GetPointId(1)
# Get cell type
cell_type = cell.GetCellType()
print(f"Cell type: {cell_type}")
proj_point1 = polydata.GetPoint(point_id1)
proj_point2 = polydata.GetPoint(point_id2)
# Get points of the cell
points = cell.GetPoints()
num_points = points.GetNumberOfPoints()
print(f"Number of points in the cell: {num_points}")
self.access_selected_points.append((proj_point1, proj_point2))
vec_points = []
point1 = np.array(proj_point1)
point2 = np.array(proj_point2)
# Get coordinates of each point
for i in range(num_points):
point = points.GetPoint(i)
print(f"Point {i}: {point}")
vec_points.append(point)
print(f"Line starts at: {point1}")
print(f"Line ends at: {point2}")
# 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}")
# Store this line for later use if needed
self.selected_edges.append((point1, point2))
# 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}")
if len(self.selected_edges) == 2:
# Compute the normal from the two selected edges
edge1 = self.selected_edges[0][1] - self.selected_edges[0][0]
edge2 = self.selected_edges[1][1] - self.selected_edges[1][0]
self.selected_normal = np.cross(edge1, edge2)
self.selected_normal = self.selected_normal / np.linalg.norm(self.selected_normal)
print("Computed normal:", self.selected_normal)
# 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}")
# Create a transform for projection
transform = vtk.vtkTransform()
transform.Identity()
if num_points and cell_data:
face_orient = {'cell_data': cell_data, 'points': vec_points }
print(face_orient)
self.face_data.emit(face_orient)
# Compute rotation to align normal with Z-axis
z_axis = np.array([0, 0, 1])
rotation_axis = np.cross(self.selected_normal, z_axis)
rotation_angle = np.arccos(np.dot(self.selected_normal, z_axis)) * 180 / np.pi
# Highlight picked face (your existing code)
ids = vtk.vtkIdTypeArray()
ids.SetNumberOfComponents(1)
ids.InsertNextValue(cell_id)
if np.linalg.norm(rotation_axis) > 1e-6: # Avoid division by zero
transform.RotateWXYZ(rotation_angle, rotation_axis[0], rotation_axis[1], rotation_axis[2])
selection_node = vtk.vtkSelectionNode()
selection_node.SetFieldType(vtk.vtkSelectionNode.CELL)
selection_node.SetContentType(vtk.vtkSelectionNode.INDICES)
selection_node.SetSelectionList(ids)
# Apply scaling to flatten
transform.Scale(1, 1, 0)
selection = vtk.vtkSelection()
selection.AddNode(selection_node)
# Apply the inverse rotation to bring it back to original orientation
transform.RotateWXYZ(-rotation_angle, rotation_axis[0], rotation_axis[1], rotation_axis[2])
extract_selection = vtk.vtkExtractSelection()
extract_selection.SetInputData(0, polydata)
extract_selection.SetInputData(1, selection)
extract_selection.Update()
# Apply the transform to the polydata
transformFilter = vtk.vtkTransformPolyDataFilter()
transformFilter.SetInputData(polydata)
transformFilter.SetTransform(transform)
transformFilter.Update()
self.picked_mapper.SetInputData(extract_selection.GetOutput())
# Get the projected polydata
projected_polydata = transformFilter.GetOutput()
# Create a mapper and actor for the projected data
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(projected_polydata)
actor = vtk.vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetColor(0.0, 1.0, 0.0) # Set color to green
actor.GetProperty().SetLineWidth(4) # Set line width
# Add the actor to the scene
self.renderer.AddActor(actor)
# Add a plane to visualize the projection plane
plane_source = vtk.vtkPlaneSource()
plane_source.SetCenter(0, 0, 0)
plane_source.SetNormal(self.selected_normal)
plane_mapper = vtk.vtkPolyDataMapper()
plane_mapper.SetInputConnection(plane_source.GetOutputPort())
plane_actor = vtk.vtkActor()
plane_actor.SetMapper(plane_mapper)
plane_actor.GetProperty().SetColor(0.8, 0.8, 0.8) # Light gray
plane_actor.GetProperty().SetOpacity(0.5)
self.renderer.AddActor(plane_actor)
# Reset camera to show all actors
self.renderer.ResetCamera()
# Render and interact
self.vtk_widget.GetRenderWindow().Render()
# Reset selected edges
self.selected_edges = []
elif len(self.access_selected_points) > 2:
self.access_selected_points = []
else:
print("Selected cell is not a line")
def start(self):
self.interactor.Initialize()
self.interactor.Start()

View File

@ -0,0 +1,337 @@
def are_coplanar(self, normal1, normal2, point1, point2, tolerance=1e-6):
# Check if normals are parallel
if np.abs(np.dot(normal1, normal2)) < 1 - tolerance:
return False
# Check if points lie on the same plane
diff = point2 - point1
return np.abs(np.dot(diff, normal1)) < tolerance
def merge_coplanar_triangles(self, polydata):
# Compute normals
normalGenerator = vtk.vtkPolyDataNormals()
normalGenerator.SetInputData(polydata)
normalGenerator.ComputePointNormalsOff()
normalGenerator.ComputeCellNormalsOn()
normalGenerator.Update()
mesh = normalGenerator.GetOutput()
n_cells = mesh.GetNumberOfCells()
# Create a map to store merged triangles
merged = {}
for i in range(n_cells):
if i in merged:
continue
cell = mesh.GetCell(i)
normal = np.array(mesh.GetCellData().GetNormals().GetTuple(i))
point = np.array(cell.GetPoints().GetPoint(0))
merged[i] = [i]
for j in range(i + 1, n_cells):
if j in merged:
continue
cell_j = mesh.GetCell(j)
normal_j = np.array(mesh.GetCellData().GetNormals().GetTuple(j))
point_j = np.array(cell_j.GetPoints().GetPoint(0))
if self.are_coplanar(normal, normal_j, point, point_j):
merged[i].append(j)
# Create new polygons
new_polygons = vtk.vtkCellArray()
for group in merged.values():
if len(group) > 1:
polygon = vtk.vtkPolygon()
points = set()
for idx in group:
cell = mesh.GetCell(idx)
for j in range(3):
point_id = cell.GetPointId(j)
points.add(point_id)
polygon.GetPointIds().SetNumberOfIds(len(points))
for j, point_id in enumerate(points):
polygon.GetPointIds().SetId(j, point_id)
new_polygons.InsertNextCell(polygon)
else:
new_polygons.InsertNextCell(mesh.GetCell(group[0]))
# Create new polydata
new_polydata = vtk.vtkPolyData()
new_polydata.SetPoints(mesh.GetPoints())
new_polydata.SetPolys(new_polygons)
return new_polydata
def create_cube_mesh(self):
# cube_source = vtk.vtkSuperquadricSource()
reader = vtk.vtkSTLReader()
reader.SetFileName("case.stl") # Replace with your mesh file path
reader.Update()
featureEdges = vtk.vtkFeatureEdges()
featureEdges.SetInputConnection(reader.GetOutputPort())
featureEdges.BoundaryEdgesOn()
featureEdges.FeatureEdgesOn()
featureEdges.ManifoldEdgesOff()
featureEdges.NonManifoldEdgesOff()
featureEdges.Update()
# print(cube_source)
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(reader.GetOutputPort())
actor = vtk.vtkActor()
actor.SetMapper(mapper)
self.renderer.AddActor(actor)
mapper_edge = vtk.vtkPolyDataMapper()
mapper_edge.SetInputConnection(featureEdges.GetOutputPort())
actor = vtk.vtkActor()
actor.SetMapper(mapper_edge)
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 create_simplified_outline(self, polydata):
featureEdges = vtk.vtkFeatureEdges()
featureEdges.SetInputData(polydata)
featureEdges.BoundaryEdgesOn()
featureEdges.FeatureEdgesOn()
featureEdges.ManifoldEdgesOff()
featureEdges.NonManifoldEdgesOff()
featureEdges.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 featureEdges
def render_from_points_direct_with_faces(self, vertices, faces):
points = vtk.vtkPoints()
for i in range(vertices.shape[0]):
points.InsertNextPoint(vertices[i])
# Create a vtkCellArray to store the triangles
triangles = vtk.vtkCellArray()
for i in range(faces.shape[0]):
triangle = vtk.vtkTriangle()
triangle.GetPointIds().SetId(0, faces[i, 0])
triangle.GetPointIds().SetId(1, faces[i, 1])
triangle.GetPointIds().SetId(2, faces[i, 2])
triangles.InsertNextCell(triangle)
"""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(points)
polydata.SetPolys(triangles)
# Calculate normals
normalGenerator = vtk.vtkPolyDataNormals()
normalGenerator.SetInputData(polydata)
normalGenerator.ComputePointNormalsOn()
normalGenerator.ComputeCellNormalsOn()
normalGenerator.Update()
self.cell_normals = vtk_to_numpy(normalGenerator.GetOutput().GetCellData().GetNormals())
# merged_polydata = self.merge_coplanar_triangles(polydata)
# Create a mapper and actor
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(polydata)
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")"""

View File

@ -1,57 +0,0 @@
from math import pi, sin, cos
from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from direct.gui.DirectGui import DirectEntry, DirectButton
from sdf import *
class MyApp(ShowBase):
def __init__(self):
ShowBase.__init__(self)
"""# Load the environment model.
self.scene = self.loader.loadModel("station_alpha_ripped.glb")
# Reparent the model to render.
self.scene.reparentTo(self.render)
# Apply scale and position transforms on the model.
self.scene.setScale(1, 1, 1)
self.scene.setPos(0, 0, 0)"""
# Add the spinCameraTask procedure to the task manager.
self.taskMgr.add(self.spinCameraTask, "SpinCameraTask")
# Create a DirectEntry widget for text input
self.textEntry = DirectEntry(
text="", scale=0.05, command=self.setText, initialText="Type here"
)
self.textEntry.reparentTo(self.aspect2d)
self.textEntry.setPos(-1.1, 0, -0.95)
self.apply = DirectButton(text="Make Cube",scale=0.05, command=self.buttonClicked)
self.apply.reparentTo(self.aspect2d)
self.apply.setPos(-1, -1, 0.8)
def setText(self, textEntered):
print("Text entered:", textEntered)
def buttonClicked(self):
print("Clciked")
obj = box(0.9)
model = obj.save("model.stl" , step=0.01)
self.scene = self.loader.loadModel("model.stl")
self.scene.reparentTo(self.render)
self.scene.setScale(1, 1, 1)
self.scene.setPos(0, 0, 0)
# Define a procedure to move the camera.
def spinCameraTask(self, task):
angleDegrees = task.time * 6.0
angleRadians = angleDegrees * (pi / 180.0)
self.camera.setPos(20 * sin(angleRadians), -20 * cos(angleRadians), 3)
self.camera.setHpr(angleDegrees, 0, 0)
return Task.cont
app = MyApp()
app.run()

View File

@ -1,51 +0,0 @@
import pygame
from pygame.locals import *
class Button:
def __init__(self, text, position, size):
self.text = text
self.font = pygame.font.Font(None, 36)
self.position = position
self.size = size
self.rect = pygame.Rect(position, size)
self.clicked = False
def draw(self, surface):
pygame.draw.rect(surface, (255, 255, 255), self.rect, 2)
text_surface = self.font.render(self.text, True, (255, 255, 255))
text_rect = text_surface.get_rect(center=self.rect.center)
surface.blit(text_surface, text_rect)
def is_clicked(self, pos):
if self.rect.collidepoint(pos):
self.clicked = True
return True
return False
def main():
pygame.init()
display = (800, 600)
screen = pygame.display.set_mode(display)
clock = pygame.time.Clock()
button = Button("Click Me!", (300, 250), (200, 50))
running = True
while running:
for event in pygame.event.get():
if event.type == QUIT:
running = False
elif event.type == MOUSEBUTTONDOWN:
if event.button == 1:
if button.is_clicked(event.pos):
print("Button Clicked!")
screen.fill((0, 0, 0))
button.draw(screen)
pygame.display.flip()
clock.tick(60)
pygame.quit()
if __name__ == "__main__":
main()

75
main.py
View File

@ -8,7 +8,8 @@ from drawing_modules.vysta_widget import PyVistaWidget
from drawing_modules.draw_widget2d import SketchWidget
from sdf import *
from python_solvespace import SolverSystem, ResultFlag
from mesh_modules import simple_mesh
from mesh_modules import simple_mesh, vesta_mesh, interactor_mesh
# main, draw_widget, gl_widget
@ -44,7 +45,8 @@ class MainWindow(QMainWindow):
self.ui.sketch_list.itemChanged.connect(self.draw_mesh)
### Sketches
self.ui.pb_origin_wp.pressed.connect(self.add_new_sketch)
self.ui.pb_origin_wp.pressed.connect(self.add_new_sketch_origin)
self.ui.pb_origin_face.pressed.connect(self.add_new_sketch_wp)
self.ui.pb_nw_sktch.pressed.connect(self.add_sketch)
self.ui.pb_del_sketch.pressed.connect(self.del_sketch)
@ -66,10 +68,21 @@ class MainWindow(QMainWindow):
self.sketchWidget.constrain_done.connect(self.draw_op_complete)
def add_new_sketch(self):
def add_new_sketch_origin(self):
self.sketchWidget.clear_sketch()
self.sketchWidget.create_workplane()
def add_new_sketch_wp(self):
self.sketchWidget.clear_sketch()
edges = [((-158.0, -20.0, -25.0), (286.0, -195.0, -25.0)), ((-158.0, -20.0, 25.0), (-158.0, -20.0, -25.0))]
#edges = self.custom_3D_Widget.access_selected_points
normal = self.custom_3D_Widget.selected_normal
self.sketchWidget.create_workplane_projected()
self.sketchWidget.create_proj_lines(edges)
#self.sketchWidget.create_workplane_space(edges, normal)
def act_line_mode(self):
if not self.ui.pb_linetool.isChecked():
self.sketchWidget.mouse_mode = 'line'
@ -77,7 +90,6 @@ class MainWindow(QMainWindow):
self.sketchWidget.mouse_mode = None
self.sketchWidget.line_draw_buffer = [None, None]
def act_constrain_pt_pt_mode(self):
if not self.ui.pb_con_ptpt.isChecked():
self.sketchWidget.mouse_mode = 'pt_pt'
@ -126,24 +138,24 @@ class MainWindow(QMainWindow):
self.sketchWidget.mouse_mode = None
self.sketchWidget.reset_buffers()
def calc_sketch_projection_3d(self, depth):
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 calc_sketch_projection_3d(self, lines, z_origin, depth):
print(f"Gemetries {lines}, {z_origin}, {depth}")
edges = interactor_mesh.generate_mesh(lines, z_origin, depth)
print("final_mesh", edges)
self.custom_3D_Widget.load_interactor_mesh(edges)
def draw_mesh(self):
#print("Update")
name = self.ui.body_list.currentItem().text()
#print("selected_for disp", name)
print("selected_for disp", name)
model = self.model['operation'][name]['sdf_object']
mesh = model.generate()
#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)
vesta = vesta_mesh
model_data = vesta.generate_mesh_from_sdf(model, resolution=64, threshold=0)
vertices, faces = model_data
vesta.save_mesh_as_stl(vertices, faces, 'test.stl')
self.custom_3D_Widget.render_from_points_direct_with_faces(vertices, faces)
def on_item_changed(self, current_item, previous_item):
if current_item:
@ -158,6 +170,19 @@ class MainWindow(QMainWindow):
return points_for_sdf
def convert_lines_for_interactor(self):
points_for_interact = []
for point_to_poly in self.sketchWidget.slv_lines_main:
start, end = point_to_poly['ui_points']
start_draw = self.translate_points_tup(start)
end_draw = self.translate_points_tup(end)
line = start_draw, end_draw
points_for_interact.append(line)
print("packed_lines", points_for_interact)
return points_for_interact
def add_sketch(self):
name = f"sketch-{str(names.get_first_name())}"
@ -226,6 +251,7 @@ class MainWindow(QMainWindow):
print(f"Item '{item_name}' not found in either 'sketch' or 'operation' dictionary.")
else:
print("No item selected.")
def update_body(self):
pass
@ -258,13 +284,12 @@ class MainWindow(QMainWindow):
selected = self.ui.sketch_list.currentItem()
name = selected.text()
points = self.model['sketch'][name]['sketch_points']
lines = self.convert_lines_for_interactor()
if points[-1] == points[0]:
#detect loop that causes problems in mesh generation
del points[-1]
print(points)
length, ok = QInputDialog.getDouble(self, 'Extrude Length', 'Enter a mm value:', decimals=2)
#TODO : Implement cancel
@ -278,20 +303,20 @@ class MainWindow(QMainWindow):
'type': 'extrude',
'sdf_object': f,
}
print(element)
#print(element)
self.model['operation'][name_op] = element
self.ui.body_list.addItem(name_op)
items = self.ui.body_list.findItems(name_op, Qt.MatchExactly)[0]
self.ui.body_list.setCurrentItem(items)
self.draw_mesh()
#self.calc_sketch_projection_3d(length)
self.calc_sketch_projection_3d(lines, 0, length)
#self.draw_mesh()
def send_cut(self):
name = self.ui.body_list.currentItem().text()
points = self.model['operation'][name]['sdf_object']
self.list_selected.append(points)
print(self.list_selected)
#print(self.list_selected)
if len(self.list_selected) > 1:
geo = Geometry()
@ -308,7 +333,7 @@ class MainWindow(QMainWindow):
self.ui.body_list.addItem(name_op)
items = self.ui.sketch_list.findItems(name_op, Qt.MatchExactly)
self.ui.body_list.setCurrentItem(items[-1])
self.draw_mesh()
#self.draw_mesh()
else:
print("mindestens 2!")

View File

@ -0,0 +1,44 @@
# Draw simple boundary based on the lines and depth
def generate_mesh(lines: list, z_origin: float, depth: float, symmetric: bool = True):
if symmetric:
depth1 = depth/2
depth2 = -depth/2
origin = create_3D(lines, z_origin, depth1)
extruded = create_3D(lines, z_origin, depth2)
else:
origin = create_3D(lines, z_origin, 0)
extruded = create_3D(lines, z_origin, depth)
vert_lines = create_vert_lines(origin, extruded)
print(f"Result = {origin} / {extruded} / {vert_lines}")
return origin + vert_lines + extruded
def create_vert_lines(origin, extruded):
vert_lines = []
for d3_point_o, d3point_e in zip(origin, extruded):
for sp3d_1, sp3d_2 in zip(d3_point_o, d3point_e):
new_line = sp3d_1, sp3d_2
vert_lines.append(new_line)
return vert_lines
def create_3D(lines, z_origin, depth):
line_loop = []
for coordinate2d in lines:
start, end = coordinate2d
xs,ys = start
coordinate3d_start_orig = xs, ys, z_origin + depth
xe, ye = end
coordinate3d_end_orig = xe, ye, z_origin + depth
line3d_orig = coordinate3d_start_orig, coordinate3d_end_orig
line_loop.append(line3d_orig)
return line_loop

View File

@ -1,9 +1,172 @@
import numpy as np
from scipy.spatial import ConvexHull
from stl import mesh
from scipy.spatial import Delaunay, ConvexHull
from shapely.geometry import Polygon, Point
def generate_mesh(points, depth):
def alpha_shape(points, alpha):
"""
Compute the alpha shape (concave hull) of a set of points.
"""
def add_edge(edges, edge_points, points, i, j):
"""Add a line between the i-th and j-th points if not in the list already"""
if (i, j) in edges or (j, i) in edges:
return
edges.add((i, j))
edge_points.append(points[[i, j]])
tri = Delaunay(points)
edges = set()
edge_points = []
# Loop over triangles:
for ia, ib, ic in tri.simplices:
pa = points[ia]
pb = points[ib]
pc = points[ic]
# Lengths of sides of triangle
a = np.sqrt((pa[0] - pb[0]) ** 2 + (pa[1] - pb[1]) ** 2)
b = np.sqrt((pb[0] - pc[0]) ** 2 + (pb[1] - pc[1]) ** 2)
c = np.sqrt((pc[0] - pa[0]) ** 2 + (pc[1] - pa[1]) ** 2)
# Semiperimeter of triangle
s = (a + b + c) / 2.0
# Area of triangle by Heron's formula
area = np.sqrt(s * (s - a) * (s - b) * (s - c))
circum_r = a * b * c / (4.0 * area)
# Here's the radius filter.
if circum_r < 1.0 / alpha:
add_edge(edges, edge_points, points, ia, ib)
add_edge(edges, edge_points, points, ib, ic)
add_edge(edges, edge_points, points, ic, ia)
m = np.array(edge_points)
return m
def generate_mesh(points, depth, alpha=0.1):
"""
Generate a mesh by extruding a 2D shape along the Z-axis, automatically detecting holes.
:param points: List of (x, y) tuples representing all points of the 2D shape, including potential holes.
:param depth: Extrusion depth along the Z-axis.
:param alpha: Alpha value for the alpha shape algorithm (controls the "tightness" of the boundary).
:return: Tuple of vertices and faces.
"""
# Convert points to a numpy array
points_2d = np.array(points)
# Compute the alpha shape (outer boundary)
boundary_edges = alpha_shape(points_2d, alpha)
# Create a Polygon from the boundary
boundary_polygon = Polygon(boundary_edges)
# Separate points into boundary and interior
boundary_points = []
interior_points = []
for point in points:
if Point(point).touches(boundary_polygon) or Point(point).within(boundary_polygon):
if Point(point).touches(boundary_polygon):
boundary_points.append(point)
else:
interior_points.append(point)
# Perform Delaunay triangulation on all points
tri = Delaunay(points_2d)
# Generate the top and bottom faces
bottom_face = np.hstack((tri.points, np.zeros((tri.points.shape[0], 1))))
top_face = np.hstack((tri.points, np.ones((tri.points.shape[0], 1)) * depth))
# Combine top and bottom vertices
vertices_array = np.vstack((bottom_face, top_face))
# Create faces
faces = []
# Bottom face triangulation
for simplex in tri.simplices:
faces.append(simplex.tolist())
# Top face triangulation (with an offset)
top_offset = len(tri.points)
for simplex in tri.simplices:
faces.append([i + top_offset for i in simplex])
# Side faces for the outer boundary
for i in range(len(boundary_points)):
next_i = (i + 1) % len(boundary_points)
current = points.index(boundary_points[i])
next_point = points.index(boundary_points[next_i])
faces.append([current, top_offset + current, top_offset + next_point])
faces.append([current, top_offset + next_point, next_point])
# Convert vertices to the desired format: list of tuples
vertices = [tuple(vertex) for vertex in vertices_array]
return vertices, faces
def generate_mesh_wholes(points, holes, depth):
"""
Generate a mesh by extruding a 2D shape along the Z-axis, including holes.
:param points: List of (x, y) tuples representing the outer boundary of the 2D shape.
:param holes: List of lists, where each inner list contains (x, y) tuples representing a hole.
: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)
# Prepare points for triangulation
triangulation_points = points_2d.tolist()
for hole in holes:
triangulation_points.extend(hole)
# Perform Delaunay triangulation
tri = Delaunay(np.array(triangulation_points))
# Generate the top and bottom faces
bottom_face = np.hstack((tri.points, np.zeros((tri.points.shape[0], 1))))
top_face = np.hstack((tri.points, np.ones((tri.points.shape[0], 1)) * depth))
# Combine top and bottom vertices
vertices_array = np.vstack((bottom_face, top_face))
# Create faces
faces = []
# Bottom face triangulation
for simplex in tri.simplices:
faces.append(simplex.tolist())
# Top face triangulation (with an offset)
top_offset = len(tri.points)
for simplex in tri.simplices:
faces.append([i + top_offset for i in simplex])
# Side faces
for i in range(len(points)):
next_i = (i + 1) % len(points)
faces.append([i, top_offset + i, top_offset + next_i])
faces.append([i, top_offset + next_i, next_i])
# Side faces for holes
start_index = len(points)
for hole in holes:
for i in range(len(hole)):
current = start_index + i
next_i = start_index + (i + 1) % len(hole)
faces.append([current, top_offset + next_i, top_offset + current])
faces.append([current, next_i, top_offset + next_i])
start_index += len(hole)
# Convert vertices to the desired format: list of tuples
vertices = [tuple(vertex) for vertex in vertices_array]
return vertices, faces
def generate_mesh_simple(points, depth):
"""
Generate a mesh by extruding a 2D shape along the Z-axis.

119
mesh_modules/vesta_mesh.py Normal file
View File

@ -0,0 +1,119 @@
import numpy as np
from skimage import measure
import multiprocessing
from functools import partial
from multiprocessing.pool import ThreadPool
import itertools
import time
def _cartesian_product(*arrays):
la = len(arrays)
dtype = np.result_type(*arrays)
arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype)
for i, a in enumerate(np.ix_(*arrays)):
arr[..., i] = a
return arr.reshape(-1, la)
class VESTA:
def __init__(self, sdf, bounds=None, resolution=64, threshold=0.0, workers=None):
self.sdf = sdf
self.bounds = bounds
self.resolution = resolution
self.threshold = threshold
self.workers = workers or multiprocessing.cpu_count()
def _estimate_bounds(self):
s = 16
x0 = y0 = z0 = -1e9
x1 = y1 = z1 = 1e9
prev = None
for i in range(32):
X = np.linspace(x0, x1, s)
Y = np.linspace(y0, y1, s)
Z = np.linspace(z0, z1, s)
d = np.array([X[1] - X[0], Y[1] - Y[0], Z[1] - Z[0]])
threshold = np.linalg.norm(d) / 2
if threshold == prev:
break
prev = threshold
P = _cartesian_product(X, Y, Z)
volume = self.sdf(P).reshape((len(X), len(Y), len(Z)))
where = np.argwhere(np.abs(volume) <= threshold)
if where.size == 0:
continue
x1, y1, z1 = (x0, y0, z0) + where.max(axis=0) * d + d / 2
x0, y0, z0 = (x0, y0, z0) + where.min(axis=0) * d - d / 2
if prev is None:
raise ValueError("Failed to estimate bounds. No points found within any threshold.")
return ((x0, y0, z0), (x1, y1, z1))
def _vesta_worker(self, chunk):
x0, x1, y0, y1, z0, z1 = chunk
X = np.linspace(x0, x1, self.resolution)
Y = np.linspace(y0, y1, self.resolution)
Z = np.linspace(z0, z1, self.resolution)
P = _cartesian_product(X, Y, Z)
V = self.sdf(P).reshape((self.resolution, self.resolution, self.resolution))
try:
verts, faces, _, _ = measure.marching_cubes(V, self.threshold)
except RuntimeError:
# Return empty arrays if marching_cubes fails
return np.array([]), np.array([])
# Scale and translate vertices to match the chunk's bounds
verts = verts / (self.resolution - 1)
verts[:, 0] = verts[:, 0] * (x1 - x0) + x0
verts[:, 1] = verts[:, 1] * (y1 - y0) + y0
verts[:, 2] = verts[:, 2] * (z1 - z0) + z0
return verts, faces
def _merge_meshes(self, results):
all_verts = []
all_faces = []
offset = 0
for verts, faces in results:
if len(verts) > 0 and len(faces) > 0:
all_verts.append(verts)
all_faces.append(faces + offset)
offset += len(verts)
if not all_verts or not all_faces:
return np.array([]), np.array([])
return np.vstack(all_verts), np.vstack(all_faces)
def generate_mesh(self):
if self.bounds is None:
self.bounds = self._estimate_bounds()
(x0, y0, z0), (x1, y1, z1) = self.bounds
chunks = [
(x0, x1, y0, y1, z0, z1)
]
with ThreadPool(self.workers) as pool:
results = pool.map(self._vesta_worker, chunks)
verts, faces = self._merge_meshes(results)
return verts, faces
def generate_mesh_from_sdf(sdf, bounds=None, resolution=64, threshold=0.0, workers=None):
vesta = VESTA(sdf, bounds, resolution, threshold, workers)
return vesta.generate_mesh()
# Helper function to save the mesh as an STL file
def save_mesh_as_stl(vertices, faces, filename):
from stl import mesh
# Create the mesh
cube = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))
for i, f in enumerate(faces):
for j in range(3):
cube.vectors[i][j] = vertices[f[j], :]
# Write the mesh to file
cube.save(filename)

BIN
model.stl

Binary file not shown.

View File

@ -1,170 +0,0 @@
import sys
from PySide6.QtOpenGLWidgets import QOpenGLWidget
from PySide6.QtWidgets import (QApplication, QMainWindow, QHBoxLayout, QVBoxLayout, QWidget, QPushButton, QGroupBox,
QTextEdit, QSizePolicy)
from PySide6.QtCore import QSize, Qt, QPoint
from OpenGL.GL import *
from OpenGL.GLU import *
from stl import mesh
class OpenGLWidget(QOpenGLWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.stl_file = "out.stl" # Replace with your STL file path
self.lastPos = QPoint()
self.xRot = 0
self.yRot = 0
self.zoom = -10.0
def load_stl(self, filename):
try:
stl_mesh = mesh.Mesh.from_file(filename)
return stl_mesh.vectors
except FileNotFoundError:
print(f"Error: File {filename} not found.")
except Exception as e:
print(f"Error loading {filename}: {e}")
return []
def initializeGL(self):
glClearColor(0, 0, 0, 1)
glEnable(GL_DEPTH_TEST)
def resizeGL(self, w, h):
glViewport(0, 0, w, h)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(45, w/h, 0.1, 100.0)
glMatrixMode(GL_MODELVIEW)
def paintGL(self):
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
glTranslatef(0.0, 0.0, self.zoom)
glRotatef(self.xRot, 1.0, 0.0, 0.0)
glRotatef(self.yRot, 0.0, 1.0, 0.0)
glColor3f(1.0, 1.0, 1.0)
mesh_data = self.load_stl(self.stl_file)
if mesh_data.any():
self.draw_stl(mesh_data)
def draw_stl(self, vertices):
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, 1, 1, 0))
glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.6, 0.6, 0.6, 0.6))
glBegin(GL_TRIANGLES)
for triangle in vertices:
for vertex in triangle:
glVertex3fv(vertex)
glEnd()
# Draw outer vertices as points
glDisable(GL_LIGHTING)
glColor3f(1.0, 0.0, 0.0) # Set color to red
glPointSize(5.0) # Set point size
glBegin(GL_POINTS)
for triangle in vertices:
for vertex in triangle:
glVertex3fv(vertex)
glEnd()
def mousePressEvent(self, event):
self.lastPos = event.pos()
def mouseMoveEvent(self, event):
dx = event.x() - self.lastPos.x()
dy = event.y() - self.lastPos.y()
if event.buttons() & Qt.LeftButton:
self.xRot += 0.5 * dy
self.yRot += 0.5 * dx
self.update()
self.lastPos = event.pos()
def wheelEvent(self, event):
delta = event.angleDelta().y()
self.zoom += delta / 120
self.update()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("fluencyCAD")
self.height = 1080
self.width = 1920
self.setFixedSize(QSize(self.width, self.height))
# Main central widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QHBoxLayout(central_widget)
# Left tools area
left_tools_layout = QVBoxLayout()
self.left_tools = QGroupBox("Left Tools")
self.left_tools.setFixedSize(100, self.height)
self.draw_rectangle = QPushButton("Draw Rectangle")
self.draw_rectangle.setFixedSize(80, 30)
left_tools_layout.addWidget(self.draw_rectangle)
self.draw_rectangle = QPushButton("Draw Rectangle2")
self.draw_rectangle.setFixedSize(80, 30)
left_tools_layout.addWidget(self.draw_rectangle) # Align the button to the top
self.draw_rectangle = QPushButton("Draw Rectangle3")
self.draw_rectangle.setFixedSize(80, 30)
left_tools_layout.addWidget(self.draw_rectangle)
self.left_tools.setLayout(left_tools_layout)
self.left_tools.setAlignment(Qt.AlignTop)
layout.addWidget(self.left_tools)
# Center OpenGL widget and QTextEdit
center_layout = QVBoxLayout()
self.openGLWidget = OpenGLWidget()
center_layout.addWidget(self.openGLWidget)
self.openGLWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.enter_code = QTextEdit()
self.enter_code.setFixedSize(QSize(self.width, self.height // 4))
center_layout.addWidget(self.enter_code)
self.code_tools = QGroupBox("Code Tools")
self.code_tools.setFixedSize(QSize(self.width, self.height // 18))
code_tools_layout = QHBoxLayout() # Layout for code tools
self.apply_code = QPushButton("Apply Code") # Creating QPushButton
self.apply_code.setFixedSize(80,30)
code_tools_layout.addWidget(self.apply_code) # Adding QPushButton to QHBoxLayout
self.code_tools.setLayout(code_tools_layout) # Setting QHBoxLayout to QGroupBox
self.code_tools.setAlignment(Qt.AlignLeft)
center_layout.addWidget(self.code_tools) # Adding QGroupBox to the center layout
layout.addLayout(center_layout) # Adding center layout to the main layout
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

View File

@ -1,37 +0,0 @@
import vtk
def loadStl(fname):
"""Load the given STL file, and return a vtkPolyData object for it."""
reader = vtk.vtkSTLReader()
reader.SetFileName(fname)
reader.Update()
polydata = reader.GetOutput()
return polydata
def polyDataToActor(polydata):
"""Wrap the provided vtkPolyData object in a mapper and an actor, returning the actor."""
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(polydata)
actor = vtk.vtkActor()
actor.SetMapper(mapper)
return actor
# Create a rendering window and renderer
ren = vtk.vtkRenderer()
renWin = vtk.vtkRenderWindow()
renWin.AddRenderer(ren)
# Create a RenderWindowInteractor to permit manipulating the camera
iren = vtk.vtkRenderWindowInteractor()
iren.SetRenderWindow(renWin)
style = vtk.vtkInteractorStyleTrackballCamera()
iren.SetInteractorStyle(style)
stlFilename = "out.stl"
polydata = loadStl(stlFilename)
ren.AddActor(polyDataToActor(polydata))
# Enable user interface interactor
iren.Initialize()
renWin.Render()
iren.Start()