Skip to content

Tools

Tools in CAD Sketcher are either exposed as a workspacetool or as an operator. Note however that either of those use the same interaction system.

Generic Tools

Bases: Operator, Operator3d

Add a sketch

Source code in operators/add_sketch.py
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
class View3D_OT_slvs_add_sketch(Operator, Operator3d):
    """Add a sketch"""

    bl_idname = Operators.AddSketch
    bl_label = "Add Sketch"
    bl_options = {"UNDO"}

    sketch_state1_doc = ["Workplane", "Pick a workplane as base for the sketch."]

    states = (
        state_from_args(
            sketch_state1_doc[0],
            description=sketch_state1_doc[1],
            pointer="wp",
            types=(SlvsWorkplane,),
            property=None,
            use_create=False,
        ),
    )

    def prepare_origin_elements(self, context):
        context.scene.sketcher.entities.ensure_origin_elements(context)
        return True

    def init(self, context: Context, event: Event):
        switch_sketch_mode(self, context, to_sketch_mode=True)
        self.prepare_origin_elements(context)
        bpy.ops.ed.undo_push(message="Ensure Origin Elements")
        context.scene.sketcher.show_origin = True
        return True

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

        # Add point at origin
        # NOTE: Maybe this could create a reference entity of the main origin?
        p = sse.add_point_2d((0.0, 0.0), sketch)
        p.fixed = True

        activate_sketch(context, sketch.slvs_index, self)
        self.target = sketch
        return True

    def fini(self, context: Context, succeed: bool):
        context.scene.sketcher.show_origin = False
        if hasattr(self, "target"):
            logger.debug("Add: {}".format(self.target))

        if succeed:
            self.wp.visible = False
        else:
            switch_sketch_mode(self, context, to_sketch_mode=False)

Bases: Operator, HighlightElement

Delete Entity by index or based on the selection if index isn't provided

Source code in operators/delete_entity.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
117
118
119
class View3D_OT_slvs_delete_entity(Operator, HighlightElement):
    """Delete Entity by index or based on the selection if index isn't provided"""

    bl_idname = Operators.DeleteEntity
    bl_label = "Delete Entity"
    bl_description = (
        "Delete Entity by index or based on the selection if index isn't provided"
    )
    bl_options = {"UNDO"}

    index: IntProperty(default=-1)
    do_report: BoolProperty(
        name="Report", description="Report entities that prevent deletion", default=True
    )

    @staticmethod
    def main(context: Context, index: int, operator: Operator):
        entities = context.scene.sketcher.entities
        entity = entities.get(index)

        if not entity:
            return {"CANCELLED"}

        if isinstance(entity, SlvsSketch):
            if context.scene.sketcher.active_sketch_i != -1:
                activate_sketch(context, -1, operator)
            entity.remove_objects()

            deps = get_sketch_deps_indicies(entity, context)

            for i in reversed(deps):
                operator.delete(entities.get(i), context)

        elif is_entity_dependency(entity, context):
            if operator.do_report:
                deps = list(get_entity_deps(entity, context))
                msg_deps = "\n".join([f" - {d}" for d in deps])
                message = f"Unable to delete {entity.name}, other entities depend on it:\n{msg_deps}"
                show_ui_message_popup(message=message, icon="ERROR")

                operator.report(
                    {"WARNING"},
                    "Cannot delete {}, other entities depend on it.".format(
                        entity.name
                    ),
                )
            return {"CANCELLED"}

        operator.delete(entity, context)

    @staticmethod
    def delete(entity, context: Context):
        entity.selected = False

        # Delete constraints that depend on entity
        constraints = context.scene.sketcher.constraints

        for data_coll, indices in get_constraint_local_indices(entity, context):
            if not indices:
                continue
            for i in reversed(indices):
                logger.debug("Delete: {}, {}".format(data_coll, i))
                data_coll.remove(i)

        logger.debug("Delete: {}".format(entity))
        entities = context.scene.sketcher.entities
        entities.remove(entity.slvs_index)

    def execute(self, context: Context):
        index = self.index
        selected = context.scene.sketcher.entities.selected_active

        if index != -1:
            # Entity is specified via property
            self.main(context, index, self)
        elif len(selected) == 1:
            # Treat single selection same as specified entity
            self.main(context, selected[0].slvs_index, self)
        else:
            # Batch deletion
            indices = []
            for e in selected:
                indices.append(e.slvs_index)

            indices.sort(reverse=True)
            for i in indices:
                e = context.scene.sketcher.entities.get(i)

                # NOTE: this might be slow when a lot of entities are selected, improve!
                if is_entity_dependency(e, context):
                    continue
                self.delete(e, context)

        solve_system(context, context.scene.sketcher.active_sketch)
        refresh(context)
        return {"FINISHED"}

