From 760267495330263a388bf95d77614bef0d6f531f Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Mon, 26 Aug 2024 05:57:52 +0200 Subject: [PATCH 01/18] Fixed issue #426 This might be a patch for the problem, but it works --- operators/add_workplane.py | 1 + 1 file changed, 1 insertion(+) diff --git a/operators/add_workplane.py b/operators/add_workplane.py index 34cfeebd..fd1c5532 100644 --- a/operators/add_workplane.py +++ b/operators/add_workplane.py @@ -137,6 +137,7 @@ def main(self, context: Context): self.target = sse.add_workplane(origin, nm) ignore_hover(self.target) + [a.tag_redraw() for a in bpy.context.screen.areas] # Force re-draw of UI (Blender doesn't update after tool usage) return True From 4528eec2916f32eb6892243159ca885e7908b622 Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Tue, 27 Aug 2024 23:09:35 +0200 Subject: [PATCH 02/18] Now less redraws, but still working --- operators/add_workplane.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operators/add_workplane.py b/operators/add_workplane.py index fd1c5532..0c006209 100644 --- a/operators/add_workplane.py +++ b/operators/add_workplane.py @@ -137,7 +137,7 @@ def main(self, context: Context): self.target = sse.add_workplane(origin, nm) ignore_hover(self.target) - [a.tag_redraw() for a in bpy.context.screen.areas] # Force re-draw of UI (Blender doesn't update after tool usage) + context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) return True From ac6f2b6e90998fa9a40056a4bb713536fe043d6e Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Wed, 28 Aug 2024 04:04:02 +0200 Subject: [PATCH 03/18] mesh to workplane projection now working Also used as a backup for me --- operators/add_workplane.py | 64 +++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/operators/add_workplane.py b/operators/add_workplane.py index 0c006209..fb2057ae 100644 --- a/operators/add_workplane.py +++ b/operators/add_workplane.py @@ -2,6 +2,7 @@ import bpy from bpy.types import Operator, Context +from mathutils import Vector from .. import global_data from ..model.types import SlvsNormal3D @@ -17,6 +18,7 @@ from .constants import types_point_3d from .utilities import ignore_hover from ..utilities.view import get_placement_pos +from .utilities import activate_sketch, switch_sketch_mode logger = logging.getLogger(__name__) @@ -123,21 +125,61 @@ class View3D_OT_slvs_add_workplane_face(Operator, Operator3d): def main(self, context: Context): sse = context.scene.sketcher.entities - ob_name, face_index = self.get_state_pointer(index=0, implicit=True) - ob = get_evaluated_obj(context, bpy.data.objects[ob_name]) - mesh = ob.data - face = mesh.polygons[face_index] - - mat_obj = ob.matrix_world - quat = get_face_orientation(mesh, face) - quat.rotate(mat_obj) - pos = mat_obj @ face.center - origin = sse.add_point_3d(pos) + obj_name, clicked_face_index = self.get_state_pointer(index=0, implicit=True) + clicked_obj = get_evaluated_obj(context, bpy.data.objects[obj_name]) + clicked_mesh = clicked_obj.data + clicked_face = clicked_mesh.polygons[clicked_face_index] + + obj_translation = clicked_obj.matrix_world + quat = get_face_orientation(clicked_mesh, clicked_face) # Quternion + quat.rotate(obj_translation) + + workplane_origin = obj_translation @ clicked_face.center + print("1: " + str(obj_translation)) + print("2: " + str(clicked_face)) + print("2.1: " + str(clicked_face.center)) + origin = sse.add_point_3d(workplane_origin) nm = sse.add_normal_3d(quat) self.target = sse.add_workplane(origin, nm) - ignore_hover(self.target) + context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) + + meshes = [o for o in context.scene.objects if o.type == 'MESH'] + # print(meshes) + + # Workplane normal in world coordinates + workplane_normal = quat @ Vector((0.0, 0.0, 1.0)) + + sketch = sse.add_sketch(self.target) + p = sse.add_point_2d((0.0, 0.0), sketch) + p.fixed = True + + activate_sketch(context, sketch.slvs_index, self) + self.target = sketch + + for clicked_mesh in meshes: + vertices = clicked_mesh.data.vertices + for vertex in vertices: + # Make vertex relative to plane + vertex_world = obj_translation @ vertex.co + translated = vertex_world - workplane_origin + + # Projection to plane + distance_to_plane = translated.dot(workplane_normal) + projection = translated - distance_to_plane * workplane_normal + + ## Used ChatGPT, quaternion rotations is too hard. + # To 2D projection relative to the workplane + # Use the workplane orientation (quat) to project into 2D + local_projection = projection.copy() + local_projection.rotate(quat.conjugated()) + x, y, _ = local_projection + + p = sse.add_point_2d((x, y), sketch) + + + return True From 721eb48a418270d573d6f79c900a4339db0a9d7b Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Wed, 28 Aug 2024 04:33:09 +0200 Subject: [PATCH 04/18] Added limiter and semi-colon for readability --- operators/add_workplane.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/operators/add_workplane.py b/operators/add_workplane.py index fb2057ae..08fb0bc2 100644 --- a/operators/add_workplane.py +++ b/operators/add_workplane.py @@ -152,31 +152,37 @@ def main(self, context: Context): workplane_normal = quat @ Vector((0.0, 0.0, 1.0)) sketch = sse.add_sketch(self.target) - p = sse.add_point_2d((0.0, 0.0), sketch) - p.fixed = True + p = sse.add_point_2d((0.0, 0.0), sketch, fixed = True) activate_sketch(context, sketch.slvs_index, self) self.target = sketch + limitDist = 0.005; + connectLines = True; + for clicked_mesh in meshes: - vertices = clicked_mesh.data.vertices + vertices = clicked_mesh.data.vertices; for vertex in vertices: # Make vertex relative to plane - vertex_world = obj_translation @ vertex.co - translated = vertex_world - workplane_origin + vertex_world = obj_translation @ vertex.co; + translated = vertex_world - workplane_origin; # Projection to plane - distance_to_plane = translated.dot(workplane_normal) - projection = translated - distance_to_plane * workplane_normal + distance_to_plane = translated.dot(workplane_normal); + projection = translated - distance_to_plane * workplane_normal; + + if abs(distance_to_plane) > limitDist: + continue; + print(f"Vertex {vertex.index} distance to plane: {abs(distance_to_plane)}"); ## Used ChatGPT, quaternion rotations is too hard. # To 2D projection relative to the workplane # Use the workplane orientation (quat) to project into 2D - local_projection = projection.copy() - local_projection.rotate(quat.conjugated()) - x, y, _ = local_projection + local_projection = projection.copy(); + local_projection.rotate(quat.conjugated()); + x, y, _ = local_projection; - p = sse.add_point_2d((x, y), sketch) + p = sse.add_point_2d((x, y), sketch, fixed = True); From f3dce910e95e1436dcf54f01f5a3f4b8164597c2 Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Wed, 28 Aug 2024 10:53:46 +0200 Subject: [PATCH 05/18] backup sync --- operators/add_workplane.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/operators/add_workplane.py b/operators/add_workplane.py index 08fb0bc2..5f536b72 100644 --- a/operators/add_workplane.py +++ b/operators/add_workplane.py @@ -158,8 +158,9 @@ def main(self, context: Context): self.target = sketch limitDist = 0.005; - connectLines = True; + connectLines = True; # May cause performance issues. idk + addedPoints = {} for clicked_mesh in meshes: vertices = clicked_mesh.data.vertices; for vertex in vertices: @@ -173,7 +174,7 @@ def main(self, context: Context): if abs(distance_to_plane) > limitDist: continue; - print(f"Vertex {vertex.index} distance to plane: {abs(distance_to_plane)}"); + # print(f"Vertex {vertex.index} distance to plane: {abs(distance_to_plane)}"); ## Used ChatGPT, quaternion rotations is too hard. # To 2D projection relative to the workplane @@ -182,7 +183,24 @@ def main(self, context: Context): local_projection.rotate(quat.conjugated()); x, y, _ = local_projection; - p = sse.add_point_2d((x, y), sketch, fixed = True); + point = sse.add_point_2d((x, y), sketch, fixed = True); + addedPoints[vertex.index] = point; + # print(point.location) + + if (connectLines != True): + continue; + + # print(addedPoints); + + compareSet = set(addedPoints.keys()) + edges = clicked_mesh.data.edges; + for edge in edges: + if (set(edge.vertices) & compareSet != True): continue + + + + + # print(f"Edge {edge.index} vertices: {[str(edge.vertices[x]) for x in range(2)]}"); From 60b18bb648cdb7cc1f50bbddcc84ab445e90e16d Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Wed, 28 Aug 2024 21:07:25 +0200 Subject: [PATCH 06/18] Line projection working --- operators/add_workplane.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/operators/add_workplane.py b/operators/add_workplane.py index 5f536b72..ca963f15 100644 --- a/operators/add_workplane.py +++ b/operators/add_workplane.py @@ -135,9 +135,9 @@ def main(self, context: Context): quat.rotate(obj_translation) workplane_origin = obj_translation @ clicked_face.center - print("1: " + str(obj_translation)) - print("2: " + str(clicked_face)) - print("2.1: " + str(clicked_face.center)) + # print("1: " + str(obj_translation)) + # print("2: " + str(clicked_face)) + # print("2.1: " + str(clicked_face.center)) origin = sse.add_point_3d(workplane_origin) nm = sse.add_normal_3d(quat) @@ -157,7 +157,12 @@ def main(self, context: Context): activate_sketch(context, sketch.slvs_index, self) self.target = sketch - limitDist = 0.005; + # TODO: Only project selected mesh/face depending on checkbox + # TODO: Option to not project after creating workplane + # TODO: Option to choose if projected lines/points should be construction + + # Make these changable when creating face + limitDist = 0.025; connectLines = True; # May cause performance issues. idk addedPoints = {} @@ -183,24 +188,26 @@ def main(self, context: Context): local_projection.rotate(quat.conjugated()); x, y, _ = local_projection; - point = sse.add_point_2d((x, y), sketch, fixed = True); + point = sse.add_point_2d((x, y), sketch, fixed = True, index_reference = True); addedPoints[vertex.index] = point; # print(point.location) if (connectLines != True): continue; - - # print(addedPoints); compareSet = set(addedPoints.keys()) + # print(compareSet); edges = clicked_mesh.data.edges; for edge in edges: - if (set(edge.vertices) & compareSet != True): continue + if (set(edge.vertices).issubset(compareSet) != True): continue; - - + p1, p2 = [addedPoints[x] for x in edge.vertices]; + # print(p1.location); + # print(p2.location); + sse.add_line_2d(p1, p2, sketch, fixed = True, index_reference = True); # print(f"Edge {edge.index} vertices: {[str(edge.vertices[x]) for x in range(2)]}"); + # break; From c37eba07c0b85012be84410a1f43e9025bee4c51 Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Wed, 28 Aug 2024 21:54:40 +0200 Subject: [PATCH 07/18] Seperated sketch on face to another button --- declarations.py | 2 + operators/add_sketch.py | 121 +++++++++++++++++++++++++++++- operators/add_workplane.py | 94 +++-------------------- workspacetools/__init__.py | 7 +- workspacetools/add_sketch_face.py | 20 +++++ 5 files changed, 160 insertions(+), 84 deletions(-) create mode 100644 workspacetools/add_sketch_face.py diff --git a/declarations.py b/declarations.py index 6660103a..372c715d 100644 --- a/declarations.py +++ b/declarations.py @@ -39,6 +39,7 @@ class Operators(str, Enum): AddRatio = "view3d.slvs_add_ratio" AddRectangle = "view3d.slvs_add_rectangle" AddSketch = "view3d.slvs_add_sketch" + AddSketchFace = "view3d.slvs_add_sketch_face" AddTangent = "view3d.slvs_add_tangent" AddVertical = "view3d.slvs_add_vertical" AddWorkPlane = "view3d.slvs_add_workplane" @@ -106,6 +107,7 @@ class WorkSpaceTools(str, Enum): AddPoint2D = "sketcher.slvs_add_point2d" AddPoint3D = "sketcher.slvs_add_point3d" AddRectangle = "sketcher.slvs_add_rectangle" + AddSketchFace = "sketcher.slvs_add_sketch_face" AddWorkplane = "sketcher.slvs_add_workplane" AddWorkplaneFace = "sketcher.slvs_add_workplane_face" Offset = "sketcher.slvs_offset" diff --git a/operators/add_sketch.py b/operators/add_sketch.py index 9813a9c0..ac836d6e 100644 --- a/operators/add_sketch.py +++ b/operators/add_sketch.py @@ -2,6 +2,7 @@ import bpy from bpy.types import Operator, Context, Event +from mathutils import Vector from ..model.types import SlvsWorkplane from ..declarations import Operators @@ -9,6 +10,8 @@ from ..stateful_operator.state import state_from_args from .base_3d import Operator3d from .utilities import activate_sketch, switch_sketch_mode +from ..stateful_operator.utilities.geometry import get_evaluated_obj, get_mesh_element +from ..utilities.geometry import get_face_orientation logger = logging.getLogger(__name__) @@ -71,4 +74,120 @@ def fini(self, context: Context, succeed: bool): switch_sketch_mode(self, context, to_sketch_mode=False) -register, unregister = register_stateops_factory((View3D_OT_slvs_add_sketch,)) + +class View3D_OT_slvs_add_sketch_face(Operator, Operator3d): + """Add a workplane and start sketch on mesh face""" + + bl_idname = Operators.AddSketchFace + bl_label = "Add sketch on mesh face" + bl_options = {"REGISTER", "UNDO"} + + wp_face_state1_doc = ( + "Face", + "Pick a mesh face to use as workplane's and sketch's surface.", + ) + + states = ( + state_from_args( + wp_face_state1_doc[0], + description=wp_face_state1_doc[1], + use_create=False, + pointer="face", + types=(bpy.types.MeshPolygon,), + interactive=True, + ), + ) + + def main(self, context: Context): + sse = context.scene.sketcher.entities + + obj_name, clicked_face_index = self.get_state_pointer(index=0, implicit=True) + clicked_obj = get_evaluated_obj(context, bpy.data.objects[obj_name]) + clicked_mesh = clicked_obj.data + clicked_face = clicked_mesh.polygons[clicked_face_index] + + obj_translation = clicked_obj.matrix_world + quat = get_face_orientation(clicked_mesh, clicked_face) # Quternion + quat.rotate(obj_translation) + + workplane_origin = obj_translation @ clicked_face.center + print("1: " + str(obj_translation)) + print("2: " + str(clicked_face)) + print("2.1: " + str(clicked_face.center)) + origin = sse.add_point_3d(workplane_origin) + nm = sse.add_normal_3d(quat) + + self.target = sse.add_workplane(origin, nm) + + context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) + + meshes = [o for o in context.scene.objects if o.type == 'MESH'] + # print(meshes) + + # Workplane normal in world coordinates + workplane_normal = quat @ Vector((0.0, 0.0, 1.0)) + + sketch = sse.add_sketch(self.target) + p = sse.add_point_2d((0.0, 0.0), sketch, fixed = True) + + activate_sketch(context, sketch.slvs_index, self) + self.target = sketch + + # TODO: Only project selected mesh/face depending on checkbox + # TODO: Option to not project after creating workplane + # TODO: Option to choose if projected lines/points should be construction + + # Make these changable when creating face + limitDist = 0.025; + connectLines = True; # May cause performance issues. idk + + addedPoints = {} + for clicked_mesh in meshes: + vertices = clicked_mesh.data.vertices; + for vertex in vertices: + # Make vertex relative to plane + vertex_world = obj_translation @ vertex.co; + translated = vertex_world - workplane_origin; + + # Projection to plane + distance_to_plane = translated.dot(workplane_normal); + projection = translated - distance_to_plane * workplane_normal; + + if abs(distance_to_plane) > limitDist: + continue; + # print(f"Vertex {vertex.index} distance to plane: {abs(distance_to_plane)}"); + + ## Used ChatGPT, quaternion rotations is too hard. + # To 2D projection relative to the workplane + # Use the workplane orientation (quat) to project into 2D + local_projection = projection.copy(); + local_projection.rotate(quat.conjugated()); + x, y, _ = local_projection; + + point = sse.add_point_2d((x, y), sketch, fixed = True, index_reference = True); + addedPoints[vertex.index] = point; + # print(point.location) + + if (connectLines != True): + continue; + + compareSet = set(addedPoints.keys()) + # print(compareSet); + edges = clicked_mesh.data.edges; + for edge in edges: + if (set(edge.vertices).issubset(compareSet) != True): continue; + + p1, p2 = [addedPoints[x] for x in edge.vertices]; + # print(p1.location); + # print(p2.location); + sse.add_line_2d(p1, p2, sketch, fixed = True, index_reference = True); + + # print(f"Edge {edge.index} vertices: {[str(edge.vertices[x]) for x in range(2)]}"); + # break; + + + + return True + + +register, unregister = register_stateops_factory((View3D_OT_slvs_add_sketch,View3D_OT_slvs_add_sketch_face)) diff --git a/operators/add_workplane.py b/operators/add_workplane.py index ca963f15..b0e50021 100644 --- a/operators/add_workplane.py +++ b/operators/add_workplane.py @@ -125,95 +125,25 @@ class View3D_OT_slvs_add_workplane_face(Operator, Operator3d): def main(self, context: Context): sse = context.scene.sketcher.entities - obj_name, clicked_face_index = self.get_state_pointer(index=0, implicit=True) - clicked_obj = get_evaluated_obj(context, bpy.data.objects[obj_name]) - clicked_mesh = clicked_obj.data - clicked_face = clicked_mesh.polygons[clicked_face_index] - - obj_translation = clicked_obj.matrix_world - quat = get_face_orientation(clicked_mesh, clicked_face) # Quternion - quat.rotate(obj_translation) - - workplane_origin = obj_translation @ clicked_face.center - # print("1: " + str(obj_translation)) - # print("2: " + str(clicked_face)) - # print("2.1: " + str(clicked_face.center)) - origin = sse.add_point_3d(workplane_origin) + ob_name, face_index = self.get_state_pointer(index=0, implicit=True) + ob = get_evaluated_obj(context, bpy.data.objects[ob_name]) + mesh = ob.data + face = mesh.polygons[face_index] + + mat_obj = ob.matrix_world + quat = get_face_orientation(mesh, face) + quat.rotate(mat_obj) + pos = mat_obj @ face.center + origin = sse.add_point_3d(pos) nm = sse.add_normal_3d(quat) self.target = sse.add_workplane(origin, nm) - + ignore_hover(self.target) context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) - - meshes = [o for o in context.scene.objects if o.type == 'MESH'] - # print(meshes) - - # Workplane normal in world coordinates - workplane_normal = quat @ Vector((0.0, 0.0, 1.0)) - - sketch = sse.add_sketch(self.target) - p = sse.add_point_2d((0.0, 0.0), sketch, fixed = True) - - activate_sketch(context, sketch.slvs_index, self) - self.target = sketch - - # TODO: Only project selected mesh/face depending on checkbox - # TODO: Option to not project after creating workplane - # TODO: Option to choose if projected lines/points should be construction - - # Make these changable when creating face - limitDist = 0.025; - connectLines = True; # May cause performance issues. idk - - addedPoints = {} - for clicked_mesh in meshes: - vertices = clicked_mesh.data.vertices; - for vertex in vertices: - # Make vertex relative to plane - vertex_world = obj_translation @ vertex.co; - translated = vertex_world - workplane_origin; - - # Projection to plane - distance_to_plane = translated.dot(workplane_normal); - projection = translated - distance_to_plane * workplane_normal; - - if abs(distance_to_plane) > limitDist: - continue; - # print(f"Vertex {vertex.index} distance to plane: {abs(distance_to_plane)}"); - - ## Used ChatGPT, quaternion rotations is too hard. - # To 2D projection relative to the workplane - # Use the workplane orientation (quat) to project into 2D - local_projection = projection.copy(); - local_projection.rotate(quat.conjugated()); - x, y, _ = local_projection; - - point = sse.add_point_2d((x, y), sketch, fixed = True, index_reference = True); - addedPoints[vertex.index] = point; - # print(point.location) - - if (connectLines != True): - continue; - - compareSet = set(addedPoints.keys()) - # print(compareSet); - edges = clicked_mesh.data.edges; - for edge in edges: - if (set(edge.vertices).issubset(compareSet) != True): continue; - - p1, p2 = [addedPoints[x] for x in edge.vertices]; - # print(p1.location); - # print(p2.location); - sse.add_line_2d(p1, p2, sketch, fixed = True, index_reference = True); - - # print(f"Edge {edge.index} vertices: {[str(edge.vertices[x]) for x in range(2)]}"); - # break; - - - return True + register, unregister = register_stateops_factory( (View3D_OT_slvs_add_workplane, View3D_OT_slvs_add_workplane_face) ) diff --git a/workspacetools/__init__.py b/workspacetools/__init__.py index 61818198..68b722c6 100644 --- a/workspacetools/__init__.py +++ b/workspacetools/__init__.py @@ -10,6 +10,7 @@ from .add_rectangle import VIEW3D_T_slvs_add_rectangle from .add_workplane import VIEW3D_T_slvs_add_workplane from .add_workplane_face import VIEW3D_T_slvs_add_workplane_face +from .add_sketch_face import VIEW3D_T_slvs_add_sketch_face from .bevel import VIEW3D_T_slvs_bevel from .offset import VIEW3D_T_slvs_offset from .select import VIEW3D_T_slvs_select @@ -38,7 +39,11 @@ (VIEW3D_T_slvs_trim, {"separator": True, "group": False}), (VIEW3D_T_slvs_bevel, {"separator": False, "group": False}), (VIEW3D_T_slvs_offset, {"separator": False, "group": False}), - (VIEW3D_T_slvs_add_workplane_face, {"separator": True, "group": True}), + (VIEW3D_T_slvs_add_sketch_face, {"separator": True, "group": True}), + ( + VIEW3D_T_slvs_add_workplane_face, + {"after": {VIEW3D_T_slvs_add_sketch_face.bl_idname}}, + ), ( VIEW3D_T_slvs_add_workplane, {"after": {VIEW3D_T_slvs_add_workplane_face.bl_idname}}, diff --git a/workspacetools/add_sketch_face.py b/workspacetools/add_sketch_face.py new file mode 100644 index 00000000..85eeb87e --- /dev/null +++ b/workspacetools/add_sketch_face.py @@ -0,0 +1,20 @@ +from bpy.types import WorkSpaceTool + +from ..declarations import GizmoGroups, Operators, WorkSpaceTools +from ..keymaps import tool_generic +from ..stateful_operator.tool import GenericStateTool +from ..stateful_operator.utilities.keymap import operator_access + + +class VIEW3D_T_slvs_add_sketch_face(GenericStateTool, WorkSpaceTool): + bl_space_type = "VIEW_3D" + bl_context_mode = "OBJECT" + bl_idname = WorkSpaceTools.AddSketchFace + bl_label = "Add a workplane and start sketch on mesh face" + bl_operator = Operators.AddSketchFace + bl_icon = "ops.mesh.primitive_grid_add_gizmo" + bl_widget = GizmoGroups.Preselection + bl_keymap = ( + *tool_generic, + *operator_access(Operators.AddSketchFace), + ) From b6e70dd564e34f245c3b165921cb673fc77641be Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Wed, 28 Aug 2024 22:07:49 +0200 Subject: [PATCH 08/18] Disabled print statements and tweaked max dist --- operators/add_sketch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/operators/add_sketch.py b/operators/add_sketch.py index ac836d6e..20501909 100644 --- a/operators/add_sketch.py +++ b/operators/add_sketch.py @@ -111,9 +111,9 @@ def main(self, context: Context): quat.rotate(obj_translation) workplane_origin = obj_translation @ clicked_face.center - print("1: " + str(obj_translation)) - print("2: " + str(clicked_face)) - print("2.1: " + str(clicked_face.center)) + # print("1: " + str(obj_translation)) + # print("2: " + str(clicked_face)) + # print("2.1: " + str(clicked_face.center)) origin = sse.add_point_3d(workplane_origin) nm = sse.add_normal_3d(quat) @@ -138,7 +138,7 @@ def main(self, context: Context): # TODO: Option to choose if projected lines/points should be construction # Make these changable when creating face - limitDist = 0.025; + limitDist = 0.001; connectLines = True; # May cause performance issues. idk addedPoints = {} From 2fcf9fb7374e09580b6047fc99a6ceeded32dcc0 Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Sun, 1 Sep 2024 02:25:12 +0200 Subject: [PATCH 09/18] Seperated projection code from add sketch code --- operators/add_sketch.py | 121 ++++++++++++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 31 deletions(-) diff --git a/operators/add_sketch.py b/operators/add_sketch.py index 20501909..e0ed1fdd 100644 --- a/operators/add_sketch.py +++ b/operators/add_sketch.py @@ -2,7 +2,8 @@ import bpy from bpy.types import Operator, Context, Event -from mathutils import Vector +from bpy.props import FloatProperty, BoolProperty, EnumProperty +from mathutils import Vector, Quaternion from ..model.types import SlvsWorkplane from ..declarations import Operators @@ -12,10 +13,26 @@ from .utilities import activate_sketch, switch_sketch_mode from ..stateful_operator.utilities.geometry import get_evaluated_obj, get_mesh_element from ..utilities.geometry import get_face_orientation +from ..model.group_entities import SlvsEntities, SlvsSketch logger = logging.getLogger(__name__) +class ProjectionData: + def __init__(self, + sketcherEntities: SlvsEntities, + sketch: SlvsSketch, + objectTranslation: bpy.types.TransformOrientation, + workplaneOrigin: tuple[float, float, float], + workplaneNormal: Vector, + quat: Quaternion): + + self.sketcherEntities = sketcherEntities + self.sketch = sketch + self.objectTranslation = objectTranslation + self.workplaneOrigin = workplaneOrigin + self.workplaneNormal = workplaneNormal + self.quat = quat # I forgot what quat was... Should've added more comments # TODO: # - Draw sketches @@ -77,40 +94,66 @@ def fini(self, context: Context, succeed: bool): class View3D_OT_slvs_add_sketch_face(Operator, Operator3d): """Add a workplane and start sketch on mesh face""" - + bl_idname = Operators.AddSketchFace bl_label = "Add sketch on mesh face" bl_options = {"REGISTER", "UNDO"} - wp_face_state1_doc = ( - "Face", - "Pick a mesh face to use as workplane's and sketch's surface.", + # Can't get default to work. idk why + projectDist: FloatProperty( + name="Project distance", + subtype="DISTANCE", + unit="LENGTH", + default=0.001, + step=0.01, + # precision=get_prefs().decimal_precision, + ) + + # # Idk why it doesn't work correctly + # connectLines: BoolProperty(name="Connect lines", description="May cause performance issues, idk", default=True) + connectLines = True + + projectFrom: EnumProperty( + name="My Search", + items=( + ('FACE', "Face", ""), + ('MESH', "Mesh", ""), + ('ALL', "All meshes", ""), + ), + default='ALL' ) states = ( state_from_args( - wp_face_state1_doc[0], - description=wp_face_state1_doc[1], + "Face", + description="Pick a mesh face to use as workplane's and sketch's surface.", use_create=False, pointer="face", types=(bpy.types.MeshPolygon,), interactive=True, ), + state_from_args( + "Additional projection distance", + description="Additional projection distance (default + extra)", + property="projectDist", + interactive=True, + no_event=True, + ), ) def main(self, context: Context): - sse = context.scene.sketcher.entities + sse: SlvsEntities = context.scene.sketcher.entities obj_name, clicked_face_index = self.get_state_pointer(index=0, implicit=True) clicked_obj = get_evaluated_obj(context, bpy.data.objects[obj_name]) clicked_mesh = clicked_obj.data - clicked_face = clicked_mesh.polygons[clicked_face_index] + clicked_face: bpy.types.MeshPolygon = clicked_mesh.polygons[clicked_face_index] - obj_translation = clicked_obj.matrix_world + obj_translation: bpy.types.TransformOrientation = clicked_obj.matrix_world quat = get_face_orientation(clicked_mesh, clicked_face) # Quternion quat.rotate(obj_translation) - workplane_origin = obj_translation @ clicked_face.center + workplane_origin: tuple[float, float, float] = obj_translation @ clicked_face.center # print("1: " + str(obj_translation)) # print("2: " + str(clicked_face)) # print("2.1: " + str(clicked_face.center)) @@ -118,10 +161,8 @@ def main(self, context: Context): nm = sse.add_normal_3d(quat) self.target = sse.add_workplane(origin, nm) - - context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) - meshes = [o for o in context.scene.objects if o.type == 'MESH'] + # print(meshes) # Workplane normal in world coordinates @@ -130,30 +171,51 @@ def main(self, context: Context): sketch = sse.add_sketch(self.target) p = sse.add_point_2d((0.0, 0.0), sketch, fixed = True) - activate_sketch(context, sketch.slvs_index, self) - self.target = sketch + # print(self.projectFrom) + + # activate_sketch(context, sketch.slvs_index, self) + # self.target = sketch # TODO: Only project selected mesh/face depending on checkbox # TODO: Option to not project after creating workplane # TODO: Option to choose if projected lines/points should be construction + # TODO: Auto align view with sketch # Make these changable when creating face - limitDist = 0.001; - connectLines = True; # May cause performance issues. idk + limitDist = 0.001 + self.projectDist; + + projectionData = ProjectionData(sse, sketch, obj_translation, workplane_origin, workplane_normal, quat) + + if self.projectFrom == 'FACE': + logger.error("Project face is not implemented yet") + elif self.projectFrom == 'MESH': + self.ProjectFromMeshes(projectionData, [clicked_obj,], limitDist, self.connectLines) + elif self.projectFrom == 'ALL': # ALL doesn't actually work. I don't think its important to fix atm + allMeshesInScene = [o for o in context.scene.objects if o.type == 'MESH'] + self.ProjectFromMeshes(projectionData, allMeshesInScene, limitDist, self.connectLines) + context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) + return True + + def ProjectFromMeshes(self, projectionData: ProjectionData, + meshes: list[bpy.types.Mesh], + maxDist: float, + connectLines: bool = True): + sse = projectionData.sketcherEntities + addedPoints = {} for clicked_mesh in meshes: vertices = clicked_mesh.data.vertices; for vertex in vertices: # Make vertex relative to plane - vertex_world = obj_translation @ vertex.co; - translated = vertex_world - workplane_origin; + vertex_world = projectionData.objectTranslation @ vertex.co; + translated = vertex_world - projectionData.workplaneOrigin; # Projection to plane - distance_to_plane = translated.dot(workplane_normal); - projection = translated - distance_to_plane * workplane_normal; + distance_to_plane = translated.dot(projectionData.workplaneNormal); + projection = translated - distance_to_plane * projectionData.workplaneNormal; - if abs(distance_to_plane) > limitDist: + if abs(distance_to_plane) > maxDist: continue; # print(f"Vertex {vertex.index} distance to plane: {abs(distance_to_plane)}"); @@ -161,10 +223,10 @@ def main(self, context: Context): # To 2D projection relative to the workplane # Use the workplane orientation (quat) to project into 2D local_projection = projection.copy(); - local_projection.rotate(quat.conjugated()); + local_projection.rotate(projectionData.quat.conjugated()); x, y, _ = local_projection; - point = sse.add_point_2d((x, y), sketch, fixed = True, index_reference = True); + point = sse.add_point_2d((x, y), projectionData.sketch, fixed = True, index_reference = True); addedPoints[vertex.index] = point; # print(point.location) @@ -180,14 +242,11 @@ def main(self, context: Context): p1, p2 = [addedPoints[x] for x in edge.vertices]; # print(p1.location); # print(p2.location); - sse.add_line_2d(p1, p2, sketch, fixed = True, index_reference = True); + sse.add_line_2d(p1, p2, projectionData.sketch, fixed = True, index_reference = True); # print(f"Edge {edge.index} vertices: {[str(edge.vertices[x]) for x in range(2)]}"); # break; - - - - return True - + pass + register, unregister = register_stateops_factory((View3D_OT_slvs_add_sketch,View3D_OT_slvs_add_sketch_face)) From 85e66860b101447fb7062987759a706cfc7ebf28 Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Sun, 1 Sep 2024 02:29:03 +0200 Subject: [PATCH 10/18] Cleanup --- operators/add_sketch.py | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/operators/add_sketch.py b/operators/add_sketch.py index e0ed1fdd..61334b1c 100644 --- a/operators/add_sketch.py +++ b/operators/add_sketch.py @@ -91,7 +91,7 @@ def fini(self, context: Context, succeed: bool): switch_sketch_mode(self, context, to_sketch_mode=False) - +# TODO: Auto align view with sketch after creation class View3D_OT_slvs_add_sketch_face(Operator, Operator3d): """Add a workplane and start sketch on mesh face""" @@ -154,35 +154,21 @@ def main(self, context: Context): quat.rotate(obj_translation) workplane_origin: tuple[float, float, float] = obj_translation @ clicked_face.center - # print("1: " + str(obj_translation)) - # print("2: " + str(clicked_face)) - # print("2.1: " + str(clicked_face.center)) origin = sse.add_point_3d(workplane_origin) nm = sse.add_normal_3d(quat) self.target = sse.add_workplane(origin, nm) - - # print(meshes) - # Workplane normal in world coordinates workplane_normal = quat @ Vector((0.0, 0.0, 1.0)) sketch = sse.add_sketch(self.target) - p = sse.add_point_2d((0.0, 0.0), sketch, fixed = True) + sse.add_point_2d((0.0, 0.0), sketch, fixed = True) # Add face centrum point - # print(self.projectFrom) - - # activate_sketch(context, sketch.slvs_index, self) + # activate_sketch(context, sketch.slvs_index, self) # This hides the pop-up with the options for the projection # self.target = sketch - # TODO: Only project selected mesh/face depending on checkbox - # TODO: Option to not project after creating workplane - # TODO: Option to choose if projected lines/points should be construction - # TODO: Auto align view with sketch - - # Make these changable when creating face - limitDist = 0.001 + self.projectDist; + limitDist = 0.001 + self.projectDist; # Should just be the project dist, but couldn't get default in property to work projectionData = ProjectionData(sse, sketch, obj_translation, workplane_origin, workplane_normal, quat) @@ -217,7 +203,6 @@ def ProjectFromMeshes(self, projectionData: ProjectionData, if abs(distance_to_plane) > maxDist: continue; - # print(f"Vertex {vertex.index} distance to plane: {abs(distance_to_plane)}"); ## Used ChatGPT, quaternion rotations is too hard. # To 2D projection relative to the workplane @@ -228,24 +213,17 @@ def ProjectFromMeshes(self, projectionData: ProjectionData, point = sse.add_point_2d((x, y), projectionData.sketch, fixed = True, index_reference = True); addedPoints[vertex.index] = point; - # print(point.location) if (connectLines != True): continue; compareSet = set(addedPoints.keys()) - # print(compareSet); edges = clicked_mesh.data.edges; for edge in edges: if (set(edge.vertices).issubset(compareSet) != True): continue; p1, p2 = [addedPoints[x] for x in edge.vertices]; - # print(p1.location); - # print(p2.location); sse.add_line_2d(p1, p2, projectionData.sketch, fixed = True, index_reference = True); - - # print(f"Edge {edge.index} vertices: {[str(edge.vertices[x]) for x in range(2)]}"); - # break; pass From adede5a3027b9a7535c2d4122e8f8eb66123ef8f Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Sun, 1 Sep 2024 02:34:34 +0200 Subject: [PATCH 11/18] Clean up --- operators/add_sketch.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/operators/add_sketch.py b/operators/add_sketch.py index 61334b1c..44d75e32 100644 --- a/operators/add_sketch.py +++ b/operators/add_sketch.py @@ -144,32 +144,36 @@ class View3D_OT_slvs_add_sketch_face(Operator, Operator3d): def main(self, context: Context): sse: SlvsEntities = context.scene.sketcher.entities + # Gets info about clicked object obj_name, clicked_face_index = self.get_state_pointer(index=0, implicit=True) clicked_obj = get_evaluated_obj(context, bpy.data.objects[obj_name]) clicked_mesh = clicked_obj.data clicked_face: bpy.types.MeshPolygon = clicked_mesh.polygons[clicked_face_index] + # Gets face rotation obj_translation: bpy.types.TransformOrientation = clicked_obj.matrix_world quat = get_face_orientation(clicked_mesh, clicked_face) # Quternion quat.rotate(obj_translation) + # Creates the workplane workplane_origin: tuple[float, float, float] = obj_translation @ clicked_face.center origin = sse.add_point_3d(workplane_origin) nm = sse.add_normal_3d(quat) - - self.target = sse.add_workplane(origin, nm) + workplane = sse.add_workplane(origin, nm) # Workplane normal in world coordinates workplane_normal = quat @ Vector((0.0, 0.0, 1.0)) - sketch = sse.add_sketch(self.target) + # Creates the sketch + sketch = sse.add_sketch(workplane) sse.add_point_2d((0.0, 0.0), sketch, fixed = True) # Add face centrum point - # activate_sketch(context, sketch.slvs_index, self) # This hides the pop-up with the options for the projection + # activate_sketch(context, sketch.slvs_index, self) # This hides the pop-up with the options for the projection. Idk why, so it is just like this # self.target = sketch limitDist = 0.001 + self.projectDist; # Should just be the project dist, but couldn't get default in property to work + # Prepares the data needed for the projection projectionData = ProjectionData(sse, sketch, obj_translation, workplane_origin, workplane_normal, quat) if self.projectFrom == 'FACE': @@ -200,7 +204,8 @@ def ProjectFromMeshes(self, projectionData: ProjectionData, # Projection to plane distance_to_plane = translated.dot(projectionData.workplaneNormal); projection = translated - distance_to_plane * projectionData.workplaneNormal; - + + # If vertex is too far from sketch, then don't create sketch point if abs(distance_to_plane) > maxDist: continue; @@ -217,6 +222,7 @@ def ProjectFromMeshes(self, projectionData: ProjectionData, if (connectLines != True): continue; + # Takes the edges of the object and checks if the earlier added sketch points are used in the edges. If yes, then create line from first point to second point compareSet = set(addedPoints.keys()) edges = clicked_mesh.data.edges; for edge in edges: From 1b33371907089af75b2b621669f2e160751f7fba Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Sun, 1 Sep 2024 02:35:42 +0200 Subject: [PATCH 12/18] Slight cleanup --- operators/add_sketch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/operators/add_sketch.py b/operators/add_sketch.py index 44d75e32..bef71f4c 100644 --- a/operators/add_sketch.py +++ b/operators/add_sketch.py @@ -92,6 +92,8 @@ def fini(self, context: Context, succeed: bool): # TODO: Auto align view with sketch after creation +# TODO: Make it auto enter sketch +# TODO: Make the properties work! class View3D_OT_slvs_add_sketch_face(Operator, Operator3d): """Add a workplane and start sketch on mesh face""" @@ -120,7 +122,7 @@ class View3D_OT_slvs_add_sketch_face(Operator, Operator3d): ('MESH', "Mesh", ""), ('ALL', "All meshes", ""), ), - default='ALL' + default='ALL' # Maybe should be 'MESH' instead for performance issues. Idk ) states = ( From 50fb8e50d114b2309221c66f4723e6a884ad742c Mon Sep 17 00:00:00 2001 From: BOTAlex Date: Sun, 1 Sep 2024 03:15:48 +0200 Subject: [PATCH 13/18] Changed the tool button text --- workspacetools/add_sketch_face.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspacetools/add_sketch_face.py b/workspacetools/add_sketch_face.py index 85eeb87e..c36de50b 100644 --- a/workspacetools/add_sketch_face.py +++ b/workspacetools/add_sketch_face.py @@ -10,7 +10,7 @@ class VIEW3D_T_slvs_add_sketch_face(GenericStateTool, WorkSpaceTool): bl_space_type = "VIEW_3D" bl_context_mode = "OBJECT" bl_idname = WorkSpaceTools.AddSketchFace - bl_label = "Add a workplane and start sketch on mesh face" + bl_label = "Project to sketch from face" bl_operator = Operators.AddSketchFace bl_icon = "ops.mesh.primitive_grid_add_gizmo" bl_widget = GizmoGroups.Preselection From c5bfe99f846fcfd80bdb4db9b3419d76bb147f41 Mon Sep 17 00:00:00 2001 From: dyami-andrews-e3 <92410110+dyami-andrews-e3@users.noreply.github.com> Date: Sun, 8 Jun 2025 17:29:55 -0700 Subject: [PATCH 14/18] feat: add project_include operator --- declarations.py | 2 + keymaps.py | 11 +- operators/__init__.py | 3 +- operators/add_sketch.py | 163 +++++++++++------- operators/base_2d.py | 9 +- operators/project_include.py | 277 ++++++++++++++++++++++++++++++ workspacetools/__init__.py | 7 +- workspacetools/project_include.py | 20 +++ 8 files changed, 414 insertions(+), 78 deletions(-) create mode 100644 operators/project_include.py create mode 100644 workspacetools/project_include.py diff --git a/declarations.py b/declarations.py index 372c715d..78814282 100644 --- a/declarations.py +++ b/declarations.py @@ -40,6 +40,7 @@ class Operators(str, Enum): AddRectangle = "view3d.slvs_add_rectangle" AddSketch = "view3d.slvs_add_sketch" AddSketchFace = "view3d.slvs_add_sketch_face" + ProjectInclude = "view3d.slvs_project_include" AddTangent = "view3d.slvs_add_tangent" AddVertical = "view3d.slvs_add_vertical" AddWorkPlane = "view3d.slvs_add_workplane" @@ -110,6 +111,7 @@ class WorkSpaceTools(str, Enum): AddSketchFace = "sketcher.slvs_add_sketch_face" AddWorkplane = "sketcher.slvs_add_workplane" AddWorkplaneFace = "sketcher.slvs_add_workplane_face" + ProjectInclude = "sketcher.slvs_project_include" Offset = "sketcher.slvs_offset" Select = "sketcher.slvs_select" Trim = "sketcher.slvs_trim" diff --git a/keymaps.py b/keymaps.py index a1d3779d..eddcbafd 100644 --- a/keymaps.py +++ b/keymaps.py @@ -146,6 +146,11 @@ WorkSpaceTools.AddCircle2D, Operators.AddCircle2D, ), + tool_invoke_kmi( + "I", + WorkSpaceTools.ProjectInclude, + Operators.ProjectInclude, + ), tool_invoke_kmi( "A", WorkSpaceTools.AddArc2D, @@ -166,11 +171,7 @@ WorkSpaceTools.Bevel, Operators.Bevel, ), - tool_invoke_kmi( - "O", - WorkSpaceTools.Offset, - Operators.Offset - ), + tool_invoke_kmi("O", WorkSpaceTools.Offset, Operators.Offset), ( Operators.AddSketch, {"type": "S", "value": "PRESS"}, diff --git a/operators/__init__.py b/operators/__init__.py index d1115433..4fb8aead 100644 --- a/operators/__init__.py +++ b/operators/__init__.py @@ -1,5 +1,5 @@ -from ..utilities.register import module_register_factory from ..stateful_operator.utilities.register import register_stateops_factory +from ..utilities.register import module_register_factory modules = [ "select", @@ -36,6 +36,7 @@ "add_geometric_constraints", "align_workplane", "align_view", + "project_include", "modifiers", "move", "duplicate", diff --git a/operators/add_sketch.py b/operators/add_sketch.py index bef71f4c..e6f6354f 100644 --- a/operators/add_sketch.py +++ b/operators/add_sketch.py @@ -1,38 +1,41 @@ import logging import bpy -from bpy.types import Operator, Context, Event -from bpy.props import FloatProperty, BoolProperty, EnumProperty -from mathutils import Vector, Quaternion +from bpy.props import BoolProperty, EnumProperty, FloatProperty +from bpy.types import Context, Event, Operator +from mathutils import Quaternion, Vector -from ..model.types import SlvsWorkplane from ..declarations import Operators -from ..stateful_operator.utilities.register import register_stateops_factory +from ..model.group_entities import SlvsEntities, SlvsSketch +from ..model.types import SlvsWorkplane from ..stateful_operator.state import state_from_args -from .base_3d import Operator3d -from .utilities import activate_sketch, switch_sketch_mode from ..stateful_operator.utilities.geometry import get_evaluated_obj, get_mesh_element +from ..stateful_operator.utilities.register import register_stateops_factory from ..utilities.geometry import get_face_orientation -from ..model.group_entities import SlvsEntities, SlvsSketch - +from .base_3d import Operator3d +from .utilities import activate_sketch, switch_sketch_mode logger = logging.getLogger(__name__) + class ProjectionData: - def __init__(self, - sketcherEntities: SlvsEntities, - sketch: SlvsSketch, - objectTranslation: bpy.types.TransformOrientation, - workplaneOrigin: tuple[float, float, float], - workplaneNormal: Vector, - quat: Quaternion): - + def __init__( + self, + sketcherEntities: SlvsEntities, + sketch: SlvsSketch, + objectTranslation: bpy.types.TransformOrientation, + workplaneOrigin: tuple[float, float, float], + workplaneNormal: Vector, + quat: Quaternion, + ): + self.sketcherEntities = sketcherEntities self.sketch = sketch self.objectTranslation = objectTranslation self.workplaneOrigin = workplaneOrigin self.workplaneNormal = workplaneNormal - self.quat = quat # I forgot what quat was... Should've added more comments + self.quat = quat # I forgot what quat was... Should've added more comments + # TODO: # - Draw sketches @@ -96,7 +99,7 @@ def fini(self, context: Context, succeed: bool): # TODO: Make the properties work! class View3D_OT_slvs_add_sketch_face(Operator, Operator3d): """Add a workplane and start sketch on mesh face""" - + bl_idname = Operators.AddSketchFace bl_label = "Add sketch on mesh face" bl_options = {"REGISTER", "UNDO"} @@ -114,15 +117,15 @@ class View3D_OT_slvs_add_sketch_face(Operator, Operator3d): # # Idk why it doesn't work correctly # connectLines: BoolProperty(name="Connect lines", description="May cause performance issues, idk", default=True) connectLines = True - + projectFrom: EnumProperty( name="My Search", items=( - ('FACE', "Face", ""), - ('MESH', "Mesh", ""), - ('ALL', "All meshes", ""), + ("FACE", "Face", ""), + ("MESH", "Mesh", ""), + ("ALL", "All meshes", ""), ), - default='ALL' # Maybe should be 'MESH' instead for performance issues. Idk + default="ALL", # Maybe should be 'MESH' instead for performance issues. Idk ) states = ( @@ -151,88 +154,116 @@ def main(self, context: Context): clicked_obj = get_evaluated_obj(context, bpy.data.objects[obj_name]) clicked_mesh = clicked_obj.data clicked_face: bpy.types.MeshPolygon = clicked_mesh.polygons[clicked_face_index] - + # Gets face rotation obj_translation: bpy.types.TransformOrientation = clicked_obj.matrix_world - quat = get_face_orientation(clicked_mesh, clicked_face) # Quternion + quat = get_face_orientation(clicked_mesh, clicked_face) # Quternion quat.rotate(obj_translation) - + # Creates the workplane - workplane_origin: tuple[float, float, float] = obj_translation @ clicked_face.center + workplane_origin: tuple[float, float, float] = ( + obj_translation @ clicked_face.center + ) origin = sse.add_point_3d(workplane_origin) nm = sse.add_normal_3d(quat) workplane = sse.add_workplane(origin, nm) # Workplane normal in world coordinates workplane_normal = quat @ Vector((0.0, 0.0, 1.0)) - + # Creates the sketch sketch = sse.add_sketch(workplane) - sse.add_point_2d((0.0, 0.0), sketch, fixed = True) # Add face centrum point + sse.add_point_2d((0.0, 0.0), sketch, fixed=True) # Add face centrum point # activate_sketch(context, sketch.slvs_index, self) # This hides the pop-up with the options for the projection. Idk why, so it is just like this # self.target = sketch - limitDist = 0.001 + self.projectDist; # Should just be the project dist, but couldn't get default in property to work + limitDist = 0.001 + self.projectDist + # Should just be the project dist, but couldn't get default in property to work # Prepares the data needed for the projection - projectionData = ProjectionData(sse, sketch, obj_translation, workplane_origin, workplane_normal, quat) + projectionData = ProjectionData( + sse, sketch, obj_translation, workplane_origin, workplane_normal, quat + ) - if self.projectFrom == 'FACE': + if self.projectFrom == "FACE": logger.error("Project face is not implemented yet") - elif self.projectFrom == 'MESH': - self.ProjectFromMeshes(projectionData, [clicked_obj,], limitDist, self.connectLines) - elif self.projectFrom == 'ALL': # ALL doesn't actually work. I don't think its important to fix atm - allMeshesInScene = [o for o in context.scene.objects if o.type == 'MESH'] - self.ProjectFromMeshes(projectionData, allMeshesInScene, limitDist, self.connectLines) - - context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) + elif self.projectFrom == "MESH": + self.ProjectFromMeshes( + projectionData, + [ + clicked_obj, + ], + limitDist, + self.connectLines, + ) + elif ( + self.projectFrom == "ALL" + ): # ALL doesn't actually work. I don't think its important to fix atm + allMeshesInScene = [o for o in context.scene.objects if o.type == "MESH"] + self.ProjectFromMeshes( + projectionData, allMeshesInScene, limitDist, self.connectLines + ) + + context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) return True - - def ProjectFromMeshes(self, projectionData: ProjectionData, - meshes: list[bpy.types.Mesh], - maxDist: float, - connectLines: bool = True): + + def ProjectFromMeshes( + self, + projectionData: ProjectionData, + meshes: list[bpy.types.Mesh], + maxDist: float, + connectLines: bool = True, + ): sse = projectionData.sketcherEntities - + addedPoints = {} for clicked_mesh in meshes: - vertices = clicked_mesh.data.vertices; + vertices = clicked_mesh.data.vertices for vertex in vertices: # Make vertex relative to plane - vertex_world = projectionData.objectTranslation @ vertex.co; - translated = vertex_world - projectionData.workplaneOrigin; - + vertex_world = projectionData.objectTranslation @ vertex.co + translated = vertex_world - projectionData.workplaneOrigin + # Projection to plane - distance_to_plane = translated.dot(projectionData.workplaneNormal); - projection = translated - distance_to_plane * projectionData.workplaneNormal; + distance_to_plane = translated.dot(projectionData.workplaneNormal) + projection = ( + translated - distance_to_plane * projectionData.workplaneNormal + ) # If vertex is too far from sketch, then don't create sketch point if abs(distance_to_plane) > maxDist: - continue; + continue ## Used ChatGPT, quaternion rotations is too hard. # To 2D projection relative to the workplane # Use the workplane orientation (quat) to project into 2D - local_projection = projection.copy(); - local_projection.rotate(projectionData.quat.conjugated()); - x, y, _ = local_projection; + local_projection = projection.copy() + local_projection.rotate(projectionData.quat.conjugated()) + x, y, _ = local_projection - point = sse.add_point_2d((x, y), projectionData.sketch, fixed = True, index_reference = True); - addedPoints[vertex.index] = point; + point = sse.add_point_2d( + (x, y), projectionData.sketch, fixed=True, index_reference=True + ) + addedPoints[vertex.index] = point - if (connectLines != True): - continue; + if connectLines != True: + continue # Takes the edges of the object and checks if the earlier added sketch points are used in the edges. If yes, then create line from first point to second point compareSet = set(addedPoints.keys()) - edges = clicked_mesh.data.edges; + edges = clicked_mesh.data.edges for edge in edges: - if (set(edge.vertices).issubset(compareSet) != True): continue; + if set(edge.vertices).issubset(compareSet) != True: + continue - p1, p2 = [addedPoints[x] for x in edge.vertices]; - sse.add_line_2d(p1, p2, projectionData.sketch, fixed = True, index_reference = True); + p1, p2 = [addedPoints[x] for x in edge.vertices] + sse.add_line_2d( + p1, p2, projectionData.sketch, fixed=True, index_reference=True + ) pass - -register, unregister = register_stateops_factory((View3D_OT_slvs_add_sketch,View3D_OT_slvs_add_sketch_face)) + +register, unregister = register_stateops_factory( + (View3D_OT_slvs_add_sketch, View3D_OT_slvs_add_sketch_face) +) diff --git a/operators/base_2d.py b/operators/base_2d.py index 4c69492d..e1cc3f67 100644 --- a/operators/base_2d.py +++ b/operators/base_2d.py @@ -5,21 +5,24 @@ from bpy.types import Context, Event from mathutils import Vector -from ..model.types import SlvsPoint2D -from ..model.types import SlvsLine2D, SlvsCircle, SlvsArc +from ..model.types import SlvsArc, SlvsCircle, SlvsLine2D, SlvsPoint2D from ..model.utilities import slvs_entity_pointer +from ..utilities.view import get_pos_2d, get_scale_from_pos from .base_stateful import GenericEntityOp from .utilities import ignore_hover -from ..utilities.view import get_pos_2d, get_scale_from_pos class Operator2d(GenericEntityOp): + + event: Event + @classmethod def poll(cls, context: Context): return context.scene.sketcher.active_sketch_i != -1 def init(self, context: Context, event: Event): self.sketch = context.scene.sketcher.active_sketch + self.event = event return True def state_func(self, context: Context, coords): diff --git a/operators/project_include.py b/operators/project_include.py new file mode 100644 index 00000000..7836aaeb --- /dev/null +++ b/operators/project_include.py @@ -0,0 +1,277 @@ +import logging +import math +from typing import Protocol, Union + +import bgl +import blf +import bpy +import bpy_extras +import gpu +from bpy.props import BoolProperty +from bpy.types import Context, MeshPolygon, Operator, TransformOrientation +from gpu_extras.batch import batch_for_shader +from mathutils import Quaternion, Vector + +from ..declarations import Operators +from ..model.group_entities import SlvsEntities, SlvsNormal3D, SlvsPoint3D, SlvsSketch +from ..model.workplane import SlvsWorkplane +from ..stateful_operator.state import state_from_args +from ..stateful_operator.utilities.geometry import get_evaluated_obj +from ..stateful_operator.utilities.register import register_stateops_factory +from ..utilities.geometry import get_face_orientation +from .base_2d import Operator2d + +logger = logging.getLogger(__name__) + + +def get_object_under_mouse(context, event): + region = context.region + rv3d = context.space_data.region_3d + coord = (event.mouse_region_x, event.mouse_region_y) + + view_vector = bpy_extras.view3d_utils.region_2d_to_vector_3d(region, rv3d, coord) + ray_origin = bpy_extras.view3d_utils.region_2d_to_origin_3d(region, rv3d, coord) + + result, location, normal, index, obj, matrix = context.scene.ray_cast( + context.view_layer.depsgraph, ray_origin, view_vector + ) + return obj if result else None + + +def project_vertex_to_workplane(vertex_world, origin, wp_quat): + """ + Project a world-space vertex to a workplane defined by origin and quaternion. + + Returns the (x, y) coordinates in the workplane’s local space. + """ + relative = vertex_world - origin + # Rotate into workplane local space (inverse rotation) + local = wp_quat.conjugated() @ relative + return Vector((local.x, local.y)) + + +class ProjectionData: + def __init__( + self, + sketcher_entities: SlvsEntities, + sketch: SlvsSketch, + object_translation: TransformOrientation, + ): + + self.sketcher_entities = sketcher_entities + self.sketch = sketch + self.object_translation = object_translation + + @property + def wp(self) -> SlvsWorkplane: + wp: SlvsWorkplane = self.sketch.wp + return wp + + @property + def workplane_origin(self) -> SlvsPoint3D: + slvs_point3d: SlvsPoint3D = self.wp.p1 + return slvs_point3d.location + + @property + def workplane_quaternion(self) -> Quaternion: + return self.wp.nm.orientation + + +class ObjectInterfaceProtocol(Protocol): + def get_verticies(self) -> list[bpy.types.MeshVertex]: + pass + + def get_edges(self) -> list[bpy.types.MeshEdges]: + pass + + +class ObjectInterface: + + obj: bpy.types.Object + + def __init__(self, obj: bpy.types.Object): + self.obj = obj + + def get_verticies(self) -> list[bpy.types.MeshVertex]: + return self.obj.data.vertices + + def get_edges(self) -> list[bpy.types.MeshEdges]: + return self.obj.data.edges + + +class FaceInterface: + + obj: bpy.types.Object + face_index: int + + def __init__(self, obj: bpy.types.Object, face_index: int): + self.obj = obj + self.face_index = face_index + + def get_verticies(self) -> list[bpy.types.MeshVertex]: + return [ + self.obj.data.vertices[vertex_index] for vertex_index in self._face.vertices + ] + + def get_edges(self) -> list[bpy.types.MeshEdges]: + edges_with_nones = [ + self._get_edge(edge_key=edge_key) for edge_key in self._face.edge_keys + ] + + return [x for x in edges_with_nones if x is not None] + + @property + def _face(self) -> MeshPolygon: + return self.obj.data.polygons[self.face_index] + + def _get_edge(self, edge_key: tuple[int, int]) -> Union[None, bpy.types.MeshEdge]: + for edge in self.obj.data.edges: + if edge.key == edge_key: + return edge + + return None + + +class View3D_OT_slvs_project_include(Operator, Operator2d): + """Add a circle to the active sketch""" + + bl_idname = Operators.ProjectInclude + bl_label = "Project a mesh onto the current sketch" + bl_options = {"REGISTER", "UNDO"} + _last_obj = None + + # continuous_draw: BoolProperty(name="Continuous Draw", default=True) + + states = ( + state_from_args( + "Face", + description="Pick a mesh face to project onto the sketch's surface.", + use_create=False, + pointer="face", + types=(MeshPolygon,), + interactive=True, + ), + ) + + def main(self, context: Context): + + sse: SlvsEntities = context.scene.sketcher.entities + + # Gets info about clicked object + obj_name, clicked_face_index = self.get_state_pointer(index=0, implicit=True) + clicked_obj = get_evaluated_obj(context, bpy.data.objects[obj_name]) + + # clicked_mesh = clicked_obj.data + # clicked_face: MeshPolygon = clicked_mesh.polygons[clicked_face_index] + + # Gets face rotation + obj_translation: TransformOrientation = clicked_obj.matrix_world + projection_data = ProjectionData( + # FOCUS HERE + sketcher_entities=sse, + sketch=self.sketch, + object_translation=obj_translation, + ) + # add code here + + # print(ctrl_held) + + if self.event.shift: + self.ProjectFromMeshes( + projection_data, + [ + ObjectInterface(clicked_obj), + ], + # self.connectLines, + ) + else: + self.ProjectFromMeshes( + projection_data, + [ + FaceInterface(clicked_obj, clicked_face_index), + ], + # self.connectLines, + ) + + context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) + return True + + def ProjectFromMeshes( + self, + projection_data: ProjectionData, + object_interfaces: list[ObjectInterfaceProtocol], + connect_lines: bool = True, + ): + sse = projection_data.sketcher_entities + + addedPoints = {} + + for object_interface in object_interfaces: + for vertex in object_interface.get_verticies(): + x, y = project_vertex_to_workplane( + # FOCUS HERE + vertex_world=projection_data.object_translation @ vertex.co, + origin=projection_data.workplane_origin, + wp_quat=projection_data.workplane_quaternion, + ) + + point = sse.add_point_2d( + (x, y), projection_data.sketch, fixed=True, index_reference=True + ) + addedPoints[vertex.index] = point + + if not connect_lines: + continue + + # Takes the edges of the object and checks if + # the earlier added sketch points are used in the edges. + # If yes, then create line from first point to second point + compareSet = set(addedPoints.keys()) + + for edge in object_interface.get_edges(): + if not set(edge.vertices).issubset(compareSet): + continue + + p1, p2 = [addedPoints[x] for x in edge.vertices] + sse.add_line_2d( + p1, p2, projection_data.sketch, fixed=True, index_reference=True + ) + pass + + # def modal(self, context, event): + # if event.type in {"ESC", "RIGHTMOUSE"}: + # if self._last_obj: + # self._last_obj.select_set(False) + # return {"CANCELLED"} + + # if event.type == "MOUSEMOVE": + # obj = get_object_under_mouse(context, event) + # if obj is not None and obj.type == "MESH": + # pass + # if obj != self._last_obj: + # # Unhighlight previous + # if self._last_obj: + # pass + + # # Highlight current + # # obj.select_set(True) + # _debug_circle(obj, context) + # self._last_obj = obj + # else: + # if self._last_obj: + # # self._last_obj.select_set(False) + # self._last_obj = None + + # return {"PASS_THROUGH"} + + # return Operator2d.modal(self, context, event) + + # def invoke(self, context, event): + # context.window_manager.modal_handler_add(self) + # return super().invoke(context, event) + + +register, unregister = register_stateops_factory((View3D_OT_slvs_project_include,)) + +if __name__ == "__main__": + register() diff --git a/workspacetools/__init__.py b/workspacetools/__init__.py index 68b722c6..b8115903 100644 --- a/workspacetools/__init__.py +++ b/workspacetools/__init__.py @@ -8,15 +8,15 @@ from .add_point2d import VIEW3D_T_slvs_add_point2d from .add_point3d import VIEW3D_T_slvs_add_point3d from .add_rectangle import VIEW3D_T_slvs_add_rectangle +from .add_sketch_face import VIEW3D_T_slvs_add_sketch_face from .add_workplane import VIEW3D_T_slvs_add_workplane from .add_workplane_face import VIEW3D_T_slvs_add_workplane_face -from .add_sketch_face import VIEW3D_T_slvs_add_sketch_face from .bevel import VIEW3D_T_slvs_bevel from .offset import VIEW3D_T_slvs_offset +from .project_include import VIEW3D_T_slvs_project_include from .select import VIEW3D_T_slvs_select from .trim import VIEW3D_T_slvs_trim - tools = ( (VIEW3D_T_slvs_select, {"separator": True, "group": False}), (VIEW3D_T_slvs_add_point2d, {"separator": True, "group": True}), @@ -39,9 +39,10 @@ (VIEW3D_T_slvs_trim, {"separator": True, "group": False}), (VIEW3D_T_slvs_bevel, {"separator": False, "group": False}), (VIEW3D_T_slvs_offset, {"separator": False, "group": False}), + (VIEW3D_T_slvs_project_include, {"separator": False, "group": False}), (VIEW3D_T_slvs_add_sketch_face, {"separator": True, "group": True}), ( - VIEW3D_T_slvs_add_workplane_face, + VIEW3D_T_slvs_add_workplane_face, {"after": {VIEW3D_T_slvs_add_sketch_face.bl_idname}}, ), ( diff --git a/workspacetools/project_include.py b/workspacetools/project_include.py new file mode 100644 index 00000000..a2c23282 --- /dev/null +++ b/workspacetools/project_include.py @@ -0,0 +1,20 @@ +from bpy.types import WorkSpaceTool + +from ..declarations import GizmoGroups, Operators, WorkSpaceTools +from ..keymaps import tool_generic +from ..stateful_operator.tool import GenericStateTool +from ..stateful_operator.utilities.keymap import operator_access + + +class VIEW3D_T_slvs_project_include(GenericStateTool, WorkSpaceTool): + bl_space_type = "VIEW_3D" + bl_context_mode = "OBJECT" + bl_idname = WorkSpaceTools.ProjectInclude + bl_label = "Project a mesh onto the sketch" + bl_operator = Operators.ProjectInclude + bl_icon = "ops.gpencil.primitive_line" + bl_widget = GizmoGroups.Preselection + bl_keymap = ( + *tool_generic, + *operator_access(Operators.ProjectInclude), + ) From 483067a075099027dcca191fd2599e24e13e692d Mon Sep 17 00:00:00 2001 From: dyami-andrews-e3 <92410110+dyami-andrews-e3@users.noreply.github.com> Date: Mon, 9 Jun 2025 20:14:39 -0700 Subject: [PATCH 15/18] fix: add highlighting workaround --- operators/project_include.py | 277 ------------------ operators/project_include/__init__.py | 5 + .../project_include/get_object_under_mouse.py | 48 +++ operators/project_include/handle_highlight.py | 64 ++++ operators/project_include/object_interface.py | 59 ++++ operators/project_include/project_include.py | 165 +++++++++++ .../project_vertex_to_workplane.py | 16 + operators/project_include/projection_data.py | 32 ++ stateful_operator/logic.py | 26 +- 9 files changed, 405 insertions(+), 287 deletions(-) delete mode 100644 operators/project_include.py create mode 100644 operators/project_include/__init__.py create mode 100644 operators/project_include/get_object_under_mouse.py create mode 100644 operators/project_include/handle_highlight.py create mode 100644 operators/project_include/object_interface.py create mode 100644 operators/project_include/project_include.py create mode 100644 operators/project_include/project_vertex_to_workplane.py create mode 100644 operators/project_include/projection_data.py diff --git a/operators/project_include.py b/operators/project_include.py deleted file mode 100644 index 7836aaeb..00000000 --- a/operators/project_include.py +++ /dev/null @@ -1,277 +0,0 @@ -import logging -import math -from typing import Protocol, Union - -import bgl -import blf -import bpy -import bpy_extras -import gpu -from bpy.props import BoolProperty -from bpy.types import Context, MeshPolygon, Operator, TransformOrientation -from gpu_extras.batch import batch_for_shader -from mathutils import Quaternion, Vector - -from ..declarations import Operators -from ..model.group_entities import SlvsEntities, SlvsNormal3D, SlvsPoint3D, SlvsSketch -from ..model.workplane import SlvsWorkplane -from ..stateful_operator.state import state_from_args -from ..stateful_operator.utilities.geometry import get_evaluated_obj -from ..stateful_operator.utilities.register import register_stateops_factory -from ..utilities.geometry import get_face_orientation -from .base_2d import Operator2d - -logger = logging.getLogger(__name__) - - -def get_object_under_mouse(context, event): - region = context.region - rv3d = context.space_data.region_3d - coord = (event.mouse_region_x, event.mouse_region_y) - - view_vector = bpy_extras.view3d_utils.region_2d_to_vector_3d(region, rv3d, coord) - ray_origin = bpy_extras.view3d_utils.region_2d_to_origin_3d(region, rv3d, coord) - - result, location, normal, index, obj, matrix = context.scene.ray_cast( - context.view_layer.depsgraph, ray_origin, view_vector - ) - return obj if result else None - - -def project_vertex_to_workplane(vertex_world, origin, wp_quat): - """ - Project a world-space vertex to a workplane defined by origin and quaternion. - - Returns the (x, y) coordinates in the workplane’s local space. - """ - relative = vertex_world - origin - # Rotate into workplane local space (inverse rotation) - local = wp_quat.conjugated() @ relative - return Vector((local.x, local.y)) - - -class ProjectionData: - def __init__( - self, - sketcher_entities: SlvsEntities, - sketch: SlvsSketch, - object_translation: TransformOrientation, - ): - - self.sketcher_entities = sketcher_entities - self.sketch = sketch - self.object_translation = object_translation - - @property - def wp(self) -> SlvsWorkplane: - wp: SlvsWorkplane = self.sketch.wp - return wp - - @property - def workplane_origin(self) -> SlvsPoint3D: - slvs_point3d: SlvsPoint3D = self.wp.p1 - return slvs_point3d.location - - @property - def workplane_quaternion(self) -> Quaternion: - return self.wp.nm.orientation - - -class ObjectInterfaceProtocol(Protocol): - def get_verticies(self) -> list[bpy.types.MeshVertex]: - pass - - def get_edges(self) -> list[bpy.types.MeshEdges]: - pass - - -class ObjectInterface: - - obj: bpy.types.Object - - def __init__(self, obj: bpy.types.Object): - self.obj = obj - - def get_verticies(self) -> list[bpy.types.MeshVertex]: - return self.obj.data.vertices - - def get_edges(self) -> list[bpy.types.MeshEdges]: - return self.obj.data.edges - - -class FaceInterface: - - obj: bpy.types.Object - face_index: int - - def __init__(self, obj: bpy.types.Object, face_index: int): - self.obj = obj - self.face_index = face_index - - def get_verticies(self) -> list[bpy.types.MeshVertex]: - return [ - self.obj.data.vertices[vertex_index] for vertex_index in self._face.vertices - ] - - def get_edges(self) -> list[bpy.types.MeshEdges]: - edges_with_nones = [ - self._get_edge(edge_key=edge_key) for edge_key in self._face.edge_keys - ] - - return [x for x in edges_with_nones if x is not None] - - @property - def _face(self) -> MeshPolygon: - return self.obj.data.polygons[self.face_index] - - def _get_edge(self, edge_key: tuple[int, int]) -> Union[None, bpy.types.MeshEdge]: - for edge in self.obj.data.edges: - if edge.key == edge_key: - return edge - - return None - - -class View3D_OT_slvs_project_include(Operator, Operator2d): - """Add a circle to the active sketch""" - - bl_idname = Operators.ProjectInclude - bl_label = "Project a mesh onto the current sketch" - bl_options = {"REGISTER", "UNDO"} - _last_obj = None - - # continuous_draw: BoolProperty(name="Continuous Draw", default=True) - - states = ( - state_from_args( - "Face", - description="Pick a mesh face to project onto the sketch's surface.", - use_create=False, - pointer="face", - types=(MeshPolygon,), - interactive=True, - ), - ) - - def main(self, context: Context): - - sse: SlvsEntities = context.scene.sketcher.entities - - # Gets info about clicked object - obj_name, clicked_face_index = self.get_state_pointer(index=0, implicit=True) - clicked_obj = get_evaluated_obj(context, bpy.data.objects[obj_name]) - - # clicked_mesh = clicked_obj.data - # clicked_face: MeshPolygon = clicked_mesh.polygons[clicked_face_index] - - # Gets face rotation - obj_translation: TransformOrientation = clicked_obj.matrix_world - projection_data = ProjectionData( - # FOCUS HERE - sketcher_entities=sse, - sketch=self.sketch, - object_translation=obj_translation, - ) - # add code here - - # print(ctrl_held) - - if self.event.shift: - self.ProjectFromMeshes( - projection_data, - [ - ObjectInterface(clicked_obj), - ], - # self.connectLines, - ) - else: - self.ProjectFromMeshes( - projection_data, - [ - FaceInterface(clicked_obj, clicked_face_index), - ], - # self.connectLines, - ) - - context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) - return True - - def ProjectFromMeshes( - self, - projection_data: ProjectionData, - object_interfaces: list[ObjectInterfaceProtocol], - connect_lines: bool = True, - ): - sse = projection_data.sketcher_entities - - addedPoints = {} - - for object_interface in object_interfaces: - for vertex in object_interface.get_verticies(): - x, y = project_vertex_to_workplane( - # FOCUS HERE - vertex_world=projection_data.object_translation @ vertex.co, - origin=projection_data.workplane_origin, - wp_quat=projection_data.workplane_quaternion, - ) - - point = sse.add_point_2d( - (x, y), projection_data.sketch, fixed=True, index_reference=True - ) - addedPoints[vertex.index] = point - - if not connect_lines: - continue - - # Takes the edges of the object and checks if - # the earlier added sketch points are used in the edges. - # If yes, then create line from first point to second point - compareSet = set(addedPoints.keys()) - - for edge in object_interface.get_edges(): - if not set(edge.vertices).issubset(compareSet): - continue - - p1, p2 = [addedPoints[x] for x in edge.vertices] - sse.add_line_2d( - p1, p2, projection_data.sketch, fixed=True, index_reference=True - ) - pass - - # def modal(self, context, event): - # if event.type in {"ESC", "RIGHTMOUSE"}: - # if self._last_obj: - # self._last_obj.select_set(False) - # return {"CANCELLED"} - - # if event.type == "MOUSEMOVE": - # obj = get_object_under_mouse(context, event) - # if obj is not None and obj.type == "MESH": - # pass - # if obj != self._last_obj: - # # Unhighlight previous - # if self._last_obj: - # pass - - # # Highlight current - # # obj.select_set(True) - # _debug_circle(obj, context) - # self._last_obj = obj - # else: - # if self._last_obj: - # # self._last_obj.select_set(False) - # self._last_obj = None - - # return {"PASS_THROUGH"} - - # return Operator2d.modal(self, context, event) - - # def invoke(self, context, event): - # context.window_manager.modal_handler_add(self) - # return super().invoke(context, event) - - -register, unregister = register_stateops_factory((View3D_OT_slvs_project_include,)) - -if __name__ == "__main__": - register() diff --git a/operators/project_include/__init__.py b/operators/project_include/__init__.py new file mode 100644 index 00000000..d14ea1d3 --- /dev/null +++ b/operators/project_include/__init__.py @@ -0,0 +1,5 @@ +from .project_include import View3D_OT_slvs_project_include, register + +__all__ = ["View3D_OT_slvs_project_include"] +if __name__ == "__main__": + register() diff --git a/operators/project_include/get_object_under_mouse.py b/operators/project_include/get_object_under_mouse.py new file mode 100644 index 00000000..4383454c --- /dev/null +++ b/operators/project_include/get_object_under_mouse.py @@ -0,0 +1,48 @@ +import bpy +import bpy_extras +from bpy.types import Context, Event + + +def get_object_under_mouse( + context: Context, event: Event, return_index: bool = False +) -> tuple[bpy.types.MeshPolygon | bpy.types.Mesh | None, int | None]: + region = context.region + rv3d = context.space_data.region_3d + coord = (event.mouse_region_x, event.mouse_region_y) + + view_vector = bpy_extras.view3d_utils.region_2d_to_vector_3d(region, rv3d, coord) + ray_origin = bpy_extras.view3d_utils.region_2d_to_origin_3d(region, rv3d, coord) + + depsgraph = context.view_layer.depsgraph + hit, location, normal, face_index, obj, matrix = context.scene.ray_cast( + depsgraph, ray_origin, view_vector + ) + if hit: + if getattr(event, "shift", False): + return _handle_shift( + context=context, + obj=obj, + face_index=face_index, + return_index=return_index, + ) + else: + if hit: + return obj, None + + return None, None + + +def _handle_shift( + context: Context, obj: bpy.types.Object, face_index: int, return_index: bool = False +) -> tuple[bpy.types.MeshPolygon | bpy.types.Mesh, int | None]: + depsgraph = context.view_layer.depsgraph + # Get the evaluated mesh for up-to-date data with modifiers + obj_eval = obj.evaluated_get(depsgraph) + mesh = obj_eval.to_mesh() + + if return_index: + return obj, face_index + + if face_index != -1 and 0 <= face_index < len(mesh.polygons): + face = mesh.polygons[face_index] + return face, None diff --git a/operators/project_include/handle_highlight.py b/operators/project_include/handle_highlight.py new file mode 100644 index 00000000..286d6986 --- /dev/null +++ b/operators/project_include/handle_highlight.py @@ -0,0 +1,64 @@ +import bpy +from bpy.types import Context, Event + +from ..base_2d import Operator2d +from .get_object_under_mouse import get_object_under_mouse + +highlight_name = "HighlightFace" + + +def clear_highlight(context: Context): + prev_highlight = context.scene.objects.get(highlight_name) + if prev_highlight: + bpy.data.objects.remove(prev_highlight, do_unlink=True) + + +def handle_highlight(self: Operator2d, context: Context, event: Event): + clear_highlight(context=context) + + obj, face_index = get_object_under_mouse(context, event, return_index=True) + + if not obj: + return # Nothing to highlight + + mesh = obj.data + if face_index is not None: + faces = [mesh.polygons[face_index]] + else: + faces = list(mesh.polygons) + + # Build vertices and face indices for all faces + verts = [] + faces_indices = [] + vert_idx = 0 + for face in faces: + face_verts = [obj.matrix_world @ mesh.vertices[i].co for i in face.vertices] + verts.extend(face_verts) + faces_indices.append(list(range(vert_idx, vert_idx + len(face_verts)))) + vert_idx += len(face_verts) + + if not verts or not faces_indices: + return + + mesh_data = bpy.data.meshes.new(highlight_name) + mesh_data.from_pydata(verts, [], faces_indices) + mesh_data.update() + + highlight_obj = bpy.data.objects.new(highlight_name, mesh_data) + context.collection.objects.link(highlight_obj) + + # Set highlight material (reuse or create) + mat_name = "HighlightMaterial" + mat = bpy.data.materials.get(mat_name) + if mat is None: + mat = bpy.data.materials.new(name=mat_name) + mat.use_nodes = True + bsdf = mat.node_tree.nodes.get("Principled BSDF") + if bsdf: + bsdf.inputs["Base Color"].default_value = (0.25, 1, 0, 1) # Yellow + bsdf.inputs["Alpha"].default_value = 1 + mat.blend_method = "BLEND" + mesh_data.materials.append(mat) + + highlight_obj.hide_select = True + highlight_obj.hide_render = True diff --git a/operators/project_include/object_interface.py b/operators/project_include/object_interface.py new file mode 100644 index 00000000..0d86a57f --- /dev/null +++ b/operators/project_include/object_interface.py @@ -0,0 +1,59 @@ +from typing import Protocol, Union + +import bpy +from bpy.types import MeshPolygon + + +class ObjectInterfaceProtocol(Protocol): + def get_verticies(self) -> list[bpy.types.MeshVertex]: + pass + + def get_edges(self) -> list[bpy.types.MeshEdges]: + pass + + +class ObjectInterface: + + obj: bpy.types.Object + + def __init__(self, obj: bpy.types.Object): + self.obj = obj + + def get_verticies(self) -> list[bpy.types.MeshVertex]: + return self.obj.data.vertices + + def get_edges(self) -> list[bpy.types.MeshEdges]: + return self.obj.data.edges + + +class FaceInterface: + + obj: bpy.types.Object + face_index: int + + def __init__(self, obj: bpy.types.Object, face_index: int): + self.obj = obj + self.face_index = face_index + + def get_verticies(self) -> list[bpy.types.MeshVertex]: + return [ + self.obj.data.vertices[vertex_index] for vertex_index in self._face.vertices + ] + + def get_edges(self) -> list[bpy.types.MeshEdges]: + edges_with_nones = [ + self._get_edge(edge_key=edge_key) for edge_key in self._face.edge_keys + ] + + return [x for x in edges_with_nones if x is not None] + + @property + def _face(self) -> MeshPolygon: + return self.obj.data.polygons[self.face_index] + + def _get_edge(self, edge_key: tuple[int, int]) -> Union[None, bpy.types.MeshEdge]: + for edge in self.obj.data.edges: + if edge.key == edge_key: + return edge + + return None diff --git a/operators/project_include/project_include.py b/operators/project_include/project_include.py new file mode 100644 index 00000000..30abdcd8 --- /dev/null +++ b/operators/project_include/project_include.py @@ -0,0 +1,165 @@ +import bpy +from bpy.types import Context, Event, MeshPolygon, Operator, TransformOrientation + +from ...declarations import Operators +from ...model.group_entities import SlvsEntities +from ...stateful_operator.state import state_from_args +from ...stateful_operator.utilities.geometry import get_evaluated_obj +from ...stateful_operator.utilities.register import register_stateops_factory +from ..base_2d import Operator2d +from .get_object_under_mouse import get_object_under_mouse +from .handle_highlight import clear_highlight, handle_highlight +from .object_interface import FaceInterface, ObjectInterface, ObjectInterfaceProtocol +from .project_vertex_to_workplane import project_vertex_to_workplane +from .projection_data import ProjectionData + + +class View3D_OT_slvs_project_include(Operator, Operator2d): + """Add a circle to the active sketch""" + + bl_idname = Operators.ProjectInclude + bl_label = "Project a mesh onto the current sketch" + bl_options = {"REGISTER", "UNDO"} + # _last_obj = None + + _handle = None + _highlight_face_index = None + _highlight_obj = None + + states = ( + state_from_args( + "Face", + description="Pick a mesh face to project onto the sketch's surface.", + use_create=False, + pointer="face", + types=(MeshPolygon,), + interactive=True, + ), + ) + + def main(self, context: Context): + + sse: SlvsEntities = context.scene.sketcher.entities + + # Gets info about clicked object + obj_name, clicked_face_index = self.get_state_pointer(index=0, implicit=True) + clicked_obj = get_evaluated_obj(context, bpy.data.objects[obj_name]) + + # Gets face rotation + obj_translation: TransformOrientation = clicked_obj.matrix_world + projection_data = ProjectionData( + sketcher_entities=sse, + sketch=self.sketch, + object_translation=obj_translation, + ) + + if not self.event.shift: + # If SHIFT held, project the entire object + self.project_from_interfaces( + projection_data, + [ + ObjectInterface(clicked_obj), + ], + ) + else: + # Otherwise project the selected face + self.project_from_interfaces( + projection_data, + [ + FaceInterface(clicked_obj, clicked_face_index), + ], + ) + # Theres also probably room to do a multi-select here. + + context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) + return True + + def project_from_interfaces( + self, + projection_data: ProjectionData, + object_interfaces: list[ObjectInterfaceProtocol], + connect_lines: bool = True, + ): + sse = projection_data.sketcher_entities + + addedPoints = {} + + for object_interface in object_interfaces: + for vertex in object_interface.get_verticies(): + x, y = project_vertex_to_workplane( + # FOCUS HERE + vertex_world=projection_data.object_translation @ vertex.co, + origin=projection_data.workplane_origin, + wp_quat=projection_data.workplane_quaternion, + ) + + point = sse.add_point_2d( + (x, y), projection_data.sketch, fixed=True, index_reference=True + ) + addedPoints[vertex.index] = point + + if not connect_lines: + continue + + # Takes the edges of the object and checks if + # the earlier added sketch points are used in the edges. + # If yes, then create line from first point to second point + compareSet = set(addedPoints.keys()) + + for edge in object_interface.get_edges(): + if not set(edge.vertices).issubset(compareSet): + continue + + p1, p2 = [addedPoints[x] for x in edge.vertices] + sse.add_line_2d( + p1, p2, projection_data.sketch, fixed=True, index_reference=True + ) + pass + + def evaluate_state(self, context: Context, event: Event, triggered): + # Overriding evaluate state method to enable highlight behavior. + # Theres likely a better way to do this, the UX isnt great. + + if event.type in {"RIGHTMOUSE", "ESC"}: + return self._end(context, False) + if event.type in { + "LEFTMOUSE", + "MOUSEMOVE", + "INBETWEEN_MOUSEMOVE", + "LEFT_SHIFT", + "RIGHT_SHIFT", + }: + + obj, _ = get_object_under_mouse(context=context, event=event) + if event.type == "LEFTMOUSE": + if obj is None: + return {"PASS_THROUGH"} + + return super().evaluate_state( + context=context, event=event, triggered=triggered + ) + + return {"PASS_THROUGH"} + + return self._end(context, False) + + def modal(self, context, event): + is_mousemove = self._is_mousemove(event=event, reset=False) + if is_mousemove: + self._handle_highlight(context=context, event=event) + + return Operator2d.modal(self, context, event) + + return Operator2d.modal(self, context, event) + + def _handle_highlight(self, context, event): + return handle_highlight(self=self, context=context, event=event) + + def fini(self, context, succeede): + clear_highlight(context=context) + + +register, unregister = register_stateops_factory((View3D_OT_slvs_project_include,)) + +if __name__ == "__main__": + register() diff --git a/operators/project_include/project_vertex_to_workplane.py b/operators/project_include/project_vertex_to_workplane.py new file mode 100644 index 00000000..409a9436 --- /dev/null +++ b/operators/project_include/project_vertex_to_workplane.py @@ -0,0 +1,16 @@ +from bpy.types import MeshVertex +from mathutils import Quaternion, Vector + + +def project_vertex_to_workplane( + vertex_world: MeshVertex, origin: Vector, wp_quat: Quaternion +): + """ + Project a world-space vertex to a workplane defined by origin and quaternion. + + Returns the (x, y) coordinates in the workplane’s local space. + """ + relative = vertex_world - origin + # Rotate into workplane local space (inverse rotation) + local = wp_quat.conjugated() @ relative + return Vector((local.x, local.y)) diff --git a/operators/project_include/projection_data.py b/operators/project_include/projection_data.py new file mode 100644 index 00000000..c4ba6ef5 --- /dev/null +++ b/operators/project_include/projection_data.py @@ -0,0 +1,32 @@ +from bpy.types import TransformOrientation +from mathutils import Quaternion + +from ...model.group_entities import SlvsEntities, SlvsPoint3D, SlvsSketch +from ...model.workplane import SlvsWorkplane + + +class ProjectionData: + def __init__( + self, + sketcher_entities: SlvsEntities, + sketch: SlvsSketch, + object_translation: TransformOrientation, + ): + + self.sketcher_entities = sketcher_entities + self.sketch = sketch + self.object_translation = object_translation + + @property + def wp(self) -> SlvsWorkplane: + wp: SlvsWorkplane = self.sketch.wp + return wp + + @property + def workplane_origin(self) -> SlvsPoint3D: + slvs_point3d: SlvsPoint3D = self.wp.p1 + return slvs_point3d.location + + @property + def workplane_quaternion(self) -> Quaternion: + return self.wp.nm.orientation diff --git a/stateful_operator/logic.py b/stateful_operator/logic.py index 7eef88c9..bc65dd5b 100644 --- a/stateful_operator/logic.py +++ b/stateful_operator/logic.py @@ -1,23 +1,22 @@ +from typing import Optional + import bpy -from bpy.props import IntProperty, BoolProperty +from bpy.props import BoolProperty, IntProperty from bpy.types import Context, Event from mathutils import Vector # TODO: Move to entity extended op from .. import global_data - -from .utilities.generic import to_list from .utilities.description import state_desc, stateful_op_desc +from .utilities.generic import to_list from .utilities.keymap import ( get_key_map_desc, - is_numeric_input, - is_unit_input, get_unit_value, get_value_from_event, + is_numeric_input, + is_unit_input, ) -from typing import Optional - class StatefulOperatorLogic: """Base class which implements the behaviour logic""" @@ -200,7 +199,9 @@ def check_event(self, event): is_confirm_button = event.type in ("LEFTMOUSE", "RET", "NUMPAD_ENTER") if is_confirm_button and event.value == "PRESS": + self.event = event # give event access to modal operators return True + if self.state_index == 0 and not self.wait_for_input: # Trigger the first state return not self.state_data.get("is_numeric_edit", False) @@ -466,6 +467,13 @@ def _handle_pass_through(self, context: Context, event: Event): return {"PASS_THROUGH"} return {"RUNNING_MODAL"} + def _is_mousemove(self, event: Event, reset: bool = False) -> bool: + coords = Vector((event.mouse_region_x, event.mouse_region_y)) + mousemove_threshold = 0.1 + if reset: + self._last_coords = coords + return (coords - self._last_coords).length > mousemove_threshold + def modal(self, context: Context, event: Event): state = self.state event_triggered = self.check_event(event) @@ -489,9 +497,7 @@ def modal(self, context: Context, event: Event): # HACK: when calling ops.ed.undo() inside an operator a mousemove event # is getting triggered. manually check if there's a mousemove... - mousemove_threshold = 0.1 - is_mousemove = (coords - self._last_coords).length > mousemove_threshold - self._last_coords = coords + is_mousemove = self._is_mousemove(event=event, reset=True) if not event_triggered: if is_numeric_event: From 68731e391f76de5a74ac758a4636e2da433d72a2 Mon Sep 17 00:00:00 2001 From: dyami-andrews-e3 <92410110+dyami-andrews-e3@users.noreply.github.com> Date: Mon, 9 Jun 2025 20:21:12 -0700 Subject: [PATCH 16/18] fix: revert changes to add_sketch --- operators/add_sketch.py | 198 +--------------------------------------- 1 file changed, 1 insertion(+), 197 deletions(-) diff --git a/operators/add_sketch.py b/operators/add_sketch.py index e6f6354f..b83660ac 100644 --- a/operators/add_sketch.py +++ b/operators/add_sketch.py @@ -1,42 +1,18 @@ import logging import bpy -from bpy.props import BoolProperty, EnumProperty, FloatProperty from bpy.types import Context, Event, Operator -from mathutils import Quaternion, Vector from ..declarations import Operators -from ..model.group_entities import SlvsEntities, SlvsSketch from ..model.types import SlvsWorkplane from ..stateful_operator.state import state_from_args -from ..stateful_operator.utilities.geometry import get_evaluated_obj, get_mesh_element from ..stateful_operator.utilities.register import register_stateops_factory -from ..utilities.geometry import get_face_orientation from .base_3d import Operator3d from .utilities import activate_sketch, switch_sketch_mode logger = logging.getLogger(__name__) -class ProjectionData: - def __init__( - self, - sketcherEntities: SlvsEntities, - sketch: SlvsSketch, - objectTranslation: bpy.types.TransformOrientation, - workplaneOrigin: tuple[float, float, float], - workplaneNormal: Vector, - quat: Quaternion, - ): - - self.sketcherEntities = sketcherEntities - self.sketch = sketch - self.objectTranslation = objectTranslation - self.workplaneOrigin = workplaneOrigin - self.workplaneNormal = workplaneNormal - self.quat = quat # I forgot what quat was... Should've added more comments - - # TODO: # - Draw sketches class View3D_OT_slvs_add_sketch(Operator, Operator3d): @@ -94,176 +70,4 @@ def fini(self, context: Context, succeed: bool): switch_sketch_mode(self, context, to_sketch_mode=False) -# TODO: Auto align view with sketch after creation -# TODO: Make it auto enter sketch -# TODO: Make the properties work! -class View3D_OT_slvs_add_sketch_face(Operator, Operator3d): - """Add a workplane and start sketch on mesh face""" - - bl_idname = Operators.AddSketchFace - bl_label = "Add sketch on mesh face" - bl_options = {"REGISTER", "UNDO"} - - # Can't get default to work. idk why - projectDist: FloatProperty( - name="Project distance", - subtype="DISTANCE", - unit="LENGTH", - default=0.001, - step=0.01, - # precision=get_prefs().decimal_precision, - ) - - # # Idk why it doesn't work correctly - # connectLines: BoolProperty(name="Connect lines", description="May cause performance issues, idk", default=True) - connectLines = True - - projectFrom: EnumProperty( - name="My Search", - items=( - ("FACE", "Face", ""), - ("MESH", "Mesh", ""), - ("ALL", "All meshes", ""), - ), - default="ALL", # Maybe should be 'MESH' instead for performance issues. Idk - ) - - states = ( - state_from_args( - "Face", - description="Pick a mesh face to use as workplane's and sketch's surface.", - use_create=False, - pointer="face", - types=(bpy.types.MeshPolygon,), - interactive=True, - ), - state_from_args( - "Additional projection distance", - description="Additional projection distance (default + extra)", - property="projectDist", - interactive=True, - no_event=True, - ), - ) - - def main(self, context: Context): - sse: SlvsEntities = context.scene.sketcher.entities - - # Gets info about clicked object - obj_name, clicked_face_index = self.get_state_pointer(index=0, implicit=True) - clicked_obj = get_evaluated_obj(context, bpy.data.objects[obj_name]) - clicked_mesh = clicked_obj.data - clicked_face: bpy.types.MeshPolygon = clicked_mesh.polygons[clicked_face_index] - - # Gets face rotation - obj_translation: bpy.types.TransformOrientation = clicked_obj.matrix_world - quat = get_face_orientation(clicked_mesh, clicked_face) # Quternion - quat.rotate(obj_translation) - - # Creates the workplane - workplane_origin: tuple[float, float, float] = ( - obj_translation @ clicked_face.center - ) - origin = sse.add_point_3d(workplane_origin) - nm = sse.add_normal_3d(quat) - workplane = sse.add_workplane(origin, nm) - - # Workplane normal in world coordinates - workplane_normal = quat @ Vector((0.0, 0.0, 1.0)) - - # Creates the sketch - sketch = sse.add_sketch(workplane) - sse.add_point_2d((0.0, 0.0), sketch, fixed=True) # Add face centrum point - - # activate_sketch(context, sketch.slvs_index, self) # This hides the pop-up with the options for the projection. Idk why, so it is just like this - # self.target = sketch - - limitDist = 0.001 + self.projectDist - # Should just be the project dist, but couldn't get default in property to work - - # Prepares the data needed for the projection - projectionData = ProjectionData( - sse, sketch, obj_translation, workplane_origin, workplane_normal, quat - ) - - if self.projectFrom == "FACE": - logger.error("Project face is not implemented yet") - elif self.projectFrom == "MESH": - self.ProjectFromMeshes( - projectionData, - [ - clicked_obj, - ], - limitDist, - self.connectLines, - ) - elif ( - self.projectFrom == "ALL" - ): # ALL doesn't actually work. I don't think its important to fix atm - allMeshesInScene = [o for o in context.scene.objects if o.type == "MESH"] - self.ProjectFromMeshes( - projectionData, allMeshesInScene, limitDist, self.connectLines - ) - - context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) - return True - - def ProjectFromMeshes( - self, - projectionData: ProjectionData, - meshes: list[bpy.types.Mesh], - maxDist: float, - connectLines: bool = True, - ): - sse = projectionData.sketcherEntities - - addedPoints = {} - for clicked_mesh in meshes: - vertices = clicked_mesh.data.vertices - for vertex in vertices: - # Make vertex relative to plane - vertex_world = projectionData.objectTranslation @ vertex.co - translated = vertex_world - projectionData.workplaneOrigin - - # Projection to plane - distance_to_plane = translated.dot(projectionData.workplaneNormal) - projection = ( - translated - distance_to_plane * projectionData.workplaneNormal - ) - - # If vertex is too far from sketch, then don't create sketch point - if abs(distance_to_plane) > maxDist: - continue - - ## Used ChatGPT, quaternion rotations is too hard. - # To 2D projection relative to the workplane - # Use the workplane orientation (quat) to project into 2D - local_projection = projection.copy() - local_projection.rotate(projectionData.quat.conjugated()) - x, y, _ = local_projection - - point = sse.add_point_2d( - (x, y), projectionData.sketch, fixed=True, index_reference=True - ) - addedPoints[vertex.index] = point - - if connectLines != True: - continue - - # Takes the edges of the object and checks if the earlier added sketch points are used in the edges. If yes, then create line from first point to second point - compareSet = set(addedPoints.keys()) - edges = clicked_mesh.data.edges - for edge in edges: - if set(edge.vertices).issubset(compareSet) != True: - continue - - p1, p2 = [addedPoints[x] for x in edge.vertices] - sse.add_line_2d( - p1, p2, projectionData.sketch, fixed=True, index_reference=True - ) - pass - - -register, unregister = register_stateops_factory( - (View3D_OT_slvs_add_sketch, View3D_OT_slvs_add_sketch_face) -) +register, unregister = register_stateops_factory((View3D_OT_slvs_add_sketch,)) From fb244f9ec29ed8dfbe062e0b30778a00c4e9f17a Mon Sep 17 00:00:00 2001 From: dyami-andrews-e3 <92410110+dyami-andrews-e3@users.noreply.github.com> Date: Mon, 9 Jun 2025 20:22:02 -0700 Subject: [PATCH 17/18] fix: revert changes to add_workplane --- operators/add_workplane.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/operators/add_workplane.py b/operators/add_workplane.py index b0e50021..6469db02 100644 --- a/operators/add_workplane.py +++ b/operators/add_workplane.py @@ -1,24 +1,21 @@ import logging import bpy -from bpy.types import Operator, Context -from mathutils import Vector +from bpy.types import Context, Operator from .. import global_data -from ..model.types import SlvsNormal3D -from ..model.categories import NORMAL3D - -from ..utilities.geometry import get_face_orientation from ..declarations import Operators -from ..stateful_operator.utilities.register import register_stateops_factory +from ..model.categories import NORMAL3D +from ..model.types import SlvsNormal3D +from ..solver import solve_system from ..stateful_operator.state import state_from_args from ..stateful_operator.utilities.geometry import get_evaluated_obj, get_mesh_element -from ..solver import solve_system +from ..stateful_operator.utilities.register import register_stateops_factory +from ..utilities.geometry import get_face_orientation +from ..utilities.view import get_placement_pos from .base_3d import Operator3d from .constants import types_point_3d from .utilities import ignore_hover -from ..utilities.view import get_placement_pos -from .utilities import activate_sketch, switch_sketch_mode logger = logging.getLogger(__name__) @@ -139,11 +136,10 @@ def main(self, context: Context): self.target = sse.add_workplane(origin, nm) ignore_hover(self.target) - context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) + context.area.tag_redraw() # Force re-draw of UI (Blender doesn't update after tool usage) return True - register, unregister = register_stateops_factory( (View3D_OT_slvs_add_workplane, View3D_OT_slvs_add_workplane_face) ) From 1f5d357ffb3fd95d7bc811674aff5c2162f0cb75 Mon Sep 17 00:00:00 2001 From: dyami-andrews-e3 <92410110+dyami-andrews-e3@users.noreply.github.com> Date: Mon, 9 Jun 2025 20:22:37 -0700 Subject: [PATCH 18/18] fix: revert changes to base2d --- operators/base_2d.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/operators/base_2d.py b/operators/base_2d.py index e1cc3f67..0e80fb1c 100644 --- a/operators/base_2d.py +++ b/operators/base_2d.py @@ -13,16 +13,12 @@ class Operator2d(GenericEntityOp): - - event: Event - @classmethod def poll(cls, context: Context): return context.scene.sketcher.active_sketch_i != -1 def init(self, context: Context, event: Event): self.sketch = context.scene.sketcher.active_sketch - self.event = event return True def state_func(self, context: Context, coords):