Skip to content

Constraints

Constraints are used to restrict the movement of entities and define their final locations. A constraint can usually be created between different entity types, check the corresponding operator's tooltip to find out what's supported.

Active

A constraint is considered to be active when the sketch it belongs to is set as the active sketch or, for constraints that don't belong to a sketch, when no sketch is active.

Failure

Whenever the solver fails to find a solution for the given system it will try to mark constraints that are causing the failure. Those constraints will be colored red, additionally the failed sketch will be marked.

Types

Constraint types follow the implementation of solvespace.

Geometric Constraints

Bases: GenericConstraint, PropertyGroup

Forces two points to be coincident, or a point to lie on a curve, or a point to lie on a plane.

The point-coincident constraint is available in both 3d and projected versions. The 3d point-coincident constraint restricts three degrees of freedom; the projected version restricts only two. If two points are drawn in a workplane, and then constrained coincident in 3d, then an error will result–they are already coincident in one dimension (the dimension normal to the plane), so the third constraint equation is redundant.

Source code in model/coincident.py
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
class SlvsCoincident(GenericConstraint, PropertyGroup):
    """Forces two points to be coincident,
    or a point to lie on a curve, or a point to lie on a plane.

    The point-coincident constraint is available in both 3d and projected versions.
    The 3d point-coincident constraint restricts three degrees of freedom;
    the projected version restricts only two. If two points are drawn in a workplane,
    and then constrained coincident in 3d, then an error will result–they are already
    coincident in one dimension (the dimension normal to the plane),
    so the third constraint equation is redundant.
    """

    type = "COINCIDENT"
    label = "Coincident"
    signature = (POINT, (*POINT, *LINE, SlvsWorkplane, SlvsCircle, SlvsArc))
    # NOTE: Coincident between 3dPoint and Workplane currently doesn't seem to work

    def needs_wp(self):
        if isinstance(self.entity2, SlvsWorkplane):
            return WpReq.FREE
        return WpReq.OPTIONAL

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        return make_coincident(
            solvesys, self.entity1.py_data, self.entity2, self.get_workplane(), group
        )

    def placements(self):
        return (self.entity1,)

Bases: GenericConstraint, PropertyGroup

Forces a line segment to be vertical. It applies in 2D Space only because the meaning of horizontal or vertical is defined by the workplane.

Source code in model/vertical.py
16
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
class SlvsVertical(GenericConstraint, PropertyGroup):
    """Forces a line segment to be vertical. It applies in 2D Space only because
    the meaning of horizontal or vertical is defined by the workplane.
    """

    type = "VERTICAL"
    label = "Vertical"
    signature = ((SlvsLine2D, SlvsPoint2D), (SlvsPoint2D,))

    @classmethod
    def get_types(cls, index, entities):
        if index == 1:
            # return None if first entity is line
            if entities[0] and entities[0].is_line():
                return None

        return cls.signature[index]

    def needs_wp(self):
        return WpReq.NOT_FREE

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        wp = self.get_workplane()
        if self.entity1.is_point():
            return solvesys.addPointsVertical(
                self.entity1.py_data, self.entity2.py_data, wp, group=group
            )
        return solvesys.addLineVertical(self.entity1.py_data, wrkpln=wp, group=group)

    def placements(self):
        return (self.entity1,)

Bases: GenericConstraint, PropertyGroup

Forces a line segment to be horizontal. It applies in 2D Space only because the meaning of horizontal or vertical is defined by the workplane.

Source code in model/horizontal.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
class SlvsHorizontal(GenericConstraint, PropertyGroup):
    """Forces a line segment to be horizontal. It applies in 2D Space only because
    the meaning of horizontal or vertical is defined by the workplane.
    """

    type = "HORIZONTAL"
    label = "Horizontal"
    signature = ((SlvsLine2D, SlvsPoint2D), (SlvsPoint2D,))

    @classmethod
    def get_types(cls, index, entities):
        if index == 1:
            # return None if first entity is line
            if entities[0] and entities[0].is_line():
                return None

        return cls.signature[index]

    def needs_wp(self):
        return WpReq.NOT_FREE

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        wp = self.get_workplane()
        if self.entity1.is_point():
            return solvesys.addPointsHorizontal(
                self.entity1.py_data, self.entity2.py_data, wp, group=group
            )
        return solvesys.addLineHorizontal(self.entity1.py_data, wrkpln=wp, group=group)

    def placements(self):
        return (self.entity1,)