Bases: Operator, HighlightElement

Delete constraint by type and index

Source code in operators/delete_constraint.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
47
48
class View3D_OT_slvs_delete_constraint(Operator, HighlightElement):
    """Delete constraint by type and index"""

    bl_idname = Operators.DeleteConstraint
    bl_label = "Delete Constraint"
    bl_description = "Delete Constraint"
    bl_options = {"UNDO"}

    type: StringProperty(name="Type")
    index: IntProperty(default=-1)

    @classmethod
    def description(cls, context, properties):
        cls.handle_highlight_hover(context, properties)
        if properties.type:
            return "Delete: " + properties.type.capitalize()
        return ""

    def execute(self, context: Context):
        constraints = context.scene.sketcher.constraints

        # NOTE: It's not really necessary to first get the
        # constraint from its index before deleting

        constr = constraints.get_from_type_index(self.type, self.index)
        logger.debug("Delete: {}".format(constr))

        constraints.remove(constr)

        sketch = context.scene.sketcher.active_sketch
        solve_system(context, sketch=sketch)
        refresh(context)
        return {"FINISHED"}

Workspacetools

Workspacetools

Workspacetools are used to interactively create entities. You can access them from the viewport's "T"-panel. Check the tools section to get familiar with the behavior of CAD Sketcher tools.

INFO: Interaction with extension geometry is only possible when one of the extension tools is active.

Workspacetool Access Keymap

Whenever one of the extension's tools is active the tool access keymap allows to quickly switch between the different tools.

Key Modifier Action
ESC - Activate Tool: Select
P - Invoke Tool: Add Point 2D
L - Invoke Tool: Add Line 2D
C - Invoke Tool: Add Circle
A - Invoke Tool: Add Arc
R - Invoke Tool: Add Rectangle
S - Invoke Tool: Add Sketch
Y - Invoke Tool: Trim

Dimensional Constraints:

Key Modifier Action
D Alt Distance
V Alt Vertical Distance
H Alt Horizontal Distance
A Alt Angle
O Alt Diameter
R Alt Radius

Geometric Constraints:

Key Modifier Action
C Shift Coincident
V Shift Vertical
H Shift Horizontal
E Shift Equal
P Shift Parallel
N Shift Perpendicular
T Shift Tangent
M Shift Midpoint
R Shift Ratio

Basic Tool Keymap

The basic tool interaction is consistent between tools.

Key Modifier Action
Tab - Jump to next tool state or property substate when in numerical edit
0-9 / (-) - Activate numeric edit
Enter / Lmb - Verify the operation
Esc / Rmb - Cancel the operation

While numeric edit is active

Key Modifier Action
Tab - Jump to next tool property substate
0-9 - Activate numeric edit
Minus(-) - Toggle between positive and negative values

Selection tools

Bases: Operator, HighlightElement

Select an entity

Either the entity specified by the index property or the hovered index if the index property is not set

