Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for non-manifold mesh errors #1981

Merged
merged 27 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
79e3e73
Add tests for mesh manifold check
Floppy Mar 28, 2024
03070ca
add manifold? method to mittsu geometry classes
Floppy Mar 28, 2024
e5c3e86
working manifold analysis for Mittsu::Geometry
Floppy Mar 28, 2024
3733cda
remove buffergeometry test and method
Floppy Mar 28, 2024
58c9bea
move solid? and manifold? to Object3D instead of Geometry
Floppy Mar 28, 2024
8298513
improve tests
Floppy Mar 28, 2024
a10d76e
detect solid objects
Floppy Mar 28, 2024
942f6be
change to use our own mittsu fork
Floppy Mar 28, 2024
372d2a2
lint
Floppy Mar 31, 2024
be544d1
add non-manifold and inside-out problem categories
Floppy Mar 31, 2024
37ba60f
add loader support for STL
Floppy Mar 31, 2024
048d04e
refactor expectations for model file analysis tests
Floppy Apr 2, 2024
731442b
change mesh analysis to included module
Floppy Apr 2, 2024
2da170d
enqueue geometric analysis job when necessary
Floppy Apr 2, 2024
ad13378
create problems for non-manifold and inside-out meshes
Floppy Apr 2, 2024
72739bf
add partials for non manifold and inside out errors
Floppy Apr 2, 2024
89d74be
add translations for manifold errors
Floppy Apr 2, 2024
9cb8eeb
fix duplicate lists showing in the wrong place
Floppy Apr 3, 2024
dbd6956
update mittsu to get STL loader fix
Floppy Apr 3, 2024
540e1a9
update messages
Floppy Apr 4, 2024
e736cde
add a lower-priority queue for deep analysis jobs
Floppy Apr 4, 2024
f6222b8
things with no geometry are manifold and solid
Floppy Apr 4, 2024
4a66e66
traverse object children to make sure they're all manifold
Floppy Apr 4, 2024
38c82aa
lint
Floppy Apr 4, 2024
fe11d96
don't use Object3D#traverse it seems to take forever
Floppy Apr 4, 2024
1f7f143
improve tests a little
Floppy Apr 4, 2024
4df741a
only recalculate digest and queue geometric check if it looks like th…
Floppy Apr 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ gem "acts_as_favoritor", "~> 6.0"

gem "sqlite3_ar_regexp", "~> 2.2"

gem "mittsu", github: "danini-the-panini/mittsu", ref: "7f44c46"
gem "mittsu", github: "manyfold3d/mittsu", ref: "manyfold"

gem "view_component", "~> 3.11"

Expand Down
12 changes: 7 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ GIT
railties (>= 6.0)

GIT
remote: https://github.com/danini-the-panini/mittsu.git
revision: 7f44c468f9d6d8b5548ca933dea2d1a873ac496d
ref: 7f44c46
remote: https://github.com/manyfold3d/mittsu.git
revision: cd538af53493c09b0a2497c51569be6c1c6fc1d6
ref: manyfold
specs:
mittsu (0.4.0)
builder
chunky_png
ffi
opengl-bindings
rubyzip

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -164,7 +166,6 @@ GEM
faker (3.3.0)
i18n (>= 1.8.11, < 2)
ffi (1.16.3)
ffi (1.16.3-x64-mingw-ucrt)
ffi-libarchive (1.1.14)
ffi (~> 1.0)
flipper (1.2.2)
Expand Down Expand Up @@ -298,7 +299,7 @@ GEM
notiffany (0.1.3)
nenv (~> 0.1)
shellany (~> 0.0)
opengl-bindings (1.6.13)
opengl-bindings (1.6.14)
orm_adapter (0.5.0)
parallel (1.24.0)
parser (3.3.0.5)
Expand Down Expand Up @@ -442,6 +443,7 @@ GEM
rubocop (~> 1.40)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
sass-rails (6.0.0)
sassc-rails (~> 2.1, >= 2.1.1)
sassc (2.4.0)
Expand Down
16 changes: 10 additions & 6 deletions app/jobs/scan/analyse_model_file_job.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
require "string/similarity"

