Skip to content
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
44 changes: 44 additions & 0 deletions source/functions/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,50 @@

#### ------------------------------ FUNCTIONS ------------------------------ ####

def setup_grid_3d(matrix, size=10, subdivisions=10) -> tuple[list[Vector], list[Vector]]:
"""Generates the grid of 3D points on the given matrix."""

if subdivisions < 4:
subdivisions = 4

points = []
indices = []

# Calculate the step size between points & number of points per row/column.
step = size / subdivisions
points_per_side = subdivisions + 1

# Start offset (to center the grid).
start = -size / 2.0

for i in range(points_per_side):
for j in range(points_per_side):
local_x = start + (i * step)
local_y = start + (j * step)
point_local = Vector((local_x, local_y, 0.0))

# Transform point to world space using the matrix.
point_world = matrix @ point_local
points.append(point_world)

# Generate indices for GPU batch.
# Horizontal lines (along j axis).
for i in range(1, points_per_side - 1):
for j in range(points_per_side - 1):
index1 = i * points_per_side + j
index2 = i * points_per_side + (j + 1)
indices.append((index1, index2))

# Vertical lines (along i axis) - skip first and last column
for j in range(1, points_per_side - 1):
for i in range(points_per_side - 1):
index1 = i * points_per_side + j
index2 = (i + 1) * points_per_side + j
indices.append((index1, index2))

return points, indices


def distance_from_point_to_segment(point, start, end) -> float:
"""
Calculates the shortest distance between a point and a segment.
Expand Down
3 changes: 3 additions & 0 deletions source/tools/carver_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Mouse,
Workplane,
Cutter,
Grid,
Effects,
)
from .common.ui import (
Expand Down Expand Up @@ -111,6 +112,7 @@ def invoke(self, context, event):
self.workplane = Workplane(*self.calculate_workplane(context))
self.cutter = Cutter(*self.create_cutter(context))
self.effects = Effects().from_invoke(self, context)
self.grid = Grid(None, None)

# cached_variables
"""Important for storing context as it was when operator was invoked (untouched by the modal)."""
Expand Down Expand Up @@ -144,6 +146,7 @@ def modal(self, context, event):
self.event_array(context, event)
self.event_flip(context, event)
self.event_move(context, event)
self.event_grid(context, event)

if event.type in {'MIDDLEMOUSE'}:
return {'PASS_THROUGH'}
Expand Down
8 changes: 6 additions & 2 deletions source/tools/carver_polyline.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Mouse,
Workplane,
Cutter,
Grid,
Effects,
)
from .common.ui import (
Expand Down Expand Up @@ -90,6 +91,7 @@ def invoke(self, context, event):
self.workplane = Workplane(*self.calculate_workplane(context))
self.cutter = Cutter(*self.create_cutter(context))
self.effects = Effects().from_invoke(self, context)
self.grid = Grid(None, None)

# cached_variables
"""Important for storing context as it was when operator was invoked (untouched by the modal)."""
Expand Down Expand Up @@ -117,6 +119,7 @@ def modal(self, context, event):
# Modifier Keys
self.event_array(context, event)
self.event_move(context, event)
self.event_grid(context, event)

if event.type in {'MIDDLEMOUSE'}:
return {'PASS_THROUGH'}
Expand Down Expand Up @@ -311,7 +314,8 @@ def _insert_polyline_point(self):

# Lock the position of the last vert to cursor position at the moment of press.
last_vert = verts[-1]
last_vert.co = Vector((x, y, 0))
last_vert_co = self._snap_to_grid(Vector((x, y, 0)))
last_vert.co = last_vert_co

# Find and remove edge between last vert and the first vert.
if verts.index(last_vert) != 1:
Expand All @@ -325,7 +329,7 @@ def _insert_polyline_point(self):
self.cutter.bm.edges.remove(edge_to_remove)

# Insert new point in bmesh and connect to last one.
new_vert = bm.verts.new(Vector((x, y, 0)))
new_vert = bm.verts.new(last_vert_co)
bm.edges.new([last_vert, new_vert])
verts.append(new_vert)

Expand Down
72 changes: 68 additions & 4 deletions source/tools/common/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
draw_circle_around_point,
)
from ...functions.math import (
setup_grid_3d,
distance_from_point_to_segment,
region_2d_to_plane_3d,
region_2d_to_line_3d,
Expand Down Expand Up @@ -310,6 +311,46 @@ def _remove_move_phase_properties(self):
self.cutter.obj.location = self._stored_cutter_location + offset


def event_grid(self, context, event):
"""Modifier key for toggling the grid and modifying its properties."""

# Set correct phase.
if event.type == 'G' and event.value == 'PRESS':
if self.phase != "DRAW":
return

if not self.use_grid:
self.use_grid = True
else:
self.use_grid = False

if self.use_grid:
if self.grid.points is None:
# Calculate & store the grid.
if self.grid_subdivision_method == 'MANUAL':
size = self.grid_increment * self.grid.subdivision
else:
size = int(context.region_data.view_distance)

self.grid.points, self.grid.indices = setup_grid_3d(self.workplane.matrix,
size=size,
subdivisions=self.grid.subdivision)

# Snap all Polyline points.
if self.shape == 'POLYLINE':
for v in self.cutter.verts:
v.co = self._snap_to_grid(v.co)

# Change grid subdivision level.
"""NOTE: We're settings grid points to `None` to force recalculating."""
if event.type == 'PAGE_UP' and event.value == 'PRESS':
self.grid.subdivision += 2
self.grid.points = None
if event.type == 'PAGE_DOWN' and event.value == 'PRESS':
self.grid.subdivision -= 2
self.grid.points = None