Source code in operators/select.py
13
14
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
42
43
44
45
46
47
48
49
50
51
52
class View3D_OT_slvs_select(Operator, HighlightElement):
    """
    Select an entity

    Either the entity specified by the index property or the hovered index
    if the index property is not set

    """

    bl_idname = Operators.Select
    bl_label = "Select Sketch Entities"

    index: IntProperty(name="Index", default=-1)
    mode: mode_property

    def execute(self, context: Context):
        index = (
            self.index
            if self.properties.is_property_set("index")
            else global_data.hover
        )
        hit = index != -1
        mode = self.mode

        if mode == "SET" or not hit:
            deselect_all(context)

        if hit:
            entity = context.scene.sketcher.entities.get(index)

            value = True
            if mode == "SUBTRACT":
                value = False
            if mode == "TOGGLE":
                value = not entity.selected

            entity.selected = value

        context.area.tag_redraw()
        return {"FINISHED"}

Bases: Operator

Select / Deselect all entities

Source code in operators/select.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class View3D_OT_slvs_select_all(Operator):
    """Select / Deselect all entities"""

    bl_idname = Operators.SelectAll
    bl_label = "Select / Deselect Entities"

    deselect: BoolProperty(name="Deselect")

    def execute(self, context: Context):
        if self.deselect:
            deselect_all(context)
        else:
            select_all(context)
        context.area.tag_redraw()
        return {"FINISHED"}

Bases: Operator

Invert entities selection

Source code in operators/select.py
72
73
74
75
76
77
78
79
80
81
class View3D_OT_slvs_select_invert(Operator):
    """Invert entities selection"""

    bl_idname = Operators.SelectInvert
    bl_label = "Invert entities selection"

    def execute(self, context: Context):
        select_invert(context)
        context.area.tag_redraw()
        return {"FINISHED"}

Bases: Operator

Select neighbour entities

Source code in operators/select.py
84
85
86
87
88
89
90
91
92
93
class View3D_OT_slvs_select_extend(Operator):
    """Select neighbour entities"""

    bl_idname = Operators.SelectExtend
    bl_label = "Select neighbour entities"

    def execute(self, context: Context):
        select_extend(context)
        context.area.tag_redraw()
        return {"FINISHED"}

Bases: Operator

Select neighbour entities

Source code in operators/select.py
 96
 97
 98
 99
100
101
102
103
104
105
106
class View3D_OT_slvs_select_extend_all(Operator):
    """Select neighbour entities"""

    bl_idname = Operators.SelectExtendAll
    bl_label = "Select neighbour entities"

    def execute(self, context: Context):
        while select_extend(context):
            pass
        context.area.tag_redraw()
        return {"FINISHED"}

Keymap:

Key Modifier Action
LMB - Toggle Select
ESC - Deselect All
I Ctrl Inverse selection
E Ctrl Extend selection in chain
E Ctrl+Shift Select full chain

INFO: LMB in empty space will also deselect all.

INFO: Chain selection works with coincident constraints too

Bases: Operator, Operator3d

Add a point in 3d space

Source code in operators/add_point_3d.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
42
43
44
45
46
47
48
49
50
class View3D_OT_slvs_add_point3d(Operator, Operator3d):
    """Add a point in 3d space"""

    bl_idname = Operators.AddPoint3D
    bl_label = "Add Solvespace 3D Point"
    bl_options = {"REGISTER", "UNDO"}

    p3d_state1_doc = ("Location", "Set point's location.")

    location: FloatVectorProperty(name="Location", subtype="XYZ", precision=5)

    states = (
        state_from_args(
            p3d_state1_doc[0],
            description=p3d_state1_doc[1],
            property="location",
        ),
    )

    def main(self, context: Context):
        self.target = context.scene.sketcher.entities.add_point_3d(self.location)
        if context.scene.sketcher.use_construction:
            self.target.construction = True

        # Store hovered entity to use for auto-coincident since it doesn't get
        # stored for non-interactive tools
        hovered = global_data.hover
        if self._check_constrain(context, hovered):
            self.state_data["hovered"] = hovered

        self.add_coincident(context, self.target, self.state, self.state_data)
        return True

    def fini(self, context: Context, succeede: bool):
        if hasattr(self, "target"):
            logger.debug("Add: {}".format(self.target))