class Scan::AnalyseModelFileJob < ApplicationJob
queue_as :scan
queue_as :analysis

def perform(file_id)
file = ModelFile.find(file_id)
return if file.nil?
# Don't run analysis if the file is missing
# The Problem is raised elsewhere.
return if !File.exist?(file.pathname)
# Update stored file metadata if not set
file.update!(
digest: file.digest || file.calculate_digest,
size: file.size || File.size(file.pathname)
)
# If the file is modified, or we're lacking metadata
if File.mtime(file.pathname) > file.updated_at || file.digest.nil? || file.size.nil?
file.digest = file.calculate_digest
file.size = File.size(file.pathname)
# If the digest has changed, queue up detailed geometric mesh analysis
Scan::GeometricAnalysisJob.perform_later(file_id) if file.digest_changed?
# Store updated file metadata
file.save!
end
# Match supported files
match_with_supported_file(file)
# Detect inefficient file formats
Expand Down
28 changes: 28 additions & 0 deletions app/jobs/scan/geometric_analysis_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class Scan::GeometricAnalysisJob < ApplicationJob
queue_as :analysis

def perform(file_id)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method perform has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring.

# Get model
file = ModelFile.find(file_id)
return if file.nil?
# Get mesh
mesh = file.mesh
if mesh
# Check for manifold mesh
manifold = mesh.manifold?
Problem.create_or_clear(
file,
:non_manifold,
!manifold
)
# If the mesh is manifold, we can check if it's inside out
if manifold
Problem.create_or_clear(
file,
:inside_out,
!mesh.solid?
)
end
end
end
end
66 changes: 66 additions & 0 deletions app/lib/mesh_analysis.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
module MeshAnalysis
def solid?
# Shortcut if there is nothing here
return true if geometry.nil? && children.empty?
# Recurse children to see if they are solid
children_are_solid = children.map { |x| x.solid? }.all?
solid = true
if geometry
# Make sure material is double sided
prev_side = material.side
material.side = Mittsu::DoubleSide
# Make a raycaster from a vertex and the face normal
face = geometry.faces.first
r = Mittsu::Raycaster.new(geometry.vertices[face.b], face.normal, 1e-9)
intersections = r.intersect_object(self, true)
# Restore material
material.side = prev_side
# We want an even number of intersections
solid = (intersections.length % 2 == 0)
end
solid && children_are_solid
end

def manifold?
# Shortcut if there is nothing here
return true if geometry.nil? && children.empty?
# Recurse children to see if they are manifold
children_are_manifold = children.map { |x| x.manifold? }.all?
# Detect manifold geometry in this object
edges = {}
# For each face, record its edges in the edge hash
geometry&.faces&.each do |face|
update_edge_hash face.a, face.b, edges
update_edge_hash face.b, face.c, edges
update_edge_hash face.c, face.a, edges
end
# If there's anything left in the edge hash, then either
# we have holes, or we have badly oriented faces
edges.empty? && children_are_manifold
end

private

# Updates edge hash with the passed vertex indexes
# First, the reverse edge is searched for in the hash
# If found, it's removed as we've got a match
# If not, we record this edge in the hash
def update_edge_hash(v1, v2, edges)
return if edges.delete "#{v2}->#{v1}"
edges["#{v1}->#{v2}"] = true
end
end

module Mittsu
class Object3D
include MeshAnalysis
end

class Face3
def flip!
tmp = @a
@a = @c
@c = tmp
end
end
end
12 changes: 7 additions & 5 deletions app/models/model_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ def self.ransackable_associations(_auth_object = nil)
["favorited", "model", "problems"]
end

def mesh
loader&.new&.load(pathname)
end
memoize :mesh

private

