Skip to content

Entities

Entities are the basic elements which are used to draw geometry in CAD Sketcher. They differ from regular blender mesh or curve elements which means native blender tools aren't able to interact with it as long as they aren't converted. See the chapter integration for further details on how to process extension specific geometry.

Entities are defined by a set of parameters and pointers to other entities which are editable at any point in time. This allows non-destructive workflows and also ensures that geometry is resolution independent. A curve will always follow a given radius no matter how it's transformed. Entities can be created with the various Workspacetools.

Active

An entity is considered to be active when the sketch it belongs to is set as the active sketch or, for 3D entities, when no sketch is active.

Visibility

Entities can be hidden. Access the setting from the entity's context menu or from the entity browser.

Construction

Entities have a construction parameter which can be set via the entity's context menu. If it's set to true the entity will be ignored when converting the geometry however it's still used to solve the geometric system. It's generally good practice to mark entities as construction if they're not part of the final geometry.

Fixed

Entities can be fixed via the entity's context menu. A fixed entity won't have any degrees of freedom and therefor cannot be adjusted by the solver. It's good practice to base geometry on a fixed origin point.

⚠️Warning: While this currently applies to all entities it's intended to be used with points only.

Types

There are different types of entities, some of them apply in 2 dimensional space which requires a sketch as a parameter.

Entity types follow the implementation of solvespace.

Only 2D entities can be converted later, check the chapter integration for details.

Bases: Point3D, PropertyGroup

Representation of a point in 3D Space.

Parameters:

Name Type Description Default
location FloatVectorProperty

Point's location in the form (x, y, z)

required
Source code in model/point_3d.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class SlvsPoint3D(Point3D, PropertyGroup):
    """Representation of a point in 3D Space.

    Arguments:
        location (FloatVectorProperty): Point's location in the form (x, y, z)
    """

    location: FloatVectorProperty(
        name="Location",
        description="The location of the point",
        subtype="XYZ",
        unit="LENGTH",
        update=SlvsGenericEntity.tag_update,
    )
    props = ("location",)

    def draw_props(self, layout):
        sub = super().draw_props(layout)
        sub.prop(self, "location")
        return sub

Bases: SlvsGenericEntity, PropertyGroup

Representation of a line in 3D Space.

Parameters:

Name Type Description Default
p1 SlvsPoint3D

Line's startpoint

required
p2 SlvsPoint3D

Line's endpoint

required
Source code in model/line_3d.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class SlvsLine3D(SlvsGenericEntity, PropertyGroup):
    """Representation of a line in 3D Space.

    Arguments:
        p1 (SlvsPoint3D): Line's startpoint
        p2 (SlvsPoint3D): Line's endpoint
    """

    @classmethod
    def is_path(cls):
        return True

    @classmethod
    def is_line(cls):
        return True

    @classmethod
    def is_segment(cls):
        return True

    def dependencies(self) -> List[SlvsGenericEntity]:
        return [self.p1, self.p2]

    def is_dashed(self):
        return self.construction

    def update(self):
        if bpy.app.background:
            return

        p1, p2 = self.p1.location, self.p2.location
        coords = (p1, p2)

        kwargs = {"pos": coords}
        self._batch = batch_for_shader(self._shader, "LINES", kwargs)

        self.is_dirty = False

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        handle = solvesys.addLineSegment(self.p1.py_data, self.p2.py_data, group=group)
        self.py_data = handle

    def closest_picking_point(self, origin, view_vector):
        """Returns the point on this entity which is closest to the picking ray"""
        p1 = self.p1.location
        d1 = self.p2.location - p1  # normalize?
        return nearest_point_line_line(p1, d1, origin, view_vector)

    def placement(self):
        return (self.p1.location + self.p2.location) / 2

    def orientation(self):
        return (self.p2.location - self.p1.location).normalized()

    @property
    def length(self):
        return (self.p2.location - self.p1.location).length

closest_picking_point(origin, view_vector)

Returns the point on this entity which is closest to the picking ray

Source code in model/line_3d.py
59
60
61
62
63
def closest_picking_point(self, origin, view_vector):
    """Returns the point on this entity which is closest to the picking ray"""
    p1 = self.p1.location
    d1 = self.p2.location - p1  # normalize?
    return nearest_point_line_line(p1, d1, origin, view_vector)

Bases: Normal3D, PropertyGroup

Representation of a normal in 3D Space which is used to store a direction.

This entity isn't currently exposed to the user and gets created implicitly when needed.

Parameters:

Name Type Description Default
orientation Quaternion

A quaternion which describes the rotation

required
Source code in model/normal_3d.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class SlvsNormal3D(Normal3D, PropertyGroup):
    """Representation of a normal in 3D Space which is used to
    store a direction.

    This entity isn't currently exposed to the user and gets created
    implicitly when needed.

    Arguments:
        orientation (Quaternion): A quaternion which describes the rotation
    """

    def get_orientation(self):
        return getattr(self, "orientation").to_euler()

    def set_orientation(self, value):
        self["orientation"] = Euler(value).to_quaternion()

    orientation: FloatVectorProperty(
        name="Orientation",
        description="Quaternion which describes the orientation of the normal",
        subtype="QUATERNION",
        size=4,
        update=SlvsGenericEntity.tag_update,
    )

    ui_orientation: FloatVectorProperty(
        name="Orientation",
        subtype="EULER",
        size=3,
        get=get_orientation,
        set=set_orientation,
        options={"SKIP_SAVE"},
        update=SlvsGenericEntity.tag_update,
    )
    props = ("ui_orientation",)

    def draw_props(self, layout):
        sub = super().draw_props(layout)
        sub.prop(self, "ui_orientation")
        return sub