Bases: Operator, Operator3d

Add a line in 3d space

Source code in operators/add_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
class View3D_OT_slvs_add_line3d(Operator, Operator3d):
    """Add a line in 3d space"""

    bl_idname = Operators.AddLine3D
    bl_label = "Add Solvespace 3D Line"
    bl_options = {"REGISTER", "UNDO"}

    l3d_state1_doc = ("Startpoint", "Pick or place line's starting point.")
    l3d_state2_doc = ("Endpoint", "Pick or place line's ending point.")

    continuous_draw: BoolProperty(name="Continuous Draw", default=True)

    states = (
        state_from_args(
            l3d_state1_doc[0],
            description=l3d_state1_doc[1],
            pointer="p1",
            types=types_point_3d,
        ),
        state_from_args(
            l3d_state2_doc[0],
            description=l3d_state2_doc[1],
            pointer="p2",
            types=types_point_3d,
            interactive=True,
        ),
    )

    def main(self, context: Context):
        p1, p2 = self.get_point(context, 0), self.get_point(context, 1)

        self.target = context.scene.sketcher.entities.add_line_3d(p1, p2)
        if context.scene.sketcher.use_construction:
            self.target.construction = True
        ignore_hover(self.target)
        return True

    def continue_draw(self):
        last_state = self._state_data[1]
        if last_state["is_existing_entity"]:
            return False

        # also not when last state has coincident constraint
        if last_state.get("coincident"):
            return False
        return True

    def fini(self, context: Context, succeede: bool):
        if hasattr(self, "target"):
            logger.debug("Add: {}".format(self.target))

        if succeede:
            if self.has_coincident():
                solve_system(context)

Bases: Operator, Operator2d

Add a point to the active sketch

Source code in operators/add_point_2d.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
47
48
49
50
51
52
53
54
55
56
57
58
class View3D_OT_slvs_add_point2d(Operator, Operator2d):
    """Add a point to the active sketch"""

    bl_idname = Operators.AddPoint2D
    bl_label = "Add Solvespace 2D Point"
    bl_options = {"REGISTER", "UNDO"}

    p2d_state1_doc = ("Coordinates", "Set point's coordinates on the sketch.")

    coordinates: FloatVectorProperty(name="Coordinates", size=2, precision=5)

    states = (
        state_from_args(
            p2d_state1_doc[0],
            description=p2d_state1_doc[1],
            property="coordinates",
        ),
    )

    def main(self, context: Context):
        sketch = self.sketch
        self.target = context.scene.sketcher.entities.add_point_2d(
            self.coordinates, sketch
        )
        if context.scene.sketcher.use_construction:
            self.target.construction = True

        # Store hovered entity to use for auto-coincident since it doesn't get
        # stored for non-interactive tools
        hovered = global_data.hover
        if self._check_constrain(context, hovered):
            self.state_data["hovered"] = hovered

        self.add_coincident(context, self.target, self.state, self.state_data)
        return True

    def fini(self, context: Context, succeede: bool):
        if hasattr(self, "target"):
            logger.debug("Add: {}".format(self.target))

        if succeede:
            if self.has_coincident():
                solve_system(context, sketch=self.sketch)

Bases: Operator, Operator2d

Add a line to the active sketch

