Skip to content

New Circle (Brush) Carver Tool #32

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 109 additions & 16 deletions functions/draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from gpu_extras.batch import batch_for_shader
from bpy_extras import view3d_utils


primary_color = (0.48, 0.04, 0.04, 1.0)
secondary_color = (0.28, 0.04, 0.04, 1.0)
magic_number = 1.41

#### ------------------------------ FUNCTIONS ------------------------------ ####
Expand Down Expand Up @@ -48,57 +51,54 @@ def draw_shader(color, alpha, type, coords, size=1, indices=None):
gpu.state.blend_set('NONE')


def carver_overlay(self, context):
def carver_overlay(self, context, shape):
"""Shape (rectangle, circle) overlay for carver tool"""

color = (0.48, 0.04, 0.04, 1.0)
secondary_color = (0.28, 0.04, 0.04, 1.0)

if self.shape == 'CIRCLE':
if shape == 'CIRCLE':
coords, indices, rows, columns = draw_circle(self, self.subdivision, 0)
# coords = coords[1:] # remove_extra_vertex
self.verts = coords
self.duplicates = {**{f"row_{k}": v for k, v in rows.items()}, **{f"column_{k}": v for k, v in columns.items()}}

draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
draw_shader(primary_color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
if not self.rotate:
bounds, __, __ = get_bounding_box_coords(self, coords)
draw_shader(color, 0.6, 'OUTLINE', bounds, size=2)
draw_shader(primary_color, 0.6, 'OUTLINE', bounds, size=2)


elif self.shape == 'BOX':
elif shape == 'BOX':
coords, indices, rows, columns = draw_circle(self, 4, 45)
self.verts = coords
self.duplicates = {**{f"row_{k}": v for k, v in rows.items()}, **{f"column_{k}": v for k, v in columns.items()}}

draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
draw_shader(primary_color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
if (self.rotate == False) and (self.bevel == False):
bounds, __, __ = get_bounding_box_coords(self, coords)
draw_shader(color, 0.6, 'OUTLINE', bounds, size=2)
draw_shader(primary_color, 0.6, 'OUTLINE', bounds, size=2)


elif self.shape == 'POLYLINE':
elif shape == 'POLYLINE':
coords, indices, first_point, rows, columns = draw_polygon(self)
self.verts = list(dict.fromkeys(self.mouse_path))
self.duplicates = {**{f"row_{k}": v for k, v in rows.items()}, **{f"column_{k}": v for k, v in columns.items()}}

draw_shader(color, 1.0, 'LINE_LOOP' if self.closed else 'LINES', coords, size=2)
draw_shader(color, 1.0, 'POINTS', coords, size=5)
draw_shader(primary_color, 1.0, 'LINE_LOOP' if self.closed else 'LINES', coords, size=2)
draw_shader(primary_color, 1.0, 'POINTS', coords, size=5)
if self.closed and len(self.mouse_path) > 2:
# polygon_fill
draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
draw_shader(primary_color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])

if (self.closed and len(coords) > 3) or (self.closed == False and len(coords) > 4):
# circle_around_first_point
draw_shader(color, 0.8, 'OUTLINE', first_point, size=3)
draw_shader(primary_color, 0.8, 'OUTLINE', first_point, size=3)


# Snapping Grid
if self.snap and self.move == False:
mini_grid(self, context)

# ARRAY
array_shader = 'LINE_LOOP' if self.shape == 'POLYLINE' and self.closed == False else 'SOLID'
array_shader = 'LINE_LOOP' if shape == 'POLYLINE' and self.closed == False else 'SOLID'
if self.rows > 1:
for i, duplicate in rows.items():
draw_shader(secondary_color, 0.4, array_shader, duplicate, size=2, indices=indices[:-2])
Expand All @@ -109,6 +109,99 @@ def carver_overlay(self, context):
gpu.state.blend_set('NONE')


def carver_brush(mode, context, obj_matrix=None, location=None, normal=None, xy=None, radius=None):
"""Draws brush circle around the cursor, 2D (view-aligned) or 3D (normal-aligned) based on raycast result"""
# This is a (modified) code from '3D Hair Brush' by VFX Grace (GPL license)

def draw_circle_2d(x, y, segments):
"""Draws circle around the cursor"""

mul = (1.0 / (segments - 1)) * (math.pi * 2)
points_2d = [(math.sin(i * mul) + x, math.cos(i * mul) + y) for i in range(segments)]
points_3d = [(v[0], v[1], 0) for v in points_2d]

indices = []
for idx in range(len(points_3d)):
i1 = idx + 1
i2 = idx + 2 if idx + 2 <= ((360 / int(segments)) * (idx + 1)) else 1
indices.append((0, i1, i2))

return points_2d, points_3d, indices


with gpu.matrix.push_pop():
if mode == '3D':
window = context.window
region = context.region
rv3d = context.region_data

# rotation_matrix
obj_matrix = obj_matrix @ mathutils.Matrix.Translation(location)
z_axis = mathutils.Vector((0, 0, 1))
if abs(normal.dot(z_axis)) < 0.999:
perp_vector = normal.cross(z_axis).normalized()
else:
perp_vector = mathutils.Vector((1, 0, 0))

aligned_y_axis = perp_vector.cross(normal).normalized()
rotation_matrix = mathutils.Matrix((perp_vector, aligned_y_axis, normal)).transposed().to_4x4()


# Prepare Matrix
gpu.state.viewport_set(region.x, region.y, region.width, region.height) # constraint_gpu_viewport_to_active_3d_viewport_(editor)_bounds
gpu.matrix.load_identity() # load_"clean/neutral"_matrix_(no_transformations_applied_yet)
gpu.matrix.push_projection() # store_original_projection
gpu.matrix.load_projection_matrix(rv3d.window_matrix)
gpu.matrix.load_matrix(rv3d.view_matrix)

gpu.matrix.push()
gpu.matrix.multiply_matrix(obj_matrix)
gpu.matrix.multiply_matrix(rotation_matrix)
gpu.matrix.scale_uniform(radius) # scales to screen-size, disabling it makes it act like Scene-unit scale, but needs way to change radius in other way


# calculate_shapes_and_draw_shaders
__, circle_3d, __ = draw_circle_2d(0, 0, 48)
__, rectangle, indices = draw_circle_2d(0, 0, 5)

draw_shader(primary_color, 0.8, 'OUTLINE', circle_3d, size=3)
draw_shader(primary_color, 0.4, 'SOLID', rectangle, size=2, indices=indices)
# draw_shader(secondary_color, 0.8, 'LINES', [(0, 0, 0), (0, 0, -2)], size=2)


# convert_vertices_to_world_space_(to_be_used_for_mesh_creation)
world_vertices = []
for vertex in rectangle:
screen_pos = mathutils.Vector((vertex[0] * region.width, vertex[1] * region.height))
world_pos = view3d_utils.region_2d_to_location_3d(region, rv3d, screen_pos, aligned_y_axis)
world_pos = obj_matrix @ world_pos
world_pos = rotation_matrix @ world_pos
world_pos = world_pos * radius
world_vertices.append(world_pos)


# Reset Matrix
gpu.matrix.pop()
gpu.matrix.pop_projection()
gpu.state.viewport_set(0, 0, window.width, window.height)

return world_vertices, indices


elif mode == '2D':
gpu.matrix.translate(xy)
gpu.matrix.scale_uniform(radius)

# calculate_shapes_and_draw_shaders
circle_2d, __, __ = draw_circle_2d(0, 0, 48)
__, rectangle, indices = draw_circle_2d(0, 0, 5)

draw_shader(primary_color, 1.0, 'OUTLINE', circle_2d, size=2)
draw_shader(primary_color, 0.4, 'SOLID', rectangle, size=2, indices=indices)

return


def draw_polygon(self):
"""Returns polygonal 2d shape in which each cursor click is taken as a new vertice"""

Expand Down
131 changes: 108 additions & 23 deletions functions/select.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import bpy, mathutils
import bpy, mathutils, math
from bpy_extras import view3d_utils
from .draw import get_bounding_box_coords
from .poll import is_linked, is_instanced_data
Expand Down Expand Up @@ -69,33 +69,33 @@ def is_inside_selection(context, obj, rect_min, rect_max):
return not (max_x < rect_min.x or min_x > rect_max.x or max_y < rect_min.y or min_y > rect_max.y)


def selection_fallback(self, context, objects, include_cutters=False):
def selection_fallback(self, context, objects, polyline=False, include_cutters=False):
"""Selects mesh objects that fall inside given 2d rectangle coordinates"""
"""Used to get exactly which objects should be cut and avoid adding and applying unnecessary modifiers"""
"""NOTE: bounding box isn't always returning correct results for objects, but full surface check would be too expensive"""

# convert_2d_rectangle_coordinates_to_world_coordinates
if self.origin == 'EDGE':
if self.shape == 'POLYLINE':
x_values = [point[0] for point in self.mouse_path]
y_values = [point[1] for point in self.mouse_path]
rect_min = mathutils.Vector((min(x_values), min(y_values)))
rect_max = mathutils.Vector((max(x_values), max(y_values)))
else:
rect_min = mathutils.Vector((min(self.mouse_path[0][0], self.mouse_path[1][0]),
min(self.mouse_path[0][1], self.mouse_path[1][1])))
rect_max = mathutils.Vector((max(self.mouse_path[0][0], self.mouse_path[1][0]),
max(self.mouse_path[0][1], self.mouse_path[1][1])))

elif self.origin == 'CENTER':
# ensure_bounding_box_(needed_when_array_is_set_before_original_is_drawn)
if len(self.center_origin) == 0:
get_bounding_box_coords(self, self.verts)

rect_min = mathutils.Vector((min(self.center_origin[0][0], self.center_origin[1][0]),
min(self.center_origin[0][1], self.center_origin[1][1])))
rect_max = mathutils.Vector((max(self.center_origin[0][0], self.center_origin[1][0]),
max(self.center_origin[0][1], self.center_origin[1][1])))
if polyline:
x_values = [point[0] for point in self.mouse_path]
y_values = [point[1] for point in self.mouse_path]
rect_min = mathutils.Vector((min(x_values), min(y_values)))
rect_max = mathutils.Vector((max(x_values), max(y_values)))
else:
if self.origin == 'EDGE':
rect_min = mathutils.Vector((min(self.mouse_path[0][0], self.mouse_path[1][0]),
min(self.mouse_path[0][1], self.mouse_path[1][1])))
rect_max = mathutils.Vector((max(self.mouse_path[0][0], self.mouse_path[1][0]),
max(self.mouse_path[0][1], self.mouse_path[1][1])))

elif self.origin == 'CENTER':
# ensure_bounding_box_(needed_when_array_is_set_before_original_is_drawn)
if len(self.center_origin) == 0:
get_bounding_box_coords(self, self.verts)

rect_min = mathutils.Vector((min(self.center_origin[0][0], self.center_origin[1][0]),
min(self.center_origin[0][1], self.center_origin[1][1])))
rect_max = mathutils.Vector((max(self.center_origin[0][0], self.center_origin[1][0]),
max(self.center_origin[0][1], self.center_origin[1][1])))

# ARRAY
if self.rows > 1:
Expand Down Expand Up @@ -130,3 +130,88 @@ def selection_fallback(self, context, objects, include_cutters=False):
intersecting_objects.append(obj)

return intersecting_objects


def raycast_from_cursor(region, rv3d, obj, xy):
"""Casts rays from cursor position and picks out mesh object (and its normals) underneath it"""
# This function and everything down below is a (modified) code from '3D Hair Brush' by VFX Grace (GPL license)

coords = xy[0] - region.x, xy[1] - region.y
clamp = None
if not rv3d.is_perspective and rv3d.view_perspective != 'CAMERA':
clamp = rv3d.view_distance * 2

# get_the_ray_from_the_viewport_and_mouse
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coords)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coords, clamp=clamp)
ray_target = ray_origin + view_vector