Bases: SlvsGenericEntity, PropertyGroup

Representation of a plane which is defined by an origin point and a normal. Workplanes are used to define the position of 2D entities which only store the coordinates on the plane.

Parameters:

Name Type Description Default
p1 SlvsPoint3D

Origin Point of the Plane

required
nm SlvsNormal3D

Normal which defines the orientation

required
Source code in model/workplane.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
class SlvsWorkplane(SlvsGenericEntity, PropertyGroup):
    """Representation of a plane which is defined by an origin point
    and a normal. Workplanes are used to define the position of 2D entities
    which only store the coordinates on the plane.

    Arguments:
        p1 (SlvsPoint3D): Origin Point of the Plane
        nm (SlvsNormal3D): Normal which defines the orientation
    """

    size = 0.4

    def dependencies(self) -> List[SlvsGenericEntity]:
        return [self.p1, self.nm]

    @property
    def size(self):
        return preferences.get_prefs().workplane_size

    def update(self):
        if bpy.app.background:
            return

        p1, nm = self.p1, self.nm

        coords = draw_rect_2d(0, 0, self.size, self.size)
        coords = [(Vector(co))[:] for co in coords]

        indices = ((0, 1), (1, 2), (2, 3), (3, 0))
        self._batch = batch_for_shader(
            self._shader, "LINES", {"pos": coords}, indices=indices
        )
        self.is_dirty = False

    # NOTE: probably better to avoid overwriting draw func..
    def draw(self, context):
        if not self.is_visible(context):
            return

        with gpu.matrix.push_pop():
            scale = context.region_data.view_distance
            gpu.matrix.multiply_matrix(self.matrix_basis)
            gpu.matrix.scale(Vector((scale, scale, scale)))

            col = self.color(context)
            # Let parent draw outline
            super().draw(context)

            # Additionally draw a face
            col_surface = col[:-1] + (0.2,)

            shader = Shaders.uniform_color_3d()
            shader.bind()
            gpu.state.blend_set("ALPHA")

            shader.uniform_float("color", col_surface)

            coords = draw_rect_2d(0, 0, self.size, self.size)
            coords = [Vector(co)[:] for co in coords]
            indices = ((0, 1, 2), (0, 2, 3))
            batch = batch_for_shader(shader, "TRIS", {"pos": coords}, indices=indices)
            batch.draw(shader)

        self.restore_opengl_defaults()

    def draw_id(self, context):
        with gpu.matrix.push_pop():
            scale = context.region_data.view_distance
            gpu.matrix.multiply_matrix(self.matrix_basis)
            gpu.matrix.scale(Vector((scale, scale, scale)))
            super().draw_id(context)

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        handle = solvesys.addWorkplane(self.p1.py_data, self.nm.py_data, group=group)
        self.py_data = handle

    @property
    def matrix_basis(self):
        mat_rot = self.nm.orientation.to_matrix().to_4x4()
        return Matrix.Translation(self.p1.location) @ mat_rot

    @property
    def normal(self):
        v = global_data.Z_AXIS.copy()
        quat = self.nm.orientation
        v.rotate(quat)
        return v

    def draw_props(self, layout):
        # Display the normals props as they're not drawn in the viewport
        sub = self.nm.draw_props(layout)
        sub.operator(Operators.AlignWorkplaneCursor).index = self.slvs_index
        return sub

Bases: SlvsGenericEntity, PropertyGroup

A sketch groups 2 dimensional entities together and is used to later convert geometry to native blender types.

Entities that belong to a sketch can only be edited as long as the sketch is active.

Parameters:

Name Type Description Default
wp SlvsWorkplane

The base workplane of the sketch

required
Source code in model/sketch.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
class SlvsSketch(SlvsGenericEntity, PropertyGroup):
    """A sketch groups 2 dimensional entities together and is used to later
    convert geometry to native blender types.

    Entities that belong to a sketch can only be edited as long as the sketch is active.

    Arguments:
        wp (SlvsWorkplane): The base workplane of the sketch
    """

    def hide_sketch(self, context):
        if self.convert_type != "NONE":
            self.visible = False

    convert_type: EnumProperty(
        name="Convert Type",
        items=convert_items,
        description="Define how the sketch should be converted in order to be usable in native blender",
        update=hide_sketch,
    )
    fill_shape: BoolProperty(
        name="Fill Shape",
        description="Fill the resulting shape if it's closed",
        default=True,
    )
    solver_state: EnumProperty(
        name="Solver Status", items=global_data.solver_state_items
    )
    dof: IntProperty(name="Degrees of Freedom", max=6)
    target_curve: PointerProperty(type=bpy.types.Curve)
    target_curve_object: PointerProperty(type=bpy.types.Object)
    target_mesh: PointerProperty(type=bpy.types.Mesh)
    target_object: PointerProperty(type=bpy.types.Object)
    curve_resolution: IntProperty(
        name="Mesh Curve Resolution", default=12, min=1, soft_max=25
    )

    def dependencies(self) -> List[SlvsGenericEntity]:
        return [
            self.wp,
        ]

    def sketch_entities(self, context):
        for e in context.scene.sketcher.entities.all:
            if not hasattr(e, "sketch"):
                continue
            if e.sketch != self:
                continue
            yield e

    def update(self):
        self.is_dirty = False

    def draw(self, context):
        pass

    def draw_id(self, context):
        pass

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        pass

    def remove_objects(self):
        for ob in (self.target_object, self.target_curve_object):
            if not ob:
                continue
            bpy.data.objects.remove(ob)

    def is_visible(self, context):
        if context.scene.sketcher.active_sketch_i == self.slvs_index:
            return True
        return self.visible

    def get_solver_state(self):
        return bpyEnum(global_data.solver_state_items, identifier=self.solver_state)

    def solve(self, context):
        return solve_system(context, sketch=self)

    @classmethod
    def is_sketch(cls):
        return True

