diff --git a/__init__.py b/__init__.py index 0a46644..b166628 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ "name": "Fast Carve", "description": "Hardsurface utility Blender addon for quick and easy boolean and bevel operations", "author": "Jayanam", - "version": (0, 8, 1, 6), + "version": (0, 9, 0, 1), "blender": (2, 80, 0), "location": "View3D", "category": "Object"} @@ -47,18 +47,22 @@ description="Delete the object after apply", default = True) -bpy.types.Scene.use_snapping = BoolProperty(name="Snapping", +bpy.types.Scene.use_snapping = BoolProperty(name="Snap to grid", description="Use snapping to the grid", default = True) -bpy.types.Scene.in_primitive_mode = BoolProperty(name="Primitive Mode", - default = False) +bpy.types.Scene.snap_to_target = BoolProperty(name="Snap to target", + description="Snap the primitive to the target", + default = True) bpy.types.Scene.draw_distance = FloatProperty( name="Draw Distance", description="Distance of primitives to the origin", default = 2.0) +bpy.types.Scene.in_primitive_mode = BoolProperty(name="Primitive Mode", + default = False) + bpy.types.Scene.extrude_mesh = BoolProperty(name="Extrude mesh", description="Extrude the mesh after creation", default = True) diff --git a/__pycache__/__init__.cpython-37.pyc b/__pycache__/__init__.cpython-37.pyc index 45c6aa4..7cf2a88 100644 Binary files a/__pycache__/__init__.cpython-37.pyc and b/__pycache__/__init__.cpython-37.pyc differ diff --git a/__pycache__/fc_immediate_mode_op.cpython-37.pyc b/__pycache__/fc_immediate_mode_op.cpython-37.pyc index 5728184..2148659 100644 Binary files a/__pycache__/fc_immediate_mode_op.cpython-37.pyc and b/__pycache__/fc_immediate_mode_op.cpython-37.pyc differ diff --git a/__pycache__/fc_primitive_panel.cpython-37.pyc b/__pycache__/fc_primitive_panel.cpython-37.pyc index 3056751..30a2f21 100644 Binary files a/__pycache__/fc_primitive_panel.cpython-37.pyc and b/__pycache__/fc_primitive_panel.cpython-37.pyc differ diff --git a/fc_immediate_mode_op.py b/fc_immediate_mode_op.py index 027e2f7..8471f11 100644 --- a/fc_immediate_mode_op.py +++ b/fc_immediate_mode_op.py @@ -43,9 +43,17 @@ def __init__(self): def invoke(self, context, event): args = (self, context) + target_obj = context.scene.carver_target + snap_to_target = context.scene.snap_to_target + + if target_obj is None: + self.report({'ERROR'}, 'Please define a target object.') + context.scene.in_primitive_mode = False + return {"FINISHED"} + context.scene.in_primitive_mode = True - self.create_shape(context) + self.create_shape(context, target_obj, snap_to_target) self.register_handlers(args, context) @@ -72,9 +80,19 @@ def unregister_handlers(self, context): self.draw_handle_3d = None self.draw_event = None + def get_3d_for_mouse(self, mouse_pos_2d, context): + if context.scene.snap_to_target: + mouse_pos_3d = self.shape.get_3d_for_2d(mouse_pos_2d, context) + else: + mouse_pos_3d = get_3d_vertex(context, mouse_pos_2d) + return mouse_pos_3d + def modal(self, context, event): if context.area: context.area.tag_redraw() + + target_obj = context.scene.carver_target + snap_to_target = context.scene.snap_to_target if event.type == "ESC" and event.value == "PRESS": @@ -91,12 +109,14 @@ def modal(self, context, event): return {'FINISHED'} # The mouse is moved - if event.type == "MOUSEMOVE": + if event.type == "MOUSEMOVE" and not self.shape.is_none(): mouse_pos_2d = (event.mouse_region_x, event.mouse_region_y) - mouse_pos_3d = get_3d_vertex(context, mouse_pos_2d) - if context.scene.use_snapping: + mouse_pos_3d = self.get_3d_for_mouse(mouse_pos_2d, context) + + if context.scene.use_snapping and mouse_pos_3d is not None: + mouse_pos_3d = get_snap_3d_vertex(context, mouse_pos_3d) mouse_pos_2d = get_2d_vertex(context, mouse_pos_3d) if self.shape.handle_mouse_move(mouse_pos_2d, mouse_pos_3d, event, context): @@ -105,13 +125,21 @@ def modal(self, context, event): # Left mouse button is pressed if event.value == "PRESS" and event.type == "LEFTMOUSE": + if target_obj is None: + self.report({'ERROR'}, 'Please define a target object.') + return {"PASS_THROUGH"} + + self.create_shape(context, target_obj, snap_to_target) + mouse_pos_2d = (event.mouse_region_x, event.mouse_region_y) - mouse_pos_3d = get_3d_vertex(context, mouse_pos_2d) - if context.scene.use_snapping: - mouse_pos_2d = get_2d_vertex(context, mouse_pos_3d) + - self.create_shape(context) + mouse_pos_3d = self.get_3d_for_mouse(mouse_pos_2d, context) + + if context.scene.use_snapping and mouse_pos_3d is not None: + mouse_pos_3d = get_snap_3d_vertex(context, mouse_pos_3d) + mouse_pos_2d = get_2d_vertex(context, mouse_pos_3d) if self.shape.is_moving(): self.shape.stop_move(context) @@ -124,7 +152,6 @@ def modal(self, context, event): if self.shape.handle_mouse_press(mouse_pos_2d, mouse_pos_3d, event, context): self.create_object(context) - # self.shape.reset() else: # So that the direction is defined during shape # creation, not when it is extruded @@ -140,14 +167,18 @@ def modal(self, context, event): # try to move the shape if event.type == "G": mouse_pos_2d = (event.mouse_region_x, event.mouse_region_y) - mouse_pos_3d = get_3d_vertex(context, mouse_pos_2d) + + mouse_pos_3d = self.get_3d_for_mouse(mouse_pos_2d, context) + if self.shape.start_move(mouse_pos_3d): return {"RUNNING_MODAL"} # try to rotate the shape if event.type == "R": mouse_pos_2d = (event.mouse_region_x, event.mouse_region_y) - mouse_pos_3d = get_3d_vertex(context, mouse_pos_2d) + + mouse_pos_3d = self.get_3d_for_mouse(mouse_pos_2d, context) + if self.shape.start_rotate(mouse_pos_3d, context): self.create_batch() return {"RUNNING_MODAL"} @@ -155,9 +186,8 @@ def modal(self, context, event): # try to extrude the shape if event.type == "E": mouse_pos_2d = (event.mouse_region_x, event.mouse_region_y) - mouse_pos_3d = get_3d_vertex(context, mouse_pos_2d) - if self.shape.start_extrude(mouse_pos_2d, mouse_pos_3d, context): + if self.shape.start_extrude(mouse_pos_2d, context): self.create_batch() return {"RUNNING_MODAL"} @@ -174,12 +204,12 @@ def modal(self, context, event): context.scene.primitive_type = next_enum(context.scene.primitive_type, context.scene, "primitive_type") - self.create_shape(context) + self.create_shape(context, target_obj, snap_to_target) return {"RUNNING_MODAL"} return {"PASS_THROUGH"} - def create_shape(self, context): + def create_shape(self, context, target_obj, snap_to_target): if self.shape.is_none(): if context.scene.primitive_type == "Circle": self.shape = Circle_Shape() @@ -188,6 +218,8 @@ def create_shape(self, context): else: self.shape = Rectangle_Shape() + self.shape.initialize(context, target_obj, snap_to_target) + def create_object(self, context): # Create a mesh and an object and diff --git a/fc_primitive_panel.py b/fc_primitive_panel.py index e6ffea8..4f7044b 100644 --- a/fc_primitive_panel.py +++ b/fc_primitive_panel.py @@ -27,6 +27,9 @@ def draw(self, context): row = layout.row() layout.prop(context.scene, "use_snapping") + row = layout.row() + layout.prop(context.scene, "snap_to_target") + row = layout.row() if not context.scene.in_primitive_mode: diff --git a/types/__pycache__/circle_shape.cpython-37.pyc b/types/__pycache__/circle_shape.cpython-37.pyc index 75aaf09..0bd10d5 100644 Binary files a/types/__pycache__/circle_shape.cpython-37.pyc and b/types/__pycache__/circle_shape.cpython-37.pyc differ diff --git a/types/__pycache__/polyline_shape.cpython-37.pyc b/types/__pycache__/polyline_shape.cpython-37.pyc index 765e43f..e521ce8 100644 Binary files a/types/__pycache__/polyline_shape.cpython-37.pyc and b/types/__pycache__/polyline_shape.cpython-37.pyc differ diff --git a/types/__pycache__/rectangle_shape.cpython-37.pyc b/types/__pycache__/rectangle_shape.cpython-37.pyc index 375c43a..e1d6ff3 100644 Binary files a/types/__pycache__/rectangle_shape.cpython-37.pyc and b/types/__pycache__/rectangle_shape.cpython-37.pyc differ diff --git a/types/__pycache__/shape.cpython-37.pyc b/types/__pycache__/shape.cpython-37.pyc index a607e78..c6ad9c6 100644 Binary files a/types/__pycache__/shape.cpython-37.pyc and b/types/__pycache__/shape.cpython-37.pyc differ diff --git a/types/circle_shape.py b/types/circle_shape.py index 7cd379f..f75afab 100644 --- a/types/circle_shape.py +++ b/types/circle_shape.py @@ -22,6 +22,9 @@ def handle_mouse_move(self, mouse_pos_2d, mouse_pos_3d, event, context): return result def create_circle(self, context): + + from mathutils import Matrix + rv3d = context.space_data.region_3d view_rot = rv3d.view_rotation @@ -30,7 +33,12 @@ def create_circle(self, context): points = [(sin(i * mul) * self._radius, cos(i * mul) * self._radius, 0) for i in range(segments)] - self._vertices = [view_rot @ Vector(point) + + rot_mat = view_rot + + if self._normal is not None: + rot_mat = self._normal.to_track_quat('Z', 'X').to_matrix() + + self._vertices = [rot_mat @ Vector(point) + self._center for point in points] self._vertices_2d = [get_2d_vertex(context, vertex) for vertex in self._vertices] @@ -38,6 +46,9 @@ def create_circle(self, context): def handle_mouse_press(self, mouse_pos_2d, mouse_pos_3d, event, context): + if mouse_pos_3d is None: + return False + if self.is_none() and event.ctrl: self._center = mouse_pos_3d diff --git a/types/polyline_shape.py b/types/polyline_shape.py index d1328da..c45dd88 100644 --- a/types/polyline_shape.py +++ b/types/polyline_shape.py @@ -24,6 +24,9 @@ def get_vertices_copy(self, mouse_pos = None): def handle_mouse_press(self, mouse_pos_2d, mouse_pos_3d, event, context): + if mouse_pos_3d is None: + return False + if (self.is_none() and event.ctrl) or (self.is_processing() and not event.ctrl): self.add_vertex(mouse_pos_3d) diff --git a/types/rectangle_shape.py b/types/rectangle_shape.py index fec6abd..719fcfd 100644 --- a/types/rectangle_shape.py +++ b/types/rectangle_shape.py @@ -12,6 +12,9 @@ def __init__(self): def handle_mouse_press(self, mouse_pos_2d, mouse_pos_3d, event, context): + if mouse_pos_3d is None: + return False + if self.is_none() and event.ctrl: self._vertices_2d[0] = mouse_pos_2d @@ -66,8 +69,12 @@ def create_rect(self, context): self._vertices.clear() # get missing 3d vertices - vertex2 = get_3d_vertex(context, self._vertices_2d[1]) - vertex4 = get_3d_vertex(context, self._vertices_2d[3]) + if self._normal is None: + vertex2 = get_3d_vertex(context, self._vertices_2d[1]) + vertex4 = get_3d_vertex(context, self._vertices_2d[3]) + else: + vertex2 = self.get_3d_for_2d(self._vertices_2d[1], context) + vertex4 = self.get_3d_for_2d(self._vertices_2d[3], context) self._vertices.extend([self._vertex1, vertex2, self._vertex3, vertex4]) @@ -89,10 +96,12 @@ def start_rotate(self, mouse_pos, context): y = oy + sin(angle) * (px - ox) + cos(angle) * (py - oy) tmp_vertices_2d.append((x,y)) - - direction = get_view_direction_by_rot_matrix(self._view_context.view_rotation) * context.scene.draw_distance - self._vertices[i] = get_3d_vertex_for_2d(self._view_context, (x,y), -direction) + if self._normal is None: + direction = get_view_direction_by_rot_matrix(self._view_context.view_rotation) * context.scene.draw_distance + self._vertices[i] = get_3d_vertex_for_2d(self._view_context, (x,y), -direction) + else: + self._vertices[i] = self.get_3d_for_2d((x,y), context) self._vertices_2d = tmp_vertices_2d diff --git a/types/shape.py b/types/shape.py index 532a280..9562a58 100644 --- a/types/shape.py +++ b/types/shape.py @@ -4,9 +4,13 @@ from mathutils import Vector, geometry +from mathutils.geometry import intersect_line_plane + from ..utils.fc_view_3d_utils import * -from bpy_extras.view3d_utils import region_2d_to_location_3d, location_3d_to_region_2d +from bpy_extras.view3d_utils import ( + region_2d_to_location_3d, + location_3d_to_region_2d ) class ShapeState(Enum): NONE = 0 @@ -83,8 +87,31 @@ def __init__(self): self._view_context = None self._mouse_pos_2d = (0,0) self._is_extruded = False + self._snap_to_target = True + self._bvhtree = None + self._hit = None + self._normal = None + + def get_3d_for_2d(self, pos_2d, context): + origin, direction = get_origin_and_direction(pos_2d, context) + result = None + + if self._hit is None: + self._hit, self._normal, *_ = self._bvhtree.ray_cast(origin, direction) + result = self._hit.copy() + else: + result = intersect_line_plane(origin, origin + direction, self._hit, self._normal) + + if result is not None: + result += self._normal.normalized() * 0.01 + + return result + def initialize(self, context, target, snap_to_target): + self._bvhtree = bvhtree_from_object(context, target) + self._snap_to_target = snap_to_target + def is_none(self): return self._state is ShapeState.NONE @@ -107,9 +134,11 @@ def is_extruding(self): return self._is_extruding def get_dir(self): - view_rot = self._view_context.view_rotation - - return get_view_direction_by_rot_matrix(view_rot) + if self._normal is None: + view_rot = self._view_context.view_rotation + return get_view_direction_by_rot_matrix(view_rot) + + return -self._normal def get_view_context(self): return self._view_context @@ -188,7 +217,7 @@ def stop_rotate(self, context): self._is_rotating = False self._rotation = 0.0 - def start_extrude(self, mouse_pos_2d, mouse_pos_3d, context): + def start_extrude(self, mouse_pos_2d, context): self._mouse_pos_2d = mouse_pos_2d self._is_extruding = True return True diff --git a/utils/__pycache__/fc_view_3d_utils.cpython-37.pyc b/utils/__pycache__/fc_view_3d_utils.cpython-37.pyc index 1ab1bf9..a04ecf2 100644 Binary files a/utils/__pycache__/fc_view_3d_utils.cpython-37.pyc and b/utils/__pycache__/fc_view_3d_utils.cpython-37.pyc differ diff --git a/utils/fc_view_3d_utils.py b/utils/fc_view_3d_utils.py index 185782c..235b573 100644 --- a/utils/fc_view_3d_utils.py +++ b/utils/fc_view_3d_utils.py @@ -1,7 +1,16 @@ +import bpy +import bmesh import mathutils +from mathutils import Vector +from mathutils.bvhtree import BVHTree +from mathutils.geometry import intersect_line_plane -from bpy_extras.view3d_utils import region_2d_to_origin_3d -from bpy_extras.view3d_utils import region_2d_to_location_3d, location_3d_to_region_2d +from bpy_extras.view3d_utils import ( + region_2d_to_origin_3d, + region_2d_to_location_3d, + region_2d_to_vector_3d, + location_3d_to_region_2d +) def get_snap_vertex_indizes(view_rot): v1 = round(abs(view_rot[0]), 3) @@ -44,7 +53,28 @@ def get_view_direction(context): def get_view_direction_by_rot_matrix(view_rotation): dir = view_rotation @ mathutils.Vector((0,0,-1)) - return dir.normalized() + return dir.normalized() + +def get_origin_and_direction(pos_2d, context): + region = context.region + region_3d = context.space_data.region_3d + + origin = region_2d_to_origin_3d(region, region_3d, pos_2d) + direction = region_2d_to_vector_3d(region, region_3d, pos_2d) + return origin, direction + +def get_3d_on_mesh(pos_2d, bvhtree, context): + + origin, direction = get_origin_and_direction(pos_2d, context) + hit, normal, *_ = bvhtree.ray_cast(origin, direction) + return hit, normal + +def get_3d_on_plane(pos_2d, hit, normal, context): + + origin, direction = get_origin_and_direction(pos_2d, context) + + # get the intersection point on infinite plane + return intersect_line_plane(origin, origin + direction, hit, normal) def get_3d_vertex_for_2d(view_context, vertex_2d, dir): @@ -64,6 +94,30 @@ def get_2d_vertex(context, vertex_3d): rv3d = context.space_data.region_3d return location_3d_to_region_2d(region, rv3d, vertex_3d) +def bvhtree_from_object(context, obj): + bm = bmesh.new() + + mesh = obj.to_mesh(context.depsgraph, True) + bm.from_mesh(mesh) + bm.transform(obj.matrix_world) + + bvhtree = BVHTree.FromBMesh(bm) + bpy.data.meshes.remove(mesh) + return bvhtree + +def get_snap_3d_vertex(context, vertex_3d): + + rv3d = context.space_data.region_3d + overlay3d = context.space_data.overlay + view_rot = rv3d.view_rotation + + if (not rv3d.is_perspective and vertex_3d is not None): + ind = get_snap_vertex_indizes(view_rot) + if ind is not None: + vertex_3d[ind[0]] = vertex_3d[ind[0]] + get_grid_snap_pos(vertex_3d[ind[0]], overlay3d) + vertex_3d[ind[1]] = vertex_3d[ind[1]] + get_grid_snap_pos(vertex_3d[ind[1]], overlay3d) + return vertex_3d + def get_3d_vertex(context, vertex_2d): region = context.region @@ -74,10 +128,10 @@ def get_3d_vertex(context, vertex_2d): dir = get_view_direction(context) * -context.scene.draw_distance vec = region_2d_to_location_3d(region, rv3d, vertex_2d, dir) - if (not rv3d.is_perspective and context.scene.use_snapping): - ind = get_snap_vertex_indizes(view_rot) - if ind is not None: - vec[ind[0]] = vec[ind[0]] + get_grid_snap_pos(vec[ind[0]], overlay3d) - vec[ind[1]] = vec[ind[1]] + get_grid_snap_pos(vec[ind[1]], overlay3d) + # if (not rv3d.is_perspective and context.scene.use_snapping): + # ind = get_snap_vertex_indizes(view_rot) + # if ind is not None: + # vec[ind[0]] = vec[ind[0]] + get_grid_snap_pos(vec[ind[0]], overlay3d) + # vec[ind[1]] = vec[ind[1]] + get_grid_snap_pos(vec[ind[1]], overlay3d) return vec \ No newline at end of file