# get_the_ray_relative_to_the_object
obj_matrix = obj.matrix_world.copy()
matrix_inv = obj_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

# Raycast
result, location, normal, index = obj.ray_cast(ray_origin_obj, ray_direction_obj)

return result, location, normal


def calc_unprojected_radius(obj_matrix, location, region, rv3d, pixel_rad):
""""""

world_location = obj_matrix @ location
perspective_matrix = rv3d.perspective_matrix.copy()
z_factor = math.fabs(world_location.dot(perspective_matrix[3].xyz) + perspective_matrix[3][3])
if z_factor < 1e-6:
z_factor = 1.0

dx = 2.0 * pixel_rad * z_factor / region.width
perspective_matrix.invert()
delta = mathutils.Vector((perspective_matrix[0][0] * dx, perspective_matrix[1][0] * dx, perspective_matrix[2][0] * dx))
radius_3d = delta.length

scale = (obj_matrix.to_3x3() @ mathutils.Vector((1, 1, 1)).normalized()).length
if scale == 0:
scale = 1.0

return radius_3d, radius_3d / scale


def calc_projected_radius(obj_matrix, location, region, rv3d, radius):
""""""

def location_3d_to_vector_3d(rv3d, location):
if rv3d.is_perspective:
p2 = rv3d.view_matrix @ mathutils.Vector((*location, 1.0))
p2.xyz *= 2.0
p2 = rv3d.view_matrix.inverted() @ p2
return (location - p2.xyz).normalized()

return rv3d.view_matrix.inverted().col[2].xyz.normalized()

view = location_3d_to_vector_3d(rv3d, location)
non_orthographic = view.copy()

if math.fabs(non_orthographic.x) < 0.1:
non_orthographic.x += 1.0
elif math.fabs(non_orthographic.y) < 0.1:
non_orthographic.y += 1.0
else:
non_orthographic.z += 1.0

ortho = non_orthographic.cross(view).normalized()
offset = location + ortho * radius
p1 = view3d_utils.location_3d_to_region_2d(region, rv3d, location)
p2 = view3d_utils.location_3d_to_region_2d(region, rv3d, offset)

if p1 and p2:
scale = (obj_matrix.to_3x3() @ mathutils.Vector((1, 1, 1)).normalized()).length
if scale == 0:
scale = 1.0

return int((p1 - p2).length * scale)

return 0
Loading