Bases: Point2D, PropertyGroup

Representation of a point in 2D space.

Parameters:

Name Type Description Default
co FloatVectorProperty

The coordinates of the point on the worpkplane in the form (U, V)

required
sketch SlvsSketch

The sketch this entity belongs to

required
Source code in model/point_2d.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
class SlvsPoint2D(Point2D, PropertyGroup):
    """Representation of a point in 2D space.

    Arguments:
        co (FloatVectorProperty): The coordinates of the point on the worpkplane in the form (U, V)
        sketch (SlvsSketch): The sketch this entity belongs to
    """

    co: FloatVectorProperty(
        name="Coordinates",
        description="The coordinates of the point on its sketch",
        subtype="XYZ",
        size=2,
        unit="LENGTH",
        update=SlvsGenericEntity.tag_update,
    )

    def dependencies(self) -> List[SlvsGenericEntity]:
        return [
            self.sketch,
        ]

    def tweak(self, solvesys, pos, group):
        wrkpln = self.sketch.wp
        u, v, _ = wrkpln.matrix_basis.inverted() @ pos

        self.create_slvs_data(solvesys, group=group)

        # NOTE: When simply initializing the point on the tweaking positions
        # the solver fails regularly, addWhereDragged fixes a point and might
        # overconstrain a system. When not using addWhereDragged the tweak point
        # might just jump to the tweaked geometry. Bypass this by creating a line
        # perpendicular to move vector and constrain that.

        orig_pos = self.co
        tweak_pos = Vector((u, v))
        tweak_vec = tweak_pos - orig_pos
        perpendicular_vec = Vector((tweak_vec[1], -tweak_vec[0]))

        params = [solvesys.addParamV(val, group) for val in (u, v)]
        startpoint = solvesys.addPoint2d(wrkpln.py_data, *params, group=group)

        p2 = tweak_pos + perpendicular_vec
        params = [solvesys.addParamV(val, group) for val in (p2.x, p2.y)]
        endpoint = solvesys.addPoint2d(wrkpln.py_data, *params, group=group)

        edge = solvesys.addLineSegment(startpoint, endpoint, group=group)
        make_coincident(
            solvesys, self.py_data, edge, wrkpln.py_data, group, entity_type=SlvsLine2D
        )

    def draw_props(self, layout):
        sub = super().draw_props(layout)
        sub.prop(self, "co")
        return sub

Bases: Entity2D, PropertyGroup

Representation of a line in 2D space. Connects p1 and p2 and lies on the sketche's workplane.

Parameters:

Name Type Description Default
p1 SlvsPoint2D

Line's startpoint

required
p2 SlvsPoint2D

Line's endpoint

required
sketch SlvsSketch

The sketch this entity belongs to