Note: It’s good to use horizontal and vertical constraints whenever possible. These constraints are very simple to solve, and will not lead to convergence problems. Whenever possible, define the workplanes so that lines are horizontal and vertical within those workplanes.

Bases: GenericConstraint, PropertyGroup

Forces two lines to be parallel. Applies only in 2D.

Source code in model/parallel.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class SlvsParallel(GenericConstraint, PropertyGroup):
    """Forces two lines to be parallel. Applies only in 2D."""

    type = "PARALLEL"
    label = "Parallel"
    signature = ((SlvsLine2D,), (SlvsLine2D,))

    def needs_wp(self):
        return WpReq.NOT_FREE

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        return solvesys.addParallel(
            self.entity1.py_data,
            self.entity2.py_data,
            wrkpln=self.get_workplane(),
            group=group,
        )

    def placements(self):
        return (self.entity1, self.entity2)

Bases: GenericConstraint, PropertyGroup

Forces two lines to be perpendicular, applies only in 2D. This constraint is equivalent to an angle constraint for ninety degrees.

Source code in model/perpendicular.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class SlvsPerpendicular(GenericConstraint, PropertyGroup):
    """Forces two lines to be perpendicular, applies only in 2D. This constraint
    is equivalent to an angle constraint for ninety degrees.
    """

    type = "PERPENDICULAR"
    label = "Perpendicular"
    signature = ((SlvsLine2D,), (SlvsLine2D,))

    def needs_wp(self):
        return WpReq.NOT_FREE

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        return solvesys.addPerpendicular(
            self.entity1.py_data,
            self.entity2.py_data,
            wrkpln=self.get_workplane(),
            group=group,
        )

    def placements(self):
        point = get_connection_point(self.entity1, self.entity2)
        if point:
            return (point,)
        return (self.entity1, self.entity2)

Bases: GenericConstraint, PropertyGroup

Forces two lengths, or radiuses to be equal.

If a line and an arc of a circle are selected, then the length of the line is forced equal to the length (not the radius) of the arc.

Source code in model/equal.py
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
class SlvsEqual(GenericConstraint, PropertyGroup):
    """Forces two lengths, or radiuses to be equal.

    If a line and an arc of a circle are selected, then the length of the line is
    forced equal to the length (not the radius) of the arc.
    """

    type = "EQUAL"
    label = "Equal"
    signature = (line_arc_circle, line_arc_circle)

    @classmethod
    def get_types(cls, index, entities):
        e = entities[1] if index == 0 else entities[0]
        if e:
            if type(e) in (SlvsLine2D, SlvsArc):
                return (SlvsLine2D, SlvsArc)
            elif type(e) == SlvsCircle:
                return CURVE
            return (type(e),)
        return cls.signature[index]

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        e1, e2 = self.entity1, self.entity2

        func = None
        set_wp = False

        if all([type(e) in LINE for e in (e1, e2)]):
            func = solvesys.addEqualLength
            set_wp = True
        elif all([type(e) in CURVE for e in (e1, e2)]):
            func = solvesys.addEqualRadius
        else:
            func = solvesys.addEqualLineArcLength
            set_wp = True

            if e1.is_curve():
                e1, e2 = e2, e1

        kwargs = {
            "group": group,
        }

        if set_wp:
            kwargs["wrkpln"] = self.get_workplane()

        return func(e1.py_data, e2.py_data, **kwargs)

    def placements(self):
        return (self.entity1, self.entity2)

Bases: GenericConstraint, PropertyGroup

Forces two curves (arc/circle) or a curve and a line to be tangent.

