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..bef71f4c 100644 --- a/operators/add_sketch.py +++ b/operators/add_sketch.py @@ -2,6 +2,8 @@ import bpy from bpy.types import Operator, Context, Event +from bpy.props import FloatProperty, BoolProperty, EnumProperty +from mathutils import Vector, Quaternion from ..model.types import SlvsWorkplane from ..declarations import Operators @@ -9,10 +11,28 @@ 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 +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 @@ -71,4 +91,148 @@ 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,)) +# 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)) diff --git a/operators/add_workplane.py b/operators/add_workplane.py index 34cfeebd..b0e50021 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__) @@ -137,9 +139,11 @@ 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) 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..c36de50b --- /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 = "Project to sketch from 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), + )