required
Source code in model/line_2d.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
class SlvsLine2D(Entity2D, PropertyGroup):
    """Representation of a line in 2D space. Connects p1 and p2 and lies on the
    sketche's workplane.

    Arguments:
        p1 (SlvsPoint2D): Line's startpoint
        p2 (SlvsPoint2D): Line's endpoint
        sketch (SlvsSketch): The sketch this entity belongs to
    """

    @classmethod
    def is_path(cls):
        return True

    @classmethod
    def is_line(cls):
        return True

    @classmethod
    def is_segment(cls):
        return True

    def dependencies(self) -> List[SlvsGenericEntity]:
        return [self.p1, self.p2, self.sketch]

    def is_dashed(self):
        return self.construction

    def update(self):
        if bpy.app.background:
            return

        p1, p2 = self.p1.location, self.p2.location
        coords = (p1, p2)

        kwargs = {"pos": coords}
        self._batch = batch_for_shader(self._shader, "LINES", kwargs)
        self.is_dirty = False

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        handle = solvesys.addLineSegment(self.p1.py_data, self.p2.py_data, group=group)
        self.py_data = handle

    def closest_picking_point(self, origin, view_vector):
        """Returns the point on this entity which is closest to the picking ray"""
        # NOTE: for 2d entities it could be enough precise to simply take the intersection point with the workplane
        p1 = self.p1.location
        d1 = self.p2.location - p1  # normalize?
        return nearest_point_line_line(p1, d1, origin, view_vector)

    def project_point(self, coords):
        """Projects a point onto the line"""
        dir_vec = self.direction_vec()
        p1 = self.p1.co

        local_co = coords - p1
        return local_co.project(dir_vec) + p1

    def placement(self):
        return (self.p1.location + self.p2.location) / 2

    def connection_points(self, direction: bool = False):
        points = [self.p1, self.p2]
        if direction:
            return list(reversed(points))
        return points

    def direction(self, point, is_endpoint=False):
        """Returns the direction of the line, true if inverted"""
        if is_endpoint:
            return point == self.p1
        else:
            return point == self.p2

    def connection_angle(self, other, **kwargs):
        """Returns the angle at the connection point between the two entities
        or None if they're not connected or not in 2d space.

        `kwargs` key values are propagated to other `get_connection_point` functions
        """

        if self.is_3d() or other.is_3d():
            return None

        if not all([e.is_line() for e in (self, other)]):
            return other.connection_angle(self, **kwargs)

        point = get_connection_point(
            self,
            other,
        )
        if not point:
            return None

        dir1 = (
            self.direction_vec()
            if self.direction(point)
            else (self.direction_vec() * (-1))
        )
        dir2 = (
            other.direction_vec()
            if other.direction(point)
            else (other.direction_vec() * (-1))
        )
        return dir1.angle_signed(dir2)

    def to_bezier(
        self, spline, startpoint, endpoint, invert_direction, set_startpoint=False
    ):
        locations = [self.p1.co.to_3d(), self.p2.co.to_3d()]
        if invert_direction:
            locations.reverse()

        if set_startpoint:
            startpoint.co = locations[0]
        endpoint.co = locations[1]

        startpoint.handle_right = locations[0]
        endpoint.handle_left = locations[1]

        return endpoint

    def midpoint(self):
        return (self.p1.co + self.p2.co) / 2

    def orientation(self):
        """Return the orientation of the line in 3d space"""
        return (self.p2.location - self.p1.location).normalized()

    def direction_vec(self):
        return (self.p2.co - self.p1.co).normalized()

    def normal(self, position=None):
        """Returns vector perpendicular to line, position is ignored"""
        mat_rot = Matrix.Rotation(-math.pi / 2, 2, "Z")
        nm = self.direction_vec()
        nm.rotate(mat_rot)
        return nm

    @property
    def length(self):
        return (self.p2.co - self.p1.co).length

    def overlaps_endpoint(self, co):
        precision = 5
        co_rounded = round_v(co, ndigits=precision)
        if any(
            [
                co_rounded == round_v(v, ndigits=precision)
                for v in (self.p1.co, self.p2.co)
            ]
        ):
            return True
        return False

    def intersect(
        self, other: SlvsGenericEntity, extended: bool = False
    ) -> Tuple[Vector]:
        # NOTE: There can be multiple intersections when intersecting with one or more curves
        def parse_retval(value):
            if not value:
                return ()
            if self.overlaps_endpoint(value) or other.overlaps_endpoint(value):
                return ()
            return (value,)

        if other.is_line():
            if extended:
                wp = self.sketch.wp
                pos = intersect_line_line(
                    self.p1.location,
                    self.p2.location,
                    other.p1.location,
                    other.p2.location,
                )
                return parse_retval((wp.matrix_basis @ pos[0])[:-1])
            return parse_retval(
                intersect_line_line_2d(self.p1.co, self.p2.co, other.p1.co, other.p2.co)
            )
        return other.intersect(self)

    def replace(self, context, p1, p2, use_self=False):
        # Replace entity by a similar entity with the connection points p1, and p2
        # This is used for trimming, points are expected to lie somewhere on the existing entity
        if use_self:
            self.p1 = p1
            self.p2 = p2
            return self

        sse = context.scene.sketcher.entities
        sketch = context.scene.sketcher.active_sketch
        line = sse.add_line_2d(
            p1,
            p2,
            sketch,
        )
        line.construction = self.construction
        return line

    def distance_along_segment(self, p1, p2):
        start, end = self.p1.co, self.p2.co
        len_1 = (p1 - end).length
        len_2 = (p2 - start).length

        threshold = 0.0000001
        retval = (len_1 + len_2) % (self.length + threshold)

        return retval

    def replace_point(self, old, new):
        for ptr in ("p1", "p2"):
            if old != getattr(self, ptr):
                continue
            setattr(self, ptr, new)
            break

    def get_offset_props(self, offset: float, direction: bool = False):
        normal = self.normal()

        if direction:
            normal *= -1

        offset_vec = normal * offset
        return (self.p1.co + offset_vec, self.p2.co + offset_vec)

    def new(self, context: Context, **kwargs) -> SlvsGenericEntity:
        kwargs.setdefault("p1", self.p1)
        kwargs.setdefault("p2", self.p2)
        kwargs.setdefault("sketch", self.sketch)
        kwargs.setdefault("construction", self.construction)
        return context.scene.sketcher.entities.add_line_2d(**kwargs)

closest_picking_point(origin, view_vector)

Returns the point on this entity which is closest to the picking ray