class CarverBase(bpy.types.Operator,
CarverEvents,
CarverPropsOperator,
Expand Down Expand Up @@ -367,6 +408,19 @@ def validate_selection(self, context):
return selected, active


def _snap_to_grid(self, vector: Vector) -> Vector:
"""Snaps the Vector to the closest point on the 3D grid (also Vector)."""

if self.use_grid:
v_co_world = self.workplane.matrix @ vector
closest_point = min(self.grid.points, key=lambda p: (p - v_co_world).length)
new_vector = self.workplane.matrix.inverted() @ closest_point
else:
new_vector = vector

return new_vector


# Core Methods
def calculate_workplane(self, context):
"""
Expand Down Expand Up @@ -468,6 +522,13 @@ def draw_shaders(self, context):
if vertices is not None and indices is not None:
draw_shader('SOLID', (0.48, 0.04, 0.04), 0.4, vertices, indices=indices)

# Draw Grid
if self.use_grid and self.phase == "DRAW":
vertices = self.grid.points
if vertices is not None:
draw_shader('POINTS', (0.8, 0.8, 0.8), 1.0, vertices)
draw_shader('LINES', (0.8, 0.8, 0.8), 0.1, vertices, indices=self.grid.indices)

# Draw Line
if self.phase in ("BEVEL", "ROTATE", "ARRAY"):
current_mouse_pos_3d = region_2d_to_plane_3d(context.region, context.region_data,
Expand Down Expand Up @@ -526,9 +587,11 @@ def update_cutter_shape(self, context):
vert_x, vert_y = corner_signs[i]

if self.origin == 'CENTER':
v.co = Vector((vert_x * size_x - size_x / 2, vert_y * size_y - size_y / 2, 0))
v_co = Vector((vert_x * size_x - size_x / 2, vert_y * size_y - size_y / 2, 0))
elif self.origin == 'EDGE':
v.co = Vector((vert_x * x, vert_y * y, 0))
v_co = Vector((vert_x * x, vert_y * y, 0))

v.co = self._snap_to_grid(v_co)

if self.shape == 'CIRCLE':
angle_step = 2 * math.pi / len(face.verts)
Expand All @@ -545,8 +608,9 @@ def update_cutter_shape(self, context):
v.co = Vector((vert_x, vert_y, 0))

if self.shape == 'POLYLINE':
vert = self.cutter.verts[-1]
vert.co = Vector((x, y, 0))
v = self.cutter.verts[-1]
v_co = Vector((x, y, 0))
v.co = self._snap_to_grid(v_co)

# Update Mesh & bmesh
bm.to_mesh(self.cutter.mesh)
Expand Down
20 changes: 20 additions & 0 deletions source/tools/common/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ class CarverPropsOperator():
default = 'MANUAL',
)

# Grid
use_grid: bpy.props.BoolProperty(
name = "Snapping Grid",
description = "Create point grid in 3D space, aligned to the workplane, that cutter vertices can snap to",
default = False,
)
grid_subdivision_method: bpy.props.EnumProperty(
name = "Subdivision Level",
items = (('ZOOM', "Based on Zoom", "Subdivide snapping grid based on viewport zoom level when initializing"),
('MANUAL', "Manual", "Subdivide snapping grid by specific increments to guarantee precise size")),
default = 'ZOOM',
)
grid_increment: bpy.props.FloatProperty(
name = "Snapping Grid Increment",
description = "Size of the snapping grid increment in scene units (this will be rounded up or down)",
subtype = 'DISTANCE',
min = 0.01,
default = 1.0,
)


class CarverPropsShape():
# SHAPE-properties
Expand Down
9 changes: 9 additions & 0 deletions source/tools/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ def __init__(self, obj, mesh, bm, faces, verts):
self.center = Vector() # Center of the geometry.


class Grid:
"""3D points created on the plane."""

def __init__(self, points, indices):
self.points = points
self.indices = indices
self.subdivision = 16


# Effects
class Effects:

Expand Down
Loading