Source code in model/tangent.py
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
74
75
76
77
78
79
80
81
class SlvsTangent(GenericConstraint, PropertyGroup):
    """Forces two curves (arc/circle) or a curve and a line to be tangent."""

    type = "TANGENT"
    label = "Tangent"
    signature = (CURVE, (SlvsLine2D, *CURVE))

    def needs_wp(self):
        return WpReq.NOT_FREE

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        e1, e2 = self.entity1, self.entity2
        wp = self.get_workplane()

        # check if entities share a point
        point = get_connection_point(e1, e2)
        if point and not isinstance(e2, SlvsCircle):
            if isinstance(e2, SlvsLine2D):
                return solvesys.addArcLineTangent(
                    e1.direction(point),
                    e1.py_data,
                    e2.py_data,
                    group=group,
                )
            elif isinstance(e2, SlvsArc):
                return solvesys.addCurvesTangent(
                    e1.direction(point),
                    e2.direction(point),
                    e1.py_data,
                    e2.py_data,
                    wrkpln=wp,
                    group=group,
                )

        elif isinstance(e2, SlvsLine2D):
            orig = e2.p1.co
            coords = (e1.ct.co - orig).project(e2.p2.co - orig) + orig
            params = [solvesys.addParamV(v, group) for v in coords]
            p = solvesys.addPoint2d(wp, *params, group=group)
            line = solvesys.addLineSegment(e1.ct.py_data, p, group=group)

            return (
                make_coincident(solvesys, p, e1, wp, group),
                make_coincident(solvesys, p, e2, wp, group),
                solvesys.addPerpendicular(e2.py_data, line, wrkpln=wp, group=group),
            )

        elif e2.is_curve():
            coords = (e1.ct.co + e2.ct.co) / 2
            params = [solvesys.addParamV(v, group) for v in coords]
            p = solvesys.addPoint2d(wp, *params, group=group)
            line = solvesys.addLineSegment(e1.ct.py_data, e2.ct.py_data, group=group)

            return (
                make_coincident(solvesys, p, e1, wp, group),
                make_coincident(solvesys, p, e2, wp, group),
                solvesys.addPointOnLine(p, line, group=group, wrkpln=wp),
            )

    def placements(self):
        point = get_connection_point(self.entity1, self.entity2)
        if point is None:
            return (self.entity1, self.entity2)
        return (point,)

Bases: GenericConstraint, PropertyGroup

Forces a point to lie on the midpoint of a line.

Source code in model/midpoint.py
15
16
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
class SlvsMidpoint(GenericConstraint, PropertyGroup):
    """Forces a point to lie on the midpoint of a line."""

    type = "MIDPOINT"
    label = "Midpoint"
    signature = (POINT, LINE)

    def needs_wp(self):
        return WpReq.NOT_FREE

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        kwargs = {
            "group": group,
        }

        wp = self.get_workplane()
        if wp:
            kwargs["wrkpln"] = wp

        return solvesys.addMidPoint(
            self.entity1.py_data,
            self.entity2.py_data,
            **kwargs,
        )

    def placements(self):
        return (self.entity2,)

Bases: DimensionalConstraint, PropertyGroup

Defines the ratio between the lengths of two line segments.

The order matters; the ratio is defined as length of entity1 : length of entity2.

Source code in model/ratio.py
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
class SlvsRatio(DimensionalConstraint, PropertyGroup):
    """Defines the ratio between the lengths of two line segments.

    The order matters; the ratio is defined as length of entity1 : length of entity2.
    """

    type = "RATIO"
    label = "Ratio"

    value: FloatProperty(
        name=label,
        subtype="UNSIGNED",
        update=update_system_cb,
        min=0.0,
    )

    signature = (
        LINE,
        LINE,
    )

    def needs_wp(self):
        if isinstance(self.entity1, SlvsLine2D) or isinstance(self.entity2, SlvsLine2D):
            return WpReq.NOT_FREE
        return WpReq.FREE

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        e1, e2 = self.entity1, self.entity2

        return solvesys.addLengthRatio(
            self.value,
            e1.py_data,
            e2.py_data,
            self.get_workplane(),
            group=group,
        )

    def init_props(self, **kwargs):
        line1, line2 = self.entity1, self.entity2
        if line2.length == 0.0:
            return {"value": 0.0}

        value = line1.length / line2.length
        return {"value": value}

    def placements(self):
        return (self.entity1, self.entity2)

Dimensional Constraints

Bases: DimensionalConstraint, PropertyGroup

Sets the distance between a point and some other entity (point/line/Workplane).

