CAD Snap Utilities

Just press ALT+E (it’s in extrude menu)

1 Like

drag longer …

it is not working well

An internal solution is in the works.
https://developer.blender.org/D5336

1 Like

Yep, indeed. So extrude and reshape could be like temporal solution until built-in version.

Before “Extrude and reshape” was working better
and “Destructive Extrude” worked perfectly.
Now is working nothing.

Try this.
Save it as ExtrudePull.py

import bpy
from bpy_extras import view3d_utils
from mathutils import Vector, kdtree
from mathutils.geometry import intersect_line_plane
from mathutils.bvhtree import BVHTree

import numpy as np

bl_info = {
	"name": "Extrude Pull",
	"location": "Edit Mode: Mesh > Extrude > Extrude Pull Geometry",
	"description": "Extrude unwanted geometry away",
	"author": "Vladislav Kindushov, Martin Capitanio",
	"version": (1, 0, 6),
	"blender": (2, 80, 0),
	"category": "Mesh",
}


def Snap(self, context, location, normal, index, object, matrix):
	# Find nearest element for snap.
	location = object.matrix_world @ location
	BestLocation, tresh4, tresh5 = self.KDTreeSnap.find(location)

	# Find nearest direction.
	tresh1, BestDirection, tresh2, tresh3 = self.BVHTree.find_nearest(BestLocation)
	BestVertex, tresh4, tresh5 = self.KDTree.find(BestLocation)

	ToVertex = BestLocation
	FromVertex = BestVertex
	dvec = ToVertex - BestDirection
	dnormal = np.dot(dvec, BestDirection)
	SnapPoint = FromVertex + Vector(dnormal * BestDirection)
	SnapDistance = (FromVertex - SnapPoint).length

	if self.NormalMove:
		return SnapDistance
	else:
		return SnapPoint


def RayCast(self, event, context):
	scene = context.scene
	region = context.region
	rv3d = context.region_data
	coord = event.mouse_region_x, event.mouse_region_y

	# Get the ray from the viewport and mouse.
	view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord).normalized()
	ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
	ray_target = ray_origin + (view_vector * 10000)

	matrix = self.MainObject.matrix_world
	matrix_inv = matrix.inverted()
	ray_origin_obj = matrix_inv @ ray_origin
	ray_target_obj = matrix_inv @ ray_target
	ray_direction_obj = ray_target_obj - ray_origin_obj
	ray_direction_obj.normalize()

	result, location, normal, index = self.MainObject.ray_cast(
		ray_origin_obj, ray_direction_obj
	)

	if result:
		value = Snap(self, context, location, normal, index, self.MainObject, matrix)
		if value is None:
			return GetMouseLocation(self, event, context) - self.StartMouseLocation
		else:
			return value
	else:
		return GetMouseLocation(self, event, context) - self.StartMouseLocation


def CreateBVHTree(self, context):
	bvh = BVHTree.FromObject(
		self.ExtrudeObject,
		context.evaluated_depsgraph_get(),
		deform=False,
		cage=False,
		epsilon=0.0
	)
	self.BVHTree = bvh
	size = len(self.ExtrudeObject.data.vertices)
	kd = kdtree.KDTree(size)

	for i in self.ExtrudeObject.data.vertices:
		kd.insert(self.ExtrudeObject.matrix_world @ i.co.copy(), i.index)

	kd.balance()
	self.KDTree = kd

	size = len(self.MainObject.data.vertices)
	size2 = len(self.MainObject.data.edges)
	size3 = len(self.MainObject.data.polygons)
	kd2 = kdtree.KDTree(size + size2 + size3)

	for i in self.MainObject.data.vertices:
		kd2.insert(self.MainObject.matrix_world @ i.co, i.index)

	for i in self.MainObject.data.edges:
		pos = (
			self.MainObject.data.vertices[i.vertices[0]].co +
			self.MainObject.data.vertices[i.vertices[1]].co
		) / 2
		kd2.insert(self.MainObject.matrix_world @ pos, i.index + size)

	for i in self.MainObject.data.polygons:
		kd2.insert(self.MainObject.matrix_world @ i.center, i.index + size + size2)

	kd2.balance()
	self.KDTreeSnap = kd2


