-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path__init__.py
More file actions
412 lines (339 loc) · 15.9 KB
/
Copy path__init__.py
File metadata and controls
412 lines (339 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
bl_info = {
"name": "NS Toolkit - Mesh Cleanup Pro",
"author": "NS Toolkit | Created by Nandur Studio (Nandang Duryat) © 2025",
"version": (1, 0, 1),
"blender": (4, 2, 0),
"location": "Object > Convert / Sidebar (N) > NS Toolkit",
"description": "Professional mesh cleanup toolkit with real-time topology visualization, smart triangulation conversion, vertex merging, normal correction, and transform reset capabilities.",
"category": "Mesh",
}
# Version constant for UI display
ADDON_VERSION = bl_info["version"]
import bpy
import bmesh
import os
from bpy.props import BoolProperty
from bpy.app.handlers import persistent
# --- Scene Properties ---
class NS_ToolkitProperties(bpy.types.PropertyGroup):
highlight_topology: BoolProperty(
name="Highlight Topology",
description="Toggle mesh topology highlighting for ALL objects in scene (Triangles, Quads, N-gons)",
default=False,
update=lambda self, context: toggle_topology_highlight(self, context)
)
def toggle_topology_highlight(self, context):
"""Toggle mesh topology highlighting"""
if self.highlight_topology:
apply_topology_highlighting(context)
# Register update handlers for live updating (both Edit and Object mode)
if depsgraph_update_handler not in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_handler)
if scene_update_handler not in bpy.app.handlers.scene_update_pre:
bpy.app.handlers.scene_update_pre.append(scene_update_handler)
else:
clear_topology_highlighting(context)
# Unregister update handlers
if depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_handler)
if scene_update_handler in bpy.app.handlers.scene_update_pre:
bpy.app.handlers.scene_update_pre.remove(scene_update_handler)
def apply_topology_highlighting(context):
"""Apply topology highlighting to all mesh objects"""
# Get ALL mesh objects in scene (not just selected)
mesh_objects = [obj for obj in context.scene.objects if obj.type == 'MESH']
if not mesh_objects:
return
# Set viewport shading to Solid with Object Color
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'SOLID'
space.shading.color_type = 'OBJECT'
break
for obj in mesh_objects:
# Get mesh data
mesh = obj.data
# Analyze topology and set object color based on dominant topology
triangle_count = 0
quad_count = 0
ngon_count = 0
# Use bmesh if in Edit mode for real-time data
if context.mode == 'EDIT_MESH' and obj == context.active_object:
# Get bmesh representation for real-time updates
bm = bmesh.from_edit_mesh(mesh)
for face in bm.faces:
vertex_count = len(face.verts)
if vertex_count == 3:
triangle_count += 1
elif vertex_count == 4:
quad_count += 1
else:
ngon_count += 1
else:
# Standard mesh analysis for Object mode
for poly in mesh.polygons:
vertex_count = len(poly.vertices)
if vertex_count == 3:
triangle_count += 1
elif vertex_count == 4:
quad_count += 1
else:
ngon_count += 1
# Set color based on dominant topology
total_faces = triangle_count + quad_count + ngon_count
if total_faces > 0:
tri_ratio = triangle_count / total_faces
quad_ratio = quad_count / total_faces
ngon_ratio = ngon_count / total_faces
# Color strategy: Prioritize N-gons for visibility since they're most important to identify
if ngon_count > 0:
obj.color = (0.0, 0.0, 1.0, 1.0) # Blue for ANY N-gons present
elif tri_ratio > quad_ratio:
obj.color = (1.0, 0.0, 0.0, 1.0) # Red for triangle-dominant (no n-gons)
else:
obj.color = (0.0, 1.0, 0.0, 1.0) # Green for quad-dominant/equal (no n-gons)
# Store original color for restoration
if not hasattr(obj, "ns_original_color"):
obj["ns_original_color"] = list(obj.color)
def clear_topology_highlighting(context):
"""Clear topology highlighting from all mesh objects"""
# Get ALL mesh objects in scene
mesh_objects = [obj for obj in context.scene.objects if obj.type == 'MESH']
if not mesh_objects:
return
for obj in mesh_objects:
# Restore original color if stored
if "ns_original_color" in obj:
obj.color = obj["ns_original_color"]
del obj["ns_original_color"]
else:
# Default white color
obj.color = (1.0, 1.0, 1.0, 1.0)
# Reset viewport shading to Object Color (Random)
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'SOLID'
space.shading.color_type = 'RANDOM'
break
@persistent
def depsgraph_update_handler(scene, depsgraph):
"""Handler for live updating topology highlighting when mesh changes"""
# Check if highlight topology is active
if hasattr(scene, 'ns_toolkit_props') and scene.ns_toolkit_props.highlight_topology:
# Check if any mesh objects were updated
for update in depsgraph.updates:
if update.id.bl_rna.identifier == 'Mesh':
# Mesh data was updated, refresh highlighting
context = bpy.context
apply_topology_highlighting(context)
break
@persistent
def scene_update_handler(scene):
"""Handler for real-time updates in Edit mode"""
# Check if highlight topology is active
if hasattr(scene, 'ns_toolkit_props') and scene.ns_toolkit_props.highlight_topology:
context = bpy.context
# Only update if in Edit mode to avoid performance issues
if context.mode == 'EDIT_MESH':
apply_topology_highlighting(context)
# --- Operator utama ---
class OBJECT_OT_tris_to_quads_merge(bpy.types.Operator):
"""Convert selected meshes: Tris → Quads + Merge by Distance + Recalculate Normals + Reset Vectors"""
bl_idname = "object.tris_to_quads_merge"
bl_label = "Clean Mesh"
bl_options = {'REGISTER', 'UNDO'}
merge_distance = bpy.props.FloatProperty(
name="Merge Distance",
description="Distance threshold for merging vertices",
default=0.0001,
min=0.0,
max=1.0,
precision=5
)
def execute(self, context):
selected_objects = [obj for obj in context.selected_objects if obj.type == 'MESH']
if not selected_objects:
self.report({'WARNING'}, "No mesh objects selected.")
return {'CANCELLED'}
# Get the merge distance value safely
merge_dist = getattr(self, 'merge_distance', 0.0001)
if not isinstance(merge_dist, (int, float)):
merge_dist = 0.0001
processed_count = 0
for obj in selected_objects:
# Ensure we're in Object mode (direct property access, no bpy.ops)
if context.active_object and context.active_object.mode != 'OBJECT':
# Force object mode directly
bpy.context.view_layer.objects.active = obj
if obj.mode != 'OBJECT':
# This is safer than bpy.ops - forces object mode
obj.mode = 'OBJECT'
# Set object as active
context.view_layer.objects.active = obj
# Get mesh data
mesh = obj.data
# Create bmesh instance
bm = bmesh.new()
try:
# Load mesh data into bmesh
bm.from_mesh(mesh)
# Ensure face indices are valid
bm.faces.ensure_lookup_table()
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
# 1. Convert triangles to quads using bmesh.ops
bmesh.ops.join_triangles(
bm,
faces=bm.faces,
angle_face_threshold=0.698132, # 40 degrees (default)
angle_shape_threshold=0.698132
)
# 2. Remove doubles (merge vertices by distance) using bmesh.ops
bmesh.ops.remove_doubles(
bm,
verts=bm.verts,
dist=merge_dist
)
# 3. Recalculate normals using bmesh.ops
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
# 4. Clear custom split normals data
bm.faces.ensure_lookup_table()
for face in bm.faces:
face.smooth = False # Reset smooth shading
# Update mesh data from bmesh
bm.to_mesh(mesh)
# Update object
mesh.update()
obj.update_tag()
processed_count += 1
except Exception as e:
self.report({'WARNING'}, f"Error processing {obj.name}: {str(e)}")
finally:
# Always free bmesh memory
bm.free()
if processed_count > 0:
# Update depsgraph to refresh viewport
context.evaluated_depsgraph_get().update()
self.report({'INFO'}, f"Cleaned {processed_count} mesh(es) successfully")
else:
self.report({'WARNING'}, "No meshes were processed successfully.")
return {'FINISHED'}
# --- Highlight Topology Operators ---
class NS_TOOLKIT_OT_highlight_topology(bpy.types.Operator):
"""Highlight mesh topology for ALL objects in scene using viewport color override"""
bl_idname = "ns_toolkit.highlight_topology"
bl_label = "Highlight Topology"
bl_options = set()
def execute(self, context):
# Get ALL mesh objects in scene (not just selected)
mesh_objects = [obj for obj in context.scene.objects if obj.type == 'MESH']
if not mesh_objects:
self.report({'WARNING'}, "No mesh objects found in scene.")
return {'CANCELLED'}
# Ensure we're in Object mode (avoid using bpy.ops)
if context.active_object and context.active_object.mode != 'OBJECT':
# Direct mode setting without bpy.ops
context.active_object.mode = 'OBJECT'
# Apply highlighting using the shared function
apply_topology_highlighting(context)
# Count colored objects for report
red_count = sum(1 for obj in mesh_objects if obj.color[:3] == (1.0, 0.0, 0.0))
green_count = sum(1 for obj in mesh_objects if obj.color[:3] == (0.0, 1.0, 0.0))
blue_count = sum(1 for obj in mesh_objects if obj.color[:3] == (0.0, 0.0, 1.0))
self.report({'INFO'}, f"Highlighted {len(mesh_objects)} objects: {red_count} Triangle, {green_count} Quad, {blue_count} N-gon")
return {'FINISHED'}
class NS_TOOLKIT_OT_clear_highlight(bpy.types.Operator):
"""Clear mesh topology highlighting and restore original colors for ALL objects"""
bl_idname = "ns_toolkit.clear_highlight"
bl_label = "Clear Highlight"
bl_options = set()
def execute(self, context):
# Get ALL mesh objects in scene
mesh_objects = [obj for obj in context.scene.objects if obj.type == 'MESH']
if not mesh_objects:
self.report({'INFO'}, "No mesh objects found in scene.")
return {'FINISHED'}
# Clear highlighting using the shared function
clear_topology_highlighting(context)
self.report({'INFO'}, f"Topology highlighting cleared for {len(mesh_objects)} objects")
return {'FINISHED'}
# --- Panel di Sidebar (N) ---
class VIEW3D_PT_ns_toolkit_panel(bpy.types.Panel):
bl_label = "NS Toolkit"
bl_idname = "VIEW3D_PT_ns_toolkit_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "NS Toolkit"
def draw(self, context):
layout = self.layout
scene = context.scene
ns_props = scene.ns_toolkit_props
# Highlight topology section
box = layout.box()
box.label(text="Topology Visualization", icon="SHADING_WIRE")
# Toggle for highlight topology
row = box.row(align=True)
row.prop(ns_props, "highlight_topology", text="Highlight All Objects", icon="RESTRICT_VIEW_OFF" if ns_props.highlight_topology else "RESTRICT_VIEW_ON")
if ns_props.highlight_topology:
col = box.column(align=True)
col.scale_y = 0.8
col.label(text="Blue = Has N-gons", icon="NONE")
col.label(text="Red = Triangle-dominant", icon="NONE")
col.label(text="Green = Quad-dominant", icon="NONE")
# Main tool section
layout.separator()
box = layout.box()
box.label(text="Mesh Cleanup Actions", icon="MESH_DATA")
box.operator(OBJECT_OT_tris_to_quads_merge.bl_idname, text="Clean Mesh", icon="FACE_MAPS")
# Credit section
layout.separator()
col = layout.column(align=True)
col.scale_y = 0.7
# Use module-level version constant
version_str = f"v{ADDON_VERSION[0]}.{ADDON_VERSION[1]}.{ADDON_VERSION[2]}"
col.label(text=f"NS Toolkit {version_str} © 2025", icon="INFO")
col.label(text="Created by Nandur Studio")
# --- Shortcut ---
addon_keymaps = []
def register_shortcut():
wm = bpy.context.window_manager
km = wm.keyconfigs.addon.keymaps.new(name='Object Mode', space_type='EMPTY')
kmi = km.keymap_items.new(OBJECT_OT_tris_to_quads_merge.bl_idname, 'J', 'PRESS', ctrl=True, alt=True)
addon_keymaps.append((km, kmi))
def unregister_shortcut():
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
# --- Registrasi ---
def menu_func(self, context):
self.layout.separator()
self.layout.operator(OBJECT_OT_tris_to_quads_merge.bl_idname, icon="FACE_MAPS")
def register():
bpy.utils.register_class(NS_ToolkitProperties)
bpy.utils.register_class(OBJECT_OT_tris_to_quads_merge)
bpy.utils.register_class(NS_TOOLKIT_OT_highlight_topology)
bpy.utils.register_class(NS_TOOLKIT_OT_clear_highlight)
bpy.utils.register_class(VIEW3D_PT_ns_toolkit_panel)
bpy.types.Scene.ns_toolkit_props = bpy.props.PointerProperty(type=NS_ToolkitProperties)
bpy.types.VIEW3D_MT_object_convert.append(menu_func)
register_shortcut()
def unregister():
# Clear any active highlighting and remove handlers
if depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_handler)
if scene_update_handler in bpy.app.handlers.scene_update_pre:
bpy.app.handlers.scene_update_pre.remove(scene_update_handler)
unregister_shortcut()
bpy.types.VIEW3D_MT_object_convert.remove(menu_func)
del bpy.types.Scene.ns_toolkit_props
bpy.utils.unregister_class(VIEW3D_PT_ns_toolkit_panel)
bpy.utils.unregister_class(NS_TOOLKIT_OT_clear_highlight)
bpy.utils.unregister_class(NS_TOOLKIT_OT_highlight_topology)
bpy.utils.unregister_class(OBJECT_OT_tris_to_quads_merge)
bpy.utils.unregister_class(NS_ToolkitProperties)
if __name__ == "__main__":
register()