Source code in operators/add_line_2d.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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class View3D_OT_slvs_add_line2d(Operator, Operator2d):
    """Add a line to the active sketch"""

    bl_idname = Operators.AddLine2D
    bl_label = "Add Solvespace 2D Line"
    bl_options = {"REGISTER", "UNDO"}

    l2d_state1_doc = ("Startpoint", "Pick or place line's starting Point.")
    l2d_state2_doc = ("Endpoint", "Pick or place line's ending Point.")

    continuous_draw: BoolProperty(name="Continuous Draw", default=True)

    states = (
        state_from_args(
            l2d_state1_doc[0],
            description=l2d_state1_doc[1],
            pointer="p1",
            types=types_point_2d,
        ),
        state_from_args(
            l2d_state2_doc[0],
            description=l2d_state2_doc[1],
            pointer="p2",
            types=types_point_2d,
            interactive=True,
        ),
    )

    def main(self, context: Context):
        wp = self.sketch.wp
        p1, p2 = self.get_point(context, 0), self.get_point(context, 1)

        self.target = context.scene.sketcher.entities.add_line_2d(p1, p2, self.sketch)
        if context.scene.sketcher.use_construction:
            self.target.construction = True

        # auto vertical/horizontal constraint
        self.has_alignment = False
        constraints = context.scene.sketcher.constraints
        vec_dir = self.target.direction_vec()
        if vec_dir.length:
            angle = vec_dir.angle(Vector((1, 0)))

            threshold = 0.1
            if angle < threshold or angle > HALF_TURN - threshold:
                constraints.add_horizontal(self.target, sketch=self.sketch)
                self.has_alignment = True
            elif (QUARTER_TURN - threshold) < angle < (QUARTER_TURN + threshold):
                constraints.add_vertical(self.target, sketch=self.sketch)
                self.has_alignment = True

        ignore_hover(self.target)
        return True

    def continue_draw(self):
        last_state = self._state_data[1]
        if last_state["is_existing_entity"]:
            return False

        # also not when last state has coincident constraint
        if last_state.get("coincident"):
            return False
        return True

    def fini(self, context: Context, succeede: bool):
        if hasattr(self, "target"):
            logger.debug("Add: {}".format(self.target))

        if succeede:
            if self.has_coincident() or self.has_alignment:
                solve_system(context, sketch=self.sketch)

Bases: Operator, Operator2d

Add a circle to the active sketch

Source code in operators/add_circle.py
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
class View3D_OT_slvs_add_circle2d(Operator, Operator2d):
    """Add a circle to the active sketch"""

    bl_idname = Operators.AddCircle2D
    bl_label = "Add Solvespace 2D Circle"
    bl_options = {"REGISTER", "UNDO"}

    circle_state1_doc = ("Center", "Pick or place circle's center point.")
    circle_state2_doc = ("Radius", "Set circle's radius.")

    radius: FloatProperty(
        name="Radius",
        subtype="DISTANCE",
        unit="LENGTH",
        precision=5,
        # precision=get_prefs().decimal_precision,
    )

    states = (
        state_from_args(
            circle_state1_doc[0],
            description=circle_state1_doc[1],
            pointer="ct",
            types=types_point_2d,
        ),
        state_from_args(
            circle_state2_doc[0],
            description=circle_state2_doc[1],
            property="radius",
            state_func="get_radius",
            interactive=True,
            allow_prefill=False,
        ),
    )

    def get_radius(self, context: Context, coords):
        wp = self.sketch.wp
        pos = get_pos_2d(context, wp, coords)
        delta = Vector(pos) - self.ct.co
        radius = delta.length
        return radius

    def main(self, context: Context):
        wp = self.sketch.wp
        ct = self.get_point(context, 0)
        self.target = context.scene.sketcher.entities.add_circle(
            wp.nm, ct, self.radius, self.sketch
        )
        if context.scene.sketcher.use_construction:
            self.target.construction = True
        ignore_hover(self.target)
        return True

    def fini(self, context: Context, succeede: bool):
        if hasattr(self, "target"):
            logger.debug("Add: {}".format(self.target))

        if succeede:
            if self.has_coincident():
                solve_system(context, sketch=self.sketch)

Bases: Operator, Operator2d

Add an arc to the active sketch

Source code in operators/add_arc.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
 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