def CursorPosition(self, context, is_Set=False):
	if is_Set and self.CursorLocation != 'NONE':
		context.scene.cursor.location = self.CursorLocation
		bpy.context.scene.tool_settings.transform_pivot_point = self.PivotPoint
	else:
		self.CursorLocation = context.scene.cursor.location
		self.PivotPoint = context.scene.tool_settings.transform_pivot_point


def CreateNewObject(self, context):
	# Duplicate the object.
	bpy.ops.mesh.duplicate_move()
	bpy.ops.mesh.separate(type='SELECTED')
	bpy.ops.object.mode_set(mode='OBJECT')
	self.ExtrudeObject = context.selected_objects[-1]

	# Clear modifiers.
	while len(self.ExtrudeObject.modifiers) != 0:
		self.ExtrudeObject.modifiers.remove(self.ExtrudeObject.modifiers[0])


def GetVisualSetings(self, context, isSet=False):
	if isSet:
		context.active_object.show_all_edges = self.ShowAllEdges
		context.active_object.show_wire = self.ShowAllEdges
	else:
		self.ShowAllEdges = context.active_object.show_all_edges
		self.ShowAllEdges = context.active_object.show_wire


def SetVisualSetings(self, context):
	self.MainObject.show_all_edges = True
	self.MainObject.show_wire = True

	self.ExtrudeObject.display_type = 'WIRE'


def GetVisualModifiers(self, context, isSet=False):
	if isSet:
		for i in self.MainObject.modifiers:
			if i.name in self.VisibilityModifiers:
				i.show_viewport = True
	else:
		for i in self.MainObject.modifiers:
			if i.show_viewport:
				self.VisibilityModifiers.append(i.name)
				i.show_viewport = False


def CreateModifier(self, context):
	# Set Boolean.
	context.view_layer.objects.active = self.MainObject
	self.bool = context.object.modifiers.new('DestructiveBoolean', 'BOOLEAN')
	bpy.context.object.modifiers["DestructiveBoolean"].operation = 'DIFFERENCE'
	bpy.context.object.modifiers["DestructiveBoolean"].object = self.ExtrudeObject
	bpy.context.object.modifiers["DestructiveBoolean"].show_viewport = True
	# Set Solidify.
	context.view_layer.objects.active = self.ExtrudeObject
	context.object.modifiers.new('DestructiveSolidify', 'SOLIDIFY')
	context.object.modifiers['DestructiveSolidify'].use_even_offset = True
	context.object.modifiers['DestructiveSolidify'].offset = -0.99959


def GetMouseLocation(self, event, context):
	region = bpy.context.region
	rv3d = bpy.context.region_data
	coord = event.mouse_region_x, event.mouse_region_y
	view_vector_mouse = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
	ray_origin_mouse = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
	V_a = ray_origin_mouse + view_vector_mouse
	V_b = rv3d.view_rotation @ Vector((0.0, 0.0, -1.0))
	pointLoc = intersect_line_plane(ray_origin_mouse, V_a, context.object.location, V_b)
	loc = (self.GeneralNormal @ pointLoc) * -1
	return loc


def SetSolidifyValue(self, context, value):
	self.ExtrudeObject.modifiers[-1].thickness = value


def CalculateNormal(self, context):
	for i in self.ExtrudeObject.data.polygons:
		self.GeneralNormal += i.normal.copy()


def TransformObject(self, context):
	selObj = context.selected_objects
	bpy.ops.object.select_all(action='DESELECT')
	self.ExtrudeObject.select_set(True)
	context.view_layer.objects.active = self.ExtrudeObject
	bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
	bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS')
	bpy.ops.view3d.snap_cursor_to_selected()

	bpy.context.scene.tool_settings.transform_pivot_point = 'CURSOR'
	self.ExtrudeObject.scale = Vector((1.001, 1.001, 1.001))

	for i in selObj:
		i.select_set(True)
	context.view_layer.objects.active = self.MainObject


