Skip to content
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

Offset: Avoid depending on objects #400

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
143 changes: 107 additions & 36 deletions operators/offset.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@
from ..stateful_operator.state import state_from_args
from ..utilities.view import refresh
from ..utilities.walker import EntityWalker
from ..utilities.intersect import get_offset_elements, get_intersections
from ..utilities.intersect import (
get_intersections,
get_offset_cb,
get_offset_elements,
get_offset_args,
get_offset_elements_args,
ElementTypes,
)
from ..model.utilities import get_connection_point
from .base_2d import Operator2d
from .utilities import ignore_hover
Expand All @@ -22,7 +29,16 @@

def _get_offset_co(point, normal, distance):
# For start or endpoint: get point translated by distance along normal
return point.co + normal * distance
return point + normal * distance


def _bool_to_signed_int(invert):
return -1 if invert else 1


def _inverted_dist(distance, invert):
sign = _bool_to_signed_int(invert) * _bool_to_signed_int(distance < 0)
return math.copysign(distance, sign)


class View3D_OT_slvs_add_offset(Operator, Operator2d):
Expand Down Expand Up @@ -50,59 +66,113 @@ class View3D_OT_slvs_add_offset(Operator, Operator2d):
),
)

def main(self, context: Context):
sketch = self.sketch
entity = self.entity
distance = self.distance
sse = context.scene.sketcher.entities

ignore_hover(entity)

if is_circle(entity):
c_new = entity.new(context, radius=entity.radius + distance)
def _handle_circle(self, context):
if self.is_circle:
c_new = self.entity.new(context, radius=self.entity.radius + self.distance)
ignore_hover(c_new)

refresh(context)
return True
return False

def init_main(self, context: Context):
ignore_hover(self.entity)

self.is_circle = False
if is_circle(self.entity):
self.is_circle = True
return True

walker = EntityWalker(context.scene, sketch, entity=entity)
# Get connected segments
walker = EntityWalker(context.scene, self.sketch, entity=self.entity)
path = walker.main_path()
is_cyclic = walker.is_cyclic_path(path[0])
self.is_cyclic = walker.is_cyclic_path(path[0])

if path is None:
self.entities, self.directions = path
self.entity_indices = [e.slvs_index for e in self.entities]

self.entity_count = len(self.entities)
self.intersection_count = (
self.entity_count if self.is_cyclic else self.entity_count - 1
)

if not path:
return False

# Store normals of start/endpoint
start, end = walker.get_limitpoints(path)
self.limitpoints = start, end
self.co_start = start.co
self.co_end = end.co
self.nm_start = self.entities[0].normal(position=start.co)
self.nm_end = self.entities[-1].normal(position=end.co)

# Get connection points
self.connection_points = []
for i in range(self.intersection_count):
neighbour_i = (i + 1) % self.entity_count
self.connection_points.append(
get_connection_point(self.entities[i], self.entities[neighbour_i])
)

self.offset_callbacks = [get_offset_cb(e) for e in self.entities]
self.offset_args = [get_offset_args(e) for e in self.entities]
self.centerpoints = [
e.ct.slvs_index if not is_line(e) else None for e in self.entities
]

return True

def main(self, context: Context):
sketch = self.sketch
distance = self.distance
sse = context.scene.sketcher.entities

if self._handle_circle(context):
return True

# Get intersections and create points
points = []
entities, directions = path
self.entities = entities
entities = self.entities
entity_indices = self.entity_indices
directions = self.directions
is_cyclic = self.is_cyclic
entity_count = self.entity_count
intersection_count = self.intersection_count

intersection_count = len(entities) if is_cyclic else len(entities) - 1
point_coords = []
for i in range(intersection_count):
entity = entities[i]
entity_dir = directions[i]
neighbour_i = (i + 1) % len(entities)
neighbour_i = (i + 1) % entity_count
neighbour = entities[neighbour_i]
neighbour_dir = directions[neighbour_i]
point = get_connection_point(entity, neighbour)

def _bool_to_signed_int(invert):
return -1 if invert else 1
point = self.connection_points[i]

def _inverted_dist(invert):
sign = _bool_to_signed_int(invert) * _bool_to_signed_int(distance < 0)
return math.copysign(distance, sign)
# offset_cb_active = self.offset_callbacks[i]
# offset_cb_neighbour = self.offset_callbacks[neighbour_i]
# intersections = sorted(
# get_intersections(
# offset_cb_active(distance),
# offset_cb_neighbour(distance),
# ),
# key=lambda i: (i - point.co).length,
# )

intersections = sorted(
get_intersections(
get_offset_elements(entity, _inverted_dist(entity_dir)),
get_offset_elements(neighbour, _inverted_dist(neighbour_dir)),
get_offset_elements(entity, _inverted_dist(distance, entity_dir)),
get_offset_elements(
neighbour, _inverted_dist(distance, neighbour_dir)
),
),
key=lambda i: (i - point.co).length,
)

for coord in intersections:
sse.add_point_2d(coord, sketch)

if not intersections:
return False

Expand All @@ -116,16 +186,16 @@ def _inverted_dist(invert):
# Add start/endpoint if not cyclic
if not is_cyclic:

start, end = walker.get_limitpoints(path)
start, end = self.limitpoints
start_co = _get_offset_co(
start,
entities[0].normal(position=start.co),
_inverted_dist(directions[0]),
self.co_start,
self.nm_start,
_inverted_dist(distance, directions[0]),
)
end_co = _get_offset_co(
end,
entities[-1].normal(position=end.co),
_inverted_dist(directions[-1]),
self.co_end,
self.nm_end,
_inverted_dist(distance, directions[-1]),
)

points.insert(0, sse.add_point_2d(start_co, sketch, index_reference=True))
Expand All @@ -139,12 +209,13 @@ def _inverted_dist(invert):
for i, entity in enumerate(entities):
direction = directions[i]

i_start = (i - 1 if is_cyclic else i) % len(entities)
i_start = (i - 1 if is_cyclic else i) % entity_count
i_end = (i_start + 1) % len(points)
p1 = points[i_start]
p2 = points[i_end]

use_construction = context.scene.sketcher.use_construction

new_entity = entity.new(
context,
p1=p1,
Expand All @@ -171,7 +242,7 @@ def fini(self, context: Context, succeede: bool):

# Add parallel constraint
# for entity, new_entity in zip(self.entities, self.new_path):
# if not is_line(entity):
# if not is_line(new_entity):
# continue
# constraints.add_parallel(entity, new_entity, sketch=self.sketch)

Expand Down
2 changes: 1 addition & 1 deletion run_tests.sh
Original file line number Diff line number Diff line change
@@ -1 +1 @@
blender --addons CAD_Sketcher --python ./testing/__init__.py -- --log_level=INFO
blender-3.5 --addons CAD_Sketcher --python ./testing/__init__.py -- --log_level=INFO
16 changes: 12 additions & 4 deletions stateful_operator/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,10 +359,10 @@ def invoke(self, context: Context, event: Event):
return self._end(context, succeede)

def run_op(self, context: Context):
if not hasattr(self, "main"):
raise NotImplementedError(
"StatefulOperators need to have a main method defined!"
)
if not self.executed:
if not self.init_main(context):
return False

retval = self.main(context)
self.executed = True
return retval
Expand Down Expand Up @@ -702,3 +702,11 @@ def description(cls, context, _properties):
# Dummy methods
def gather_selection(self, context: Context):
raise NotImplementedError

def init_main(self, context: Context) -> bool:
return True

def main(self, context: Context) -> bool:
return NotImplementedError(
"StatefulOperators need to have a main method defined!"
)
1 change: 1 addition & 0 deletions utilities/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def get_line_intersection(a1, b1, c1, a2, b2, c2) -> Vector:
det = a1 * b2 - a2 * b1
if det == 0:
# Parallel lines
print("lines are parallel")
return Vector((math.inf, math.inf))
else:
x = (b2 * c1 - b1 * c2) / det
Expand Down
60 changes: 60 additions & 0 deletions utilities/intersect.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
intersect_sphere_sphere_2d,
)

from ..model.identifiers import is_line
from .geometry import intersect_line_line_2d, intersect_line_sphere_2d
from ..model.base_entity import SlvsGenericEntity
from .data_handling import to_list
Expand Down Expand Up @@ -46,6 +47,36 @@ def _get_offset_sphere(arc, offset):
return arc.ct.co, arc.radius + offset


# create versions which directly accept the entity's props
def _get_offset_line_props(offset, args):
normal, p1, p2 = args
offset_vec = normal * offset
return (p1 + offset_vec, p2 + offset_vec)


def _get_offset_sphere_props(offset, args):
"""Return sphere_co and sphere_radius of offset sphere"""
ct, radius = args
return ct, radius + offset


def get_offset_args(entity):
"""Returns the entity's arguments that are used to create an offset element"""
if is_line(entity):
return entity.normal().copy(), entity.p1.co.copy(), entity.p2.co.copy()
return entity.ct.co.copy(), entity.radius


def get_offset_elements_args(
t: ElementTypes, offset: float, args: tuple
) -> Tuple[ElementTypes, Callable]:
"""Returns the elements of a new offsetted element"""
func = (
_get_offset_sphere_props if t == ElementTypes.Sphere else _get_offset_line_props
)
return (t, func(offset, args))


# Note: for arcs the radius might increse or decrease depending of the curvature


Expand All @@ -57,6 +88,35 @@ def get_offset_elements(
return (t, func(entity, offset))


def _get_offset_line_cb(entity):
def func(
offset,
normal=entity.normal().copy(),
p1=entity.p1.co.copy(),
p2=entity.p2.co.copy(),
):
offset_vec = normal * offset
return (ElementTypes.Line, (p1 + offset_vec, p2 + offset_vec))

return func


def _get_offset_sphere_cb(entity):
def func(offset, ct=entity.ct.co.copy(), radius=entity.radius):
return (ElementTypes.Sphere, (ct, radius + offset))

return func


def get_offset_cb(entity: SlvsGenericEntity):
"""Create a callback to generate an offset element from a given offset"""

t = ElementTypes.Line if entity.type == "SlvsLine2D" else ElementTypes.Sphere
func = _get_offset_sphere_cb if t == ElementTypes.Sphere else _get_offset_line_cb

return func(entity)


def get_intersections(*element_list, segment=False):
"""Find all intersections between all combinations of elements, (type, element)"""
intersections = []
Expand Down