Source code in model/line_2d.py
65
66
67
68
69
70
def closest_picking_point(self, origin, view_vector):
    """Returns the point on this entity which is closest to the picking ray"""
    # NOTE: for 2d entities it could be enough precise to simply take the intersection point with the workplane
    p1 = self.p1.location
    d1 = self.p2.location - p1  # normalize?
    return nearest_point_line_line(p1, d1, origin, view_vector)

connection_angle(other, **kwargs)

Returns the angle at the connection point between the two entities or None if they're not connected or not in 2d space.

kwargs key values are propagated to other get_connection_point functions

Source code in model/line_2d.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def connection_angle(self, other, **kwargs):
    """Returns the angle at the connection point between the two entities
    or None if they're not connected or not in 2d space.

    `kwargs` key values are propagated to other `get_connection_point` functions
    """

    if self.is_3d() or other.is_3d():
        return None

    if not all([e.is_line() for e in (self, other)]):
        return other.connection_angle(self, **kwargs)

    point = get_connection_point(
        self,
        other,
    )
    if not point:
        return None

    dir1 = (
        self.direction_vec()
        if self.direction(point)
        else (self.direction_vec() * (-1))
    )
    dir2 = (
        other.direction_vec()
        if other.direction(point)
        else (other.direction_vec() * (-1))
    )
    return dir1.angle_signed(dir2)

direction(point, is_endpoint=False)

Returns the direction of the line, true if inverted

Source code in model/line_2d.py
89
90
91
92
93
94
def direction(self, point, is_endpoint=False):
    """Returns the direction of the line, true if inverted"""
    if is_endpoint:
        return point == self.p1
    else:
        return point == self.p2

normal(position=None)

Returns vector perpendicular to line, position is ignored

Source code in model/line_2d.py
154
155
156
157
158
159
def normal(self, position=None):
    """Returns vector perpendicular to line, position is ignored"""
    mat_rot = Matrix.Rotation(-math.pi / 2, 2, "Z")
    nm = self.direction_vec()
    nm.rotate(mat_rot)
    return nm

orientation()

Return the orientation of the line in 3d space

Source code in model/line_2d.py
147
148
149
def orientation(self):
    """Return the orientation of the line in 3d space"""
    return (self.p2.location - self.p1.location).normalized()

project_point(coords)

Projects a point onto the line

Source code in model/line_2d.py
72
73
74
75
76
77
78
def project_point(self, coords):
    """Projects a point onto the line"""
    dir_vec = self.direction_vec()
    p1 = self.p1.co

    local_co = coords - p1
    return local_co.project(dir_vec) + p1

Bases: Entity2D, PropertyGroup

Representation of an arc in 2D space around the centerpoint ct. Connects p2 to p3 or (vice-versa if the option invert_direction is true) with a circle segment that is resolution independent. The arc lies on the sketche's workplane.

Parameters:

Name Type Description Default
p1 SlvsPoint2D

Arc's centerpoint

required
p2 SlvsPoint2D

Arc's startpoint

required
p2 SlvsPoint2D

Arc's endpoint

required
nm SlvsNormal3D

Orientation

required
sketch SlvsSketch

The sketch this entity belongs to

