diff --git a/modules/software/utilities/addons/quad_remesher_1_4/QuadRemesher_Version.txt b/modules/software/utilities/addons/quad_remesher_1_4/QuadRemesher_Version.txt
new file mode 100644
index 0000000..8310fe7
--- /dev/null
+++ b/modules/software/utilities/addons/quad_remesher_1_4/QuadRemesher_Version.txt
@@ -0,0 +1 @@
+1.4.1.3852
diff --git a/modules/software/utilities/addons/quad_remesher_1_4/__init__.py b/modules/software/utilities/addons/quad_remesher_1_4/__init__.py
new file mode 100644
index 0000000..8499972
--- /dev/null
+++ b/modules/software/utilities/addons/quad_remesher_1_4/__init__.py
@@ -0,0 +1,324 @@
+# "Quad-Remesher Bridge for Blender"
+# Author : Maxime Rouca
+#
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see .
+#
+# ##### END GPL LICENSE BLOCK #####
+
+#
+
+__QR_plugin_version__ = "1.4"
+
+bl_info = {
+ "name": "Quad Remesher 1.4 Bridge",
+ "author": "Maxime",
+ "version": (1, 4, 1),
+ "blender": (2, 80, 0),
+ "description": "Quad Remesher 1.4 Bridge",
+ "location": "",
+ "warning": "",
+ "wiki_url": "",
+ "tracker_url": "",
+ "category": "Mesh"
+}
+
+import bpy
+import bpy.props
+from rna_keymap_ui import draw_kmi
+from bl_operators.presets import AddPresetBase
+
+from .qr_operators import (QREMESHER_OT_remesh, QREMESHER_OT_reset_settings, QREMESHER_OT_license_manager, QREMESHER_OT_facemap_to_materials, QREMESHER_OT_faceset_to_materials, QREMESHER_OT_online_help, QREMESHER_OT_News_LatestVer)
+
+addon_name = __name__.split(".")[0]
+
+#def addon_prefs():
+# return bpy.context.preferences.addons[addon_name].preferences
+
+def paintDensityPropertyCB(self, context):
+ #try:
+ props = bpy.context.scene.qremesher
+ vertexColorSliderValue = getattr(props, 'painted_quad_density')
+
+ #print("vertexColorSliderValue = " + str(vertexColorSliderValue) + "\n")
+
+ #Mapping: Slider in [0.25, 4]
+ maxSliderValue = 4
+ minSliderValue = 0.25
+ normalizedValue = 0.0
+ if vertexColorSliderValue > 1.0:
+ normalizedValue = (vertexColorSliderValue - 1.0) / (maxSliderValue - 1.0)
+ elif vertexColorSliderValue < 1.0:
+ normalizedValue = - ((1.0/vertexColorSliderValue) - 1.0) / ((1.0/minSliderValue) - 1.0)
+
+ if (normalizedValue > 1):
+ normalizedValue = 1
+ if (normalizedValue < -1):
+ normalizedValue = -1
+
+ # -- normalizedValue to color
+ r = 1.0
+ g = 1.0
+ b = 1.0
+ if normalizedValue > 0.0:
+ r = 1
+ g = 1-normalizedValue
+ b = 1-normalizedValue
+ elif normalizedValue < 0.0:
+ r = 1+normalizedValue
+ g = 1
+ b = 1
+
+ # set the color
+ mycolor=(r, g, b)
+ # from Blender 4.3
+ if bpy.app.version >= (4,3,0):
+ bpy.context.tool_settings.unified_paint_settings.color = mycolor
+ else:
+ bpy.data.brushes["Draw"].color = mycolor
+
+ #except Exception:
+ # print("Exception: in paintDensityPropertyCB..\n")
+ return
+
+
+
+# ----- Properties container ------
+class QRSettingsPropertyGroup(bpy.types.PropertyGroup):
+ # Target Quad Count
+ target_count: bpy.props.IntProperty(name="Quad Count", description="Set the desired number of Quads",
+ default=5000, soft_min=100, soft_max=10000, step=20, min = 1)
+
+ curvatureAdaptivness_Tooltip = "Allows to control how quad's size locally adapts to curvatures.\nThe higher it is, the smaller the quads will be on high curvature areas.\nSet it at 0, to get uniform quads size"
+ adaptQuadCount_Tooltip = "Adaptive Quad-Count :\nOn: Creates more polygons than asked to fit high curvatures area. \nOff(default): Respect the Target-Quad-Count more exactly.\nIt's advised to let it 'Off' to better respect the Target-Quad-Count. see the doc for more explanations. "
+ useVertexColors_Tooltip = "Use 'Vertex Colors' to control Quads size density."
+ vertexColorWidget_Tooltip = "Defines the Color to paint to control locally the desired Quad Density variations (using 'Draw' Tool, in 'Vertex Paint' mode) \n . from 0.25 => 'divide density by 4' = Big Quads = Cyan color \n . to 4 => 'multiply density by 4' = Small Quads = Red color."
+ useMaterials_Tooltip="If On, QuadRemesher will use existing 'Materials' to guide the quad remeshing on Materials borders.\nMaterialIds will be maintained after remeshing."
+ useNormals_Tooltip="TAKE CARE: this option is usefull in specific cases, BUT should be 'Off' by default (facetted mesh). Read the doc for more informations..."
+ useNormals_Tooltip+="\nIf On, QuadRemesher will use the existing 'Normals' to guide the remeshing on edge loops where the normals are split/creased.\nBy default Blenders creates mesh with normals split everywhere.\nIt usefull to enable this option only with SmoothShade + AutoSmooth enabled...\nOn smooth organic shapes, it's advised to disable it."
+ detectHardEdges_Tooltip="If On, QuadRemesher will detect/compute Hard-Edges automatically based on the geometry (using a mix of edge's angles and other geometrical considerations).\nIf 'Use Normals Splitting' is checked, it's often better to uncheck 'Detect Hard Edges by angle'.\nOn smooth organic shapes, it's advised to disable it."
+ symToolTip = "These options allows to perform symmetrical quad remeshing. It's possible to combine all 3 symmetry axis."
+ #symToolTip += "\nTAKE CARE: The axis are Local Coordinates axis! It's advised to set the Gizmo in 'Object' mode to better see the Local Coordinates axis."
+ hideInputTip = "If On (default), the input object will be hidden after remeshing."
+
+ # Quads size settings
+ adaptive_size: bpy.props.FloatProperty(name="Adaptive size",
+ description=curvatureAdaptivness_Tooltip,
+ default=50, min=0, max=100, step=0.5, precision=0, subtype = 'PERCENTAGE')
+
+ adapt_quad_count: bpy.props.BoolProperty(name="Adapt Quad Count", default=True,
+ description=adaptQuadCount_Tooltip)
+
+ use_vertex_color: bpy.props.BoolProperty(name="Use Vertex Color",
+ description=useVertexColors_Tooltip,
+ default=False)
+
+ painted_quad_density: bpy.props.FloatProperty(name="Quads density (paint)",
+ description=vertexColorWidget_Tooltip,
+ default=1.0, min=0.25, max=4.0, step=0.4,
+ update=paintDensityPropertyCB)
+
+ # Edge loops control
+ use_materials: bpy.props.BoolProperty(name="Use Materials", default=False,
+ description = useMaterials_Tooltip)
+ use_normals: bpy.props.BoolProperty(name="Use Normals Splitting",
+ default=False, #because when I create a sphere, all edges are 'creased', not a good idea to enable this by default...
+ description=useNormals_Tooltip)
+ autodetect_hard_edges: bpy.props.BoolProperty(name="Detect Hard Edges by angle",
+ default=True,
+ description=detectHardEdges_Tooltip)
+
+ # Misc category
+ symmetry_x: bpy.props.BoolProperty(name="X", default=False, description=symToolTip)
+ symmetry_y: bpy.props.BoolProperty(name="Y", default=False, description=symToolTip)
+ symmetry_z: bpy.props.BoolProperty(name="Z", default=False, description=symToolTip)
+
+ hide_input: bpy.props.BoolProperty(name="Hide Input Object", default=True, description=hideInputTip)
+
+ # progress bar value
+ progress_value: bpy.props.FloatProperty(default=0, subtype='PERCENTAGE', precision=1, min=0, soft_min=0, soft_max=100, max=100)
+
+
+
+def draw_panel_content(context, layout):
+ #print("draw_panel_content called")
+ # NB: this function is called very often (example, quite at each mouse move in the panel...)
+
+ props = bpy.context.scene.qremesher
+ #props = addon_prefs().props
+
+ wm = context.window_manager
+
+ # "REMESH IT" button
+# if QREMESHER_OT_remesh.IsRemeshing:
+# myrow = layout.row(align=True)
+# myrow.label(text="(ESC)")
+# myrow.prop(props, 'progress_value')
+# #layout.prop(props, 'progress_value')
+ layout.operator(QREMESHER_OT_remesh.bl_idname)
+ layout.separator()
+
+ # Settings
+ col = layout.column(align=True)
+ # row = col.row(align=True)
+ col.prop(props, 'target_count')
+
+ col.separator()
+
+ # --- Quad Size settings ---
+ box = col.box()
+ box.label(text=" Quad Size Settings")
+
+ box.prop(props, 'adaptive_size')
+ box.prop(props, 'adapt_quad_count')
+ box.prop(props, 'use_vertex_color')
+
+ #box.separator()
+ box.prop(props, 'painted_quad_density')
+
+ col.separator()
+
+ # --- Quad Size settings ---
+ box = col.box()
+ box.label(text=" Edge Loops Control")
+
+ box.prop(props, 'use_materials')
+ box.prop(props, 'use_normals')
+ box.prop(props, 'autodetect_hard_edges')
+
+ col.separator()
+
+ # --- Misc.... ---
+ box = col.box()
+ box.label(text=" Misc")
+ box.label(text="Symmetry:")
+ myrow = box.row(align=True)
+ myrow.prop(props, 'symmetry_x')
+ myrow.prop(props, 'symmetry_y')
+ myrow.prop(props, 'symmetry_z')
+ box.prop(props, 'hide_input')
+ box.operator(QREMESHER_OT_reset_settings.bl_idname)
+ box.operator(QREMESHER_OT_license_manager.bl_idname)
+ if bpy.app.version[0] >= 4:
+ box.operator(QREMESHER_OT_faceset_to_materials.bl_idname)
+ else:
+ box.operator(QREMESHER_OT_facemap_to_materials.bl_idname)
+ box.operator(QREMESHER_OT_online_help.bl_idname)
+
+ box.operator(QREMESHER_OT_News_LatestVer.bl_idname, text=QREMESHER_OT_News_LatestVer.getNewsButtonLabel())
+
+
+
+# Side panel ui
+class QREMESHER_PT_qremesher(bpy.types.Panel):
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_category = 'Quad Remesh' # name of the VerticalTab
+ bl_label = "Quad Remesher "+__QR_plugin_version__
+
+ bl_idname = "QREMESHER_PT_qremesher"
+
+ # bl_options = {'DEFAULT_CLOSED'}
+
+ @classmethod
+ def poll(cls, context):
+ return True
+
+ def draw(self, context):
+ draw_panel_content(context, self.layout)
+
+
+# Scene settings subpanel
+'''
+class QREMESHER_PT_qremesher_setting_panel(bpy.types.Panel):
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_category = 'QRemesher'
+ bl_label = "Settings"
+
+ bl_idname = "QREMESHER_PT_qremesher_setting_panel"
+ bl_parent_id = "QREMESHER_PT_qremesher" # NB: ca suffit a ajouter ce sub panel dans le panel QREMESHER_PT_qremesher
+
+ # bl_options = {'DEFAULT_CLOSED'}
+
+ @classmethod
+ def poll(cls, context):
+ return True
+
+ def draw(self, context):
+
+ self.layout.operator(QREMESHER_OT_reset_settings.bl_idname)
+
+ self.layout.separator()
+
+ #draw_panel_content(context, self.layout)
+'''
+
+
+classes = [QRSettingsPropertyGroup,
+
+ QREMESHER_PT_qremesher,
+ #QREMESHER_PT_qremesher_setting_panel,
+
+ QREMESHER_OT_remesh,
+ QREMESHER_OT_reset_settings,
+ QREMESHER_OT_license_manager,
+ QREMESHER_OT_facemap_to_materials, # Blender 3.x
+ QREMESHER_OT_faceset_to_materials, # Blender 4.x
+ QREMESHER_OT_online_help,
+ QREMESHER_OT_News_LatestVer,
+ ]
+addon_keymaps = []
+
+
+def hotkeys():
+ wm = bpy.context.window_manager
+ kc = wm.keyconfigs.addon
+
+ if kc:
+ if '3D View' not in kc.keymaps:
+ km_view3d = kc.keymaps.new('3D View', space_type='VIEW_3D', region_type='WINDOW')
+ else:
+ km_view3d = kc.keymaps['3D View']
+
+ kmi = km_view3d.keymap_items.new(QREMESHER_OT_remesh.bl_idname, head=True, type='R', value='PRESS',
+ ctrl=True, alt=True)
+
+ addon_keymaps.append((km_view3d, kmi))
+
+
+def register():
+ for cls in classes:
+ bpy.utils.register_class(cls)
+
+ bpy.types.Scene.qremesher = bpy.props.PointerProperty(type=QRSettingsPropertyGroup)
+ QREMESHER_OT_News_LatestVer.readNews()
+
+ hotkeys()
+
+
+def unregister():
+ for cls in classes:
+ bpy.utils.unregister_class(cls)
+
+ del bpy.types.Scene.qremesher
+
+ wm = bpy.context.window_manager
+ kc = wm.keyconfigs.addon
+
+ if kc:
+ for km, kmi in addon_keymaps:
+ km.keymap_items.remove(kmi)
+ addon_keymaps.clear()
diff --git a/modules/software/utilities/addons/quad_remesher_1_4/qr_exe.py b/modules/software/utilities/addons/quad_remesher_1_4/qr_exe.py
new file mode 100644
index 0000000..98e81e1
--- /dev/null
+++ b/modules/software/utilities/addons/quad_remesher_1_4/qr_exe.py
@@ -0,0 +1,79 @@
+# "Quad-Remesher Bridge for Blender"
+# Author : Maxime Rouca
+#
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see .
+#
+# ##### END GPL LICENSE BLOCK #####
+
+import bpy
+
+import os
+import subprocess
+import sys
+import platform
+import shutil
+import tempfile
+import time
+
+
+def qr_launch_prefix():
+ launcher = os.environ.get("QUADREMESHER_LAUNCHER", "").strip()
+ return launcher.split() if launcher else []
+
+
+def writeSettingsFile(settingsFilename, inputFilename, theOp, enableTiming):
+ props = bpy.context.scene.qremesher
+
+ settings_file = open(settingsFilename, "w")
+ settings_file.write('HostApp=Blender\n')
+ settings_file.write('HostAppVer=%s\n' % bpy.app.version_string)
+ settings_file.write('FileIn="%s"\n' % inputFilename)
+ settings_file.write('FileOut="%s"\n' % theOp.retopoFilename)
+ settings_file.write('ProgressFile="%s"\n' % theOp.progressData.ProgressFilename)
+
+ settings_file.write("TargetQuadCount=%s\n" % str(getattr(props, 'target_count')))
+ settings_file.write("CurvatureAdaptivness=%s\n" % str(getattr(props, 'adaptive_size')))
+ settings_file.write("ExactQuadCount=%d\n" % (not getattr(props, 'adapt_quad_count')))
+
+ settings_file.write("UseVertexColorMap=%s\n" % str(getattr(props, 'use_vertex_color')))
+
+ settings_file.write("UseMaterialIds=%d\n" % getattr(props, 'use_materials'))
+ settings_file.write("UseIndexedNormals=%d\n" % getattr(props, 'use_normals'))
+ settings_file.write("AutoDetectHardEdges=%d\n" % getattr(props, 'autodetect_hard_edges'))
+
+ symAxisText = ''
+ if getattr(props, 'symmetry_x') : symAxisText = symAxisText + 'X'
+ if getattr(props, 'symmetry_y') : symAxisText = symAxisText + 'Y'
+ if getattr(props, 'symmetry_z') : symAxisText = symAxisText + 'Z'
+ if symAxisText != '':
+ settings_file.write('SymAxis=%s\n' % symAxisText)
+ settings_file.write("SymLocal=1\n")
+
+ if enableTiming:
+ settings_file.write("Timing_StartRemesh_Time=%f\n" % chrono_startRemeshingTime)
+ settings_file.write("Timing_InputMeshExported_Time=%f\n" % time.time()) # this time must include (partially) the txt settings writing.
+
+ settings_file.close()
+
+def exe_launchRemeshing(enginePath, settingsFilename, theOp, verboseDebug):
+ if (verboseDebug):
+ print("Launch : path=" + enginePath + "\n settings_path=" + settingsFilename + "\n")
+
+ theOp.progressData.RemeshingProcess = subprocess.Popen([*qr_launch_prefix(), enginePath, "-s", settingsFilename])
+
+ if (verboseDebug):
+ print(" -> theOp.progressData.RemeshingProcess = " + str(theOp.progressData.RemeshingProcess) + "\n")
+ #NB: theOp.progressData.RemeshingProcess.poll()!=None can be done just after subprocess.Popen(...), no need to wait for something... (checked)
diff --git a/modules/software/utilities/addons/quad_remesher_1_4/qr_operators.py b/modules/software/utilities/addons/quad_remesher_1_4/qr_operators.py
new file mode 100644
index 0000000..9938ef3
--- /dev/null
+++ b/modules/software/utilities/addons/quad_remesher_1_4/qr_operators.py
@@ -0,0 +1,1210 @@
+# "Quad-Remesher Bridge for Blender"
+# Author : Maxime Rouca
+#
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see .
+#
+# ##### END GPL LICENSE BLOCK #####
+
+
+import bpy
+
+import os
+import stat # for mac fix chmod
+import subprocess
+import sys
+import platform
+import shutil
+import tempfile
+import time
+
+g_useDLL = False
+if g_useDLL:
+ from .qr_dll import *
+else:
+ from .qr_exe import *
+
+
+# --- global variables ---
+verboseDebug = False
+g_enableTiming = False
+
+__QR_plugin_version__ = "1.4"
+__QR_plugin_versionMinorMinor__ = "1"
+
+
+# ------------------- Check Progress ---------------------
+# Returns (ProgressValueFloat, ProgressText, SleepTimeAdvice)
+# ProgressValues:
+# . in [0, 1] : computing
+# . 2 : success
+# . -3 or -4: remesher crashed
+# . -10: "no progress file, after significant time"
+# . -11: bad progress file format
+# . -xxx : errors returned by engine
+class QRCheckProgressData:
+ def __init__(self):
+ self.ProgressFilename = ''
+ self.CheckProgressCounter = 0
+ self.RemeshingProcess = None
+ self.StartRemeshingTime = 0
+ self.ProgressFileHasBeenCreatedYet = False
+
+ def reset(self):
+ self.ProgressFilename = ''
+ self.CheckProgressCounter = 0
+ self.RemeshingProcess = None
+ self.StartRemeshingTime = 0
+ self.ProgressFileHasBeenCreatedYet = False
+
+ def onStartRemeshing(self): # called just before subprocess.Popen(...)
+ self.CheckProgressCounter = 0
+ self.StartRemeshingTime = time.time()
+ self.ProgressFileHasBeenCreatedYet = False
+
+ def get_progress_status(self):
+
+ self.CheckProgressCounter = self.CheckProgressCounter+1
+
+ # outputs:
+ ProgressText = ""
+ ProgressValueFloat = None
+ SleepTimeAdvice = None
+
+ # read progress file:
+ progressLines=[]
+ try:
+ pf = open(self.ProgressFilename, "r")
+ progressLines = pf.read().splitlines()
+ pf.close()
+ except Exception:
+ # progressFile does not exist or can't be open!
+ CurTimeFromStart = time.time() - self.StartRemeshingTime
+ if self.ProgressFileHasBeenCreatedYet == False: # and progressFile has not yet been created previously
+ if CurTimeFromStart < 3 :
+ return 0, "", None # simply wait more
+ elif CurTimeFromStart < 40 : # after 3 seconds without progressFile...
+ if (self.RemeshingProcess != None and self.RemeshingProcess.poll() != None):
+ ProgressText = "Remeshing Failed! (Returned without ProgressFile - 0)"
+ return -4, ProgressText, None
+ return 0, "", None
+ else: # after 40 seconds without progressFile... => return error !
+ print(' ERROR : no progressFile after 40 sec....')
+ return -10, "", None
+ else:
+ return 0, "", None # strange case: progressFile can't be read (dont exist) but has already been created...)
+
+ self.ProgressFileHasBeenCreatedYet = True
+
+ if len(progressLines)>=1:
+ try:
+ ProgressValueFloat = float(progressLines[0]) # value in [0,1] or <0 : error
+ except Exception:
+ return -11, "", None
+
+ if ProgressValueFloat != None:
+ # Error ?
+ if (ProgressValueFloat < 0):
+ if len(progressLines)>=2:
+ ProgressText = progressLines[1]
+
+ return ProgressValueFloat, ProgressText, 0
+
+ # Succeded ?
+ if ProgressValueFloat == 2:
+ return ProgressValueFloat, "", 0
+
+ # check process is running: (NB need to be done in 2 cases : during remeshing + if remesher can't
+ #if (GetProgressCounter > 5 and GetProgressCounter % 4 == 0): # only every 4 timers (~1 sec)
+ if (self.CheckProgressCounter > 5 and self.CheckProgressCounter % 4 == 0): # only every 4 timers (~1 sec)
+ #if (RemeshingProcess.poll() != None):
+ if (self.RemeshingProcess != None and self.RemeshingProcess.poll() != None):
+ ProgressText = "Remeshing Failed! (Stopped without result)"
+ return -3, ProgressText, 0
+
+ # computing...
+ if len(progressLines)>=3:
+ try:
+ SleepTimeAdvice = float(progressLines[2])
+ except Exception:
+ SleepTimeAdvice = None
+
+ return ProgressValueFloat, ProgressText, SleepTimeAdvice
+
+ # check process is running: (before any no progress file written)
+ if (self.CheckProgressCounter > 8 and self.CheckProgressCounter % 4 == 0):
+ if (self.RemeshingProcess != None and self.RemeshingProcess.poll() != None):
+ ProgressValueFloat = -4 # this means that the remesher crashed
+ ProgressText = "Remeshing Failed! (Returned without ProgressFile)"
+ return ProgressValueFloat, ProgressText, None
+
+ ProgressValueFloat = -11
+ ProgressText = "Remeshing Failed! (Bad ProgressFile Data)"
+ return ProgressValueFloat, ProgressText, None
+
+
+# ----- misc functions -----
+# print() fait un print dans la console windows (terminal)
+# console_print() fait un print dans la Python console si elle est ouverte ! (+ print in windows terminal)
+def console_print(*args, clear=False):
+ s = " ".join([str(arg) for arg in args])
+ print(s)
+ for a in bpy.context.screen.areas:
+ if a.type == 'CONSOLE':
+ c = {}
+ c['area'] = a
+ c['space_data'] = a.spaces.active
+ c['region'] = a.regions[-1]
+ c['window'] = bpy.context.window
+ c['screen'] = bpy.context.screen
+
+ if clear:
+ bpy.ops.console.clear(c)
+
+ for line in s.split("\n"):
+ bpy.ops.console.scrollback_append(c, text=line)
+ if (verboseDebug): print(line)
+
+def unixifyPath(path):
+ path = path.replace('\\', '/')
+ return path
+
+def getHostAppVer():
+ return str(bpy.app.version[0]) + "." + str(bpy.app.version[1])
+
+def getQREngineFolder():
+ isMacOS = (platform.system()=="Darwin") or (platform.system()=="macosx")
+ isLinux = (platform.system()=="Linux")
+ if (isMacOS):
+ engineFolder = "/Users/Shared/Exoside/QuadRemesher/Datas_Blender/QuadRemesherEngine_"+__QR_plugin_version__
+ elif isLinux:
+ engineFolder = os.path.expanduser("~/.local/share/Exoside/QuadRemesher/Datas_Blender/QuadRemesherEngine_"+__QR_plugin_version__)
+ else:
+ #appData = os.getenv('APPDATA') windows ... UserName/../Roaming...
+ appData = os.getenv('ALLUSERSPROFILE') # on windows : C:\Users\All Users == C:\ProgramData\
+ engineFolder = os.path.join(appData, "Exoside/QuadRemesher/Datas_Blender/QuadRemesherEngine_"+__QR_plugin_version__)
+ return engineFolder
+
+def getEnginePath():
+ engineFolder = getQREngineFolder()
+ isMacOS = (platform.system()=="Darwin") or (platform.system()=="macosx")
+ isLinux = (platform.system()=="Linux")
+ if isMacOS :
+ enginePath = os.path.join(engineFolder, "xremesh")
+ elif isLinux:
+ enginePath = os.path.join(engineFolder, "xremesh")
+ else:
+ enginePath = os.path.join(engineFolder, "xremesh.exe")
+ return enginePath
+
+def debug_returnLoadRetopoTime(startLoadRetopoTime, endLoadRetopoTime):
+ try:
+ enginePath = getEnginePath()
+ subprocess.Popen([*qr_launch_prefix(), enginePath, "-returnChrono", "-startImportRetopoTime", str(startLoadRetopoTime), "-endImportRetopoTime", str(endLoadRetopoTime)])
+ except:
+ return None
+
+def export_selected_mesh_fbx(filepath):
+ try:
+ # bpy.ops.export_scene.fbx(filepath=filepath, use_selection=True, bake_anim=False)
+ # new in 1.4
+ # fix: issue : previous code with suzanne and no Rotation : simple export and import FBX with default axis conversion leads to different matrix !!!
+ # So we use, option that saves as it is in Blender (raw values), and during import we copy the matrix
+ bpy.ops.export_scene.fbx(filepath=filepath, use_selection=True, bake_anim=False, global_scale=1, apply_unit_scale=False, apply_scale_options='FBX_SCALE_NONE', use_space_transform=False, axis_forward='-X', axis_up='Z')
+ except Exception:
+ import traceback
+ print("fbx export exception: " + str(traceback.format_exc()) + "\n")
+ return False
+ return True
+
+'''
+def export_selected_mesh_extra_maps(filepath, o):
+ # https://docs.blender.org/api/current/bpy.types.FaceMaps.html
+ # https://docs.blender.org/api/2.90/bpy.types.FaceMap.html
+ mapCount = len(o.face_maps)
+ print ("MapCount = %d" % mapCount)
+ index = -1
+ for fm in o.face_maps:
+ index = index+1
+ print ("------------- FaceMap[%d] %d name=", index, fm.index, fm.name)
+ # select this face map
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.mesh.select_all(action='DESELECT')
+ o.face_maps.active_index = index
+
+ # select all faces of this face map
+ bpy.ops.object.face_map_select()
+
+ # set materialId for selected faces:
+ # NB: in Edit Mode ".select" returns the selected status when entering the EditMode !!! ????!!!!!
+ bpy.ops.object.mode_set(mode='OBJECT')
+ for f in o.data.polygons:
+ #print("test select polygon %d -> %d" % (f.index, f.select))
+ if f.select:
+ print("face[%d] FaceSet = %d" % (f.index, index))
+ #f.material_index = matIndex
+'''
+
+# NB: the imported objects are automatically selected.
+def import_mesh_fbx(filepath):
+ # https://docs.blender.org/api/current/bpy.ops.import_scene.html
+ # bpy.ops.import_scene.fbx(filepath=filepath)
+ # new 1.4 :
+ # use_manual_orientation = Manual Orientation, Specify orientation and scale, instead of using embedded data in FBX file
+ # axis_forward + axis_up
+ bpy.ops.import_scene.fbx(filepath=filepath, global_scale=1, use_manual_orientation=True, axis_forward='-X', axis_up='Z')
+
+ sel_objects = bpy.context.selected_objects
+ if len(sel_objects) >= 1:
+ retopo_object = sel_objects[0]
+ #2.79 : bpy.context.scene.objects.active = retopo_object
+ bpy.context.view_layer.objects.active = retopo_object
+ #bpy.context.render_layer.objects.active = retopo_object
+
+
+
+def fixExecutableMode(path):
+ st = os.stat(path)
+ # NB:S_IEXEC=executeByUser S_IXUSR=executeByUser S_IXGRP=executeByGroup S_IXOTH=executeByOther
+ newMode = st.st_mode | (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
+ if (st.st_mode != newMode):
+ os.chmod(path, newMode)
+
+
+#def IsQuadRemesherEngineInstalled(enginePath):
+#return os.path.exists(enginePath)
+
+# return values:
+# 0 : nothing done
+# 1 : if the QuadRemesher has been installed
+# 2,3 : if the QuadRemesher has not been downloaded/installed : error!
+# NB: enginePath can be either xremesh or xrLicenseManager
+def InstallQuadRemesherEngineIfNeeded(op, context, enginePath):
+ if not os.path.exists(enginePath):
+ console_print("Engine not found: downloading and installing...("+enginePath+")")
+
+ op.report({'WARNING'}, "Downloading QuadRemesher Engine...")
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=2)
+
+ # test invoke
+ #context.window_manager.invoke_confirm(self, event)
+
+ #if (op.ReCallFromTimer_DownloadMsgDone != True):
+ # op.report({'INFO'}, "Downloading QuadRemesher Engine...")
+ # bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+
+ #engineFolder = os.path.join(script_folder, "QuadRemesherEngine")
+ engineFolder = getQREngineFolder()
+ if not os.path.exists(engineFolder):
+ os.makedirs(engineFolder)
+
+ # download first (NB: python version = 3.7 in Blender 2.8)
+ isMacOSX = (platform.system()=="Darwin") or (platform.system()=="macosx")
+ isLinux = (platform.system()=="Linux")
+
+ urlList = []
+ if isMacOSX:
+ #NB: https fails on macOS
+ urlList.append("http://exoside.com/quadremesherdata/download_engine.php?qrVer="+__QR_plugin_version__+"&qrVerMM="+__QR_plugin_versionMinorMinor__+"&os=macOS&ha=Blender&haVer="+getHostAppVer())
+ urlList.append("http://exoside.com/quadremesherdata/quad_remesher_engine_"+__QR_plugin_version__+"_macOS.zip")
+ elif isLinux :
+ urlList.append("http://exoside.com/quadremesherdata/download_engine.php?qrVer="+__QR_plugin_version__+"&qrVerMM="+__QR_plugin_versionMinorMinor__+"&os=linux&ha=Blender&haVer="+getHostAppVer())
+ urlList.append("http://exoside.com/quadremesherdata/quad_remesher_engine_"+__QR_plugin_version__+"_linux.zip")
+ else:
+ urlList.append("https://exoside.com/quadremesherdata/download_engine.php?qrVer="+__QR_plugin_version__+"&qrVerMM="+__QR_plugin_versionMinorMinor__+"&os=win&ha=Blender&haVer="+getHostAppVer())
+ urlList.append("https://exoside.com/quadremesherdata/quad_remesher_engine_"+__QR_plugin_version__+"_win.zip")
+ urlList.append("http://exoside.com/quadremesherdata/quad_remesher_engine_"+__QR_plugin_version__+"_win.zip")
+ #urlList.append("http://www.exoside.com/quadremesherdata/quad_remesher_engine_"+__QR_plugin_version__+"_win.zip")
+
+ downloadOK = False
+ downloadExceptionCount = 0
+ downloadExceptionText = ""
+ zip_file_name = os.path.join(engineFolder, "../install_engine.zip")
+ #zip_file_name = os.path.join(script_folder, "install_engine.zip")
+ for url in urlList:
+ try:
+ import urllib.request
+ urllib.request.urlretrieve(url, zip_file_name)
+ if os.path.exists(zip_file_name):
+ downloadOK = True
+ break
+ except Exception:
+ import traceback
+ downloadExceptionText = downloadExceptionText + "Install remesher Engine ERROR: Download Failed (exception) " + str(traceback.format_exc()) + "\n"
+ downloadExceptionCount = downloadExceptionCount + 1
+ #console_print("Install remesher Engine ERROR: Download Failed (exception) " + str(traceback.format_exc()) + "\n")
+
+ if downloadOK == False:
+ if downloadExceptionCount > 0:
+ console_print(downloadExceptionText)
+ helpLinkStr = "\nPlease check Trouble-Shooting section in the FAQ (https://www.exoside.com/php_ext/qr_tsl.php?ha=Blender&v="+__QR_plugin_version__+"&id=DLengine2)"
+ op.report({'ERROR'}, "Failed to download Quad Remesher Engine (exception)." + helpLinkStr)
+ return 2
+ if not os.path.exists(zip_file_name):
+ helpLinkStr = "\nPlease check Trouble-Shooting section in the FAQ (https://www.exoside.com/php_ext/qr_tsl.php?ha=Blender&v="+__QR_plugin_version__+"&id=DLengine)"
+ op.report({'ERROR'}, "Failed to download Quad Remesher Engine." + helpLinkStr)
+ return 2
+
+ # then unzip
+ op.report({'WARNING'}, "unzipping QuadRemesher Engine...")
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+ try:
+ if isMacOSX:
+ # use command line because pythons unzip is losing attributes + from Ventura, macOS says that xrLicenseManager is damaged if unzipped from python
+ #os.system('unzip "%s" -d "%s"' % (zip_file_name,engineFolder))
+ # using ditto to keep signature and notarization
+ #print('ditto -x -k "%s" "%s"' % (zip_file_name, engineFolder))
+ os.system('ditto -x -k "%s" "%s"' % (zip_file_name, engineFolder))
+
+ else: # Win + Linux
+ from zipfile import ZipFile
+ with ZipFile(zip_file_name, 'r') as zip:
+ zip.extractall(engineFolder)
+ #return 1
+
+ #return 1
+ # fix executable mode:
+ if isMacOSX:
+ fixExecutableMode(os.path.join(engineFolder, 'xremesh'))
+ fixExecutableMode(os.path.join(engineFolder, 'xrLicenseManager.app/Contents/MacOS/xrLicenseManager'))
+ elif isLinux:
+ fixExecutableMode(os.path.join(engineFolder, 'xremesh'))
+ fixExecutableMode(os.path.join(engineFolder, 'xrLicenseManager'))
+
+ except Exception:
+ import traceback
+ console_print("Install remesher Engine ERROR: Unzip Failed" + str(traceback.format_exc()) + "\n")
+ helpLinkStr = "\nPlease check Trouble-Shooting section in the FAQ (https://www.exoside.com/php_ext/qr_tsl.php?ha=Blender&v="+__QR_plugin_version__+"&id=unzipEngine)"
+ op.report({'ERROR'}, "Failed to unzip Quad Remesher Engine." + helpLinkStr)
+ return 3
+
+ #delete zip
+ os.remove(zip_file_name)
+
+ return 1
+
+ return 0
+
+def setSelectedObjectShadeFlat(context):
+ try:
+ sel_objects = context.selected_objects
+ if len(sel_objects) != 1:
+ return
+ obj = sel_objects[0]
+ for f in obj.data.polygons:
+ f.use_smooth = False
+ except Exception:
+ import traceback
+ console_print("setSelectedObjectShadeFlat exception" + str(traceback.format_exc()) + "\n")
+
+
+def transfer_attribute_from_input_obj_to_retopo(inputObject):
+ retopo = bpy.context.view_layer.objects.active
+
+ try:
+ retopo.use_mesh_mirror_x = inputObject.use_mesh_mirror_x
+ retopo.use_mesh_mirror_y = inputObject.use_mesh_mirror_y
+ retopo.use_mesh_mirror_z = inputObject.use_mesh_mirror_z
+ except Exception:
+ a=0 # not an error, use_mesh_mirror_x does not exist in Blender 2.92, added in 2.93!
+
+ # new 1.4 (fix matrix issue)
+ # transfer the transform (QR is using the local mesh coords, returns the fbx with local coordinates)
+ #retopo.transform = inputObject.transform
+ retopo.location = inputObject.location
+
+ retopo.lock_location = inputObject.lock_location
+ retopo.lock_rotation = inputObject.lock_rotation
+ retopo.lock_rotation_w = inputObject.lock_rotation_w
+ retopo.lock_rotations_4d = inputObject.lock_rotations_4d
+ retopo.lock_scale = inputObject.lock_scale
+
+ retopo.matrix_basis = inputObject.matrix_basis
+ retopo.matrix_local = inputObject.matrix_local
+
+ retopo.scale = inputObject.scale
+
+ retopo.rotation_mode = inputObject.rotation_mode
+ retopo.rotation_axis_angle = inputObject.rotation_axis_angle
+ retopo.rotation_euler = inputObject.rotation_euler
+ retopo.rotation_quaternion = inputObject.rotation_quaternion
+
+ # transfer materials:
+ try:
+ mats = inputObject.material_slots
+
+ if len(mats) == len(retopo.material_slots):
+ for i in range(len(mats)):
+ retopo.material_slots[i].material = mats[i].material
+ else:
+ if len(mats) != 0:
+ console_print("warning: original Materials NOT transfered to retopo !")
+ except Exception:
+ console_print("EXCEPTIN material_slots")
+ a=0 # protect from error messages if material_slots was not existing in previous version of Blender ... (exists in 2.83)
+
+
+def doRemeshing_Start(theOp, context) :
+ global g_enableTiming
+
+ #print("------- name = ------")
+ #print(__name__) # -> "quad_remesher.qr_operators"
+ #console_print(__name__)
+ #print("------ Script/addons ------")
+ #print(bpy.utils.user_resource('SCRIPTS', "addons")) # OK can use it!
+ #print("------__file__------")
+ #print(__file__) # -> the real path !!!
+
+ #addon_name = __name__.split(".")[0]
+ #prefs = bpy.context.preferences.addons[addon_name].preferences
+ #args = generate_command_line_args(theOp, prefs)
+
+ if g_enableTiming: chrono_startRemeshingTime = time.time()
+
+ # reset data:
+ theOp.the_input_object = None
+ theOp.retopoFilename = None
+ theOp.progressData.reset()
+ theOp.IsRemeshing = False
+
+ props = bpy.context.scene.qremesher
+
+ # check selection
+ sel_objects = context.selected_objects
+ if len(sel_objects) != 1:
+ theOp.report({'ERROR'}, "You must select one and only one object.")
+ return
+ theOp.the_input_object = sel_objects[0]
+ if theOp.the_input_object.type != 'MESH':
+ theOp.report({'ERROR'}, "You must select one MESH object !")
+ return
+
+
+ # ------------ 0 - set folders and path ----------------
+ isMacOSX = (platform.system()=="Darwin") or (platform.system()=="macosx")
+ isLinux = (platform.system()=="Linux")
+ if isMacOSX :
+ QRTempFolder = "/var/tmp/Exoside"
+ elif isLinux :
+ QRTempFolder = "/tmp/Exoside"
+ else :
+ QRTempFolder = os.path.join(tempfile.gettempdir(), "Exoside")
+ #console_print("TempFolder = "+ QRTempFolder + "\n")
+
+ if not os.path.exists(QRTempFolder):
+ os.makedirs(QRTempFolder)
+
+ export_path = os.path.join(QRTempFolder, "QuadRemesher/Blender")
+ export_path = unixifyPath(export_path)
+
+ if not os.path.exists(export_path):
+ os.makedirs(export_path)
+
+ settingsFilename = os.path.join(export_path, 'RetopoSettings.txt')
+ inputFilename = os.path.join(export_path, 'inputMesh.fbx')
+ inputFilename_extraMaps = os.path.join(export_path, 'inputMesh_extra.exm')
+ theOp.retopoFilename = os.path.join(export_path, 'retopo.fbx')
+ theOp.progressData.ProgressFilename = os.path.join(export_path, 'progress.txt')
+
+ enginePath = getEnginePath()
+
+ # unixify path
+ inputFilename = unixifyPath(inputFilename)
+ inputFilename_extraMaps = unixifyPath(inputFilename_extraMaps)
+ theOp.retopoFilename = unixifyPath(theOp.retopoFilename)
+ theOp.progressData.ProgressFilename = unixifyPath(theOp.progressData.ProgressFilename)
+ settingsFilename = unixifyPath(settingsFilename)
+ enginePath = unixifyPath(enginePath)
+
+ # --------------- install QuadRemesher Engine if needed -------------------
+ installRes = InstallQuadRemesherEngineIfNeeded(theOp, context, enginePath)
+ if installRes == 1: # 1 = Installed
+ theOp.report({'WARNING'}, "QuadRemesher Engine has been downloaded and installed, please click <> again...")
+ #theOp.NeedReCallStartFromTimer = True
+ #return
+ if installRes >= 2: # 2 or 3 or 4
+ return
+
+
+ # -------------- 1 - Export mesh + settings ---------------
+ # 1.0 - remove old files
+ try:
+ if (os.path.isfile(theOp.retopoFilename)):
+ os.remove(theOp.retopoFilename)
+ if (os.path.isfile(theOp.progressData.ProgressFilename)):
+ os.remove(theOp.progressData.ProgressFilename)
+ except Exception:
+ import traceback
+ console_print("remove file error: " + str(traceback.format_exc()) + "\n")
+
+ # 1.1 - Export Selected Mesh
+ if export_selected_mesh_fbx(inputFilename) == False:
+ theOp.report({'ERROR'}, "Cannot use FBX exporter. Please make sure the FBX format addon is enabled.")
+ return
+ #export_selected_mesh_extra_maps(inputFilename_extraMaps, theOp.the_input_object)
+
+ if (verboseDebug): print(" inputFile exported!")
+
+
+ # 1.2 - Write settings file
+ if (g_useDLL == True):
+ g_remeshData = startOneRemeshing()
+ try:
+ dll_setInputOptions(inputFilename, theOp, extraDataFile)
+ except Exception:
+ import traceback
+ console_print("setInputOptions error: " + str(traceback.format_exc()) + "\n")
+ theOp.report({'ERROR'}, "Can't set input options!")
+ return
+ else:
+ try:
+ writeSettingsFile(settingsFilename, inputFilename, theOp, g_enableTiming)
+ if (verboseDebug): print(" ----------- settingsFile exported!")
+ except Exception:
+ import traceback
+ console_print("write settings file error: " + str(traceback.format_exc()) + "\n")
+ theOp.report({'ERROR'}, "Can't write settings file!")
+ return
+
+
+
+
+
+ # --------------- 2 - Start Remeshing ------------
+ theOp.progressData.onStartRemeshing()
+
+ if (g_useDLL == True):
+ try:
+ dll_launchRemeshing()
+ except Exception:
+ import traceback
+ print("Launch remeshing ERROR: " + str(traceback.format_exc()) + "\n")
+ helpLinkStr = "\nPlease check Trouble-Shooting section in the FAQ (https://www.exoside.com/php_ext/qr_tsl.php?ha=Blender&v="+__QR_plugin_version__+"&id=engExec)"
+ theOp.report({'ERROR'}, "Cannot execute the remesher engine...." + helpLinkStr)
+ return
+ else:
+ try:
+ exe_launchRemeshing(enginePath, settingsFilename, theOp, verboseDebug)
+
+ except Exception:
+ import traceback
+ print("Execute remesher ERROR: " + str(traceback.format_exc()) + "\n")
+ helpLinkStr = "\nPlease check Trouble-Shooting section in the FAQ (https://www.exoside.com/php_ext/qr_tsl.php?ha=Blender&v="+__QR_plugin_version__+"&id=engExec)"
+ theOp.report({'ERROR'}, "Cannot execute the remesher engine...." + helpLinkStr)
+ return
+
+ theOp.IsRemeshing = True
+
+ if (verboseDebug): console_print("theOp.progressData.RemeshingProcess: " + str(theOp.progressData.RemeshingProcess) + "\n")
+
+ return
+
+
+
+
+def doRemeshing_Finish(theOp, context) :
+
+ global g_enableTiming
+ if g_enableTiming: startLoadRetopoTime = time.time()
+
+ inputObject = theOp.the_input_object
+ #inputObject = context.selected_objects[0]
+
+ # ----------------- 3 - Import Remeshing result -------------------------
+ # mode is changed by fbx-import: need to save and restore it
+ #current_mode = bpy.context.object.mode
+ current_mode = inputObject.mode
+ props = bpy.context.scene.qremesher
+
+ # Fix Crash with 2.83 and 2.90:
+ # Before 2.83 crash: Hide InputMeshObj, then load retopo. But this crashes Blender (in import_mesh_fbx).
+ # Just switching : LoadRetopo, Then hide InputMeshObj -> works in 2.83 !
+
+ # first hide the selected object (old code, see crash 2.83)
+ #if getattr(props, 'hide_input'):
+ # inputObject.hide_set(state=True) # NB: 2.8 only, 2.7 must use 'hide'
+
+ # get the Smooth/Flat shading value of the input object (assuming constant all over the mesh...)
+ inputUseSmoothShading = True
+ try:
+ if len(inputObject.data.polygons) >= 1:
+ inputUseSmoothShading = inputObject.data.polygons[0].use_smooth
+ # add warning for "Use Normals Splitting":
+ if inputUseSmoothShading == False:
+ if getattr(props, 'use_normals'):
+ theOp.report({'WARNING'}, "Remeshing with 'Use Normals Splitting' enabled and with 'Flat Shading' generally leads to poor results. You probably should switch one of them.")
+ if (verboseDebug): print ("poly[0] : use_smooth = %d" % (inputUseSmoothShading))
+ except Exception:
+ #import traceback
+ #print("EXCEPTION: " + str(traceback.format_exc()) + "\n")
+ print("warning: exception with shade flat/smooth...")
+
+
+ # then import result (and automatically select it)
+ import_mesh_fbx(theOp.retopoFilename)
+
+ # hide the selected object (see note about crash 2.83)
+ if getattr(props, 'hide_input'):
+ # NB: hide_set() controls the Visibility (eye icon); 'hide_viewport' controls the 'Disabled in Viewport' (Monitor icon) 'hide_render' controls the 'Disabled in Render' (Camera icon)
+ inputObject.hide_set(state=True) # NB: 2.8 only, 2.7 must use 'hide'
+ # new 17 june 2025: 1.4 (just after 1st beta)
+ try:
+ inputObject.hide_viewport = True
+ inputObject.hide_render = True
+ except Exception:
+ print("no hide_render")
+
+ # set the shade flat/smooth... (NB: by default, the output retopo.fbx has no normals -> Blender imports it as Shade Smooth (tested with 2.80))
+ if inputUseSmoothShading == False:
+ setSelectedObjectShadeFlat(context)
+
+ transfer_attribute_from_input_obj_to_retopo(inputObject)
+
+ # restore previous mode
+ bpy.ops.object.mode_set(mode=current_mode)
+
+ if g_enableTiming:
+ debug_returnLoadRetopoTime(startLoadRetopoTime, time.time())
+
+# -------------- Download QuadRemesher Operator ----------------
+'''
+class QREMESHER_OT_downloadEngine(bpy.types.Operator):
+ bl_idname = "qremesher.download_engine"
+ bl_label = "Download and install QuadRemesher Engine ?"
+ bl_description = "Download and install QuadRemesher Engine ..."
+ bl_options = {'REGISTER'}
+
+ @classmethod
+ def poll(self, context):
+ return True
+
+ def execute(self, context):
+ engineFolder = getQREngineFolder()
+ if isMacOSX :
+ enginePath = os.path.join(engineFolder, "xremesh")
+ else:
+ enginePath = os.path.join(engineFolder, "xremesh.exe")
+ InstallQuadRemesherEngineIfNeeded(self, context, enginePath)
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ #console_print("invoke called!!!")
+ self.execute(context)
+ return {'RUNNING_MODAL'}
+'''
+
+
+# --------------- Main Operator called when <> is pressed! -------
+class QREMESHER_OT_remesh(bpy.types.Operator):
+ bl_idname = "qremesher.remesh"
+ bl_label = "<< REMESH IT >> "
+ bl_description = "Remesh the selected mesh.\n It also downloads and installs the RemesherEngine the 1st time, if needed."
+ bl_options = {'REGISTER', 'UNDO'}
+
+ # class variables
+ IsRemeshing = False
+ retopoFilename = ""
+ Aborted = False
+ progressData = QRCheckProgressData()
+
+ @classmethod
+ def poll(self, context):
+ return True
+
+ def execute(self, context):
+
+ doRemeshing_Start(self, context)
+
+ if (self.IsRemeshing):
+ wm = context.window_manager
+ # add timer
+ self.timer = wm.event_timer_add(0.3, window=context.window)
+ wm.modal_handler_add(self)
+ return {'RUNNING_MODAL'}
+ else:
+ return {'FINISHED'}
+
+ # reset things (for FINISHED or CANCELLED)
+ def onEndingOperator(self, context, isSuccess, isCancelCase):
+ wm = context.window_manager
+ if isCancelCase and self.progressData.RemeshingProcess:
+ subprocess.Popen.terminate(self.progressData.RemeshingProcess)
+ #subprocess.Popen.kill(self.progressData.RemeshingProcess)
+ # reset datas:
+ if self.timer != None:
+ wm.event_timer_remove(self.timer)
+ self.timer = None
+ self.IsRemeshing = False
+ self.retopoFilename = ""
+ self.Aborted = False
+ self.progressData.reset()
+
+
+ def modal(self, context, event):
+ #console_print("modal called. event.type="+str(event.type))
+ if event.type in {'ESC'}:
+ self.report({'INFO'}, "Remeshing CANCELLED !")
+ self.onEndingOperator(context, False, True)
+ return {'CANCELLED'}
+
+ if event.type == 'TIMER':
+ # update progress bar (and detect crash) + update progress bar
+ ProgressValueFloat, ProgressText, SleepTimeAdvice = self.progressData.get_progress_status()
+
+ # Choose: RUNNING/FINISHED/CANCELLED
+ if ProgressValueFloat == -10: # no progress file found
+ console_print(' ERROR : no progressFile after 40s....')
+ self.report({'ERROR'}, "Remeshing FAILED !")
+ self.onEndingOperator(context, False, False)
+ return {'FINISHED'}
+
+ elif ProgressValueFloat == -11: # BadSyntax in ProgressFile : wait/continue...
+ return {'RUNNING_MODAL'}
+
+ elif ProgressValueFloat == -2: # in EULA or Activation: User will have to click Remesh It again !
+ return {'FINISHED'}
+
+ elif ProgressValueFloat < 0: # error returned
+ console_print(' RETURNING ERROR.... ProgressValueFloat='+str(ProgressValueFloat))
+ self.onEndingOperator(context, False, False)
+ if (ProgressText != None and len(ProgressText)>0):
+ self.report({'ERROR'}, ProgressText)
+ else:
+ self.report({'ERROR'}, "Remeshing FAILED !")
+ return {'FINISHED'}
+
+ elif ProgressValueFloat == 2: # SUCCESS -> import the result
+ doRemeshing_Finish(self, context)
+
+ self.onEndingOperator(context, True, False)
+ self.report({'INFO'}, "Remeshing Succeded !")
+
+ return {'FINISHED'}
+
+ # in [0, 1] : RUNNING
+ if ProgressValueFloat >= 0 and ProgressValueFloat <= 1.0:
+ newPBarValue = int( (99.0 * ProgressValueFloat + 1.0) )
+ #console_print('ProgressValueFloat=%s newPBarValue=%s' % (str(ProgressValueFloat),str(newPBarValue)))
+ self.report({'INFO'}, "Remeshing progress:"+str(newPBarValue)+"% (ESC=Abort)")
+ else:
+ self.report({'INFO'}, "Remeshing progress... (ESC=Abort)")
+
+ return {'RUNNING_MODAL'}
+
+ return {'RUNNING_MODAL'}
+ #return {'PASS_THROUGH'}
+
+ def invoke(self, context, event):
+ #console_print("invoke called!!!")
+
+ self.execute(context)
+
+ if self.IsRemeshing:
+ return {'RUNNING_MODAL'}
+ else:
+ return {'FINISHED'}
+
+ def cancel(self, context):
+ console_print("cancel called!!!")
+ self.onEndingOperator(context, False, True)
+ return {'CANCELLED'}
+
+
+def getLicenseManagerPath():
+ try:
+ engineFolder = getQREngineFolder()
+ #script_folder = os.path.dirname(os.path.realpath(__file__))
+ isMacOSX = (platform.system()=="Darwin") or (platform.system()=="macosx")
+ isLinux = (platform.system()=="Linux")
+ if isMacOSX :
+ licenseManagerPath = os.path.join(engineFolder, "xrLicenseManager.app/Contents/MacOS/xrLicenseManager")
+ elif isLinux :
+ licenseManagerPath = os.path.join(engineFolder, "xrLicenseManager")
+ else:
+ licenseManagerPath = os.path.join(engineFolder, "xrLicenseManager.exe")
+ licenseManagerPath = unixifyPath(licenseManagerPath)
+ except Exception:
+ licenseManagerPath = None
+ return licenseManagerPath
+
+# -------------- License Manager button Operator ----------------
+class QREMESHER_OT_license_manager(bpy.types.Operator):
+ bl_idname = "qremesher.license_manager"
+ bl_label = "License Manager"
+ bl_description = "Launches the License manager"
+ bl_options = {'REGISTER'}
+
+ @classmethod
+ def poll(self, context):
+ return True
+
+ def execute(self, context):
+ # 1 - get license manager path
+ licenseManagerPath = getLicenseManagerPath()
+ if licenseManagerPath == None:
+ helpLinkStr = "\nPlease check Trouble-Shooting section in the FAQ (https://www.exoside.com/php_ext/qr_tsl.php?ha=Blender&v="+__QR_plugin_version__+"&id=LicMgrPath)"
+ self.report({'ERROR'}, "Error while settings LicenseManager path!"+helpLinkStr)
+ return {'CANCELLED'}
+
+ # 2 - launch licenseManager
+ InstallQuadRemesherEngineIfNeeded(self, context, licenseManagerPath)
+
+ #console_print("Launching LicenseManager : " + str(licenseManagerPath))
+ try:
+ subprocess.Popen([*qr_launch_prefix(), licenseManagerPath, "-hostApp", "Blender", "-haVer", getHostAppVer()])
+ except Exception:
+ #console_print("ERROR : " + str(licenseManagerPath))
+ helpLinkStr = "\nPlease check Trouble-Shooting section in the FAQ (https://www.exoside.com/php_ext/qr_tsl.php?ha=Blender&v="+__QR_plugin_version__+"&id=LicMgrLaunch)"
+ self.report({'ERROR'}, "Exception: while launching LicenseManager.... (" + str(licenseManagerPath) + ")" + helpLinkStr)
+ return {'CANCELLED'}
+
+ #console_print("OK : " + str(licenseManagerPath))
+ return {'FINISHED'}
+
+
+# -------------- Reset Setting button Operator ----------------
+class QREMESHER_OT_reset_settings(bpy.types.Operator):
+ bl_idname = "qremesher.reset_scene_prefs"
+ bl_label = "Reset Settings"
+ bl_description = "Reset settings to default values"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(self, context):
+ return True
+
+ def execute(self, context):
+ #ne marche pas .... bpy.ops.qremesher.custom_confirm_dialog.('INVOKE_DEFAULT')
+
+ props = bpy.context.scene.qremesher
+ props.target_count = 5000
+ props.adaptive_size = 50
+ props.adapt_quad_count = True
+ props.use_vertex_color = False
+
+ props.use_materials = False
+ props.use_normals = False
+ props.autodetect_hard_edges = True
+
+ props.symmetry_x = False
+ props.symmetry_y = False
+ props.symmetry_z = False
+
+ props.hide_input = True
+
+ return {'FINISHED'}
+
+
+# -------------- FaceMap -> Material button Operator (Blender 3.x) ----------------
+class QREMESHER_OT_facemap_to_materials(bpy.types.Operator):
+ bl_idname = "qremesher.facemap_to_materials"
+ bl_label = "Face Maps 2 Materials"
+ bl_description = "Assign new Materials to each FaceMap (using random colors) so that:\n- FaceMaps can be used with 'Use Materials' option.\n- FaceMaps can be easily visualized."
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(self, context):
+ return True
+
+ def execute(self, context):
+ # NB: FOUND NOT ACCESS TO FaceMaps (neither from mesh nor from mesh) -> need to select them and read selection...
+
+ import random
+
+ sel_objects = context.selected_objects
+ if len(sel_objects) != 1:
+ self.report({'ERROR'}, "You must select one and only one object.")
+ return {'FINISHED'}
+
+ o = sel_objects[0]
+ #print ("%d face maps!" % len(o.face_maps))
+
+ #test: export_selected_mesh_extra_maps("", o)
+
+ # reset all materialIds:
+ for f in o.data.polygons:
+ f.material_index = 0
+
+ ### fsAttrib = o.data.attributes['.sculpt_face_set']
+ matIndex = 0
+ mapCount = len(o.face_maps)
+ for fm in o.face_maps:
+ matIndex += 1
+
+ # create the material (assign random color)
+ matName = ("FaceMapMat%d" % matIndex)
+ mat = bpy.data.materials.new(matName)
+ r = random.randint(0, 255) / 255.0
+ g = random.randint(0, 255) / 255.0
+ b = random.randint(0, 255) / 255.0
+ #print ("face map [%d] : (%f %f %f)" % (matIndex-1, r, g, b))
+ #r = (matIndex-1.0) / mapCount
+ mat.diffuse_color = (r, g, b, 1)
+ if o.data.materials:
+ if matIndex < len(o.data.materials):
+ o.data.materials[matIndex] = mat
+ else:
+ o.data.materials.append(mat)
+ else:
+ o.data.materials.append(mat)
+
+ # select this face map
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.mesh.select_all(action='DESELECT')
+ o.face_maps.active_index = matIndex-1
+
+ # select all faces of this face map
+ bpy.ops.object.face_map_select()
+
+ # set materialId for selected faces:
+ # NB: in Edit Mode ".select" returns the selected status when entering the EditMode !!! ????!!!!!
+ bpy.ops.object.mode_set(mode='OBJECT')
+ for f in o.data.polygons:
+ #print("test select polygon %d -> %d" % (f.index, f.select))
+ if f.select:
+ #print("face[%d] set material = %d" % (f.index, matIndex))
+ f.material_index = matIndex
+
+ # CHECKING:
+ #for f in o.data.polygons: print(str(f.material_index))
+
+ return {'FINISHED'}
+
+
+
+# -------------- FaceSet -> Material button Operator ----------------
+class QREMESHER_OT_faceset_to_materials(bpy.types.Operator):
+ bl_idname = "qremesher.faceset_to_materials"
+ bl_label = "Face Sets 2 Materials"
+ bl_description = "Assign new Materials to each Face Set (using random colors) so that:\n- FaceSets can be used with 'Use Materials' option.\n- FaceSets can be easily visualized."
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(self, context):
+ return True
+
+ def execute(self, context):
+ # NB: FOUND NOT ACCESS TO FaceMaps (neither from mesh nor from mesh) -> need to select them and read selection...
+
+ import random
+
+ sel_objects = context.selected_objects
+ if len(sel_objects) != 1:
+ self.report({'ERROR'}, "You must select one and only one object.")
+ return {'FINISHED'}
+
+ o = sel_objects[0]
+
+ #test: export_selected_mesh_extra_maps("", o)
+
+ # reset all materialIds:
+ for f in o.data.polygons:
+ f.material_index = 0
+
+ ### fsAttrib = o.data.attributes['.sculpt_face_set']
+ # set materialId for selected faces:
+ # NB: in Edit Mode ".select" returns the selected status when entering the EditMode !!! ????!!!!!
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ try:
+ fsAttrib = o.data.attributes['.sculpt_face_set']
+ except Exception: # no FaceSets in the mesh
+ self.report({'ERROR'}, "No FaceSets found in the mesh.")
+ return {'FINISHED'}
+ #print("fsAttrib = %s" % str(fsAttrib))
+
+ fsMax = -1
+ fsMin = 100000
+ for fs in fsAttrib.data:
+ if fs.value > fsMax:
+ fsMax = fs.value
+ if fs.value < fsMin:
+ fsMin = fs.value
+ if fsMax == -1 or fsMin >= fsMax:
+ self.report({'ERROR'}, "No valid FaceSets found in the mesh.")
+ return {'FINISHED'}
+ #print(" maxFS = %d" % (fsMax))
+
+ #matCount = len(bpy.data.materials)
+ matCount = len(o.data.materials)
+ initialMatCount = matCount
+ fsToMatIdx = [None] * (fsMax-fsMin+1) # zero based fsIdx -> MatIndex
+ p = 0
+ np=len(fsAttrib.data)
+ for p in range(np):
+ #for fs in fsAttrib.data:
+ fs = fsAttrib.data[p];
+ realFsIdx = fs.value
+ fsIdxZB = fs.value - fsMin # Zero Based
+ #print(" fac %d : fs=%d (ZB:%d) " % (p, realFsIdx, fsIdxZB))
+
+ if fsIdxZB < 0:
+ continue
+
+ #if not fsIdxZB in fsToMatIdx:
+ if fsToMatIdx[fsIdxZB] == None:
+ fsToMatIdx[fsIdxZB] = matCount
+ o.data.polygons[p].material_index = matCount
+ matCount = matCount+1
+
+ # create the material (assign random color)
+ #matName = ("FaceSetMat%d" % (fsIdxZB+1))
+ matName = ("FaceSetMat%d" % (realFsIdx))
+ mat = bpy.data.materials.new(matName)
+ r = random.randint(0, 255) / 255.0
+ g = random.randint(0, 255) / 255.0
+ b = random.randint(0, 255) / 255.0
+ #print (" DEBUG: face set ZB#%d real:%d : (%f %f %f) -> append new material: #%d : %s" % (fsIdxZB, realFsIdx, r, g, b, matCount-1, matName))
+ #r = (matIndex-1.0) / mapCount
+ mat.diffuse_color = (r, g, b, 1)
+ if o.data.materials:
+ #if matIndex < len(o.data.materials):
+ # o.data.materials[matIndex] = mat
+ #else:
+ o.data.materials.append(mat)
+ else:
+ o.data.polygons[p].material_index = fsToMatIdx[fsIdxZB]
+ #print(" F[%d] : fs=%d assign existing material %d" % (p, fsIdxZB, fsToMatIdx[fsIdxZB]))
+
+ #p = p+1
+
+ # CHECKING:
+ for f in o.data.polygons: print(str(f.material_index))
+
+ self.report({'INFO'}, "%d Materials created" % (matCount-initialMatCount))
+ return {'FINISHED'}
+
+
+
+# -------------------------- Online Help operator --------------------
+class QREMESHER_OT_online_help(bpy.types.Operator):
+ bl_idname = "qremesher.online_help"
+ bl_label = "Online Help"
+ bl_description = "Opens QuadRemesher's online help in a web browser."
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(self, context):
+ return True
+
+ def execute(self, context):
+ try:
+ import webbrowser
+ webbrowser.open('https://exoside.com/quadremesherdata/plugins_webdoc_link.php?App=Blender')
+ except :
+ import traceback
+ print("Exception: in Online Help operator..\n")
+ print(str(traceback.format_exc()) + "\n")
+
+ return {'FINISHED'}
+
+
+# -------------------------- News or LatestVersion operator --------------------
+class QREMESHER_OT_News_LatestVer(bpy.types.Operator):
+ # class variables:
+ bl_idname = "qremesher.latestver"
+ bl_label = "Check Version"
+ bl_description = "Checks whether you have the latest version of the addon or not"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ #addon_version = None # set in register. ex: (1,2,2)
+ newReadDone = False
+ newsButtonLabel = ""
+ newsButtonDesc = ""
+
+ @classmethod
+ def poll(self, context):
+ return True
+
+ @classmethod
+ def description(self, context, properties):
+ try:
+ return newsButtonDesc
+ except Exception:
+ #return "Latest version"
+ return bl_description
+
+
+ def execute(self, context):
+ # 1 - get license manager path
+ licenseManagerPath = getLicenseManagerPath()
+ if licenseManagerPath == None:
+ helpLinkStr = "\nPlease check Trouble-Shooting section in the FAQ (https://www.exoside.com/php_ext/qr_tsl.php?ha=Blender&v="+__QR_plugin_version__+"&id=LicMgrPath)"
+ self.report({'ERROR'}, "Error while settings LicenseManager path!"+helpLinkStr)
+ return {'CANCELLED'}
+
+ # 2 - launch licenseManager
+ InstallQuadRemesherEngineIfNeeded(self, context, licenseManagerPath)
+
+ #console_print("Launching LicenseManager : " + str(licenseManagerPath))
+ try:
+ verStr = "1.4.1"
+ subprocess.Popen([*qr_launch_prefix(), licenseManagerPath, "-cn", "-hostApp", "Blender", "-prodVer", verStr, "-haVer", getHostAppVer()])
+
+ except Exception:
+ #console_print("ERROR : " + str(licenseManagerPath))
+ helpLinkStr = "\nPlease check Trouble-Shooting section in the FAQ (https://www.exoside.com/php_ext/qr_tsl.php?ha=Blender&v="+__QR_plugin_version__+"&id=LicMgrLaunch)"
+ self.report({'ERROR'}, "Exception: while launching LicenseManager.... (" + str(licenseManagerPath) + ")" + helpLinkStr)
+ #import traceback
+ #print(str(traceback.format_exc()) + "\n")
+ return {'CANCELLED'}
+
+ #console_print("OK : " + str(licenseManagerPath))
+ return {'FINISHED'}
+
+
+ # called in register()
+ @classmethod
+ def getNewsButtonLabel(cls):
+ return cls.newsButtonLabel
+
+ @classmethod
+ def readNews(cls):
+ cls.newsReadDone = True
+ cls.newsButtonLabel = "Check Version"
+ cls.newsButtonDesc = "Check Version description"
+
+ try:
+ folder = os.getenv('LOCALAPPDATA')
+ newsFilePath = os.path.join(folder, "Exoside/QuadRemesher/Datas_Blender/ServerNews.txt")
+ #print("newsFilePath=")
+ #print(newsFilePath)
+ f = open(newsFilePath, "r")
+ newsFileLines = f.read().split("\n")
+ f.close()
+ except Exception:
+ # go here if the ServerNews file does not exist!
+ #import traceback
+ #print(str(traceback.format_exc()) + "\n")
+ #print("Missing Server file")
+ return
+
+ try:
+ # add the button
+ if (newsFileLines!=None) and (len(newsFileLines)>=2):
+ cls.newsButtonLabel = newsFileLines[0]
+ #print("newsFileLines[0]=")
+ #print(newsFileLines[0])
+ #newsFileLines[2] = type!
+ #newsFileLines[3] = date!
+ if (len(newsFileLines)>4):
+ cls.newsButtonDesc = newsFileLines[4]
+ #bpy.types.Scene.qremesher.newsButtonDescription = newsButtonDesc
+
+ except Exception:
+ import traceback
+ print(str(traceback.format_exc()) + "\n")
+
+
diff --git a/modules/software/utilities/cad.nix b/modules/software/utilities/cad.nix
index 846c92a..c913dbe 100644
--- a/modules/software/utilities/cad.nix
+++ b/modules/software/utilities/cad.nix
@@ -2,10 +2,37 @@
den.aspects.cad = {
nixos = { pkgs, ... }: {
users.users.bug.packages = with pkgs; [
- blender
+ #blender
plasticity
#orca-slicer
];
};
+
+ homeManager = { pkgs , ... }: {
+ home.packages = let
+ blenderScripts = pkgs.runCommand "blender-user-scripts" { } ''
+ mkdir -p $out/addons
+ cp -r ${./addons}/. $out/addons/
+ '';
+
+ quadRemesherLauncher = pkgs.writeShellScript "quadremesher-launcher" ''
+ engineDir="$(dirname "$1")"
+ export NIX_LD_LIBRARY_PATH="$engineDir:/run/current-system/sw/share/nix-ld/lib''${NIX_LD_LIBRARY_PATH:+:$NIX_LD_LIBRARY_PATH}"
+ export LD_PRELOAD="${pkgs.zlib}/lib/libz.so.1''${LD_PRELOAD:+:$LD_PRELOAD}"
+ exec "$@"
+ '';
+
+ blenderWithAddons = pkgs.symlinkJoin {
+ name = "blender-with-addons";
+ paths = [ pkgs.blender ];
+ nativeBuildInputs = [ pkgs.makeWrapper ];
+ postBuild = ''
+ wrapProgram $out/bin/blender \
+ --set BLENDER_USER_SCRIPTS ${blenderScripts} \
+ --set QUADREMESHER_LAUNCHER ${quadRemesherLauncher}
+ '';
+ };
+ in [ blenderWithAddons ];
+ };
};
}
diff --git a/modules/software/utilities/ghostty.nix b/modules/software/utilities/ghostty.nix
index fe13ddf..f96f509 100644
--- a/modules/software/utilities/ghostty.nix
+++ b/modules/software/utilities/ghostty.nix
@@ -1,5 +1,5 @@
{ inputs, ... }: {
- den.aspects.fetch = {
+ den.aspects.ghostty = {
nixos = { pkgs, ... }: {
environment.systemPackages = with pkgs; [
ghostty