def presupported_files_cannot_have_presupported_version
Expand All @@ -116,13 +121,10 @@ def presupported_version_is_presupported
end
end

def mesh
loader&.new&.load(pathname)
end
memoize :mesh

def loader
case extension
when "stl"
Mittsu::STLLoader
when "obj"
Mittsu::OBJLoader
end
Expand Down
8 changes: 6 additions & 2 deletions app/models/problem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class Problem < ApplicationRecord
:inefficient,
:duplicate,
:no_image,
:no_3d_model
:no_3d_model,
:non_manifold,
:inside_out
]
enum :category, CATEGORIES

Expand All @@ -36,7 +38,9 @@ class Problem < ApplicationRecord
inefficient: :info,
duplicate: :warning,
no_image: :silent,
no_3d_model: :silent
no_3d_model: :silent,
non_manifold: :warning,
inside_out: :warning
}

def self.create_or_clear(problematic, cat, present, options = {})
Expand Down
2 changes: 1 addition & 1 deletion app/views/model_files/_problem.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<%= card(problem_severity(problem), t("problems.model_file.%{cat}.title" % {cat: problem.category})) do %>
<%= t(("problems.model_file.%{cat}.description" % {cat: problem.category}), note: problem.note) %>

<% unless @duplicates.empty? %>
<% if problem.category == "duplicate" && !@duplicates.empty? %>
<ul>
<% @duplicates.each do |file| %>
<li><%= link_to "#{file.model.name}/#{file.filename}", [file.model.library, file.model, file] %></li>
Expand Down
4 changes: 4 additions & 0 deletions app/views/problems/model_file/_inside_out.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<td><%= icon("files", t(".title")) %></td>
<td><%= link_to problem.problematic.name, [problem.problematic.model.library, problem.problematic.model, problem.problematic] %></td>
<td><%= t ".title" %></td>
<td><%= link_to "Open", [problem.problematic.model.library, problem.problematic.model, problem.problematic], class: "btn btn-primary" %></td>
4 changes: 4 additions & 0 deletions app/views/problems/model_file/_non_manifold.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<td><%= icon("files", t(".title")) %></td>
<td><%= link_to problem.problematic.name, [problem.problematic.model.library, problem.problematic.model, problem.problematic] %></td>
<td><%= t ".title" %></td>
<td><%= link_to "Open", [problem.problematic.model.library, problem.problematic.model, problem.problematic], class: "btn btn-primary" %></td>
6 changes: 6 additions & 0 deletions config/initializers/delayed_job_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ def self.ransackable_attributes(auth_object = nil)
["attempts", "created_at", "failed_at", "handler", "id", "last_error", "locked_at", "locked_by", "priority", "queue", "run_at", "updated_at"]
end
end

Delayed::Worker.queue_attributes = {
default: {priority: 0},
scan: {priority: 0},
analysis: {priority: 10}
}
2 changes: 2 additions & 0 deletions config/initializers/mittsu.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Load mittsu extensions
require Rails.root.join("app/lib/mesh_analysis")
8 changes: 8 additions & 0 deletions config/locales/problems/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ en:
nesting: Model contains other models
no_3d_model: Model has no 3D files
no_image: Model has no image files
non_manifold: Mesh is non-manifold
inside_out: Mesh is inside-out
filters:
apply_filters: Apply
clear_filters: Clear
Expand Down Expand Up @@ -48,6 +50,12 @@ en:
missing:
description: This file is missing on disk; either delete it, or find where it went!
title: File not found
non_manifold:
description: This mesh appears to be non-manifold, meaning it has holes or has some faces backwards, and may cause errors when printed. Repair it in a 3d modeling tool such as MeshLab or 3D Builder.
title: Non-manifold mesh
inside_out:
description: This mesh looks like its faces might be pointed the wrong way, which could cause print errors. Repair it in a 3d modeling tool such as MeshLab or 3D Builder.
title: Inside-out mesh
severities:
danger: Danger
info: Info
Expand Down
Loading
Loading