class View3D_OT_slvs_add_arc2d(Operator, Operator2d):
    """Add an arc to the active sketch"""

    bl_idname = Operators.AddArc2D
    bl_label = "Add Solvespace 2D Arc"
    bl_options = {"REGISTER", "UNDO"}

    arc_state1_doc = ("Center", "Pick or place center point.")
    arc_state2_doc = ("Startpoint", "Pick or place starting point.")
    arc_state3_doc = ("Endpoint", "Pick or place ending point.")

    states = (
        state_from_args(
            arc_state1_doc[0],
            description=arc_state1_doc[1],
            pointer="ct",
            types=types_point_2d,
        ),
        state_from_args(
            arc_state2_doc[0],
            description=arc_state2_doc[1],
            pointer="p1",
            types=types_point_2d,
            allow_prefill=False,
        ),
        state_from_args(
            arc_state3_doc[0],
            description=arc_state3_doc[1],
            pointer="p2",
            types=types_point_2d,
            state_func="get_endpoint_pos",
            interactive=True,
        ),
    )

    def get_endpoint_pos(self, context: Context, coords):
        mouse_pos = get_pos_2d(context, self.sketch.wp, coords)
        if mouse_pos is None:
            return None

        # Get angle to mouse pos
        ct = self.get_point(context, 0).co
        x, y = Vector(mouse_pos) - ct
        angle = math.atan2(y, x)

        # Get radius from distance ct - p1
        p1 = self.get_point(context, 1).co
        radius = (p1 - ct).length
        pos = pol2cart(radius, angle) + ct
        return pos

    def solve_state(self, context: Context, _event: Event):
        sketch = context.scene.sketcher.active_sketch
        solve_system(context, sketch=sketch)
        return True

    def main(self, context):
        ct, p1, p2 = (
            self.get_point(context, 0),
            self.get_point(context, 1),
            self.get_point(context, 2),
        )
        sketch = self.sketch
        sse = context.scene.sketcher.entities
        arc = sse.add_arc(sketch.wp.nm, ct, p1, p2, sketch)

        center = ct.co
        start = p1.co - center
        end = p2.co - center
        a = end.angle_signed(start)
        arc.invert_direction = a < 0

        ignore_hover(arc)
        self.target = arc
        if context.scene.sketcher.use_construction:
            self.target.construction = True
        return True

    def fini(self, context: Context, succeede: bool):
        if hasattr(self, "target"):
            logger.debug("Add: {}".format(self.target))
            self.solve_state(context, self.sketch)

Bases: Operator, Operator2d

Add a rectangle to the active sketch

Source code in operators/add_rectangle.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
 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