def GetFaceNormal(self, context):
	for i in self.ExtrudeObject.data.polygons:
		self.FaceNormal.append(i.normal.copy())


def GetMainVertsIndex(self, context):
	for i in self.ExtrudeObject.data.vertices:
		self.MainVertsIndex.append(i.index)


def SetForAxis(self, context):
	GetMainVertsIndex(self, context)
	context.view_layer.objects.active = self.ExtrudeObject
	for i in range(0, len(self.MainVertsIndex) - 1):
		self.StartVertsPos.append(self.ExtrudeObject.data.vertices[i].co.copy())
	index = []
	for f in self.ExtrudeObject.data.polygons:
		normal = f.normal
		for v in f.vertices:
			if v not in index:
				self.ExtrudeObject.data.vertices[
					v
				].co = normal * 0.02 + self.ExtrudeObject.data.vertices[v].co.copy()
				index.append(v)

	self.ExtrudeObject.modifiers[0].thickness = 0.00
	self.ExtrudeObject.modifiers[0].offset = 0
	bpy.ops.object.modifier_apply(
		apply_as='DATA', modifier=self.ExtrudeObject.modifiers[0].name
	)
	for i in range(len(self.MainVertsIndex) - 1, len(self.ExtrudeObject.data.vertices)):
		self.StartVertsPos.append(self.ExtrudeObject.data.vertices[i].co.copy())
		context.view_layer.objects.active = self.MainObject


def ReturnStartPosition(self, context):
	for i in range(len(self.MainVertsIndex) - 1, len(self.ExtrudeObject.data.vertices)):
		self.ExtrudeObject.data.vertices[i].co = self.StartVertsPos[i]


def AxisMove(self, context, value):
	axis = Vector()
	if self.AxisMove == 'X':
		axis = Vector((-1.0, 0.0, 0.0))
	elif self.AxisMove == 'Y':
		axis = Vector((0.0, -1.0, 0.0))
	elif self.AxisMove == 'Z':
		axis = Vector((0.0, 0.0, -1.0))

	for i in range(len(self.MainVertsIndex), len(self.ExtrudeObject.data.vertices)):
		vertPos = ((axis * value) + self.StartVertsPos[i])
		self.ExtrudeObject.data.vertices[i].co = vertPos


def Cancel(self, context):
	bpy.data.objects.remove(self.ExtrudeObject)
	context.view_layer.objects.active = self.MainObject
	bpy.ops.object.modifier_remove(modifier='DestructiveBoolean')
	GetVisualSetings(self, context, True)
	GetVisualModifiers(self, context, True)
	bpy.ops.object.mode_set(mode='EDIT')