Source code in model/distance.py
 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
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
class SlvsDistance(DimensionalConstraint, PropertyGroup):
    """Sets the distance between a point and some other entity (point/line/Workplane)."""

    def _set_value_force(self, value):
        DimensionalConstraint._set_value_force(self, abs(value))

    def _set_align(self, value: int):
        alignment = bpyEnum(align_items, value).identifier
        distance = _get_aligned_distance(self.entity1, self.entity2, alignment)
        setprop(self, "align", value)
        setprop(self, "value", distance)

    def _get_align(self) -> int:
        return self.get("align", 0)

    label = "Distance"
    value: FloatProperty(
        name=label,
        subtype="DISTANCE",
        unit="LENGTH",
        precision=6,
        update=update_system_cb,
        get=_get_value,
        set=DimensionalConstraint._set_value,
    )
    flip: BoolProperty(name="Flip", update=update_system_cb)
    align: EnumProperty(
        name="Align",
        items=align_items,
        update=update_system_cb,
        get=_get_align,
        set=_set_align,
    )
    draw_offset: FloatProperty(name="Draw Offset", default=0.3)
    draw_outset: FloatProperty(name="Draw Outset", default=0.0)
    type = "DISTANCE"
    signature = ((*POINT, *LINE, SlvsCircle, SlvsArc), (*POINT, *LINE, SlvsWorkplane))
    props = ("value",)

    @classmethod
    def get_types(cls, index, entities):
        e = entities[1] if index == 0 else entities[0]

        if e:
            if index == 1 and e.is_line():
                # Allow constraining a single line
                return None
            if e.is_3d():
                return ((SlvsPoint3D,), (SlvsPoint3D, SlvsLine3D, SlvsWorkplane))[index]
            return (POINT2D, (*POINT2D, SlvsLine2D))[index]
        return cls.signature[index]

    def needs_wp(self):
        if isinstance(self.entity2, SlvsWorkplane):
            return WpReq.FREE
        return WpReq.OPTIONAL

    def use_flipping(self):
        # Only use flipping for constraint between point and line/workplane
        if self.entity1.is_curve():
            return False
        return type(self.entity2) in (*LINE, SlvsWorkplane)

    def use_align(self):
        """Returns True if constraint's entities allow distance to be aligned"""
        if type(self.entity2) in (*LINE, SlvsWorkplane):
            return False
        if self.entity1.is_curve():
            return False
        return True

    def is_align(self):
        """Returns True if constraint is aligned"""
        return self.use_align() and self.align != "NONE"

    def get_value(self):
        value = self.value
        if self.use_flipping() and self.flip:
            return value * -1
        return value

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        if self.entity1 == self.entity2:
            raise AttributeError("Cannot create constraint between one entity itself")
        # TODO: don't allow Distance if Point -> Line if (Point in Line)

        e1, e2 = self.entity1, self.entity2
        if e1.is_line():
            e1, e2 = e1.p1, e1.p2

        func = None
        set_wp = False
        wp = self.get_workplane()
        alignment = self.align
        align = self.is_align()
        handles = []

        value = self.get_value()

        # circle/arc -> line/point
        if type(e1) in CURVE:
            # TODO: make Horizontal and Vertical alignment work
            if type(e2) in LINE:
                return solvesys.addPointLineDistance(
                    value + e1.radius, e1.ct.py_data, e2.py_data, wp, group
                )
            else:
                assert isinstance(e2, SlvsPoint2D)
                return solvesys.addPointsDistance(
                    value + e1.radius, e1.ct.py_data, e2.py_data, wp, group
                )

        elif type(e2) in LINE:
            func = solvesys.addPointLineDistance
            set_wp = True
        elif isinstance(e2, SlvsWorkplane):
            func = solvesys.addPointPlaneDistance
        elif type(e2) in POINT:
            if align and all([e.is_2d() for e in (e1, e2)]):
                # Get Point in between
                p1, p2 = e1.co, e2.co
                coords = (p2.x, p1.y)

                params = [solvesys.addParamV(v, group) for v in coords]
                p = solvesys.addPoint2d(wp, *params, group=group)

                handles.append(
                    solvesys.addPointsHorizontal(p, e2.py_data, wp, group=group)
                )
                handles.append(
                    solvesys.addPointsVertical(p, e1.py_data, wp, group=group)
                )

                base_point = e1 if alignment == "VERTICAL" else e2
                handles.append(
                    solvesys.addPointsDistance(
                        value, p, base_point.py_data, wrkpln=wp, group=group
                    )
                )
                return handles
            else:
                func = solvesys.addPointsDistance
            set_wp = True

        kwargs = {
            "group": group,
        }

        if set_wp:
            kwargs["wrkpln"] = self.get_workplane()

        return func(value, e1.py_data, e2.py_data, **kwargs)

    def matrix_basis(self):
        if self.sketch_i == -1 or not self.entity1.is_2d():
            # TODO: Support distance in 3d
            return Matrix()

        sketch = self.sketch
        x_axis = Vector((1, 0))
        alignment = self.align
        align = self.is_align()
        angle = 0

        e1, e2 = self.entity1, self.entity2
        #   e1       e2
        #   ----------------
        #   line     [none]
        #   point    point
        #   point    line
        #   arc      point
        #   arc      line
        #   circle   point
        #   circle   line

        # set p1 and p2
        if e1.is_curve():
            # reframe as point->point and continue
            centerpoint = e1.ct.co
            if e2.is_line():
                p2, _ = intersect_point_line(centerpoint, e2.p1.co, e2.p2.co)
            else:
                assert isinstance(e2, SlvsPoint2D)
                p2 = e2.co
            if (p2 - centerpoint).length > 0:
                vec = (p2 - centerpoint) / (p2 - centerpoint).length
                p1 = centerpoint + (e1.radius * Vector(vec))
            else:
                # This is a curve->line where the centerpoint of the curve is
                # coincident with the line.  By reassigning p1 to an endpoint
                # of the line, we avoid p1=p2 errors and the result is
                # (correctly) an invalid constraint
                p1 = e2.p1.co
        elif e1.is_line():
            # reframe as point->point and continue
            e1, e2 = e1.p1, e1.p2
            p1, p2 = e1.co, e2.co
        else:
            assert isinstance(e1, SlvsPoint2D)
            p1 = e1.co

        if type(e2) in POINT2D:
            # this includes "Line Length" (now point->point)
            # and curve -> point
            p2 = e2.co
            if not align:
                v_rotation = p2 - p1
            else:
                v_rotation = (
                    Vector((1.0, 0.0))
                    if alignment == "HORIZONTAL"
                    else Vector((0.0, 1.0))
                )

            if v_rotation.length != 0:
                angle = v_rotation.angle_signed(x_axis)

            mat_rot = Matrix.Rotation(angle, 2, "Z")
            v_translation = (p2 + p1) / 2

        elif e2.is_line():
            # curve -> line
            # or point -> line
            if e1.is_curve():
                if not align:
                    v_rotation = p2 - p1
                else:
                    v_rotation = (
                        Vector((1.0, 0.0))
                        if alignment == "HORIZONTAL"
                        else Vector((0.0, 1.0))
                    )
                if v_rotation.length != 0:
                    angle = v_rotation.angle_signed(x_axis)

                mat_rot = Matrix.Rotation(angle, 2, "Z")
                v_translation = (p2 + p1) / 2
            else:
                assert isinstance(e1, SlvsPoint2D)
                orig = e2.p1.co
                end = e2.p2.co
                vec = end - orig
                angle = (math.tau / 4) + range_2pi(math.atan2(vec[1], vec[0]))
                mat_rot = Matrix.Rotation(angle, 2, "Z")
                p1 = p1 - orig
                v_translation = orig + (p1 + p1.project(vec)) / 2

        mat_local = Matrix.Translation(v_translation.to_3d()) @ mat_rot.to_4x4()
        return sketch.wp.matrix_basis @ mat_local

    def _get_init_value(self, alignment):
        e1, e2 = self.entity1, self.entity2

        if e1.is_3d():
            return (e1.location - e2.location).length

        if e1.is_line():
            return _get_aligned_distance(e1.p1, e1.p2, alignment)
        if type(e1) in CURVE:
            centerpoint = e1.ct.co
            if isinstance(e2, SlvsLine2D):
                endpoint, _ = intersect_point_line(centerpoint, e2.p1.co, e2.p2.co)
            else:
                assert isinstance(e2, SlvsPoint2D)
                endpoint = e2.co
            return (centerpoint - endpoint).length - e1.radius
        if isinstance(e2, SlvsWorkplane):
            # Returns the signed distance to the plane
            return distance_point_to_plane(e1.co, e2.p1.co, e2.normal)
        if type(e2) in LINE:
            orig = e2.p1.co
            end = e2.p2.co - orig
            p1 = e1.co - orig

            # NOTE: Comment from solvespace documentation:
            # When constraining the distance between a point and a plane,
            # or a point and a plane face, or a point and a line in a workplane,
            # the distance is signed. The distance may be positive or negative,
            # depending on whether the point is above or below the plane.
            # The distance is always shown positive on the sketch;
            # to flip to the other side, enter a negative value.
            return math.copysign(
                (p1 - (p1).project(end)).length,
                get_side_of_line(e2.p1.co, e2.p2.co, e1.co),
            )

        return _get_aligned_distance(e1, e2, alignment)

    def init_props(self, **kwargs):

        # NOTE: Flip is currently ignored when passed in kwargs
        alignment = kwargs.get("align")
        retval = {}

        value = kwargs.get("value", self._get_init_value(alignment))

        if self.use_flipping() and value < 0:
            value = abs(value)
            retval["flip"] = not self.flip

        retval["value"] = value
        retval["align"] = alignment
        return retval

    def text_inside(self, ui_scale):
        return (ui_scale * abs(self.draw_outset)) < self.value / 2

    def update_draw_offset(self, pos, ui_scale):
        self.draw_offset = pos[1] / ui_scale
        self.draw_outset = pos[0] / ui_scale

    def draw_props(self, layout):
        sub = super().draw_props(layout)

        row = sub.row()
        row.enabled = self.use_flipping()
        row.prop(self, "flip")

        sub.label(text="Alignment:")
        row = sub.row()
        row.enabled = self.use_align()
        row.prop(self, "align", text="")

        if preferences.is_experimental():
            sub.prop(self, "draw_offset")

        return sub

    def value_placement(self, context):
        """location to display the constraint value"""
        region = context.region
        rv3d = context.space_data.region_3d
        ui_scale = context.preferences.system.ui_scale

        offset = ui_scale * self.draw_offset
        outset = ui_scale * self.draw_outset
        coords = self.matrix_basis() @ Vector((outset, offset, 0))
        return location_3d_to_region_2d(region, rv3d, coords)