class View3D_OT_slvs_add_rectangle(Operator, Operator2d):
    """Add a rectangle to the active sketch"""

    bl_idname = Operators.AddRectangle
    bl_label = "Add Rectangle"
    bl_options = {"REGISTER", "UNDO"}

    rect_state1_doc = ("Startpoint", "Pick or place starting point.")
    rect_state2_doc = ("Endpoint", "Pick or place ending point.")

    states = (
        state_from_args(
            rect_state1_doc[0],
            description=rect_state1_doc[1],
            pointer="p1",
            types=types_point_2d,
        ),
        state_from_args(
            rect_state2_doc[0],
            description=rect_state2_doc[1],
            pointer="p2",
            types=types_point_2d,
            interactive=True,
            create_element="create_point",
        ),
    )

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

        p1, p2 = self.get_point(context, 0), self.get_point(context, 1)
        p_lb, p_rt = p1, p2

        p_rb = sse.add_point_2d((p_rt.co.x, p_lb.co.y), sketch)
        p_lt = sse.add_point_2d((p_lb.co.x, p_rt.co.y), sketch)

        if context.scene.sketcher.use_construction:
            p_lb.construction = True
            p_rb.construction = True
            p_rt.construction = True
            p_lt.construction = True

        lines = []
        points = (p_lb, p_rb, p_rt, p_lt)
        for i, start in enumerate(points):
            end = points[i + 1 if i < len(points) - 1 else 0]

            line = sse.add_line_2d(start, end, sketch)
            if context.scene.sketcher.use_construction:
                line.construction = True
            lines.append(line)

        self.lines = lines

        for e in (*points, *lines):
            ignore_hover(e)
        return True

    def fini(self, context: Context, succeede: bool):
        if hasattr(self, "lines") and self.lines:
            ssc = context.scene.sketcher.constraints
            for i, line in enumerate(self.lines):
                func = ssc.add_horizontal if (i % 2) == 0 else ssc.add_vertical
                func(line, sketch=self.sketch)

            data = self._state_data.get(1)
            if data.get("is_numeric_edit", False):
                input = data.get("numeric_input")

                # constrain distance
                startpoint = getattr(self, self.get_states()[0].pointer)
                for val, line in zip(input, (self.lines[1], self.lines[2])):
                    if val is None:
                        continue
                    ssc.add_distance(
                        startpoint,
                        line,
                        sketch=self.sketch,
                        init=True,
                    )

        if succeede:
            if self.has_coincident():
                solve_system(context, sketch=self.sketch)

    def create_point(self, context: Context, values, state, state_data):
        value = values[0]

        if state_data.get("is_numeric_edit", False):
            data = self._state_data.get(1)
            input = data.get("numeric_input")
            # use relative coordinates
            orig = getattr(self, self.get_states()[0].pointer).co

            for i, val in enumerate(input):
                if val is None:
                    continue
                value[i] = orig[i] + val

        sse = context.scene.sketcher.entities
        point = sse.add_point_2d(value, self.sketch)
        ignore_hover(point)
        if context.scene.sketcher.use_construction:
            point.construction = True

        self.add_coincident(context, point, state, state_data)
        state_data["type"] = SlvsPoint2D
        return point.slvs_index

Bases: Operator, Operator3d

Add a workplane

Source code in operators/add_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
class View3D_OT_slvs_add_workplane(Operator, Operator3d):
    """Add a workplane"""

    bl_idname = Operators.AddWorkPlane
    bl_label = "Add Solvespace Workplane"
    bl_options = {"REGISTER", "UNDO"}

    wp_state1_doc = ("Origin", "Pick or place workplanes's origin.")
    wp_state2_doc = ("Orientation", "Set workplane's orientation.")

    states = (
        state_from_args(
            wp_state1_doc[0],
            description=wp_state1_doc[1],
            pointer="p1",
            types=types_point_3d,
        ),
        state_from_args(
            wp_state2_doc[0],
            description=wp_state2_doc[1],
            state_func="get_orientation",
            pointer="nm",
            types=NORMAL3D,
            interactive=True,
            create_element="create_normal3d",
        ),
    )

    def get_normal(self, context: Context, index: int):
        states = self.get_states_definition()
        state = states[index]
        data = self._state_data[index]
        type = data["type"]
        sse = context.scene.sketcher.entities

        if type == bpy.types.MeshPolygon:
            ob_name, nm_index = self.get_state_pointer(index=index, implicit=True)
            ob = bpy.data.objects[ob_name]
            return sse.add_ref_normal_3d(ob, nm_index)
        return getattr(self, state.pointer)

    def get_orientation(self, context: Context, coords):
        # TODO: also support edges
        data = self.state_data
        ob, type, index = get_mesh_element(context, coords, edge=False, face=True)

        p1 = self.get_point(context, 0)
        mousepos = get_placement_pos(context, coords)
        vec = mousepos - p1.location
        return global_data.Z_AXIS.rotation_difference(vec).to_euler()

    def create_normal3d(self, context: Context, values, state, state_data):
        sse = context.scene.sketcher.entities

        v = values[0].to_quaternion()
        nm = sse.add_normal_3d(v)
        state_data["type"] = SlvsNormal3D
        return nm.slvs_index

    def main(self, context: Context):
        sse = context.scene.sketcher.entities
        p1 = self.get_point(context, 0)
        nm = self.get_normal(context, 1)
        self.target = sse.add_workplane(p1, nm)
        ignore_hover(self.target)
        return True

    def fini(self, context: Context, succeede: bool):
        if hasattr(self, "target"):
            logger.debug("Add: {}".format(self.target))

        if succeede:
            if self.has_coincident():
                solve_system(context)