def Finish(self, context, BevelUpdate=False):
	rayCastFace = []
	if self.NormalMove:
		context.view_layer.objects.active = self.ExtrudeObject
		GetMainVertsIndex(self, context)
		bpy.ops.object.modifier_apply(
			apply_as='DATA', modifier=self.ExtrudeObject.modifiers[0].name
		)
		bpy.ops.object.mode_set(mode='EDIT')
		bpy.ops.object.mode_set(mode='OBJECT')

	context.view_layer.objects.active = self.MainObject
	bpy.ops.object.modifier_apply(apply_as='DATA', modifier='DestructiveBoolean')
	bpy.context.view_layer.update()
	context.active_object.data.update()
	context.active_object.data.update(calc_edges=False)
	context.active_object.update_tag(refresh={'OBJECT', 'DATA', 'TIME'})
	bpy.ops.object.mode_set(mode='EDIT')
	bpy.ops.object.mode_set(mode='OBJECT')

	for f in self.ExtrudeObject.data.polygons:
		faceCenter = self.ExtrudeObject.matrix_world @ f.center
		faceNormal = self.ExtrudeObject.matrix_world @ f.normal
		StartPoint = ((faceNormal * -1) * 0.003) + faceCenter

		center = self.MainObject.matrix_world.inverted() @ StartPoint
		normal = self.MainObject.matrix_world.inverted() @ faceNormal
		result, location, normal, index = self.MainObject.ray_cast(
			center, normal, distance=0.005
		)
		if result:
			rayCastFace.append(index)
			self.MainObject.data.polygons[index].select = True

	bpy.context.scene.tool_settings.transform_pivot_point = 'CURSOR'
	bpy.ops.transform.resize(value=(1 - 0.001, 1 - 0.001, 1 - 0.001))
	bpy.context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT'

	for f in self.MainObject.data.polygons:
		f.select = False

	for f in self.ExtrudeObject.data.polygons:
		lose = False
		for v in f.vertices:
			if v in self.MainVertsIndex:
				lose = True
				break
		if lose:
			continue
		else:
			faceCenter = self.ExtrudeObject.matrix_world @ f.center
			faceNormal = self.ExtrudeObject.matrix_world @ f.normal
			StartPoint = ((faceNormal * -1) * 0.003) + faceCenter

			center = self.MainObject.matrix_world.inverted() @ StartPoint
			normal = self.MainObject.matrix_world.inverted() @ faceNormal
			result, location, normal, index = self.MainObject.ray_cast(
				center, normal, distance=0.005
			)
			if result:
				self.MainObject.data.polygons[index].select = True

	bpy.data.objects.remove(self.ExtrudeObject)
	GetVisualSetings(self, context, True)
	GetVisualModifiers(self, context, True)
	bpy.ops.object.mode_set(mode='EDIT')
	bpy.ops.mesh.remove_doubles(threshold=0.001, use_unselected=True)


class ExtrudePull(bpy.types.Operator):
	bl_idname = "mesh.extrude_pull"
	bl_label = "Extrude Pull Geometry"
	bl_options = {"REGISTER", "UNDO", "GRAB_CURSOR", "BLOCKING"}
	bl_description = "Extrude unwanted geometry away"

	@classmethod
	def poll(cls, context):
		# Disable for Vertex and Edge select mode.
		if tuple(bpy.context.tool_settings.mesh_select_mode) == (False, False, True):
			if context.active_object.data.count_selected_items()[2] > 0:
				return (context.mode == "EDIT_MESH")
		return False

	def modal(self, context, event):
		if event.type == 'MOUSEMOVE':
			value = GetMouseLocation(self, event, context) - self.StartMouseLocation
			if self.NormalMove:
				SetSolidifyValue(self, context, value)
			else:
				AxisMove(self, context, value)

		if event.ctrl:
			value = RayCast(self, event, context)
			if self.NormalMove:
				SetSolidifyValue(self, context, value)
			else:
				AxisMove(self, context, value)

		if event.type == 'X':
			if self.NormalMove:
				SetForAxis(self, context)
				self.NormalMove = False
			ReturnStartPosition(self, context)
			self.AxisMove = 'X'

		if event.type == 'Y':
			if self.NormalMove:
				SetForAxis(self, context)
				self.NormalMove = False
			ReturnStartPosition(self, context)
			self.AxisMove = 'Y'

		if event.type == 'Z':
			if self.NormalMove:
				SetForAxis(self, context)
				self.NormalMove = False
			ReturnStartPosition(self, context)
			self.AxisMove = 'Z'

		if event.type == 'LEFTMOUSE':
			Finish(self, context, BevelUpdate=False)
			return {'FINISHED'}

		if event.type in {'RIGHTMOUSE', 'ESC'}:
			Cancel(self, context)
			return {'CANCELLED'}
		return {'RUNNING_MODAL'}

	def invoke(self, context, event):
		if context.space_data.type == 'VIEW_3D':
			self.KDTreeSnap = None
			self.KDTree = None
			self.BVHTree = None
			self.PivotPoint = None
			self.MainVertsIndex = []
			self.AxisMove = 'Z'
			self.StartVertsPos = []
			self.NormalMove = True
			self.GeneralNormal = Vector((0.0, 0.0, 0.0))
			self.FaceNormal = []
			self.ShowAllEdges = None
			self.ShowWire = None
			self.CursorLocation = None
			self.VisibilityModifiers = []
			self.MainObject = context.active_object
			self.ExtrudeObject = None
			self.SaveSelectFaceForCancel = None

			GetVisualModifiers(self, context)
			GetVisualSetings(self, context)
			CursorPosition(self, context)
			CreateNewObject(self, context)
			CreateBVHTree(self, context)
			CreateModifier(self, context)
			SetVisualSetings(self, context)
			TransformObject(self, context)
			CalculateNormal(self, context)
			self.StartMouseLocation = GetMouseLocation(self, event, context)
			# print('StartMouseLocation', self.StartMouseLocation)

			context.window_manager.modal_handler_add(self)
			return {'RUNNING_MODAL'}
		else:
			self.report({'WARNING'}, "The operator is not called in 3D Viewport.")
			return {'CANCELLED'}