is_align()

Returns True if constraint is aligned

Source code in model/distance.py
135
136
137
def is_align(self):
    """Returns True if constraint is aligned"""
    return self.use_align() and self.align != "NONE"

use_align()

Returns True if constraint's entities allow distance to be aligned

Source code in model/distance.py
127
128
129
130
131
132
133
def use_align(self):
    """Returns True if constraint's entities allow distance to be aligned"""
    if type(self.entity2) in (*LINE, SlvsWorkplane):
        return False
    if self.entity1.is_curve():
        return False
    return True

value_placement(context)

location to display the constraint value

Source code in model/distance.py
392
393
394
395
396
397
398
399
400
401
def value_placement(self, context):
    """location to display the constraint value"""
    region = context.region
    rv3d = context.space_data.region_3d
    ui_scale = context.preferences.system.ui_scale

    offset = ui_scale * self.draw_offset
    outset = ui_scale * self.draw_outset
    coords = self.matrix_basis() @ Vector((outset, offset, 0))
    return location_3d_to_region_2d(region, rv3d, coords)

Bases: DimensionalConstraint, PropertyGroup

Sets the diameter of an arc or a circle.

Source code in model/diameter.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
class SlvsDiameter(DimensionalConstraint, PropertyGroup):
    """Sets the diameter of an arc or a circle."""

    def use_radius_getter(self):
        return self.get("setting", self.bl_rna.properties["setting"].default)

    def use_radius_setter(self, setting):
        old_setting = self.get("setting", self.bl_rna.properties["setting"].default)
        self["setting"] = setting

        distance = None
        if old_setting and not setting:
            distance = self.value * 2
        elif not old_setting and setting:
            distance = self.value / 2

        if distance is not None:
            # Avoid triggering the property's update callback
            self["value"] = distance

    @property
    def label(self):
        return "Radius" if self.setting else "Diameter"

    value: FloatProperty(
        name="Size",
        subtype="DISTANCE",
        unit="LENGTH",
        precision=6,
        get=DimensionalConstraint._get_value,
        set=DimensionalConstraint._set_value,
        update=update_system_cb,
    )
    setting: BoolProperty(
        name="Use Radius", get=use_radius_getter, set=use_radius_setter
    )
    leader_angle: FloatProperty(name="Leader Angle", default=45, subtype="ANGLE")
    draw_offset: FloatProperty(name="Draw Offset", default=0)
    type = "DIAMETER"
    signature = (CURVE,)
    props = ("value",)

    @property
    def diameter(self):
        value = self.value
        if self.setting:
            return value * 2
        return value

    @property
    def radius(self):
        value = self.value
        if self.setting:
            return value
        return value / 2

    def needs_wp(self):
        return WpReq.OPTIONAL

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        return solvesys.addDiameter(self.diameter, self.entity1.py_data, group=group)

    def _get_init_value(self, setting):
        value = self.entity1.radius
        if not setting:
            return value * 2
        return value

    def init_props(self, **kwargs):
        setting = kwargs.get("setting", self.setting)
        value = kwargs.get("value", self._get_init_value(setting))
        return {"value": value, "setting": setting}

    def matrix_basis(self):
        if self.sketch_i == -1:
            return Matrix()
        sketch = self.sketch
        origin = self.entity1.ct.co
        rotation = range_2pi(math.radians(self.leader_angle))
        mat_local = Matrix.Translation(origin.to_3d())
        return sketch.wp.matrix_basis @ mat_local

    def text_inside(self):
        return self.draw_offset < self.radius

    def update_draw_offset(self, pos, ui_scale):
        self.draw_offset = pos.length
        self.leader_angle = math.atan2(pos.y, pos.x)

    def value_placement(self, context):
        """location to display the constraint value"""
        region = context.region
        rv3d = context.space_data.region_3d
        offset = self.draw_offset
        coords = pol2cart(offset, self.leader_angle)
        coords2 = self.matrix_basis() @ Vector((coords[0], coords[1], 0.0))
        return location_3d_to_region_2d(region, rv3d, coords2)

