Added sanity check baseline made by claude.
Also changed the location to a panel in VIEW_3D instead of menu.
This commit is contained in:
1 parent
71b1a61b5c
commit
4617c47ad0
33 files changed
+2771
-52
No files matched your search
@@ -0,0 +1,13 @@
|
||||
from . import operators, panel, properties
|
||||
|
||||
modules = (properties, operators, panel)
|
||||
|
||||
|
||||
def register():
|
||||
for module in modules:
|
||||
module.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
for module in reversed(modules):
|
||||
module.unregister()
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
import os
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
import numpy as np
|
||||
from bpy.props import IntProperty, StringProperty
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
|
||||
from ..core import CheckContext, char_reference, get_check, runner
|
||||
|
||||
REPORT_TEXT_NAME = "WTD_Sanity_Report"
|
||||
|
||||
# Checks whose issue list is unfolded in the panel (UI state only, not saved).
|
||||
EXPANDED: set[str] = set()
|
||||
|
||||
|
||||
def _ensure_object_mode(context):
|
||||
if context.mode != "OBJECT" and context.view_layer.objects.active is not None:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
|
||||
def _report_messages(operator, messages, success):
|
||||
if messages:
|
||||
for message in messages:
|
||||
print(f"[WTD Sanity] {message}")
|
||||
operator.report({"WARNING"}, f"{len(messages)} item(s) not fixed: {messages[0]}")
|
||||
else:
|
||||
operator.report({"INFO"}, success)
|
||||
|
||||
|
||||
class WTD_OT_run_checks(bpy.types.Operator):
|
||||
bl_idname = "wtd_sanity.run_checks"
|
||||
bl_label = "Run Checks"
|
||||
bl_description = "Run the sanity checks of the selected context (or a single check)"
|
||||
|
||||
check_id: StringProperty(options={"SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
check_ids = {self.check_id} if self.check_id else None
|
||||
report = runner.run_checks(context.scene, check_ids=check_ids)
|
||||
counts = report.counts()
|
||||
failed = counts[runner.Status.ERROR] + counts[runner.Status.CRASHED]
|
||||
warnings = counts[runner.Status.WARNING]
|
||||
# Not {"ERROR"}: that would raise when the operator is called from a script.
|
||||
level = {"WARNING"} if failed or warnings else {"INFO"}
|
||||
self.report(level, f"Sanity check: {failed} error(s), {warnings} warning(s)")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class WTD_OT_fix_check(bpy.types.Operator):
|
||||
bl_idname = "wtd_sanity.fix_check"
|
||||
bl_label = "Fix"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
check_id: StringProperty()
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
check = get_check(properties.check_id)
|
||||
return check.fix_description if check else "Fix the reported issues"
|
||||
|
||||
def execute(self, context):
|
||||
_ensure_object_mode(context)
|
||||
messages = runner.fix_check(self.check_id, context.scene)
|
||||
_report_messages(self, messages, "Fixed")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class WTD_OT_fix_all(bpy.types.Operator):
|
||||
bl_idname = "wtd_sanity.fix_all"
|
||||
bl_label = "Fix All"
|
||||
bl_description = "Run the fix of every failing check that has one, then re-run all checks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
report = runner.LAST_REPORT
|
||||
return report is not None and any(
|
||||
result.failed and (check := get_check(result.check_id)) and check.can_fix()
|
||||
for result in report.results.values()
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
_ensure_object_mode(context)
|
||||
messages = []
|
||||
for result in list(runner.LAST_REPORT.results.values()):
|
||||
check = get_check(result.check_id)
|
||||
if result.failed and check and check.can_fix():
|
||||
messages += runner.fix_check(result.check_id, context.scene)
|
||||
runner.run_checks(context.scene)
|
||||
_report_messages(self, messages, "Fixed all fixable issues")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def _objects_for_issue(issue, scene):
|
||||
id_block = issue.get_id()
|
||||
if id_block is None:
|
||||
return []
|
||||
if isinstance(id_block, bpy.types.Object):
|
||||
return [id_block]
|
||||
if isinstance(id_block, bpy.types.Collection):
|
||||
return list(id_block.all_objects)
|
||||
if isinstance(id_block, bpy.types.Material):
|
||||
return [ob for ob in scene.objects if any(slot.material == id_block for slot in ob.material_slots)]
|
||||
return [ob for ob in scene.objects if ob.data == id_block]
|
||||
|
||||
|
||||
def _select_mesh_elements(mesh, element_type, indices):
|
||||
"""Set the selection flags of a mesh in object mode."""
|
||||
for collection in (mesh.vertices, mesh.edges, mesh.polygons):
|
||||
collection.foreach_set("select", np.zeros(len(collection), dtype=bool))
|
||||
indices = np.asarray(sorted(indices), dtype=np.int64)
|
||||
if element_type == "VERT":
|
||||
verts = indices
|
||||
elif element_type == "EDGE":
|
||||
edge_flags = np.zeros(len(mesh.edges), dtype=bool)
|
||||
edge_flags[indices] = True
|
||||
mesh.edges.foreach_set("select", edge_flags)
|
||||
edge_verts = np.empty(len(mesh.edges) * 2, dtype=np.int32)
|
||||
mesh.edges.foreach_get("vertices", edge_verts)
|
||||
verts = edge_verts.reshape(-1, 2)[indices].ravel()
|
||||
else: # FACE
|
||||
poly_flags = np.zeros(len(mesh.polygons), dtype=bool)
|
||||
poly_flags[indices] = True
|
||||
mesh.polygons.foreach_set("select", poly_flags)
|
||||
totals = np.empty(len(mesh.polygons), dtype=np.int32)
|
||||
mesh.polygons.foreach_get("loop_total", totals)
|
||||
loop_mask = np.repeat(poly_flags, totals)
|
||||
loop_verts = np.empty(len(mesh.loops), dtype=np.int32)
|
||||
loop_edges = np.empty(len(mesh.loops), dtype=np.int32)
|
||||
mesh.loops.foreach_get("vertex_index", loop_verts)
|
||||
mesh.loops.foreach_get("edge_index", loop_edges)
|
||||
edge_flags = np.zeros(len(mesh.edges), dtype=bool)
|
||||
edge_flags[loop_edges[loop_mask]] = True
|
||||
mesh.edges.foreach_set("select", edge_flags)
|
||||
verts = loop_verts[loop_mask]
|
||||
vert_flags = np.zeros(len(mesh.vertices), dtype=bool)
|
||||
vert_flags[verts] = True
|
||||
mesh.vertices.foreach_set("select", vert_flags)
|
||||
|
||||
|
||||
class WTD_OT_select_issues(bpy.types.Operator):
|
||||
bl_idname = "wtd_sanity.select_issues"
|
||||
bl_label = "Select"
|
||||
bl_description = "Select the objects (and mesh elements) of the issues"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
check_id: StringProperty()
|
||||
issue_index: IntProperty(default=-1)
|
||||
|
||||
def execute(self, context):
|
||||
result = runner.get_result(self.check_id)
|
||||
if result is None or not result.issues:
|
||||
return {"CANCELLED"}
|
||||
issues = result.issues if self.issue_index < 0 else result.issues[self.issue_index:self.issue_index + 1]
|
||||
_ensure_object_mode(context)
|
||||
|
||||
scene = context.scene
|
||||
view_layer = context.view_layer
|
||||
objects = []
|
||||
elements = {} # object name -> {element type: indices}
|
||||
for issue in issues:
|
||||
for ob in _objects_for_issue(issue, scene):
|
||||
if ob not in objects:
|
||||
objects.append(ob)
|
||||
if issue.elements and ob.type == "MESH":
|
||||
by_type = elements.setdefault(ob.name, {})
|
||||
by_type.setdefault(issue.element_type, set()).update(issue.elements)
|
||||
|
||||
selectable = [ob for ob in objects if ob.name in view_layer.objects and ob.visible_get()]
|
||||
if not selectable:
|
||||
self.report({"WARNING"}, "Nothing selectable (hidden, excluded or not an object)")
|
||||
return {"CANCELLED"}
|
||||
for ob in view_layer.objects:
|
||||
ob.select_set(False)
|
||||
for ob in selectable:
|
||||
ob.select_set(True)
|
||||
view_layer.objects.active = selectable[0]
|
||||
|
||||
if elements:
|
||||
types = set()
|
||||
for ob in selectable:
|
||||
by_type = elements.get(ob.name)
|
||||
if not by_type:
|
||||
ob.select_set(False)
|
||||
continue
|
||||
# Several element types on one mesh: select their vertices.
|
||||
if len(by_type) == 1:
|
||||
(element_type, indices), = by_type.items()
|
||||
else:
|
||||
element_type, indices = "VERT", _as_vertices(ob.data, by_type)
|
||||
_select_mesh_elements(ob.data, element_type, indices)
|
||||
types.add(element_type)
|
||||
view_layer.objects.active = next(ob for ob in selectable if ob.name in elements)
|
||||
context.tool_settings.mesh_select_mode = ("VERT" in types, "EDGE" in types, "FACE" in types)
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
for ob in context.objects_in_mode:
|
||||
bm = bmesh.from_edit_mesh(ob.data)
|
||||
bm.select_flush_mode()
|
||||
bmesh.update_edit_mesh(ob.data, loop_triangles=False, destructive=False)
|
||||
|
||||
if context.area and context.area.type == "VIEW_3D":
|
||||
bpy.ops.view3d.view_selected()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def _as_vertices(mesh, by_type):
|
||||
verts = set(by_type.get("VERT", ()))
|
||||
if "EDGE" in by_type:
|
||||
for index in by_type["EDGE"]:
|
||||
verts.update(mesh.edges[index].vertices)
|
||||
if "FACE" in by_type:
|
||||
for index in by_type["FACE"]:
|
||||
verts.update(mesh.polygons[index].vertices)
|
||||
return verts
|
||||
|
||||
|
||||
class WTD_OT_toggle_check(bpy.types.Operator):
|
||||
bl_idname = "wtd_sanity.toggle_check"
|
||||
bl_label = "Enable / Disable Check"
|
||||
bl_description = "Enable or disable this check for this file (saved in the file)"
|
||||
bl_options = {"UNDO"}
|
||||
|
||||
check_id: StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
settings = context.scene.wtd_sanity
|
||||
disabled = runner.disabled_check_ids(context.scene)
|
||||
disabled ^= {self.check_id}
|
||||
settings.disabled_checks = ";".join(sorted(disabled))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class WTD_OT_toggle_expand(bpy.types.Operator):
|
||||
bl_idname = "wtd_sanity.toggle_expand"
|
||||
bl_label = "Show Details"
|
||||
bl_description = "Show or hide the details of this check"
|
||||
|
||||
check_id: StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
EXPANDED.symmetric_difference_update({self.check_id})
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class WTD_OT_write_report(bpy.types.Operator):
|
||||
bl_idname = "wtd_sanity.write_report"
|
||||
bl_label = "Write Report"
|
||||
bl_description = f"Write the last report to the text datablock '{REPORT_TEXT_NAME}'"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return runner.LAST_REPORT is not None
|
||||
|
||||
def execute(self, context):
|
||||
text = bpy.data.texts.get(REPORT_TEXT_NAME) or bpy.data.texts.new(REPORT_TEXT_NAME)
|
||||
text.clear()
|
||||
text.write(runner.LAST_REPORT.to_text())
|
||||
self.report({"INFO"}, f"Report written to text '{REPORT_TEXT_NAME}'")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class WTD_OT_export_char_reference(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "wtd_sanity.export_char_reference"
|
||||
bl_label = "Export Character Reference"
|
||||
bl_description = (
|
||||
"Export the body topology and inner mouth / eye names of this character "
|
||||
"as the reference for every other character"
|
||||
)
|
||||
|
||||
filename_ext = ".json"
|
||||
filter_glob: StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
ctx = CheckContext(context.scene, context.scene.wtd_sanity.context)
|
||||
if not ctx.asset_name:
|
||||
self.report({"ERROR"}, "Set the character name first")
|
||||
return {"CANCELLED"}
|
||||
self.filepath = os.path.join(
|
||||
os.path.dirname(bpy.data.filepath) or os.path.expanduser("~"), "char_reference.json"
|
||||
)
|
||||
return super().invoke(context, event)
|
||||
|
||||
def execute(self, context):
|
||||
ctx = CheckContext(context.scene, context.scene.wtd_sanity.context)
|
||||
try:
|
||||
data = char_reference.export_reference(context.scene, ctx.asset_name, self.filepath)
|
||||
except (ValueError, OSError) as exc:
|
||||
self.report({"ERROR"}, str(exc))
|
||||
return {"CANCELLED"}
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Reference written ({data['body']['vertices']} vertices, {len(data['parts'])} parts). "
|
||||
"Set it in the add-on preferences to use it.",
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
classes = (
|
||||
WTD_OT_run_checks,
|
||||
WTD_OT_fix_check,
|
||||
WTD_OT_fix_all,
|
||||
WTD_OT_select_issues,
|
||||
WTD_OT_toggle_check,
|
||||
WTD_OT_toggle_expand,
|
||||
WTD_OT_write_report,
|
||||
WTD_OT_export_char_reference,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
import textwrap
|
||||
|
||||
import bpy
|
||||
|
||||
from .. import config, tools_ops
|
||||
from ..core import CheckContext, Severity, checks_for_context, contexts, runner
|
||||
from ..core.runner import Status
|
||||
from .operators import EXPANDED
|
||||
|
||||
STATUS_ICONS = {
|
||||
Status.NOT_RUN: "DOT",
|
||||
Status.PASSED: "CHECKMARK",
|
||||
Status.INFO: "INFO",
|
||||
Status.WARNING: "ERROR",
|
||||
Status.ERROR: "CANCEL",
|
||||
Status.SKIPPED: "REMOVE",
|
||||
Status.CRASHED: "GHOST_ENABLED",
|
||||
}
|
||||
SEVERITY_ICONS = {
|
||||
Severity.INFO: "INFO",
|
||||
Severity.WARNING: "ERROR",
|
||||
Severity.ERROR: "CANCEL",
|
||||
}
|
||||
|
||||
|
||||
def _wrapped_label(layout, context, text, icon="NONE", margin=0):
|
||||
"""Label wrapped to the region width. ``margin`` is the room (in characters) taken by buttons."""
|
||||
char_width = 6.5 * context.preferences.system.ui_scale
|
||||
width = max(16, int(context.region.width / char_width) - 6 - margin)
|
||||
col = layout.column(align=True)
|
||||
for index, line in enumerate(textwrap.wrap(text, width) or [""]):
|
||||
col.label(text=line, icon=icon if index == 0 else "BLANK1")
|
||||
|
||||
|
||||
def draw_panel(self, context):
|
||||
layout = self.layout
|
||||
scene = context.scene
|
||||
settings = scene.wtd_sanity
|
||||
report = runner.LAST_REPORT
|
||||
if report is not None and report.context_id != settings.context:
|
||||
report = None
|
||||
|
||||
layout.prop(settings, "context", text="")
|
||||
if settings.context in contexts.ASSET:
|
||||
row = layout.row(align=True)
|
||||
row.prop(settings, "asset_name", text="Asset")
|
||||
if not settings.asset_name:
|
||||
guessed = CheckContext(scene, settings.context).asset_name
|
||||
row.label(text=guessed or "(from file name)")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 1.4
|
||||
row.operator("wtd_sanity.run_checks", text="Run Checks", icon="PLAY")
|
||||
row.operator("wtd_sanity.fix_all", text="Fix All", icon="TOOL_SETTINGS")
|
||||
row.operator("wtd_sanity.write_report", text="", icon="TEXT")
|
||||
|
||||
if settings.context == contexts.CHAR_MOD:
|
||||
layout.operator("wtd_sanity.export_char_reference", icon="EXPORT")
|
||||
|
||||
if report is not None:
|
||||
counts = report.counts()
|
||||
row = layout.row()
|
||||
for status in (
|
||||
Status.PASSED,
|
||||
Status.WARNING,
|
||||
Status.ERROR,
|
||||
Status.SKIPPED,
|
||||
Status.CRASHED,
|
||||
):
|
||||
if counts[status]:
|
||||
row.label(text=str(counts[status]), icon=STATUS_ICONS[status])
|
||||
|
||||
disabled = runner.disabled_check_ids(scene)
|
||||
box = None
|
||||
category = None
|
||||
for check in checks_for_context(settings.context):
|
||||
if check.category != category:
|
||||
category = check.category
|
||||
box = layout.box()
|
||||
box.label(text=category)
|
||||
_draw_check(box, context, check, report, check.id in disabled)
|
||||
|
||||
|
||||
def _draw_check(layout, context, check, report, disabled):
|
||||
result = report.results.get(check.id) if report else None
|
||||
status = result.status if result else Status.NOT_RUN
|
||||
expanded = check.id in EXPANDED
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator(
|
||||
"wtd_sanity.toggle_check",
|
||||
text="",
|
||||
emboss=False,
|
||||
icon="CHECKBOX_DEHLT" if disabled else "CHECKBOX_HLT",
|
||||
).check_id = check.id
|
||||
row.operator(
|
||||
"wtd_sanity.toggle_expand",
|
||||
text="",
|
||||
emboss=False,
|
||||
icon="DOWNARROW_HLT" if expanded else "RIGHTARROW",
|
||||
).check_id = check.id
|
||||
|
||||
label_row = row.row(align=True)
|
||||
label_row.enabled = not disabled
|
||||
label_row.alert = status in {Status.ERROR, Status.CRASHED}
|
||||
count = f" ({len(result.issues)})" if result and result.issues else ""
|
||||
label_row.label(text=check.label + count, icon=STATUS_ICONS[status])
|
||||
|
||||
buttons = row.row(align=True)
|
||||
if result and any(issue.id_name or issue.elements for issue in result.issues):
|
||||
buttons.operator(
|
||||
"wtd_sanity.select_issues", text="", icon="RESTRICT_SELECT_OFF"
|
||||
).check_id = check.id
|
||||
if result and result.failed and check.can_fix():
|
||||
buttons.operator(
|
||||
"wtd_sanity.fix_check", text="", icon="TOOL_SETTINGS"
|
||||
).check_id = check.id
|
||||
buttons.operator(
|
||||
"wtd_sanity.run_checks", text="", icon="FILE_REFRESH"
|
||||
).check_id = check.id
|
||||
|
||||
if not expanded:
|
||||
return
|
||||
col = layout.column()
|
||||
col.separator(factor=0.3)
|
||||
_wrapped_label(col, context, check.description, icon="QUESTION")
|
||||
if result is None:
|
||||
return
|
||||
if result.message:
|
||||
_wrapped_label(
|
||||
col,
|
||||
context,
|
||||
result.message.strip().splitlines()[-1],
|
||||
icon=STATUS_ICONS[status],
|
||||
)
|
||||
for index, issue in enumerate(result.issues[: config.MAX_ISSUES_DRAWN]):
|
||||
severity = issue.severity or check.severity
|
||||
issue_row = col.row(align=True)
|
||||
selectable = bool(issue.id_name or issue.elements)
|
||||
_wrapped_label(
|
||||
issue_row,
|
||||
context,
|
||||
issue.message,
|
||||
icon=SEVERITY_ICONS[severity],
|
||||
margin=7 if selectable else 0,
|
||||
)
|
||||
if selectable:
|
||||
op = issue_row.operator(
|
||||
"wtd_sanity.select_issues",
|
||||
text="",
|
||||
icon="RESTRICT_SELECT_OFF",
|
||||
emboss=False,
|
||||
)
|
||||
op.check_id = check.id
|
||||
op.issue_index = index
|
||||
if check.editable_names:
|
||||
id_block = issue.get_id()
|
||||
if id_block is not None and id_block.library is None:
|
||||
col.prop(id_block, "name", text="", icon="BLANK1")
|
||||
hidden = len(result.issues) - config.MAX_ISSUES_DRAWN
|
||||
if hidden > 0:
|
||||
col.label(text=f"... {hidden} more (see Write Report)", icon="BLANK1")
|
||||
col.separator(factor=0.5)
|
||||
|
||||
|
||||
class _SanityPanel:
|
||||
bl_region_type = "UI"
|
||||
bl_category = "WTD"
|
||||
bl_label = "Sanity Check"
|
||||
bl_options = {"DEFAULT_CLOSED"} # panel starts collapsed
|
||||
|
||||
def draw(self, context):
|
||||
draw_panel(self, context)
|
||||
|
||||
|
||||
class _ToolsPanel:
|
||||
bl_region_type = "UI"
|
||||
bl_category = "WTD"
|
||||
bl_label = "Tools"
|
||||
|
||||
def draw(self, context) -> None:
|
||||
layout = self.layout
|
||||
layout.operator(
|
||||
tools_ops.WTDGetOidForPath.bl_idname,
|
||||
text=tools_ops.WTDGetOidForPath.bl_label,
|
||||
)
|
||||
|
||||
# Adds a separator line
|
||||
layout.separator()
|
||||
|
||||
|
||||
class WTD_PT_sanity_view3d(_SanityPanel, bpy.types.Panel):
|
||||
bl_idname = "WTD_PT_sanity_view3d"
|
||||
bl_space_type = "VIEW_3D"
|
||||
|
||||
|
||||
class WTD_PT_tools_view3d(_ToolsPanel, bpy.types.Panel):
|
||||
bl_idname = "WTD_PT_tools_view3d"
|
||||
bl_space_type = "VIEW_3D"
|
||||
|
||||
|
||||
class WTD_PT_sanity_sequencer(_SanityPanel, bpy.types.Panel):
|
||||
bl_idname = "WTD_PT_sanity_sequencer"
|
||||
bl_space_type = "SEQUENCE_EDITOR"
|
||||
|
||||
|
||||
classes = (WTD_PT_sanity_view3d, WTD_PT_tools_view3d, WTD_PT_sanity_sequencer)
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -0,0 +1,80 @@
|
||||
import bpy
|
||||
from bpy.props import EnumProperty, PointerProperty, StringProperty
|
||||
|
||||
from .. import config
|
||||
from ..core import contexts, runner
|
||||
from ..core.check import ADDON_ID
|
||||
|
||||
|
||||
class WTD_SanitySettings(bpy.types.PropertyGroup):
|
||||
"""Per-file settings, saved in the .blend."""
|
||||
|
||||
context: EnumProperty(
|
||||
name="Context",
|
||||
description="Production context the file is checked against",
|
||||
items=contexts.enum_items(),
|
||||
update=lambda self, context: runner.clear(),
|
||||
)
|
||||
asset_name: StringProperty(
|
||||
name="Asset Name",
|
||||
description="Asset / character name. Leave empty to read it from the file name",
|
||||
)
|
||||
disabled_checks: StringProperty(
|
||||
name="Disabled Checks",
|
||||
description="Ids of the checks disabled in this file, separated by ';'",
|
||||
)
|
||||
|
||||
|
||||
def get_prefs():
|
||||
addon = bpy.context.preferences.addons.get(ADDON_ID)
|
||||
return addon.preferences if addon else None
|
||||
|
||||
|
||||
def _apply_config_override(self, context):
|
||||
load_config_override(self.config_path)
|
||||
|
||||
|
||||
def load_config_override(path=""):
|
||||
try:
|
||||
applied = config.apply_overrides(bpy.path.abspath(path) if path else "")
|
||||
except (OSError, ValueError) as exc:
|
||||
print(f"[WTD Sanity] Could not load config override: {exc}")
|
||||
else:
|
||||
if applied:
|
||||
print(f"[WTD Sanity] Config override: {', '.join(applied)}")
|
||||
|
||||
|
||||
class WTD_SanityPreferences(bpy.types.AddonPreferences):
|
||||
bl_idname = ADDON_ID
|
||||
|
||||
char_reference_path: StringProperty(
|
||||
name="Character Reference",
|
||||
description="Reference JSON exported from the master character (falls back to $WTD_CHAR_REFERENCE)",
|
||||
subtype="FILE_PATH",
|
||||
)
|
||||
config_path: StringProperty(
|
||||
name="Config Override",
|
||||
description="JSON file overriding values of config.py (falls back to $WTD_SANITY_CONFIG)",
|
||||
subtype="FILE_PATH",
|
||||
update=_apply_config_override,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "char_reference_path")
|
||||
layout.prop(self, "config_path")
|
||||
|
||||
|
||||
classes = (WTD_SanitySettings, WTD_SanityPreferences)
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.Scene.wtd_sanity = PointerProperty(type=WTD_SanitySettings)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.wtd_sanity
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
Reference in new issue
Block a user