FreeCAD-macros
236 строк · 8.5 Кб
1# -*- coding: utf-8 -*-
2# view loacal axis of selected datum plane macro
3# Author: Avinash Pudale
4# License: LGPL v 2.1
5
6from __future__ import annotations
7
8__Name__ = 'Datum-Plane Local Axis'
9__Comment__ = 'Select a datum plane in the 3D view then run, this macro will shows local axes for all selected planes or delete all created local axes if nothing is selected'
10__Author__ = 'Avinash Pudale'
11__Date__ = '2023-08-11'
12__Version__ = '0.3.0'
13__License__ = 'LGPL-2.1'
14__Web__ = 'https://forum.freecad.org/viewtopic.php?t=79562'
15__Wiki__ = ''
16__Icon__ = 'DatumPlaneLocalAxis.svg'
17__Xpm__ = ''
18__Help__ = 'The macro will add small X-, Y-, and Z-axis representations on the selected datum planes in the 3D view. The X-axis is represented in red, the Y-axis in green, and the Z-axis in blue. To clear the axis representations, simply run the macro again without selecting anything.'
19__Status__ = 'Stable'
20__Requires__ = 'FreeCAD >=0.20'
21__Communication__ = 'https://github.com/FabLabBaramati/freecadDatumLoacalAxisMacro/issues'
22__Files__ = 'DatumPlaneLocalAxis.svg'
23
24
25import FreeCAD as app
26import FreeCADGui as gui
27
28import Draft
29
30# Typing hints.
31DO = app.DocumentObject
32PL = DO # A `PartDesign::Plane` or a `Part::Link` to a `PartDesign::Plane`.
33
34# The 3 axes representing a plane will be inside a group that
35# starts with this label.
36GROUP_LABEL = 'Datum_Plane_Axis'
37
38
39def strip_subelement(sub_fullpath: str) -> str:
40"""Return sub_fullpath without the last sub-element.
41
42A sub-element is a face, edge or vertex.
43Parameters
44----------
45- subobject_fullpath: SelectionObject.SubElementNames[i], where
46SelectionObject is obtained with gui.Selection.getSelectionEx('', 0)
47(i.e. not gui.Selection.getSelectionEx()).
48Examples:
49- 'Face6' if you select the top face of a cube solid made in Part.
50- 'Body.Box001.' if you select the tip of a Part->Body->"additive
51primitve" in PartDesign.
52- 'Body.Box001.Face6' if you select the top face of a Part->Body->
53"additive primitve" in PartDesign.
54
55"""
56if (not sub_fullpath) or ('.' not in sub_fullpath):
57return ''
58return sub_fullpath.rsplit('.', maxsplit=1)[0]
59
60
61def get_subobject_by_name(object_: app.DocumentObject,
62subobject_name: str,
63) -> app.DocumentObject:
64"""Return the appropriate object from object_.OutListRecursive."""
65for o in object_.OutListRecursive:
66if o.Name == subobject_name:
67return o
68
69
70def get_subobjects_by_full_name(
71root_object: app.DocumentObject,
72subobject_fullpath: str,
73) -> list[app.DocumentObject]:
74"""Return the list of objects from root_object excluded to the named object.
75
76The last part of ``subobject_fullpath`` is then a specific vertex, edge or
77face and is ignored.
78So, subobject_fullpath has the form 'name0.name1.Edge001', for example; in
79this case, the returned objects are
80[object_named_name0, object_named_name1].
81
82Parameters
83----------
84- root_object: SelectionObject.Object, where SelectionObject is obtained
85with gui.Selection.getSelectionEx('', 0)
86(i.e. not gui.Selection.getSelectionEx()).
87- subobject_fullpath: SelectionObject.SubElementNames[i].
88Examples:
89- 'Face6' if you select the top face of a cube solid made in Part.
90- 'Body.Box001.' if you select the tip of a Part->Body->"additive
91primitve" in PartDesign.
92- 'Body.Box001.Face6' if you select the top face of a Part->Body->
93"additive primitve" in PartDesign.
94
95"""
96objects = []
97names = strip_subelement(subobject_fullpath).split('.')
98subobject = root_object
99for name in names:
100subobject = get_subobject_by_name(subobject, name)
101if subobject is None:
102# This should not append.
103return []
104objects.append(subobject)
105return objects
106
107
108def get_global_placement_expression(
109root_object: app.DocumentObject,
110subobject_fullpath: str,
111) -> str:
112"""Return the global placement by recursively going through parents.
113
114Return the expression to compute the global placement of `root_object`.
115
116Parameters
117----------
118- root_object: SelectionObject.Object, where SelectionObject is obtained
119with gui.Selection.getSelectionEx('', 0)
120(i.e. not gui.Selection.getSelectionEx()).
121- subobject_fullpath: SelectionObject.SubElementNames[i].
122Examples:
123- 'Face6' if you select the top face of a cube solid made in Part.
124- 'Body.Box001.' if you select the tip of a Part->Body->"additive
125primitve" in PartDesign.
126- 'Body.Box001.Face6' if you select the top face of a Part->Body->
127"additive primitve" in PartDesign.
128
129"""
130objects = get_subobjects_by_full_name(root_object, subobject_fullpath)
131expr = f'{root_object.Name}.Placement'
132for o in objects:
133expr += f' * {o.Name}.Placement'
134return expr
135
136
137def _add_local_axis_to_plane(
138plane_or_link: app.DocumentObject,
139placement_expression: str,
140) -> None:
141"""Add the local axis to the plane or link."""
142doc = plane_or_link.Document
143# Implementation note: returns self for non-links.
144plane = plane_or_link.getLinkedObject()
145
146# Create a group to contain the axis representations.
147group = doc.addObject('App::DocumentObjectGroup',
148f'{GROUP_LABEL}_{plane_or_link.Name}')
149
150# Size of the line representing an axis.
151size = 0.95 * min(plane.Length, plane.Width) / 2.0
152
153labels_points_colors = (
154('x', app.Vector(size, 0.0, 0.0), (1.0, 0.0, 0.0)), # Red.
155('y', app.Vector(0.0, size, 0.0), (0.0, 1.0, 0.0)), # Green.
156('z', app.Vector(0.0, 0.0, size), (0.0, 0.0, 1.0)), # Blue.
157)
158
159for label, point, color in labels_points_colors:
160# Create Draft objects for the axis.
161axis = Draft.makeLine(app.Vector(0.0, 0.0, 0.0), point)
162axis.Label = label
163
164# Set the placements for the axis representations
165axis.setExpression('Placement', placement_expression)
166axis.setPropertyStatus('Placement', ['ReadOnly'])
167
168axis.ViewObject.DisplayMode = 'Wireframe'
169
170axis.ViewObject.LineColor = color
171axis.ViewObject.PointColor = color
172
173# Add the axis representations as child objects to the group
174group.addObject(axis)
175
176
177def add_local_axis():
178doc = app.activeDocument()
179
180if not doc:
181return
182
183# Check if a datum plane is selected in the tree view.
184selection = gui.Selection.getSelectionEx('', 0)
185
186if not selection:
187# Delete all axis representations if no object is selected.
188objs_to_delete: list[DO] = []
189for obj in doc.Objects:
190if (obj.isDerivedFrom('App::DocumentObjectGroup')
191and obj.Label.startswith(GROUP_LABEL)):
192for child_obj in obj.Group:
193objs_to_delete.append(child_obj)
194objs_to_delete.append(obj)
195# Delete the groups and lines.
196for obj in objs_to_delete:
197doc.removeObject(obj.Name)
198doc.recompute()
199return
200
201# The list of selected planes (or links to a plane).
202planes_or_links: set[PL] = set()
203# For each plane, the expressions to place the axis representations.
204placement_expression: dict[PL] = {}
205for sel_obj in selection:
206obj = sel_obj.Object
207sub_fullpaths = sel_obj.SubElementNames
208if not sub_fullpaths:
209# Cannot be a datum plane because the plane itself must be
210# the last element of `sub_fullpaths`.
211continue
212sub_fullpath = sub_fullpaths[0]
213objects = get_subobjects_by_full_name(obj, sub_fullpath)
214if ((not objects)
215or (not hasattr(objects[-1], 'isDerivedFrom'))
216or (not hasattr(objects[-1], 'getLinkedObject'))):
217# Cannot be a datum plane.
218continue
219plane_or_link = objects[-1]
220# Implementation note: returns self for non-links.
221plane = plane_or_link.getLinkedObject()
222if not plane.isDerivedFrom('PartDesign::Plane'):
223continue
224planes_or_links.add(plane_or_link)
225placement_expression[plane_or_link] = get_global_placement_expression(
226obj, sub_fullpath)
227
228for plane_or_link in planes_or_links:
229_add_local_axis_to_plane(plane_or_link,
230placement_expression[plane_or_link])
231
232doc.recompute()
233
234
235if __name__ == '__main__':
236add_local_axis()
237