value_placement(context)

location to display the constraint value

Source code in model/diameter.py
111
112
113
114
115
116
117
118
def value_placement(self, context):
    """location to display the constraint value"""
    region = context.region
    rv3d = context.space_data.region_3d
    offset = self.draw_offset
    coords = pol2cart(offset, self.leader_angle)
    coords2 = self.matrix_basis() @ Vector((coords[0], coords[1], 0.0))
    return location_3d_to_region_2d(region, rv3d, coords2)

Bases: DimensionalConstraint, PropertyGroup

Sets the angle between two lines, applies in 2D only.

The constraint's setting can be used to to constrain the supplementary angle.

Source code in model/angle.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
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
class SlvsAngle(DimensionalConstraint, PropertyGroup):
    """Sets the angle between two lines, applies in 2D only.

    The constraint's setting can be used to to constrain the supplementary angle.
    """

    def assign_init_props(self, context: Context = None, **kwargs):
        # Updating self.setting will create recursion loop

        super().assign_init_props(context)

        line1, line2 = self.entity1, self.entity2
        origin = get_line_intersection(
            *line_abc_form(line1.p1.co, line1.p2.co),
            *line_abc_form(line2.p1.co, line2.p2.co),
        )
        dist = max(
            (line1.midpoint() - origin).length, (line2.midpoint() - origin).length, 0.5
        )
        self.draw_offset = dist if not self.setting else -dist

    label = "Angle"
    value: FloatProperty(
        name=label,
        subtype="ANGLE",
        unit="ROTATION",
        precision=6,
        update=update_system_cb,
        get=DimensionalConstraint._get_value,
        set=DimensionalConstraint._set_value,
    )
    setting: BoolProperty(
        name="Measure supplementary angle",
        update=DimensionalConstraint.assign_init_props,
    )
    draw_offset: FloatProperty(name="Draw Offset", default=1)
    draw_outset: FloatProperty(name="Draw Outset", default=0)
    type = "ANGLE"
    signature = ((SlvsLine2D,), (SlvsLine2D,))
    props = ("value",)

    def needs_wp(self):
        return WpReq.NOT_FREE

    def to_displayed_value(self, value):
        return HALF_TURN - value if self.setting else value

    def from_displayed_value(self, value):
        return HALF_TURN - value if self.setting else value

    def create_slvs_data(self, solvesys, group=Solver.group_fixed):
        kwargs = {
            "group": group,
        }

        wp = self.get_workplane()
        if wp:
            kwargs["wrkpln"] = wp

        return solvesys.addAngle(
            math.degrees(self.value),
            self.setting,
            self.entity1.py_data,
            self.entity2.py_data,
            **kwargs,
        )

    def matrix_basis(self):
        if self.sketch_i == -1:
            return Matrix()

        sketch = self.sketch

        line1 = self.entity1
        line2 = self.entity2

        origin = get_line_intersection(
            *line_abc_form(line1.p1.co, line1.p2.co),
            *line_abc_form(line2.p1.co, line2.p2.co),
        )

        rotation = range_2pi((self.orientation(line2) + self.orientation(line1)) / 2)

        if self.setting:
            rotation = rotation - QUARTER_TURN

        mat_rot = Matrix.Rotation(rotation, 2, "Z")
        mat_local = Matrix.Translation(origin.to_3d()) @ mat_rot.to_4x4()
        return sketch.wp.matrix_basis @ mat_local

    @staticmethod
    def orientation(line):
        pos = line.p2.co - line.p1.co
        return math.atan2(pos[1], pos[0])

    @staticmethod
    def _get_angle(A, B):
        # (A dot B)/(|A||B|) = cos(valA)
        divisor = A.length * B.length
        if not divisor:
            return 0.0

        x = A.dot(B) / divisor
        x = max(-1, min(x, 1))
        return math.degrees(math.acos(x))

    def _get_init_value(self, setting):
        vec1, vec2 = self.entity1.direction_vec(), self.entity2.direction_vec()
        return self._get_angle(vec1, vec2)

    def init_props(self, **kwargs):
        """
        initializes value (angle, in radians),
            setting ("measure supplimentary angle")
            and distance to dimension text (draw_offset)
        """

        setting = kwargs.get("setting", self.setting)
        angle = kwargs.get("value", self._get_init_value(setting))

        return {
            "value": math.radians(angle),
            "setting": setting,
        }

    def text_inside(self):
        return abs(self.draw_outset) < (self.value / 2)

    def update_draw_offset(self, pos, ui_scale):
        self.draw_offset = math.copysign(pos.length / ui_scale, pos.x)
        self.draw_outset = math.atan(pos.y / pos.x)

    def value_placement(self, context):
        """location to display the constraint value"""
        region = context.region
        rv3d = context.space_data.region_3d
        ui_scale = context.preferences.system.ui_scale

        offset = ui_scale * self.draw_offset
        outset = self.draw_outset
        co = pol2cart(offset, outset)
        coords = self.matrix_basis() @ Vector((co[0], co[1], 0))
        return location_3d_to_region_2d(region, rv3d, coords)

init_props(**kwargs)

initializes value (angle, in radians), setting ("measure supplimentary angle") and distance to dimension text (draw_offset)

Source code in model/angle.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def init_props(self, **kwargs):
    """
    initializes value (angle, in radians),
        setting ("measure supplimentary angle")
        and distance to dimension text (draw_offset)
    """

    setting = kwargs.get("setting", self.setting)
    angle = kwargs.get("value", self._get_init_value(setting))

    return {
        "value": math.radians(angle),
        "setting": setting,
    }

value_placement(context)

location to display the constraint value

Source code in model/angle.py
157
158
159
160
161
162
163
164
165
166
167
def value_placement(self, context):
    """location to display the constraint value"""
    region = context.region
    rv3d = context.space_data.region_3d
    ui_scale = context.preferences.system.ui_scale

    offset = ui_scale * self.draw_offset
    outset = self.draw_outset
    co = pol2cart(offset, outset)
    coords = self.matrix_basis() @ Vector((co[0], co[1], 0))
    return location_3d_to_region_2d(region, rv3d, coords)