-
Notifications
You must be signed in to change notification settings - Fork 0
Spec 070: from-blank pen mesh authoring + re-editable outline #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -42,8 +42,10 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| compute_outer, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| compute_outer_preview, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| compute_triangulation_preview, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| read_authored_outer_contour, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| read_user_outer_strokes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| read_user_strokes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| write_authored_outer_contour, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| write_user_outer_strokes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| write_user_strokes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -66,6 +68,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Stroke, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default_tool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| next_tool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve_launch_mode, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stage_tools, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_is_pen, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -202,6 +205,11 @@ class PROSCENIO_OT_automesh_authoring(bpy.types.Operator): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Active tool of the current stage (spec 066: bare Tab cycles it). Mirrored | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # at class level so the statusbar draw callback can highlight it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _current_active_tool: str = "auto" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Set by ``proscenio.pen_mesh_new`` right before it launches this modal on a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # freshly created (empty, no alpha to trace) mesh element: the next invoke | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # starts from a blank outer with the contour pen armed and the SIMPLE stage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # list, instead of alpha-tracing. Read-and-cleared in invoke (spec 070). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _launch_from_blank: ClassVar[bool] = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def poll(cls, context: bpy.types.Context) -> bool: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -238,6 +246,18 @@ def invoke(self, context: bpy.types.Context, _event: bpy.types.Event) -> set[str | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Stage list depends on interior mode; navigation walks this ordered | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # list by index, not raw enum arithmetic. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._interior_mode: str = params.interior_mode | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Launch mode (spec 070): a re-edit (the element carries authored anchors) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # loads them instead of alpha-tracing; a from-blank launch starts empty | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # with the contour pen on the SIMPLE stage list. Re-edit wins if both hold | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # (a re-launch on an authored element is a re-edit, not a fresh blank). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authored_outer = read_authored_outer_contour(obj) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| launch_mode = resolve_launch_mode( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| has_authored_outer=authored_outer is not None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from_blank=type(self)._launch_from_blank, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type(self)._launch_from_blank = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if launch_mode == "blank": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._interior_mode = "SIMPLE" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._active_stages: list[AuthoringStage] = _stages_for_mode(self._interior_mode) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._output = StageOutput() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._handles = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -310,7 +330,21 @@ def invoke(self, context: bpy.types.Context, _event: bpy.types.Event) -> set[str | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._user_outer_strokes: list[Stroke] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._output.outer = compute_outer(obj, image, params) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if authored_outer is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Re-edit: load the stored anchors as the outer ring and arm the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # contour pen so the artist can redraw / extend it (add points in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # EDIT_OUTLINE), instead of re-tracing the alpha silhouette. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._output.outer = list(authored_outer) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._active_tool = "contour" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._enter_draw(context, "stroke") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif launch_mode == "blank": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # From blank: nothing to trace; arm the contour pen on an empty | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # outer so the first click drops the first vert. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._output.outer = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._active_tool = "contour" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._enter_draw(context, "stroke") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+333
to
+345
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Re-edit still starts from an empty contour.
Suggested fix if authored_outer is not None:
# Re-edit: load the stored anchors as the outer ring and arm the
# contour pen so the artist can redraw / extend it (add points in
# EDIT_OUTLINE), instead of re-tracing the alpha silhouette.
self._output.outer = list(authored_outer)
self._active_tool = "contour"
self._enter_draw(context, "stroke")
+ self._pen_points[:] = list(authored_outer)
+ self._pen_active = True📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._output.outer = compute_outer(obj, image, params) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._handles = register_overlay(self._stage, self._output) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type(self)._current_stage_label = _stage_label(self._stage, self._interior_mode) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type(self)._current_stage = self._stage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -867,6 +901,13 @@ def _commit_contour( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| report_warn(self, "manual contour needs at least 3 points", always=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._output.outer = ring | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Persist the user's clicked anchors (pre-subdivision) so a later launch | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # reloads this outline for re-editing instead of re-tracing the alpha | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # silhouette (spec 070). The ring above is the subdivided render/build | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # geometry; the anchors are the editable handles. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| obj = context.active_object | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if obj is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| write_authored_outer_contour(obj, list(pts)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+904
to
+910
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win The authored-outline CP is written before the final outer is chosen.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._handles = refresh_overlay( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._handles, self._stage, self._output, **self._overlay_kwargs() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,138 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """From-blank pen mesh creation (spec 070). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Creates a Proscenio mesh element from a chosen image - the same textured quad an | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import would build - then launches the interactive authoring modal in from-blank | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mode so the artist draws the outline point by point over the image. This is the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Spine / Moho model the STUDY locked: a mesh always overlays a texture region, so | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "from blank" means "from an image with no mesh yet", not "from nothing". | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Note: like every other ``bpy.types.Operator`` in the addon this file does NOT use | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ``from __future__ import annotations`` - Blender's RNA metaclass evaluates the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| operator's property annotations eagerly, and PEP 563 would leave them as strings. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import ClassVar | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import bpy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from bpy.props import StringProperty | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from ...core._shared.props_access import resolve_pixels_per_unit # type: ignore[import-not-found] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from ...core._shared.report import report_error, report_info # type: ignore[import-not-found] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # The element-build reuses the photoshop importer's quad + material + tag helpers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # so a from-blank pen element is byte-identical to an imported mesh element (quad, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # UV-mapped image material, placement tag, element_type) and re-imports / exports | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # the same way. The only difference is it carries no manifest origin. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from ...importers.photoshop.planes import ( # type: ignore[import-not-found] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _attach_material, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ensure_mesh, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _tag_element_type, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .automesh_authoring import PROSCENIO_OT_automesh_authoring | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def create_pen_mesh_element( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| context: bpy.types.Context, image_path: Path, name: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> bpy.types.Object: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Build + activate a Proscenio mesh element from ``image_path``. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Raises ``RuntimeError`` (image load failed) or ``ValueError`` (no pixels) so | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| the operator surfaces the reason. The element is the imported-mesh shape: a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UV-mapped quad sized from the image and the scene pixels-per-unit, the unlit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| image material, the placement tag (so APPLY's UVs land in real texture space), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and ``element_type="mesh"``. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| image = bpy.data.images.load(str(image_path), check_existing=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width_px, height_px = int(image.size[0]), int(image.size[1]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if width_px <= 0 or height_px <= 0: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("image has no pixel dimensions") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ppu = resolve_pixels_per_unit(context) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| size = (width_px / ppu, height_px / ppu) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| obj = _ensure_mesh(name, size) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _attach_material(obj, image_path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _tag_element_type(obj, "mesh") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for other in context.selected_objects: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| other.select_set(False) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| obj.select_set(True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| context.view_layer.objects.active = obj | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return obj | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _launch_authoring_from_blank(context: bpy.types.Context) -> bool: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Invoke the authoring modal in from-blank mode in a VIEW_3D, if one exists. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| The operator may execute from the file-browser context (after the image | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| picker), so resolve a real VIEW_3D area + WINDOW region and override into it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns False when no 3D viewport is open (the caller then tells the user to | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| open Author Mesh manually). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| screen = context.screen | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if screen is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for area in screen.areas: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if area.type != "VIEW_3D": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| region = next((r for r in area.regions if r.type == "WINDOW"), None) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if region is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PROSCENIO_OT_automesh_authoring._launch_from_blank = True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with context.temp_override(window=context.window, area=area, region=region): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bpy.ops.proscenio.automesh_authoring("INVOKE_DEFAULT") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+70
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Search all Blender windows for the Line 70 only walks Suggested fix def _launch_authoring_from_blank(context: bpy.types.Context) -> bool:
@@
- screen = context.screen
- if screen is None:
- return False
- for area in screen.areas:
- if area.type != "VIEW_3D":
- continue
- region = next((r for r in area.regions if r.type == "WINDOW"), None)
- if region is None:
- continue
- PROSCENIO_OT_automesh_authoring._launch_from_blank = True
- with context.temp_override(window=context.window, area=area, region=region):
- bpy.ops.proscenio.automesh_authoring("INVOKE_DEFAULT")
- return True
+ for window in context.window_manager.windows:
+ screen = window.screen
+ if screen is None:
+ continue
+ for area in screen.areas:
+ if area.type != "VIEW_3D":
+ continue
+ region = next((r for r in area.regions if r.type == "WINDOW"), None)
+ if region is None:
+ continue
+ PROSCENIO_OT_automesh_authoring._launch_from_blank = True
+ with context.temp_override(window=window, area=area, region=region):
+ bpy.ops.proscenio.automesh_authoring("INVOKE_DEFAULT")
+ return True
return False📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class PROSCENIO_OT_pen_mesh_new(bpy.types.Operator): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Create a mesh element from an image and open the pen to draw its outline.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bl_idname = "proscenio.pen_mesh_new" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bl_label = "Proscenio: New Pen Mesh" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bl_description = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Pick an image and draw a new mesh element over it point by point with the " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "pen, like Spine / Illustrator. Creates the element, then opens the " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "interactive authoring pen on a blank outline" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bl_options: ClassVar[set[str]] = {"REGISTER", "UNDO"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filepath: StringProperty(subtype="FILE_PATH") # type: ignore[valid-type] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: StringProperty( # type: ignore[valid-type] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="Name", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description="Name for the new mesh element", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default="PenMesh", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def invoke(self, context: bpy.types.Context, _event: bpy.types.Event) -> set[str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| context.window_manager.fileselect_add(self) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"RUNNING_MODAL"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def execute(self, context: bpy.types.Context) -> set[str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not self.filepath or not Path(self.filepath).is_file(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| report_error(self, "pick an image file to draw the mesh over") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"CANCELLED"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| obj = create_pen_mesh_element(context, Path(self.filepath), self.name) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except (RuntimeError, ValueError) as exc: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| report_error(self, f"could not create pen mesh: {exc}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"CANCELLED"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _launch_authoring_from_blank(context): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| report_info(self, f"created '{obj.name}' - draw its outline with the pen") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| report_info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"created '{obj.name}' - open Mesh Generation > Author Mesh to draw it", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"FINISHED"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _classes: tuple[type, ...] = (PROSCENIO_OT_pen_mesh_new,) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def register() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for cls in _classes: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bpy.utils.register_class(cls) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def unregister() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for cls in reversed(_classes): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bpy.utils.unregister_class(cls) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -446,6 +446,61 @@ def test_user_strokes_round_trip(automesh_fixture): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert len(restored[1]["points"]) == 3 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_create_pen_mesh_element_builds_a_mesh_element(automesh_fixture, tmp_path): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """The from-blank helper builds an imported-shape mesh element from an image: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| a MESH with an image material, the placement tag, element_type 'mesh', active.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from proscenio.core._shared.cp_keys import ( # type: ignore[import-not-found] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PROSCENIO_IMPORT_PLACEMENT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from proscenio.core._shared.material_images import ( # type: ignore[import-not-found] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| first_material_image, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from proscenio.core._shared.props_access import ( # type: ignore[import-not-found] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| element_type_of, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from proscenio.operators.automesh.pen_mesh_new import ( # type: ignore[import-not-found] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| create_pen_mesh_element, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| img_path = Path(tmp_path) / "pen_src.png" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| img = bpy.data.images.new("pen_src", width=64, height=32) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| img.filepath_raw = str(img_path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| img.file_format = "PNG" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| img.save() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| obj = create_pen_mesh_element(bpy.context, img_path, "PenTest") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert obj.type == "MESH" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert element_type_of(obj) == "mesh" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert PROSCENIO_IMPORT_PLACEMENT in obj | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+460
to
+476
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Assert the persisted element-type tag here.
Suggested fix- from proscenio.core._shared.props_access import ( # type: ignore[import-not-found]
- element_type_of,
- )
@@
- assert element_type_of(obj) == "mesh"
+ props = getattr(obj, "proscenio", None)
+ if props is not None:
+ assert props.element_type == "mesh"
+ assert obj.get("proscenio_type") == "mesh"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert first_material_image(obj) is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert bpy.context.view_layer.objects.active is obj | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_authored_outer_contour_round_trip(automesh_fixture): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """The clicked outer anchors round-trip; absent reads None; degenerate reads None.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| obj = _activate("hand") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from proscenio.core.bpy_helpers.automesh.authoring_pipeline import ( # type: ignore[import-not-found] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| read_authored_outer_contour, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| write_authored_outer_contour, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "proscenio_authored_outer_contour" in obj: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| del obj["proscenio_authored_outer_contour"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Never authored -> None (so a launch alpha-traces as usual, not re-edits). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert read_authored_outer_contour(obj) is None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| anchors = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| write_authored_outer_contour(obj, anchors) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| restored = read_authored_outer_contour(obj) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert restored == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # A degenerate payload (cannot form a loop) reads None, not a broken ring. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| write_authored_outer_contour(obj, [(0.0, 0.0), (1.0, 0.0)]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert read_authored_outer_contour(obj) is None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_user_strokes_legacy_fallback(automesh_fixture): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Legacy proscenio_user_steiners (flat list) reads as kind=point strokes.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| obj = _activate("hand") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
_launch_from_blankcan leak into the next unrelated launch.apps/blender/operators/automesh/pen_mesh_new.py:62-83arms this class flag before invoking the modal, but it is only cleared here after the early validation path. If poll fails orinvoke()exits on the mesh/image guards, the flag staysTrue, and the next normal authoring launch can resolve to"blank"instead of"trace". Clear it before the guards and reset it in the caller withtry/finally, or stop using cross-invocation class state here.Also applies to: 249-258
🤖 Prompt for AI Agents