| 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 |
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.
Available in Addon manager, or manual install.
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.freecad.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.')