required
Source code in model/arc.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
class SlvsArc(Entity2D, PropertyGroup):
    """Representation of an arc in 2D space around the centerpoint ct. Connects
    p2 to p3 or (vice-versa if the option invert_direction is true) with a
    circle segment that is resolution independent. The arc lies on the sketche's workplane.

    Arguments:
        p1 (SlvsPoint2D): Arc's centerpoint
        p2 (SlvsPoint2D): Arc's startpoint
        p2 (SlvsPoint2D): Arc's endpoint
        nm (SlvsNormal3D): Orientation
        sketch (SlvsSketch): The sketch this entity belongs to
    """

    invert_direction: BoolProperty(
        name="Invert direction",
        description="Connect the points in the inverted order",
        update=tag_update,
    )

    @classmethod
    def is_path(cls):
        return True

    @classmethod
    def is_curve(cls):
        return True

    @classmethod
    def is_segment(cls):
        return True

    @property
    def start(self):
        return self.p2 if self.invert_direction else self.p1

    @property
    def end(self):
        return self.p1 if self.invert_direction else self.p2

    def dependencies(self) -> List[SlvsGenericEntity]:
        return [self.nm, self.ct, self.start, self.end, self.sketch]

    def is_dashed(self):
        return self.construction

    def update(self):
        if bpy.app.background:
            return

        ct = self.ct.co
        p1 = self.start.co - ct
        p2 = self.end.co - ct

        radius = p1.length

        coords = []
        if radius and p2.length:
            offset = p1.angle_signed(Vector((1, 0)))
            angle = range_2pi(p2.angle_signed(p1))

            # TODO: resolution should depend on segment length?!
            segments = round(CURVE_RESOLUTION * (angle / FULL_TURN))

            coords = coords_arc_2d(0, 0, radius, segments, angle=angle, offset=offset)

            mat_local = Matrix.Translation(self.ct.co.to_3d())
            mat = self.wp.matrix_basis @ mat_local
            coords = [(mat @ Vector((*co, 0)))[:] for co in coords]

        kwargs = {"pos": coords}
        self._batch = batch_for_shader(self._shader, "LINE_STRIP", kwargs)
        self.is_dirty = False

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        handle = solvesys.addArcOfCircle(
            self.wp.py_data,
            self.ct.py_data,
            self.start.py_data,
            self.end.py_data,
            group=group,
        )
        self.py_data = handle

    @property
    def radius(self):
        return (self.ct.co - self.start.co).length

    @property
    def angle(self):
        """Returns an angle in radians from zero to 2*PI"""
        center = self.ct.co
        start, end = self.start.co - center, self.end.co - center
        return _get_angle(start, end)

    @property
    def start_angle(self):
        center, start = self.ct.co, self.start.co
        return math.atan2((start - center)[1], (start - center)[0])

    def normal(self, position: Vector = None):
        """Return the normal vector at a given position"""

        normal = position - self.ct.co
        return normal.normalized()

    def placement(self):
        coords = self.ct.co + pol2cart(self.radius, self.start_angle + self.angle / 2)

        return self.wp.matrix_basis @ coords.to_3d()

    def connection_points(self, direction: bool = False):
        points = [self.start, self.end]
        if direction:
            return list(reversed(points))
        return points

    def direction(self, point, is_endpoint=False):
        """Returns the direction of the arc, true if inverted"""
        if is_endpoint:
            return point == self.start
        return point == self.end

    @staticmethod
    def _direction(start, end, center):
        pass

    def bezier_segment_count(self):
        max_angle = QUARTER_TURN
        return math.ceil(self.angle / max_angle)

    def bezier_point_count(self):
        return self.bezier_segment_count() + 1

    def point_on_curve(self, angle, relative=True):
        start_angle = self.start_angle if relative else 0
        return pol2cart(self.radius, start_angle + angle) + self.ct.co

    def project_point(self, coords):
        """Projects a point onto the arc"""
        local_co = coords - self.ct.co
        angle = range_2pi(math.atan2(local_co[1], local_co[0]))
        return self.point_on_curve(angle, relative=False)

    def connection_angle(self, other, connection_point=None, **kwargs):
        """Returns the angle at the connection point between the two entities
        or None if they're either not connected or not in 2d space

        You may use `connection_point` in order to remove ambiguity in case
        multiple intersections point exist with other entity.

        `kwargs` key values are propagated to other `get_connection_point` functions
        """

        point = connection_point or get_connection_point(self, other)

        if not point:
            return None
        if self.is_3d() or other.is_3d():
            return None

        def _get_tangent(arc, point):
            local_co = point.co - arc.ct.co
            angle = range_2pi(math.atan2(local_co.y, local_co.x))
            mat_rot = Matrix.Rotation(angle, 2, "Z")
            tangent = Vector((0, 1))
            tangent.rotate(mat_rot)
            invert = arc.direction(point)
            if invert:
                tangent *= -1
            return tangent

        # Get directions
        directions = []
        for entity in (self, other):
            if entity.is_curve():
                directions.append(_get_tangent(entity, point))
            else:
                directions.append(
                    entity.direction_vec()
                    if entity.direction(point)
                    else entity.direction_vec() * (-1)
                )

        dir1, dir2 = directions
        return dir1.angle_signed(dir2)

    def to_bezier(
        self,
        spline,
        startpoint,
        endpoint,
        invert_direction,
        set_startpoint=False,
        midpoints=[],
    ):
        # Get midpoint positions
        segment_count = len(midpoints) + 1
        curve_angle = self.angle
        radius, center, start = self.radius, self.ct.co, self.start.co

        midpoint_positions = get_bezier_curve_midpoint_positions(
            self, segment_count, midpoints, curve_angle
        )

        angle = curve_angle / segment_count

        locations = [self.start.co, *midpoint_positions, self.end.co]
        bezier_points = [startpoint, *midpoints, endpoint]

        if invert_direction:
            locations.reverse()

        if set_startpoint:
            startpoint.co = locations[0].to_3d()

        n = FULL_TURN / angle if angle != 0.0 else 0
        q = (4 / 3) * math.tan(HALF_TURN / (2 * n))
        base_offset = Vector((radius, q * radius))

        create_bezier_curve(
            segment_count,
            bezier_points,
            locations,
            center,
            base_offset,
            invert=invert_direction,
        )

        return endpoint

    def draw_props(self, layout):
        sub = super().draw_props(layout)
        sub.prop(self, "invert_direction")
        return sub

    def is_inside(self, coords):
        # Checks if a position is inside the arcs angle range
        ct = self.ct.co
        p = coords - ct
        p1 = self.start.co - ct
        p2 = self.end.co - ct

        x_axis = Vector((1, 0))

        # angle_signed interprets clockwise as positive, so invert..
        a1 = range_2pi(p.angle_signed(p1))
        a2 = range_2pi(p2.angle_signed(p))

        angle = self.angle

        if not p.length or not p1.length or not p2.length:
            return False

        if a1 < angle > a2:
            return True
        return False

    def overlaps_endpoint(self, co):
        precision = 5
        co_rounded = round_v(co, ndigits=precision)
        if any(
            [
                co_rounded == round_v(v, ndigits=precision)
                for v in (self.p1.co, self.p2.co)
            ]
        ):
            return True
        return False

    def intersect(self, other):
        def parse_retval(retval):
            # Intersect might return None, (value, value) or (value, None)
            values = []
            if hasattr(retval, "__len__"):
                for val in retval:
                    if val is None:
                        continue
                    if not self.is_inside(val):
                        continue
                    if isinstance(other, SlvsArc) and not other.is_inside(val):
                        continue
                    if self.overlaps_endpoint(val) or other.overlaps_endpoint(val):
                        continue

                    values.append(val)
            elif retval is not None:
                if self.overlaps_endpoint(retval) or other.overlaps_endpoint(retval):
                    return ()
                values.append(retval)

            return tuple(values)

        if other.is_line():
            return parse_retval(
                intersect_line_sphere_2d(
                    other.p1.co, other.p2.co, self.ct.co, self.radius
                )
            )
        elif other.is_curve():
            return parse_retval(
                intersect_sphere_sphere_2d(
                    self.ct.co, self.radius, other.ct.co, other.radius
                )
            )

    def distance_along_segment(self, p1, p2):
        ct = self.ct.co
        start, end = self.start.co - ct, self.end.co - ct
        points = (p1, p2) if self.invert_direction else (p2, p1)

        len_1 = range_2pi(end.angle_signed(points[1] - ct))
        len_2 = range_2pi((points[0] - ct).angle_signed(start))

        threshold = 0.000001
        retval = (len_1 + len_2) % (self.angle + threshold)

        return retval

    def replace(self, context, p1, p2, use_self=False):
        if use_self:
            self.p1 = p1
            self.p2 = p2
            return self

        sketch = context.scene.sketcher.active_sketch
        arc = context.scene.sketcher.entities.add_arc(
            sketch.wp.nm, self.ct, p1, p2, sketch
        )
        arc.construction = self.construction
        arc.invert_direction = self.invert_direction
        return arc

    def replace_point(self, old, new):
        for ptr in ("ct", "p1", "p2"):
            if old != getattr(self, ptr):
                continue
            setattr(self, ptr, new)
            break

    def new(self, context: Context, **kwargs) -> SlvsGenericEntity:
        kwargs.setdefault("p1", self.p1)
        kwargs.setdefault("p2", self.p2)
        kwargs.setdefault("sketch", self.sketch)
        kwargs.setdefault("nm", self.nm)
        kwargs.setdefault("ct", self.ct)
        kwargs.setdefault("invert", self.invert_direction)
        kwargs.setdefault("construction", self.construction)
        return context.scene.sketcher.entities.add_arc(**kwargs)