classes = (ExtrudePull)


def operator_draw(self, context):
	layout = self.layout
	col = layout.column(align=True)
	self.layout.operator_context = 'INVOKE_REGION_WIN'
	col.operator("mesh.extrude_pull", text="Extrude Pull Geometry")


def register():
	bpy.utils.register_class(classes)
	bpy.types.VIEW3D_MT_edit_mesh_extrude.append(operator_draw)


def unregister():
	bpy.utils.unregister_class(classes)
	bpy.types.VIEW3D_MT_edit_mesh_extrude.remove(operator_draw)


if __name__ == "__main__":
	register() 
3 Likes

Thanks, partially works, but there are a lot of mistakes.

Edit: but for easy tasks it is working well.
Specially making holes through is really good.

But still it is very easy to make bad topology.

Did you make that one by yourself? (Don’t recognize the names in the info block)

No, I didn’t write the script.

But I just see that there is a new version (1.07).

It would be nice to get an updated video showing how the same features of the deprecated paid addon can be accessed in blender stock version if they have been merged. It’s a little confusing as it is right now

1 Like

Hopefully when the re-do the boolean operations in blender, that will fix the extrude issues as well…
Although Undo is a bigger issue for me than anything else… waiting 10-20-30 seconds per object undo.

That’s fixed for me not for all case but in the 2.83 beta , i can see big improvements in undo speed.
The last operation that are problematic are describe in their to do (undo duplicate object and delete are still slow in some case). But i can model on big object and undo modeling task as quick as i was on a low poly.

1 Like

Thank you for the awesome addon! It’s really useful.

Is there a way to remap hotkeys for Y/Z/X constraints?
I tried to change all the values from ‘Y’,‘Z’,‘X’ to ‘A’,'S,‘D’ in two files: common_classes.py and drawing_utilities.py. However it cause some errors.

Hello, I’m currently using this add-on in Blender 2.82, and I cannot apply textures to anything I make with it.

If I use the Add function in Edit mode to make a Plane, I can apply textures to it just fine with this method shown here: https://www.youtube.com/watch?v=r5YNJghc81U

If I use the Make Line tool to make almost the exact same plane (with the Create Faces function on), doing that method just results in weird colors and no visible texture.

Do I need to use a different method to apply textures with the Make Lines Tool?

The move tool from this addon is exactly what I have been looking for having come to Blender from CAD software. I see that the Snap Utilities addon is no longer available. Does anyone know of another addon that has similar functionality as the move tool (and perhaps the rotate tool)?

Hi Xayzer,
look at this

Thank you very much, @renderthings! This looks just like what I’m looking for!

1 Like

@mano-wii
Hello,

thanks for addon!
Can it make perpendicular cut from starting point? like it does to the destination edge

and option to cut only mesh will be very useful (to not make points in space)

1 Like