Source code for linkforge.blender.properties.validation_props

"""Blender Property Groups for validation results.

These properties store the last validation result for display in the UI.
"""

from __future__ import annotations

import contextlib

import bpy
from bpy.props import BoolProperty, CollectionProperty, IntProperty, StringProperty
from bpy.types import PropertyGroup

from ..constants import (
    PROP_VALIDATION,
)


[docs] class ValidationIssueProperty(PropertyGroup): """A single validation issue (error or warning).""" title: StringProperty( # type: ignore[valid-type] name="Title", description="Short title of the issue", default="", ) message: StringProperty( # type: ignore[valid-type] name="Message", description="Detailed message", default="", ) suggestion: StringProperty( # type: ignore[valid-type] name="Suggestion", description="How to fix this issue", default="", ) affected_objects: StringProperty( # type: ignore[valid-type] name="Affected Objects", description="Comma-separated list of affected object names", default="", ) error_code: StringProperty( # type: ignore[valid-type] name="Code", description="Machine-readable error code", default="", ) @property def has_suggestion(self) -> bool: """Check if this issue has a suggestion.""" return bool(self.suggestion) @property def has_objects(self) -> bool: """Check if this issue has affected objects.""" return bool(self.affected_objects) @property def objects_str(self) -> str: """Get affected objects as a formatted string.""" import typing return typing.cast(str, self.affected_objects) @property def message_lines(self) -> list[str]: """Split message into lines for display (max 60 chars per line).""" max_width = 60 words = self.message.split() lines: list[str] = [] current_line: list[str] = [] current_length = 0 for word in words: word_length = len(word) + (1 if current_line else 0) if current_length + word_length > max_width and current_line: lines.append(" ".join(current_line)) current_line = [word] current_length = len(word) else: current_line.append(word) current_length += word_length if current_line: lines.append(" ".join(current_line)) return lines @property def suggestion_lines(self) -> list[str]: """Split suggestion into lines for display (max 58 chars per line).""" if not self.suggestion: return [] max_width = 58 # Account for " " prefix words = self.suggestion.split() lines: list[str] = [] current_line: list[str] = [] current_length = 0 for word in words: word_length = len(word) + (1 if current_line else 0) if current_length + word_length > max_width and current_line: lines.append(" ".join(current_line)) current_line = [word] current_length = len(word) else: current_line.append(word) current_length += word_length if current_line: lines.append(" ".join(current_line)) return lines
[docs] class ValidationResultProperty(PropertyGroup): """Validation result stored in window manager.""" has_results: BoolProperty( # type: ignore[valid-type] name="Has Results", description="Whether validation has been run", default=False, ) is_valid: BoolProperty( # type: ignore[valid-type] name="Is Valid", description="Whether robot passed validation (no errors)", default=False, ) error_count: IntProperty( # type: ignore[valid-type] name="Error Count", description="Number of errors", default=0, ) warning_count: IntProperty( # type: ignore[valid-type] name="Warning Count", description="Number of warnings", default=0, ) link_count: IntProperty( # type: ignore[valid-type] name="Link Count", description="Number of links in robot", default=0, ) joint_count: IntProperty( # type: ignore[valid-type] name="Joint Count", description="Number of joints in robot", default=0, ) dof_count: IntProperty( # type: ignore[valid-type] name="DOF Count", description="Degrees of freedom", default=0, ) errors: CollectionProperty( # type: ignore[valid-type] type=ValidationIssueProperty, name="Errors", description="List of validation errors", ) warnings: CollectionProperty( # type: ignore[valid-type] type=ValidationIssueProperty, name="Warnings", description="List of validation warnings", ) show_errors: BoolProperty( # type: ignore[valid-type] name="Show Errors", description="Expand errors section", default=True, ) show_warnings: BoolProperty( # type: ignore[valid-type] name="Show Warnings", description="Expand warnings section", default=False, )
[docs] def clear(self) -> None: """Clear all validation results.""" self.has_results = False self.is_valid = False self.error_count = 0 self.warning_count = 0 self.link_count = 0 self.joint_count = 0 self.dof_count = 0 self.errors.clear() self.warnings.clear()
[docs] def get_error(self, index: int) -> ValidationIssueProperty: """Get error by index.""" import typing return typing.cast(ValidationIssueProperty, self.errors[index])
[docs] def get_warning(self, index: int) -> ValidationIssueProperty: """Get warning by index.""" import typing return typing.cast(ValidationIssueProperty, self.warnings[index])
# Registration
[docs] def register() -> None: """Register property groups.""" # Register ValidationIssueProperty try: bpy.utils.register_class(ValidationIssueProperty) except ValueError: bpy.utils.unregister_class(ValidationIssueProperty) bpy.utils.register_class(ValidationIssueProperty) # Register ValidationResultProperty try: bpy.utils.register_class(ValidationResultProperty) except ValueError: bpy.utils.unregister_class(ValidationResultProperty) bpy.utils.register_class(ValidationResultProperty) # Register window manager property setattr( bpy.types.WindowManager, PROP_VALIDATION, bpy.props.PointerProperty(type=ValidationResultProperty), # type: ignore[func-returns-value] )
[docs] def unregister() -> None: """Unregister property groups.""" with contextlib.suppress(AttributeError): delattr(bpy.types.WindowManager, PROP_VALIDATION) with contextlib.suppress(RuntimeError): bpy.utils.unregister_class(ValidationResultProperty) bpy.utils.unregister_class(ValidationIssueProperty)
if __name__ == "__main__": register()