angle property

Returns an angle in radians from zero to 2*PI

connection_angle(other, connection_point=None, **kwargs)

Returns the angle at the connection point between the two entities or None if they're either not connected or not in 2d space

You may use connection_point in order to remove ambiguity in case multiple intersections point exist with other entity.

kwargs key values are propagated to other get_connection_point functions

Source code in model/arc.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def connection_angle(self, other, connection_point=None, **kwargs):
    """Returns the angle at the connection point between the two entities
    or None if they're either not connected or not in 2d space

    You may use `connection_point` in order to remove ambiguity in case
    multiple intersections point exist with other entity.

    `kwargs` key values are propagated to other `get_connection_point` functions
    """

    point = connection_point or get_connection_point(self, other)

    if not point:
        return None
    if self.is_3d() or other.is_3d():
        return None

    def _get_tangent(arc, point):
        local_co = point.co - arc.ct.co
        angle = range_2pi(math.atan2(local_co.y, local_co.x))
        mat_rot = Matrix.Rotation(angle, 2, "Z")
        tangent = Vector((0, 1))
        tangent.rotate(mat_rot)
        invert = arc.direction(point)
        if invert:
            tangent *= -1
        return tangent

    # Get directions
    directions = []
    for entity in (self, other):
        if entity.is_curve():
            directions.append(_get_tangent(entity, point))
        else:
            directions.append(
                entity.direction_vec()
                if entity.direction(point)
                else entity.direction_vec() * (-1)
            )

    dir1, dir2 = directions
    return dir1.angle_signed(dir2)

direction(point, is_endpoint=False)

Returns the direction of the arc, true if inverted

Source code in model/arc.py
152
153
154
155
156
def direction(self, point, is_endpoint=False):
    """Returns the direction of the arc, true if inverted"""
    if is_endpoint:
        return point == self.start
    return point == self.end

normal(position=None)

Return the normal vector at a given position

Source code in model/arc.py
135
136
137
138
139
def normal(self, position: Vector = None):
    """Return the normal vector at a given position"""

    normal = position - self.ct.co
    return normal.normalized()

project_point(coords)

Projects a point onto the arc

Source code in model/arc.py
173
174
175
176
177
def project_point(self, coords):
    """Projects a point onto the arc"""
    local_co = coords - self.ct.co
    angle = range_2pi(math.atan2(local_co[1], local_co[0]))
    return self.point_on_curve(angle, relative=False)

Bases: Entity2D, PropertyGroup

Representation of a circle in 2D space. The circle is centered at ct with its size defined by the radius and is resoulution independent.

Parameters:

Name Type Description Default
ct SlvsPoint2D

Circle's centerpoint

required
radius FloatProperty

The radius of the circle

required
nm SlvsNormal2D
required
sketch SlvsSketch