Bases: Operator, Operator3d

Add a statically placed workplane, orientation and location is copied from selected mesh face

Source code in operators/add_workplane.py
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
class View3D_OT_slvs_add_workplane_face(Operator, Operator3d):
    """Add a statically placed workplane, orientation and location is copied from selected mesh face"""

    bl_idname = Operators.AddWorkPlaneFace
    bl_label = "Add Solvespace Workplane"
    bl_options = {"REGISTER", "UNDO"}

    wp_face_state1_doc = (
        "Face",
        "Pick a mesh face to use as workplanes's transformation.",
    )

    states = (
        state_from_args(
            wp_face_state1_doc[0],
            description=wp_face_state1_doc[1],
            use_create=False,
            pointer="face",
            types=(bpy.types.MeshPolygon,),
            interactive=True,
        ),
    )

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

        ob_name, face_index = self.get_state_pointer(index=0, implicit=True)
        ob = get_evaluated_obj(context, bpy.data.objects[ob_name])
        mesh = ob.data
        face = mesh.polygons[face_index]

        mat_obj = ob.matrix_world
        quat = get_face_orientation(mesh, face)
        quat.rotate(mat_obj)
        pos = mat_obj @ face.center
        origin = sse.add_point_3d(pos)
        nm = sse.add_normal_3d(quat)

        self.target = sse.add_workplane(origin, nm)
        ignore_hover(self.target)
        return True

Bases: Operator, Operator2d

Trim segment to its closest intersections

Source code in operators/trim.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
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
class View3D_OT_slvs_trim(Operator, Operator2d):
    """Trim segment to its closest intersections"""

    bl_idname = Operators.Trim
    bl_label = "Trim Segment"
    bl_options = {"REGISTER", "UNDO"}

    trim_state1_doc = ("Segment", "Segment to trim.")

    states = (
        state_from_args(
            trim_state1_doc[0],
            description=trim_state1_doc[1],
            pointer="segment",
            types=SEGMENT,
            pick_element="pick_element_coords",
            use_create=False,
            # interactive=True
        ),
    )

    # TODO: Disable execution based on selection
    # NOTE: That does not work if run with select -> action
    def pick_element_coords(self, context, coords):
        data = self.state_data
        data["mouse_pos"] = get_pos_2d(context, self.sketch.wp, coords)
        return super().pick_element(context, coords)

    def main(self, context: Context):
        return True

    def fini(self, context: Context, succeede: bool):
        if not succeede:
            return False

        sketch = context.scene.sketcher.active_sketch
        segment = self.segment

        mouse_pos = self._state_data[0].get("mouse_pos")
        if mouse_pos is None:
            return False

        trim = TrimSegment(segment, mouse_pos)

        # Find intersections
        for e in sketch.sketch_entities(context):
            if not e.is_segment():
                continue
            if e == segment:
                continue

            for co in segment.intersect(e):
                # print("intersect", co)
                trim.add(e, co)

        # Find points that are connected to the segment through a coincident constraint
        for c in (
            *context.scene.sketcher.constraints.coincident,
            *context.scene.sketcher.constraints.midpoint,
        ):
            ents = c.entities()
            if segment not in ents:
                continue
            p = ents[0]
            trim.add(c, p.co)

        # TODO: Get rid of the coincident constraint as it will be a shared connection point

        if not trim.check():
            return

        trim.replace(context)
        refresh(context)