Macro Shake Sketch: Difference between revisions
Renatorivo (talk | contribs) mNo edit summary |
(Marked this version for translation) |
||
(27 intermediate revisions by 8 users not shown) | |||
Line 1: | Line 1: | ||
<languages/> |
|||
<translate> |
<translate> |
||
{{Macro|Icon=Text-x-python|Name=Macro Shake Sketch|Description=Shake a sketch in order to discover its unconstrained parts|Author=Gaël Ecorchard}} |
|||
<!--T:1--> |
|||
Shake a sketch in order to discover its unconstrained parts. Enter edit mode for a sketch and launch the macro. The macro will add a random noise on all sketch points. The sketch is then solved, constrained parts will retain their position, free parts will move. |
|||
{{Macro |
|||
|Name=Macro Shake Sketch |
|||
|Icon=Macro_Shake_Sketch.png |
|||
|Description=Shake a sketch in order to discover its unconstrained parts. Enter edit mode for a sketch and launch the macro. The macro will add a random displacement on all sketch points. The sketch is then solved, constrained parts will retain their position, free parts will move.<br/> {{ColoredText|#ff0000|#ffff00|Use the Undo (Ctrl+Z) and Redo (Ctrl+Y) button (labelled "Shake Sketch") or run this macro on a copy of the original sketch.}} |
|||
|Author=Gaël Ecorchard, heda |
|||
|Version=1.3 |
|||
|Date=2022-06-01 |
|||
|FCVersion=All |
|||
|Download=[https://www.freecadweb.org/wiki/images/4/4a/Macro_Shake_Sketch.png ToolBar Icon] |
|||
}} |
|||
==Description== <!--T:5--> |
|||
<!--T:9--> |
|||
===Script=== |
|||
Shake a sketch in order to discover its unconstrained parts. |
|||
<!--T:2--> |
|||
Enter edit mode for a sketch and launch the macro. The macro will add a random displacement on all sketch points. The sketch is then solved, constrained parts will retain their position, free parts will move. Always run this macro on a copy of the original sketch. |
|||
<!--T:4--> |
|||
Restore the original Sketch with the '''Undo (Ctrl+Z)''' and '''Redo (Ctrl+Y)''' button (labeled "Shake Sketch") '''or be careful working on a copy of your file because the macro "dismantles all" to display and you may need to start over.''' |
|||
==Install== <!--T:7--> |
|||
<!--T:8--> |
|||
Available in Addon manager, or manual install. |
|||
==Script== <!--T:3--> |
|||
<!--T:6--> |
|||
ToolBar Icon |
|||
</translate> |
</translate> |
||
[[Image:Macro_Shake_Sketch.png]] |
|||
Macro Shake_Sketch.py |
|||
'''Macro Shake_Sketch.py''' |
|||
<!--DO NOT CHANGE THE <pre> </pre> TAG, code contains pipe or space character which breaks the wiki macro--> |
|||
<syntaxhighlight> |
|||
<pre> |
|||
# -*- coding: utf-8 -*- |
# -*- coding: utf-8 -*- |
||
Line 16: | Line 47: | ||
# A Gaussian noise is introduced in all sketch points and the sketch is then |
# A Gaussian noise is introduced in all sketch points and the sketch is then |
||
# solved. |
# solved. |
||
# Enter edit mode for a sketch and launch the macro. |
|||
# Beware that the sketch can look different because some constraints have |
# Beware that the sketch can look different because some constraints have |
||
# several solutions. In this case, just undo. |
# several solutions. In this case, just undo. |
||
# |
# |
||
# This file is released under the MIT License. |
# This file is released under the MIT License. |
||
# Author: Gaël Ecorchard |
# Author: Gaël Ecorchard v1.0 & v1.1 |
||
# Author: heda v1.2 |
|||
# Version: 1.0, 2014-08, first release. |
|||
# Version: |
|||
# - 1.3: 2022-06-01 |
|||
# * added openTransaction(u"Shake Sketch") for restore the original Sketch |
|||
# - 1.2: 2021-10-22 |
|||
# * made macro runnable for current versions of fc |
|||
# * added Part.LineSegment |
|||
# * added start info & result dialogue |
|||
# * hides constraints during shake |
|||
# * added simple debug printing |
|||
# - 1.1: 2014-10-31 |
|||
# * correct import for Part |
|||
# - 1.0: 2014-08 |
|||
# * first release |
|||
# Amplitude of the point displacements. |
# Amplitude of the point displacements. |
||
# The standard deviation of the Gaussian noise is the largest sketch dimension |
# The standard deviation of the Gaussian noise is the largest sketch dimension |
||
# multiplied by this factor. |
# multiplied by this factor. |
||
__title__ = 'Sketch Shaker' |
|||
__version__ = '1.3' |
|||
__date__ = '2022/06/01' |
|||
__icon__ = 'https://wiki.freecadweb.org/images/4/4a/Macro_Shake_Sketch.png' |
|||
displacement_amplitude = 0.1 |
displacement_amplitude = 0.1 |
||
debug_print = False |
|||
# End of configuration. |
# End of configuration. |
||
from random import gauss |
from random import gauss |
||
from PySide.QtGui import QMessageBox |
|||
import FreeCADGui as Gui |
import FreeCADGui as Gui |
||
from FreeCAD import Base |
from FreeCAD import Base |
||
import Part |
|||
print('Running Macro ' + __title__ + ' ver: ' + __version__) |
|||
FreeCAD.ActiveDocument.openTransaction(u"Shake Sketch") # memorise les actions (avec annuler restore) |
|||
# For each sketch geometry type, map a list of points to move. |
# For each sketch geometry type, map a list of points to move. |
||
g_geom_points = { |
|||
Base.Vector: [1], |
|||
Part.Line: [1, 2], # first point, last point |
|||
Part.LineSegment: [1, 2], # first point, last point |
|||
Part.Circle: [0, 3], # curve, center |
|||
Part.ArcOfCircle: [1, 2, 3], # first point, last point, center |
|||
} |
|||
} # moves bsplines and conics via lines and circles, no op for Part.Points |
|||
def dprint(msg, *args): |
|||
class BoundingBox(object): |
|||
if debug_print: |
|||
if args: |
|||
print(msg.format(*args)) |
|||
ymin = None |
|||
else: |
|||
print(msg) |
|||
class BoundingBox: |
|||
xmin = xmax = ymin = ymax = None |
|||
def enlarge_x(self, x): |
def enlarge_x(self, x): |
||
if self.xmin is None: |
if self.xmin is None: |
||
self.xmin = self.xmax = x |
|||
elif self.xmin > x: |
|||
self.xmin = x |
self.xmin = x |
||
elif self.xmax < x: |
|||
self.xmax = x |
self.xmax = x |
||
return |
|||
if self.xmin > x: |
|||
self.xmin = x |
|||
return |
|||
if self.xmax < x: |
|||
self.xmax = x |
|||
return |
|||
def enlarge_y(self, y): |
def enlarge_y(self, y): |
||
if self.ymin is None: |
if self.ymin is None: |
||
self.ymin = self.ymax = y |
|||
elif self.ymin > y: |
|||
self.ymin = y |
self.ymin = y |
||
elif self.ymax < y: |
|||
self.ymax = y |
self.ymax = y |
||
return |
|||
if self.ymin > y: |
|||
self.ymin = y |
|||
return |
|||
if self.ymax < y: |
|||
self.ymax = y |
|||
return |
|||
def enlarge_point(self, point): |
def enlarge_point(self, point): |
||
Line 103: | Line 154: | ||
if isinstance(geom, Base.Vector): |
if isinstance(geom, Base.Vector): |
||
bbox.enlarge_point(geom) |
bbox.enlarge_point(geom) |
||
elif isinstance(geom, Part.Line): |
elif isinstance(geom, (Part.Line, Part.LineSegment)): |
||
bbox.enlarge_line(geom) |
bbox.enlarge_line(geom) |
||
elif isinstance(geom, Part.Circle): |
elif isinstance(geom, Part.Circle): |
||
Line 109: | Line 160: | ||
elif isinstance(geom, Part.ArcOfCircle): |
elif isinstance(geom, Part.ArcOfCircle): |
||
bbox.enlarge_arc_of_circle(geom) |
bbox.enlarge_arc_of_circle(geom) |
||
if (bbox.xmin |
if None in (bbox.xmin, bbox.ymin): |
||
dprint('sketch bbox not found') |
|||
else: |
|||
return 0, 0 |
return 0, 0 |
||
else: |
|||
dprint('sketch bbox found') |
|||
return bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin |
|||
def add_noise(point, sigma): |
def add_noise(point, sigma): |
||
"""Add a Gaussian noise with standard deviation sigma""" |
"""Add a Gaussian noise with standard deviation sigma""" |
||
dprint(' x0:{:>9.3f} y0:{:>9.3f}', point.x, point.y) |
|||
point.x = gauss(point.x, sigma) |
point.x = gauss(point.x, sigma) |
||
point.y = gauss(point.y, sigma) |
point.y = gauss(point.y, sigma) |
||
dprint(' xt:{:>9.3f} yt:{:>9.3f}', point.x, point.y) |
|||
def move_points(sketch, geom_index |
def move_points(sketch, geom_index, sigma): |
||
point_indexes = g_geom_points.get(type(sketch.Geometry[i]), []) |
|||
# Direct access to sketch.Geometry[index] does not work. This would, |
# Direct access to sketch.Geometry[index] does not work. This would, |
||
# however prevent repeated recompute. |
# however prevent repeated recompute. |
||
# not checked validity of comment for v1.2 |
|||
moved = False |
|||
for point_index in point_indexes: |
for point_index in point_indexes: |
||
dprint('---- geo idx [{:>3} ] -- pt idx [{:>3} ] ----', |
|||
geom_index, point_index) |
|||
point = sketch.getPoint(geom_index, point_index) |
point = sketch.getPoint(geom_index, point_index) |
||
add_noise(point, sigma) |
add_noise(point, sigma) |
||
try: |
|||
sketch.movePoint(geom_index, point_index, point) |
|||
sketch.movePoint(geom_index, point_index, point) |
|||
except ValueError as e: |
|||
dprint(repr(e)) |
|||
new_pos = sketch.getPoint(geom_index, point_index) |
|||
test = point.isEqual(new_pos, 0) |
|||
dprint(' did it move? {}', test) |
|||
if test: moved = True |
|||
return moved |
|||
def toggle_constraints(sketch, toggle): |
|||
for cid in toggle: |
|||
sketch.toggleVirtualSpace(cid) |
|||
view_provider = Gui.activeDocument().getInEdit() |
view_provider = Gui.activeDocument().getInEdit() |
||
shake_it = False |
|||
# Don't know how to exit from a macro. |
|||
do_move = True |
|||
if not view_provider: |
if not view_provider: |
||
msg = 'A sketch needs to be in edit to be shaken.' |
|||
do_move = False |
|||
print(msg) |
|||
_ = QMessageBox.information(None, __title__, msg) |
|||
if do_move: |
|||
else: |
|||
sketch = view_provider.Object |
sketch = view_provider.Object |
||
if sketch.TypeId == 'Sketcher::SketchObject': |
|||
sketch.recompute() # ensure update |
|||
to_virtual = [cid for cid, cts in enumerate(sketch.Constraints) |
|||
if cts.InVirtualSpace] |
|||
sketch_span = get_sketch_dims(sketch) |
|||
sigma = max(sketch_span) * displacement_amplitude |
|||
dprint('sketch span: dx:{:>9.3f} dy:{:>9.3f}', *sketch_span) |
|||
dprint('sigma for gauss-dist: {:.3f}', sigma) |
|||
toggle_constraints(sketch, to_virtual) |
|||
msg = ('Shake sketch will deform the loose parts of the sketch.\n' |
|||
'The deformation can be restored with : \n' |
|||
'the Undo (Ctrl+Z) and Redo (Ctrl+Y) button (labeled "Shake Sketch").\n' |
|||
'If that is not desired, click Cancel,\n' |
|||
'and run the macro on a copy of the sketch.\n\n' |
|||
'Visibility of constraints has been toggled.\n' |
|||
'Visibility of constraints is restored' |
|||
' after shaking the sketch.') |
|||
reply = QMessageBox.information(None, 'Running Macro ' + __title__ + ' ver: ' + __version__, msg, |
|||
QMessageBox.Ok | QMessageBox.Cancel, |
|||
QMessageBox.Ok) |
|||
shake_it = reply == QMessageBox.Ok |
|||
if not shake_it: |
|||
toggle_constraints(sketch, to_virtual) |
|||
if shake_it: |
|||
if sketch.TypeId != 'Sketcher::SketchObject': |
|||
nbr_moves = 0 |
|||
for i in range(sketch.GeometryCount): |
|||
did_move = move_points(sketch, i, sigma) |
|||
if did_move: |
|||
nbr_moves += 1 |
|||
msg = 'Did {} moves. Sketch has a total of {} geometry entities.\n\n' |
|||
if do_move: |
|||
msg = msg.format(nbr_moves, sketch.GeometryCount) |
|||
sigma = max(get_sketch_dims(sketch)) * displacement_amplitude |
|||
open_verts = sketch.OpenVertices |
|||
if open_verts: |
|||
if len(open_verts) == 1: |
|||
ov, form = 'one', 'vertex' |
|||
else: |
|||
ov, form = len(open_verts), 'vertices' |
|||
msg += 'Sketch has {} open {}.'.format(ov, form) |
|||
msg += ('\nA sketch with open vertices' |
|||
' cannot be used to create a solid.') |
|||
else: |
|||
msg += 'Sketch is free from open vertices.' |
|||
msg += ('\n\nMenu: "Sketch/Validate Sketch..." can be used\n' |
|||
for i, geom in enumerate(sketch.Geometry): |
|||
'for additional info about sketch status.') |
|||
if isinstance(geom, Base.Vector): |
|||
move_points(sketch, i, geom_points['point'], sigma) |
|||
_ = QMessageBox.information(None, 'Running Macro ' + __title__ + ' ver: ' + __version__, msg) |
|||
elif isinstance(geom, Part.Line): |
|||
toggle_constraints(sketch, to_virtual) |
|||
elif isinstance(geom, Part.Circle): |
|||
move_points(sketch, i, geom_points['circle'], sigma) |
|||
print('Macro finished.') |
|||
elif isinstance(geom, Part.ArcOfCircle): |
|||
</pre> |
|||
move_points(sketch, i, geom_points['arc'], sigma) |
|||
</syntaxhighlight> |
|||
{{clear}} |
{{clear}} |
||
<languages/> |
Latest revision as of 10:04, 20 September 2023
Description |
---|
Shake a sketch in order to discover its unconstrained parts. Enter edit mode for a sketch and launch the macro. The macro will add a random displacement on all sketch points. The sketch is then solved, constrained parts will retain their position, free parts will move. Use the Undo (Ctrl+Z) and Redo (Ctrl+Y) button (labelled "Shake Sketch") or run this macro on a copy of the original sketch. Macro version: 1.3 Last modified: 2022-06-01 FreeCAD version: All Download: ToolBar Icon Author: Gaël Ecorchard, heda |
Author |
Gaël Ecorchard, heda |
Download |
ToolBar Icon |
Links |
Macros recipes How to install macros How to customize toolbars |
Macro Version |
1.3 |
Date last modified |
2022-06-01 |
FreeCAD Version(s) |
All |
Default shortcut |
None |
See also |
None |
Description
Shake a sketch in order to discover its unconstrained parts.
Enter edit mode for a sketch and launch the macro. The macro will add a random displacement on all sketch points. The sketch is then solved, constrained parts will retain their position, free parts will move. Always run this macro on a copy of the original sketch.
Restore the original Sketch with the Undo (Ctrl+Z) and Redo (Ctrl+Y) button (labeled "Shake Sketch") or be careful working on a copy of your file because the macro "dismantles all" to display and you may need to start over.
Install
Available in Addon manager, or manual install.
Script
Macro Shake_Sketch.py
# -*- coding: utf-8 -*- # FreeCAD macro to shake a sketch in order to discover its unconstrained parts. # # A Gaussian noise is introduced in all sketch points and the sketch is then # solved. # Beware that the sketch can look different because some constraints have # several solutions. In this case, just undo. # # This file is released under the MIT License. # Author: Gaël Ecorchard v1.0 & v1.1 # Author: heda v1.2 # Version: # - 1.3: 2022-06-01 # * added openTransaction(u"Shake Sketch") for restore the original Sketch # - 1.2: 2021-10-22 # * made macro runnable for current versions of fc # * added Part.LineSegment # * added start info & result dialogue # * hides constraints during shake # * added simple debug printing # - 1.1: 2014-10-31 # * correct import for Part # - 1.0: 2014-08 # * first release # Amplitude of the point displacements. # The standard deviation of the Gaussian noise is the largest sketch dimension # multiplied by this factor. __title__ = 'Sketch Shaker' __version__ = '1.3' __date__ = '2022/06/01' __icon__ = 'https://wiki.freecadweb.org/images/4/4a/Macro_Shake_Sketch.png' displacement_amplitude = 0.1 debug_print = False # End of configuration. from random import gauss from PySide.QtGui import QMessageBox import FreeCADGui as Gui from FreeCAD import Base import Part print('Running Macro ' + __title__ + ' ver: ' + __version__) FreeCAD.ActiveDocument.openTransaction(u"Shake Sketch") # memorise les actions (avec annuler restore) # For each sketch geometry type, map a list of points to move. g_geom_points = { Base.Vector: [1], Part.Line: [1, 2], # first point, last point Part.LineSegment: [1, 2], # first point, last point Part.Circle: [0, 3], # curve, center Part.ArcOfCircle: [1, 2, 3], # first point, last point, center } # moves bsplines and conics via lines and circles, no op for Part.Points def dprint(msg, *args): if debug_print: if args: print(msg.format(*args)) else: print(msg) class BoundingBox: xmin = xmax = ymin = ymax = None def enlarge_x(self, x): if self.xmin is None: self.xmin = self.xmax = x elif self.xmin > x: self.xmin = x elif self.xmax < x: self.xmax = x def enlarge_y(self, y): if self.ymin is None: self.ymin = self.ymax = y elif self.ymin > y: self.ymin = y elif self.ymax < y: self.ymax = y def enlarge_point(self, point): self.enlarge_x(point.x) self.enlarge_y(point.y) def enlarge_line(self, line): self.enlarge_x(line.StartPoint.x) self.enlarge_x(line.EndPoint.x) self.enlarge_y(line.StartPoint.y) self.enlarge_y(line.EndPoint.y) def enlarge_circle(self, circle): self.enlarge_x(circle.Center.x - circle.Radius) self.enlarge_x(circle.Center.x + circle.Radius) self.enlarge_y(circle.Center.y - circle.Radius) self.enlarge_y(circle.Center.y + circle.Radius) def enlarge_arc_of_circle(self, arc): # TODO: correctly compute the arc extrema (cf. toShape().BoundBox) self.enlarge_x(arc.Center.x) self.enlarge_y(arc.Center.y) def get_sketch_dims(sketch): bbox = BoundingBox() for geom in sketch.Geometry: if isinstance(geom, Base.Vector): bbox.enlarge_point(geom) elif isinstance(geom, (Part.Line, Part.LineSegment)): bbox.enlarge_line(geom) elif isinstance(geom, Part.Circle): bbox.enlarge_circle(geom) elif isinstance(geom, Part.ArcOfCircle): bbox.enlarge_arc_of_circle(geom) if None in (bbox.xmin, bbox.ymin): dprint('sketch bbox not found') return 0, 0 else: dprint('sketch bbox found') return bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin def add_noise(point, sigma): """Add a Gaussian noise with standard deviation sigma""" dprint(' x0:{:>9.3f} y0:{:>9.3f}', point.x, point.y) point.x = gauss(point.x, sigma) point.y = gauss(point.y, sigma) dprint(' xt:{:>9.3f} yt:{:>9.3f}', point.x, point.y) def move_points(sketch, geom_index, sigma): point_indexes = g_geom_points.get(type(sketch.Geometry[i]), []) # Direct access to sketch.Geometry[index] does not work. This would, # however prevent repeated recompute. # not checked validity of comment for v1.2 moved = False for point_index in point_indexes: dprint('---- geo idx [{:>3} ] -- pt idx [{:>3} ] ----', geom_index, point_index) point = sketch.getPoint(geom_index, point_index) add_noise(point, sigma) try: sketch.movePoint(geom_index, point_index, point) except ValueError as e: dprint(repr(e)) new_pos = sketch.getPoint(geom_index, point_index) test = point.isEqual(new_pos, 0) dprint(' did it move? {}', test) if test: moved = True return moved def toggle_constraints(sketch, toggle): for cid in toggle: sketch.toggleVirtualSpace(cid) view_provider = Gui.activeDocument().getInEdit() shake_it = False if not view_provider: msg = 'A sketch needs to be in edit to be shaken.' print(msg) _ = QMessageBox.information(None, __title__, msg) else: sketch = view_provider.Object if sketch.TypeId == 'Sketcher::SketchObject': sketch.recompute() # ensure update to_virtual = [cid for cid, cts in enumerate(sketch.Constraints) if cts.InVirtualSpace] sketch_span = get_sketch_dims(sketch) sigma = max(sketch_span) * displacement_amplitude dprint('sketch span: dx:{:>9.3f} dy:{:>9.3f}', *sketch_span) dprint('sigma for gauss-dist: {:.3f}', sigma) toggle_constraints(sketch, to_virtual) msg = ('Shake sketch will deform the loose parts of the sketch.\n' 'The deformation can be restored with : \n' 'the Undo (Ctrl+Z) and Redo (Ctrl+Y) button (labeled "Shake Sketch").\n' 'If that is not desired, click Cancel,\n' 'and run the macro on a copy of the sketch.\n\n' 'Visibility of constraints has been toggled.\n' 'Visibility of constraints is restored' ' after shaking the sketch.') reply = QMessageBox.information(None, 'Running Macro ' + __title__ + ' ver: ' + __version__, msg, QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok) shake_it = reply == QMessageBox.Ok if not shake_it: toggle_constraints(sketch, to_virtual) if shake_it: nbr_moves = 0 for i in range(sketch.GeometryCount): did_move = move_points(sketch, i, sigma) if did_move: nbr_moves += 1 msg = 'Did {} moves. Sketch has a total of {} geometry entities.\n\n' msg = msg.format(nbr_moves, sketch.GeometryCount) open_verts = sketch.OpenVertices if open_verts: if len(open_verts) == 1: ov, form = 'one', 'vertex' else: ov, form = len(open_verts), 'vertices' msg += 'Sketch has {} open {}.'.format(ov, form) msg += ('\nA sketch with open vertices' ' cannot be used to create a solid.') else: msg += 'Sketch is free from open vertices.' msg += ('\n\nMenu: "Sketch/Validate Sketch..." can be used\n' 'for additional info about sketch status.') _ = QMessageBox.information(None, 'Running Macro ' + __title__ + ' ver: ' + __version__, msg) toggle_constraints(sketch, to_virtual) print('Macro finished.')