The sketch this entity belongs to

required
Source code in model/circle.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
class SlvsCircle(Entity2D, PropertyGroup):
    """Representation of a circle in 2D space. The circle is centered at ct with
    its size defined by the radius and is resoulution independent.

    Arguments:
        ct (SlvsPoint2D): Circle's centerpoint
        radius (FloatProperty): The radius of the circle
        nm (SlvsNormal2D):
        sketch (SlvsSketch): The sketch this entity belongs to
    """

    radius: FloatProperty(
        name="Radius",
        description="The radius of the circle",
        subtype="DISTANCE",
        min=0.0,
        unit="LENGTH",
        update=tag_update,
    )

    @classmethod
    def is_path(cls):
        return True

    @classmethod
    def is_curve(cls):
        return True

    @classmethod
    def is_segment(cls):
        return True

    def dependencies(self) -> List[SlvsGenericEntity]:
        return [self.nm, self.ct, self.sketch]

    def is_dashed(self):
        return self.construction

    def update(self):
        if bpy.app.background:
            return

        coords = coords_arc_2d(0, 0, self.radius, CURVE_RESOLUTION)

        u, v = self.ct.co

        mat_local = Matrix.Translation(Vector((u, v, 0)))
        mat = self.wp.matrix_basis @ mat_local
        coords = [(mat @ Vector((*co, 0)))[:] for co in coords]

        kwargs = {"pos": coords}
        self._batch = batch_for_shader(self._shader, "LINE_STRIP", kwargs)
        self.is_dirty = False

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        self.param = solvesys.addParamV(self.radius, group)

        nm = None
        if self.nm != -1:
            nm = self.nm
        else:
            nm = self.wp.nm

        handle = solvesys.addCircle(
            self.ct.py_data,
            self.nm.py_data,
            solvesys.addDistance(self.param),
            group=group,
        )
        self.py_data = handle

    def update_from_slvs(self, solvesys):
        self.radius = solvesys.getParam(self.param).val

    def point_on_curve(self, angle):
        return pol2cart(self.radius, angle) + self.ct.co

    def placement(self):
        return self.wp.matrix_basis @ self.point_on_curve(45).to_3d()

    @classmethod
    def is_closed(cls):
        return True

    def connection_points(self):
        # NOTE: it should probably be possible to lookup coincident points on circle
        return []

    def direction(self, point, is_endpoint=False):
        return False

    def bezier_segment_count(self):
        return 4

    def bezier_point_count(self):
        return self.bezier_segment_count()

    def to_bezier(
        self,
        spline,
        startpoint,
        endpoint,
        invert_direction,
        set_startpoint=False,
        midpoints=[],
    ):
        # Get midpoint positions
        segment_count = len(midpoints) + 1
        radius, center = self.radius, self.ct.co

        bezier_points = [startpoint, *midpoints]

        locations = get_bezier_curve_midpoint_positions(
            self, segment_count, bezier_points, FULL_TURN, cyclic=True
        )
        angle = FULL_TURN / segment_count

        n = FULL_TURN / angle
        q = (4 / 3) * math.tan(HALF_TURN / (2 * n))
        base_offset = Vector((radius, q * radius))

        create_bezier_curve(
            segment_count,
            bezier_points,
            locations,
            center,
            base_offset,
            invert=invert_direction,
            cyclic=True,
        )
        return endpoint

    def overlaps_endpoint(self, co):
        return False

    def intersect(self, other):
        def parse_retval(retval):
            # Intersect might return None, (value, value) or (value, None)
            values = []
            if hasattr(retval, "__len__"):
                for val in retval:
                    if val is None:
                        continue
                    if other.overlaps_endpoint(val):
                        continue
                    values.append(val)
            elif retval is not None:
                if other.overlaps_endpoint(retval):
                    return ()
                values.append(retval)

            return tuple(values)

        if other.is_line():
            return parse_retval(
                intersect_line_sphere_2d(
                    other.p1.co, other.p2.co, self.ct.co, self.radius
                )
            )
        elif isinstance(other, SlvsCircle):
            return parse_retval(
                intersect_sphere_sphere_2d(
                    self.ct.co, self.radius, other.ct.co, other.radius
                )
            )
        else:
            return other.intersect(self)

    def replace(self, context, p1, p2, use_self=False):
        if use_self:
            self.p1 = p1
            self.p2 = p2
            return self

        sketch = context.scene.sketcher.active_sketch
        arc = context.scene.sketcher.entities.add_arc(
            sketch.wp.nm, self.ct, p1, p2, sketch
        )
        arc.construction = self.construction
        return arc

    def distance_along_segment(self, p1, p2):
        ct = self.ct.co
        start, end = p1 - ct, p2 - ct
        angle = range_2pi(math.atan2(*end.yx) - math.atan2(*start.yx))
        retval = self.radius * angle
        return retval


    def new(self, context, **kwargs) -> SlvsGenericEntity:
        kwargs.setdefault("ct", self.ct)
        kwargs.setdefault("nm", self.nm)
        kwargs.setdefault("radius", self.radius)
        kwargs.setdefault("sketch", self.sketch)
        kwargs.setdefault("construction", self.construction)
        return context.scene.sketcher.entities.add_circle(**kwargs)