Macro Unroll Ruled Surface: Difference between revisions

From FreeCAD Documentation
(v1.1 - py3/qt5 compat)
No edit summary
Line 30: Line 30:


<!--T:6-->
<!--T:6-->
[[File:Macro_UnrollRuledSurface_start_form.png|Macro_unrollRuledSurface]]
[[File:Macro_UnrollRuledSurface_start_form1.png|Macro_unrollRuledSurface]]


== Instruction for use == <!--T:7-->
== Instruction for use == <!--T:7-->
Line 52: Line 52:
<pre>
<pre>
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-

#***************************************************************************
#***************************************************************************
#* *
#* *
Line 81: Line 80:
__Date__ = '2022-07-24'
__Date__ = '2022-07-24'
__License__ = 'LGPL-2.0-or-later'
__License__ = 'LGPL-2.0-or-later'
__Web__ = 'https://www.freecadweb.org/wiki/Macro_Unroll_Ruled_Surface'
__Web__ = 'https://wiki.freecad.org/Macro_Unroll_Ruled_Surface'
__Wiki__ = 'https://www.freecadweb.org/wiki/Macro_Unroll_Ruled_Surface'
__Wiki__ = 'https://wiki.freecad.org/Macro_Unroll_Ruled_Surface'
__Icon__ = ''
__Icon__ = ''
__Help__ = ('Select ruled surfaces, Explode them (cf Draft menu), '
__Help__ = ('Select ruled surfaces, Explode them (cf Draft menu), '
Line 93: Line 92:
__doc__ = """
__doc__ = """
select a face, or several and run the macro.
select a face, or several and run the macro.
a solid needs to be draft/downgraded to get the faces as separate objects
a shell/solid needs to be draft/downgraded to get the faces as separate objects


the macro is intended to unroll lofted faces,
the macro is intended to unroll lofted faces,
Line 104: Line 103:
v1.1 (2022-07-24) py3/qt5 compat, cosmetic code changes, minor code tweaks,
v1.1 (2022-07-24) py3/qt5 compat, cosmetic code changes, minor code tweaks,
used gridlayout for form, added option to skip drawing,
used gridlayout for form, added option to skip drawing,
added simplistic alternative layout engine
made new layout engine with techdraw,
the drawing wb layout engine is now called "legacy"
v1.0.1 (2019-02-01) - on git
v1.0.1 (2019-02-01) - on git
v1.0 (2013-09-14) - on wiki
v1.0 (2013-09-14) - on wiki
Line 110: Line 110:
note:
note:
- unfolding sometimes works and sometimes not
- unfolding sometimes works and sometimes not
- not all surfaces are theoretically possible to unroll
- should be converted to use techdraw


"""
"""
Line 125: Line 125:
settings = dict()
settings = dict()
unroll_l = []
unroll_l = []
dwgtpl = 'Mod/Drawing/Templates'
dwgtpllegacy = 'Mod/Drawing/Templates'
dwgtpl = 'Mod/TechDraw/Templates'




Line 154: Line 155:
pts_nbr = float(sts("dpts").text())
pts_nbr = float(sts("dpts").text())
makedwg = sts("mkdwg").isChecked()
makedwg = sts("mkdwg").isChecked()
altlay = sts("altlayout").isChecked()
leglay = sts("legacylayout").isChecked()
scale = float(sts("scale").text()) # ignored if autoscale is set
scale = float(sts("scale").text()) # ignored if autoscale is set
scale_auto = sts("autoscale").isChecked()
scale_auto = sts("autoscale").isChecked()
Line 166: Line 167:
PrintMessage("UnrollRuledSurface.edge0: {}\n".format(edge0))
PrintMessage("UnrollRuledSurface.edge0: {}\n".format(edge0))
PrintMessage("UnrollRuledSurface.makedwg: {}\n".format(makedwg))
PrintMessage("UnrollRuledSurface.makedwg: {}\n".format(makedwg))
PrintMessage("UnrollRuledSurface.altlay: {}\n".format(altlay))
PrintMessage("UnrollRuledSurface.leglay: {}\n".format(leglay))
PrintMessage("UnrollRuledSurface.scale_check: {}\n".format(scale_auto))
PrintMessage("UnrollRuledSurface.scale_check: {}\n".format(scale_auto))
PrintMessage("UnrollRuledSurface.scale: {}\n".format(scale))
PrintMessage("UnrollRuledSurface.scale: {}\n".format(scale))
Line 185: Line 186:
unrollRS = unrollRuledSurface(file_name, pts_nbr, edge0)
unrollRS = unrollRuledSurface(file_name, pts_nbr, edge0)


## Get selection
## Get selection
sel = FreeCADGui.Selection.getSelection()
sel = FreeCADGui.Selection.getSelection()
if not sel:
if not sel:
Line 213: Line 214:
objnames0_l.append([obji, name])
objnames0_l.append([obji, name])
faceid += 1
faceid += 1



if not makedwg:
if not makedwg:
Line 219: Line 219:
return
return


if leglay:

if altlay:
draw = Drawing2dAlt(scale, scale_auto, a3)
n = 4 if onedrawing else 1
chunks = [objnames_l[i:i + n] for i in range(0, len(objnames_l), n)]
for i, chunk in enumerate(chunks, start=1):
draw.drawpage(chunk, "{}_page{:02}".format(file_name, i))

else:
idx = 0
idx = 0
while len(objnames_l) > 0:
while len(objnames_l) > 0:
draw = Drawing2d(scale, scale_auto, a3, cartridge, onedrawing,
draw = Drawing2dLegacy(scale, scale_auto, a3,
"{}_page{:02}".format(file_name, idx))
cartridge, onedrawing,
"{}_page{:02}".format(file_name, idx))
objnames_l = draw.all2d(objnames_l)
objnames_l = draw.all2d(objnames_l)
idx += 1
idx += 1
Line 237: Line 230:
PrintMessage(msg.format(len(objnames_l)))
PrintMessage(msg.format(len(objnames_l)))


else:
draw = Drawing2d(scale, scale_auto, a3, cartridge)
n = 4 if onedrawing else 1
chunks = [objnames_l[i:i + n] for i in range(0, len(objnames_l), n)]
for i, chunk in enumerate(chunks, start=1):
draw.drawpage(chunk, "{}_page{:02}".format(file_name, i))

ending()




Line 309: Line 310:
else:
else:
E0, E1, E2, E3, *_ = face.Edges
E0, E1, E2, E3, *_ = face.Edges
# Choose more complexe curve as edge
## Choose more complexe curve as edge
nbpol0 = self.nbpoles(E0.Curve)
nbpol0 = self.nbpoles(E0.Curve)
nbpol1 = self.nbpoles(E1.Curve)
nbpol1 = self.nbpoles(E1.Curve)
Line 329: Line 330:
sd1 = self.discretize(e1)
sd1 = self.discretize(e1)
sd2 = self.discretize(e2)
sd2 = self.discretize(e2)
# Reverse if curves cross over
## Reverse if curves cross over
if not (sd2[0].__eq__(v0) or not sd2[0].__eq__(v1)):
if not (sd2[0].__eq__(v0) or not sd2[0].__eq__(v1)):
sd2.reverse()
sd2.reverse()


# Create a polygon object and set its nodes
## Create a polygon object and set its nodes
devlxy_l = self.devlxyz(sd1, sd2)
devlxy_l = self.devlxyz(sd1, sd2)
msg = "UnrollRuledSurface.unroll: size devlxy_l: {}\n"
msg = "UnrollRuledSurface.unroll: size devlxy_l: {}\n"
Line 374: Line 375:
devlxy_l, devl1xy_l, devl2xy_l = [], [], []
devlxy_l, devl1xy_l, devl2xy_l = [], [], []
errormax = 0.0
errormax = 0.0
# Init unroll
## Init unroll
# AB
## AB
a1b1 = vect2[0].sub(vect1[0])
a1b1 = vect2[0].sub(vect1[0])
oa1 = Vector(0, 0, 0)
oa1 = Vector(0, 0, 0)
Line 385: Line 386:
for j in range(1, self.pts_nbr):
for j in range(1, self.pts_nbr):


# AB
## AB
ab = vect2[j-1].sub(vect1[j-1])
ab = vect2[j-1].sub(vect1[j-1])
#self.draw_line(vect1[j-1], vect2[j-1])
#self.draw_line(vect1[j-1], vect2[j-1])


# AC
## AC
ac = vect1[j].sub(vect1[j-1])
ac = vect1[j].sub(vect1[j-1])


# BD
## BD
bd = vect2[j].sub(vect2[j-1])
bd = vect2[j].sub(vect2[j-1])


# CD
## CD
cd = vect2[j].sub(vect1[j])
cd = vect2[j].sub(vect1[j])


# A1B1 in unroll plan
## A1B1 in unroll plan
a1b1 = devl2xy_l[j-1].sub(devl1xy_l[j-1])
a1b1 = devl2xy_l[j-1].sub(devl1xy_l[j-1])
a1b1n = self.vect_copy(a1b1)
a1b1n = self.vect_copy(a1b1)
Line 404: Line 405:
a1b1on = Vector(-a1b1n.y, a1b1n.x, 0)
a1b1on = Vector(-a1b1n.y, a1b1n.x, 0)


# A1C1
## A1C1
cosalp = self.vect_cos(ab, ac)
cosalp = self.vect_cos(ab, ac)
sinalp = self.vect_sin(ab, ac)
sinalp = self.vect_sin(ab, ac)
Line 416: Line 417:
devl1xy_l.append(oc1)
devl1xy_l.append(oc1)


# B1D1
## B1D1
cosalp = self.vect_cos(ab, bd)
cosalp = self.vect_cos(ab, bd)
sinalp = self.vect_sin(ab, bd)
sinalp = self.vect_sin(ab, bd)
Line 428: Line 429:
devl2xy_l.append(od1)
devl2xy_l.append(od1)


# Draw generatrice
## Draw generatrice
#self.draw_line(devl1xy_l[j], devl2xy_l[j])
#self.draw_line(devl1xy_l[j], devl2xy_l[j])
c1d1 = devl2xy_l[j].sub(devl1xy_l[j])
c1d1 = devl2xy_l[j].sub(devl1xy_l[j])
Line 438: Line 439:
PrintMessage(msg.format(errormax*100))
PrintMessage(msg.format(errormax*100))


# Close polygone
## Close polygone
devlxy_l = devl1xy_l
devlxy_l = devl1xy_l
devl2xy_l.reverse()
devl2xy_l.reverse()
Line 457: Line 458:


class Scale:
class Scale:
"""keeps autoscaling to known integers"""
"""keeps autoscaling to integers"""
def __init__(self, scale):
def __init__(self, scale):
self.scale = scale if scale >= 1 else 1 / scale
self.scale = scale if scale >= 1 else 1 / scale
Line 470: Line 471:




class Drawing2dAlt:
class Drawing2d:
"""
"""
Alternative layout engine (Drawing wb), only does plain templates.
TechDraw wb layout engine, diffeent logic compared to drawing wb layout.
serves basic purpose...
not much testing done, not perfect, however probably good enough...
makes 2d drawing with Drawing wb
- obj_l: list of objects
"""
"""
def __init__(self, scale, scale_auto, a3, cartridge):
# untouched in v1.1
def __init__(self, scale, scale_auto, a3):


self.a3 = a3
self.a3 = a3
self.scale = scale
self.scale = scale
self.scale_auto = scale_auto
self.scale_auto = scale_auto
if self.a3:
self.cartridge = cartridge
if a3:
self.WH = 420, 297
self.WH = 420, 297
else:
else:
self.WH = 297, 210
self.WH = 297, 210
self.doc = FreeCAD.ActiveDocument




def _mkquadrants(self, nbr):
def _mkquadrants(self, nbr):
"""lower lefts w/o margin"""
"""centers of quadrants w/o margin"""
w, h = self.WH
w, h = self.WH
w2, h2 = w/2, h/2
w2, h2 = w/2, h/2
d = (w**2 + h**2)**(1/2)
w4, h4 = w/4, h/4


q = {1: [[w, h, d], [[0, h]]],
q = {1: [[w, h], [[w2, h2]]],
2: [[w2, h, d/2], [[0, h], [w2, h]]],
2: [[w2, h], [[w2 - w4, h2], [w2 + w4, h2]]],
3: [[w2, h2, d/4], [[0, h2], [w2, h2], [0, h]]],
3: [[w2, h2], [[w2 - w4, h2 + h4], [w2 + w4, h2 + h4],
4: [[w2, h2, d/4], [[0, h2], [w2, h2], [0, h], [w2, h]]]}
[w2 - w4, h2 - h4]]],
4: [[w2, h2], [[w2 - w4, h2 + h4], [w2 + w4, h2 + h4],
[w2 - w4, h2 - h4], [w2 + w4, h2 - h4]]]}
return q.get(nbr)
return q.get(nbr)


Line 504: Line 506:
def newPage(self, doc, page_name):
def newPage(self, doc, page_name):
freecad_dir = os.path.join(FreeCAD.getResourceDir(), dwgtpl)
freecad_dir = os.path.join(FreeCAD.getResourceDir(), dwgtpl)
page = doc.addObject('Drawing::FeaturePage', page_name)
page = doc.addObject('TechDraw::DrawPage', page_name)
template = self.doc.addObject('TechDraw::DrawSVGTemplate', 'Template')
size = 'A3' if self.a3 else 'A4'
size = 'A3' if self.a3 else 'A4'
frame = 'TD' if self.cartridge else '_blank'
page.Template = freecad_dir + '/{}_Landscape_plain.svg'.format(size)
template.Template = freecad_dir + '/{}_Landscape{}.svg'.format(size, frame)
page.Template = template
return page
return page



def drawpage(self, faces, page_name):
def drawpage(self, faces, page_name):
"""max 4 per page, simple layout with halfs or quadrants"""
"""max 4 per page, simple layout with halfs or quadrants"""
doc = FreeCAD.ActiveDocument
page = self.newPage(self.doc, page_name)
page = self.newPage(doc, page_name)
[W, H], ll = self._mkquadrants(len(faces))
fontsize = 5 if self.a3 else 7
[W, H, D], ll = self._mkquadrants(len(faces))
for [face, name], [x0, y0] in zip(faces, ll):
for [face, name], [x0, y0] in zip(faces, ll):
bb = face.Shape.BoundBox
bb = face.Shape.BoundBox
aspect = max(bb.XLength / bb.YLength, bb.YLength / bb.XLength)
xr, yr = W / bb.XLength, H / bb.YLength
adjusted = max(W / H, H / W) / aspect
adjust = 0.7 if self.cartridge else 0.85
scale = min(xr, yr) * adjust if self.scale_auto else self.scale
scale, scr = Scale(scale).get()
bb.scale(scale, scale, 1) # unrolled faces in xy-plane


if self.scale_auto:
TopView = self.doc.addObject('TechDraw::DrawViewPart', name + ' view')
scale = D / (bb.DiagonalLength * adjusted)
page.addView(TopView)
else:
TopView.Source = face
scale = self.scale
TopView.Direction = (0, 0, 1)
sc, scr = Scale(scale * 0.95).get()
TopView.XDirection = (1, 0, 0)
TopView.Scale = scale

dx = (W - bb.XLength * sc) / 2
TopView.X = x0
dy = (H - bb.YLength * sc) / 2
TopView.Y = y0
dyt = dy - fontsize * 2 if dy > fontsize else dy


Text = doc.addObject('Drawing::FeatureViewAnnotation', f"{name}_txt")
Text = self.doc.addObject('TechDraw::DrawViewAnnotation', name + ' txt')
page.addView(Text)
Text.Text = '{} [{}]'.format(name, scr)
Text.Text = '{} [{}]'.format(name, scr)
Text.X = int(x0 + dx + fontsize / 2)
Text.recompute() # for size
Text.Y = int(y0 - dyt - fontsize / 2)
Text.X = x0
Text.Scale = fontsize
yt = y0 - bb.YLength / 2 - Text.TextSize.Value * 1.5
Text.Y = max(1, yt)


TopView = doc.addObject('Drawing::FeatureViewPart', name)
self.doc.recompute()
TopView.Source = face
TopView.Direction = (0, 0, 1)
TopView.Rotation = 0
TopView.X = int(x0 + dx)
TopView.Y = int(y0 - dy)
TopView.ShowHiddenLines = False
TopView.Scale = sc
page.addObject(TopView)
page.addObject(Text)

doc.recompute()
page.ViewObject.doubleClicked()
page.ViewObject.doubleClicked()
FreeCADGui.runCommand('TechDraw_ToggleFrame', 0)




class Drawing2d:
class Drawing2dLegacy:
"""
"""
makes 2d drawing with Drawing wb (original layout engine)
makes 2d drawing with Drawing wb (original layout engine, now legacy)
- obj_l: list of objects
- obj_l: list of objects
"""
"""
# untouched in v1.1
## untouched logic in v1.1
def __init__(self, scale, scale_auto, a3, cartridge, onedrawing, page_str):
def __init__(self, scale, scale_auto, a3, cartridge, onedrawing, page_str):
self.TopX_H = self.TopY_H = 0
self.TopX_H = self.TopY_H = 0
Line 576: Line 572:


def newPage(self):
def newPage(self):
freecad_dir = os.path.join(FreeCAD.getResourceDir(), dwgtpl)
freecad_dir = os.path.join(FreeCAD.getResourceDir(), dwgtpllegacy)
doc = FreeCAD.ActiveDocument
doc = FreeCAD.ActiveDocument
page = doc.addObject('Drawing::FeaturePage', self.name)
page = doc.addObject('Drawing::FeaturePage', self.name)
Line 616: Line 612:


if idx == 0 or not self.onedrawing:
if idx == 0 or not self.onedrawing:
PrintMessage("Dawing2d: init\n")
PrintMessage("Drawing2d: init\n")
TopX = self.TopX_H = marge * 2
TopX = self.TopX_H = marge * 2
TopY = self.TopY_H = marge * 2
TopY = self.TopY_H = marge * 2
Line 631: Line 627:
if self.TopX_H + xmax * scale < self.L:
if self.TopX_H + xmax * scale < self.L:
if self.TopY_H + ymax * scale + marge * 2 < self.H:
if self.TopY_H + ymax * scale + marge * 2 < self.H:
# H Add at right on same horizontal line
## H Add at right on same horizontal line
PrintMessage("Dawing2d: horizontal\n")
PrintMessage("Drawing2d: horizontal\n")
TopX, TopY = self.TopX_H, self.TopY_H
TopX, TopY = self.TopX_H, self.TopY_H
self.TopX_H = self.TopX_H + xmax * scale + marge
self.TopX_H = self.TopX_H + xmax * scale + marge
Line 643: Line 639:


else:
else:
# V Add at right on same horizontal line
## V Add at right on same horizontal line
PrintMessage("Dawing2d: vertival\n")
PrintMessage("Drawing2d: vertival\n")
if (self.TopX_V + ymax * scale + 2 * marge < self.L
if (self.TopX_V + ymax * scale + 2 * marge < self.L
and self.TopY_V + xmax * scale + 2 * marge < self.H):
and self.TopY_V + xmax * scale + 2 * marge < self.H):
Line 659: Line 655:
obj_l.append([obj, self.name])
obj_l.append([obj, self.name])
return obj_l
return obj_l

else:
# H Carriage return
if self.TopY_Hmax + ymax * scale + self.marge*2 < self.H:
msg = "Drawing2d: carriage return: {} > {}\n"
PrintMessage(msg.format(self.TopY_H + ymax * scale, self.H))
TopX = self.marge * 2
TopY = self.TopY_Hmax
self.TopX_H = TopX + xmax * scale + self.marge
self.TopY_H = TopY
self.TopX_Hmax = max(self.TopX_Hmax, self.TopX_H)
self.TopY_Hmax = self.TopY_Hmax + ymax * scale + self.marge
self.TopX_Vmax = max(self.TopX_Vmax, self.TopX_Hmax)
self.TopX_V = max(self.TopX_Vmax, self.TopX_V)


else:
else:
# V Add at right on same horizontal line
## H Carriage return
msg = "Drawing2d: vertival: {} , {}\n"
if self.TopY_Hmax + ymax * scale + self.marge*2 < self.H:
PrintMessage(msg.format(self.TopX_V, self.TopX_Vmax))
msg = "Drawing2d: carriage return: {} > {}\n"
if (self.TopX_V + ymax * scale + 2 * marge < self.L
PrintMessage(msg.format(self.TopY_H + ymax * scale, self.H))
and self.TopY_V + xmax * scale + 2 * marge < self.H):
TopX = self.marge * 2
Draft.rotate(obj, 90)
TopY = self.TopY_Hmax
Draft.move(obj, Vector(-bb.XMin, -bb.YMin, 0))
self.TopX_H = TopX + xmax * scale + self.marge
x0 = xmax; xmax = ymax; ymax = x0
self.TopY_H = TopY
TopX, TopY = self.TopX_V, self.TopY_V
self.TopX_Hmax = max(self.TopX_Hmax, self.TopX_H)
self.TopX_V = self.TopX_V + xmax * scale + marge
self.TopY_Hmax = self.TopY_Hmax + ymax * scale + self.marge
self.TopY_Vmax = max(self.TopY_Vmax,
self.TopX_Vmax = max(self.TopX_Vmax, self.TopX_Hmax)
self.TopY_V + ymax * scale + marge)
self.TopX_V = max(self.TopX_Vmax, self.TopX_V)

else:
else:
obj_l.append([obj, objname])
## V Add at right on same horizontal line
return obj_l
msg = "Drawing2d: vertival: {} , {}\n"
PrintMessage(msg.format(self.TopX_V, self.TopX_Vmax))
if (self.TopX_V + ymax * scale + 2 * marge < self.L
and self.TopY_V + xmax * scale + 2 * marge < self.H):
Draft.rotate(obj, 90)
Draft.move(obj, Vector(-bb.XMin, -bb.YMin, 0))
x0 = xmax; xmax = ymax; ymax = x0
TopX, TopY = self.TopX_V, self.TopY_V
self.TopX_V = self.TopX_V + xmax * scale + marge
self.TopY_Vmax = max(self.TopY_Vmax,
self.TopY_V + ymax * scale + marge)
else:
obj_l.append([obj, objname])
return obj_l


doc = FreeCAD.ActiveDocument
doc = FreeCAD.ActiveDocument
Line 712: Line 708:
page.addObject(Text)
page.addObject(Text)
doc.recompute()
doc.recompute()
page.ViewObject.doubleClicked()
return obj_l
return obj_l





Line 751: Line 747:
la.addWidget(mkdwg, 5, 0, 1, 2)
la.addWidget(mkdwg, 5, 0, 1, 2)


altlayout = QtGui.QCheckBox("Alternative layout")
legacylayout = QtGui.QCheckBox("Legacy layout")
altlayout.setChecked(True)
legacylayout.setChecked(False)
la.addWidget(altlayout, 5, 2, 1, 2)
la.addWidget(legacylayout, 5, 2, 1, 2)




Line 776: Line 772:
###
###
cartridge = QtGui.QCheckBox("Cartridge")
cartridge = QtGui.QCheckBox("Cartridge")
cartridge.setChecked(True)
cartridge.setChecked(False)
la.addWidget(cartridge, 8, 0, 1, 2)
la.addWidget(cartridge, 8, 0, 1, 2)


Line 786: Line 782:
box = QtGui.QDialogButtonBox(DialogBox)
box = QtGui.QDialogButtonBox(DialogBox)
box.setOrientation(QtCore.Qt.Horizontal)
box.setOrientation(QtCore.Qt.Horizontal)
box.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok)
box.setStandardButtons(QtGui.QDialogButtonBox.Cancel | QtGui.QDialogButtonBox.Ok)
la.addWidget(box, 9, 0, 1, cols)
la.addWidget(box, 9, 0, 1, cols)


###
###
settings.update(dict(fname=fname, dpts=dpts, edge=buttonGrpEdge,
settings.update(dict(fname=fname, dpts=dpts, edge=buttonGrpEdge,
mkdwg=mkdwg, altlayout=altlayout,
mkdwg=mkdwg, legacylayout=legacylayout,
autoscale=autoscale, scale=scale,
autoscale=autoscale, scale=scale,
papersize=buttonGrpFormat,
papersize=buttonGrpFormat,
Line 800: Line 796:
QtCore.QMetaObject.connectSlotsByName(DialogBox)
QtCore.QMetaObject.connectSlotsByName(DialogBox)
DialogBox.show()
DialogBox.show()



</pre>
</pre>

Revision as of 21:32, 28 July 2022

Macro Unroll Ruled Surface

Description
The macro allows to unroll ruled surfaces and to draw them on a page.

Macro version: 1.1
Last modified: 2022-07-24
Download: ToolBar icon
Author: Hervé B., heda
Author
Hervé B., heda
Download
ToolBar icon
Links
Macro Version
1.1
Date last modified
2022-07-24
FreeCAD Version(s)
None
Default shortcut
None
See also
None

Description

The macro allows to unroll ruled surfaces and to draw them on a page.

Macro_unrollRuledSurface

Installation

Available in Add-on manager.

Cf Macro for unrolling ruled surfaces

Options

  • Number of generatrix
  • Scale manual or automatic
  • Page format: a3/a4, cartridge (cf FreeCAD templates)
  • Group drawings in the same page as possible.

Macro_unrollRuledSurface

Instruction for use

  1. Select ruled surfaces
  2. Explode them (cf Draft menu - downgrade)
  3. Select the surfaces
  4. Execute the macro

Script

The lastest version of the macro is here on the wiki. An earlier version can be found at UnrollRuledSurface.FCMacro but the easiest way to install this macro is through the Addon Manager Std_AddonMgr .

ToolBar Icon

Macro_unrollRuledSurface.py

# -*- coding: utf-8 -*-
#***************************************************************************
#*                                                                         *
#*   Copyright (c) 2013 - DoNovae/Herve BAILLY <hbl13@donovae.com>         *
#*   Copyright (c) 2022 - heda <heda@fc-forum>                             *
#*                                                                         *
#*   This program is free software; you can redistribute it and/or modify  *
#*   it under the terms of the GNU Lesser General Public License (LGPL)    *
#*   as published by the Free Software Foundation; either version 2 of     *
#*   the License, or (at your option) any later version.                   *
#*   for detail see the LICENCE text file.                                 *
#*                                                                         *
#*   This program is distributed in the hope that it will be useful,       *
#*   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
#*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
#*   GNU Library General Public License for more details.                  *
#*                                                                         *
#*   You should have received a copy of the GNU Library General Public     *
#*   License along with this program; if not, write to the Free Software   *
#*   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
#*   USA                                                                   *
#*                                                                         *
#***************************************************************************
__Name__ = 'Unroll Ruled Surface'
__Comment__ = 'Unroll of a ruled surface and draw it on a page.'
__Author__ = 'Hervé B., heda'
__Version__ = '1.1'
__Date__ = '2022-07-24'
__License__ = 'LGPL-2.0-or-later'
__Web__ = 'https://wiki.freecad.org/Macro_Unroll_Ruled_Surface'
__Wiki__ = 'https://wiki.freecad.org/Macro_Unroll_Ruled_Surface'
__Icon__ = ''
__Help__ = ('Select ruled surfaces, Explode them (cf Draft menu), '
            'Select the surfaces, Execute the macro')
__Status__ = ''
__Requires__ = ''
__Communication__ = ''
__Files__ = ''

__doc__ = """
select a face, or several and run the macro.
a shell/solid needs to be draft/downgraded to get the faces as separate objects

the macro is intended to unroll lofted faces,
function beyond that is (in current version) a bonus

settings are not context aware, all settings not applicable are ignored.
as example, using autoscaling ignores the scale-value in the form


v1.1   (2022-07-24) py3/qt5 compat, cosmetic code changes, minor code tweaks,
       used gridlayout for form, added option to skip drawing,
       made new layout engine with techdraw,
       the drawing wb layout engine is now called "legacy"
v1.0.1 (2019-02-01) - on git
v1.0   (2013-09-14) - on wiki

note:
- unfolding sometimes works and sometimes not
- not all surfaces are theoretically possible to unroll

"""

import os
from PySide import QtGui, QtCore
import FreeCAD, FreeCADGui
import Part, Draft

Vector = FreeCAD.Base.Vector
PrintMessage = FreeCAD.Console.PrintMessage
PrintError = FreeCAD.Console.PrintError

settings = dict()
unroll_l = []
dwgtpllegacy = 'Mod/Drawing/Templates'
dwgtpl = 'Mod/TechDraw/Templates'


#####################################
###   Functions
#####################################

def errorDialog(msg):
    diag = QtGui.QMessageBox(QtGui.QMessageBox.Critical, "Error Message", msg)
    diag.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
    diag.exec_()

def ending():
    PrintMessage("UnrollRuledSurface: end.\n")
    PrintMessage("===========================================\n")
    FreeCAD.ActiveDocument.recompute()


def proceed():
    QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)

    PrintMessage("===========================================\n")
    PrintMessage("UnrollRuledSurface: start.\n")
    try:
        sts = lambda s: settings.get(s)

        file_name = sts("fname").text()
        pts_nbr = float(sts("dpts").text())
        makedwg = sts("mkdwg").isChecked()
        leglay = sts("legacylayout").isChecked()
        scale = float(sts("scale").text()) # ignored if autoscale is set
        scale_auto = sts("autoscale").isChecked()
        edge0 = sts("edge").checkedId() == -2
        a3 = sts("papersize").checkedId() == -3
        cartridge = sts("cartridge").isChecked()
        onedrawing = sts("groupdwg").isChecked()

        PrintMessage("UnrollRuledSurface.file_name: {}\n".format(file_name))
        PrintMessage("UnrollRuledSurface.pts_nbr: {}\n".format(pts_nbr))
        PrintMessage("UnrollRuledSurface.edge0: {}\n".format(edge0))
        PrintMessage("UnrollRuledSurface.makedwg: {}\n".format(makedwg))
        PrintMessage("UnrollRuledSurface.leglay: {}\n".format(leglay))
        PrintMessage("UnrollRuledSurface.scale_check: {}\n".format(scale_auto))
        PrintMessage("UnrollRuledSurface.scale: {}\n".format(scale))
        PrintMessage("UnrollRuledSurface.a3_check: {}\n".format(a3))
        PrintMessage("UnrollRuledSurface.cartridge: {}\n".format(cartridge))
        PrintMessage("UnrollRuledSurface.onedrawing: {}\n".format(onedrawing))
    except:
        msg = "UnrollRuledSurface: wrong inputs...\n"
        PrintError(msg)
        errorDialog(msg)
        QtGui.QApplication.restoreOverrideCursor()
        DialogBox.hide()
        ending()
        return

    QtGui.QApplication.restoreOverrideCursor()
    DialogBox.hide()
    unrollRS = unrollRuledSurface(file_name, pts_nbr, edge0)

    ## Get selection
    sel = FreeCADGui.Selection.getSelection()
    if not sel:
        PrintMessage("UnrollRuledSurface: no selection...\n")
        ending()
        return

    faceid = 0
    objnames_l, objnames0_l = [], []
    grp = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup",
                                           "{}_objs".format(file_name))

    for objid, obji in enumerate(sel):
        shape = obji.Shape
        faces = shape.Faces
        for idx in range(len(faces)):
            msg = "UnrollRuledSurface.proceed: ObjId = {}, faceId = {}\n"
            PrintMessage(msg.format(objid, faceid))
            name = obji.Name
            if len(faces) > 1:
                name = "{}.face_{}".format(name, idx)
            obj = unrollRS.unroll(faces[idx], name)
            obj.ViewObject.Visibility = not makedwg
            grp.addObject(obj)

        objnames_l.append([obj, name])
        objnames0_l.append([obji, name])
        faceid += 1

    if not makedwg:
        ending()
        return

    if leglay:
        idx = 0
        while len(objnames_l) > 0:
            draw = Drawing2dLegacy(scale, scale_auto, a3,
                                   cartridge, onedrawing,
                                   "{}_page{:02}".format(file_name, idx))
            objnames_l = draw.all2d(objnames_l)
            idx += 1
            msg = "UnrollRuledSurface: obj_l = {}\n"
            PrintMessage(msg.format(len(objnames_l)))

    else:
        draw = Drawing2d(scale, scale_auto, a3, cartridge)
        n = 4 if onedrawing else 1
        chunks = [objnames_l[i:i + n] for i in range(0, len(objnames_l), n)]
        for i, chunk in enumerate(chunks, start=1):
            draw.drawpage(chunk, "{}_page{:02}".format(file_name, i))

    ending()


def close():
    DialogBox.hide()

def getType(obj):
    return type(obj).__name__


class unrollRuledSurface:
    """
    unroll ruled surfaces
    :file_name: ouput file
    :pts_nbr: nbr point of discretization
    """
    def __init__(self, file_name, pts_nbr, edge0):
        self.doc = FreeCAD.ActiveDocument
        self.file_name = file_name
        self.pts_nbr = int(pts_nbr)
        self.edge0 = edge0
        msg = "UnrollRuledSurface.unroll - file_name: {}, pts_nbr: {}\n"
        PrintMessage(msg.format(file_name, pts_nbr))


    def discretize(self, curve):
        """discretize a curve"""
        if getType(curve) in ('GeomLineSegment', 'GeomCircle'):
            sd = curve.discretize(self.pts_nbr)
        elif getType(curve) == 'GeomBSplineCurve':
            nodes = curve.getPoles()
            spline = Part.BSplineCurve()
            spline.buildFromPoles(nodes)
            sd = spline.discretize(self.pts_nbr)
        else:
            sd = curve.discretize(self.pts_nbr)
        return sd

    def nbpoles(self, curve):
        """find number of poles for a curve"""
        if getType(curve) == 'GeomLineSegment':
            nbpol=1
        elif getType(curve) == 'GeomBSplineCurve':
            nbpol=curve.NbPoles
        elif getType(curve) == 'GeomCircle':
            nbpol=2
        elif getType(curve) == 'GeomBezierCurve':
            nbpol=4
        else:
            nbpol=0

        msg = "UnrollRulrdSurface.nbpole {:s} = {:d}\n"
        PrintMessage(msg.format(getType(curve), nbpol))
        return nbpol

    def unroll(self, face, name):
        """unrolls a face composed of 2 to 4 edges"""
        nbredges = len(face.Edges)
        msg = "UnrollRuledSurface.unroll: Edge Nbr = {}\n"
        PrintMessage(msg.format(nbredges))

        if nbredges == 2:
            e1, e2 = face.Edges
            sd1 = e1.Curve.discretize(self.pts_nbr)
            sd2 = e2.Curve.discretize(self.pts_nbr)

        elif nbredges == 3:
            e1, _, e2 = face.Edges
            sd1 = e1.Curve.discretize(self.pts_nbr)
            sd2 = e2.Curve.discretize(self.pts_nbr)

        else:
            E0, E1, E2, E3, *_ = face.Edges
            ## Choose more complexe curve as edge
            nbpol0 = self.nbpoles(E0.Curve)
            nbpol1 = self.nbpoles(E1.Curve)
            nbpol2 = self.nbpoles(E2.Curve)
            nbpol3 = self.nbpoles(E3.Curve)
            msg = ("UnrollRuledSurface.unroll: nbpol0= {:d}, nbpol1= {:d},"
                   " nbpol2= {:d}, nbpol3= {:d}\n")
            PrintMessage(msg.format(nbpol0, nbpol1, nbpol2, nbpol3))

            if self.edge0:
                e1, e2 = E0, E2
                v = self.discretize(E1)
                v0, v1 = v[0], v[self.pts_nbr-1]
            else:
                e1, e2 = E1, E3
                v = self.discretize(E2)
                v0, v1 = v[0], v[self.pts_nbr-1]

            sd1 = self.discretize(e1)
            sd2 = self.discretize(e2)
            ## Reverse if curves cross over
            if not (sd2[0].__eq__(v0) or not sd2[0].__eq__(v1)):
                sd2.reverse()

        ## Create a polygon object and set its nodes
        devlxy_l = self.devlxyz(sd1, sd2)
        msg = "UnrollRuledSurface.unroll: size devlxy_l: {}\n"
        PrintMessage(msg.format(len(devlxy_l)))
        p = self.doc.addObject("Part::Polygon", name)
        p.Nodes = devlxy_l
        self.doc.recompute()
        FreeCADGui.ActiveDocument.ActiveView.fitAll()
        return p

    def vect_copy(self, vect):
        """returns copy of vector"""
        return vect.add(Vector())

    def vect_cos(self, vect1, vect2):
        """returns cosine angle between 2 vectors"""
        return vect1.dot(vect2) / vect1.Length / vect2.Length

    def vect_sin(self, vect1, vect2):
        """returns absolute sinus angle between 2 vectors"""
        v = Vector()
        v.x = vect1.y * vect2.z - vect1.z * vect2.y
        v.y = vect1.z * vect2.x - vect1.x * vect2.z
        v.z = vect1.x * vect2.y - vect1.y * vect2.x
        return v.Length / vect1.Length / vect2.Length

    def devlxyz(self, vect1, vect2):
        """
        unrolls a face composed of 4 edges
        args: vect1, vect2 --> 2 edges of the shape
        returns: dvlxy
        """
        lenv1, lenv2 = len(vect1), len(vect2)
        if lenv1 != lenv2 or lenv1 != self.pts_nbr or lenv2 != self.pts_nbr:
            msg = ("UnrollRuledSurface.devlxyz: incompatility of sizes vect1,"
                   " vect2, pts_nbr: ({}, {}, {})\n")
            PrintError(msg.format(lenv1, lenv2, self.pts_nbr))
            errorDialog(msg)

        devlxy_l, devl1xy_l, devl2xy_l = [], [], []
        errormax = 0.0
        ## Init unroll
        ## AB
        a1b1 = vect2[0].sub(vect1[0])
        oa1 = Vector(0, 0, 0)
        devl1xy_l.append(oa1) #A1
        ob1 = Vector(a1b1.Length, 0, 0)
        devl2xy_l.append(ob1) #B1
        #self.draw_line(devl1xy_l[0], devl2xy_l[0])
        #self.draw_line(vect1[0], vect2[0])
        for j in range(1, self.pts_nbr):

            ## AB
            ab = vect2[j-1].sub(vect1[j-1])
            #self.draw_line(vect1[j-1], vect2[j-1])

            ## AC
            ac = vect1[j].sub(vect1[j-1])

            ## BD
            bd = vect2[j].sub(vect2[j-1])

            ## CD
            cd = vect2[j].sub(vect1[j])

            ## A1B1 in unroll plan
            a1b1 = devl2xy_l[j-1].sub(devl1xy_l[j-1])
            a1b1n = self.vect_copy(a1b1)
            a1b1n.normalize()
            a1b1on = Vector(-a1b1n.y, a1b1n.x, 0)

            ## A1C1
            cosalp = self.vect_cos(ab, ac)
            sinalp = self.vect_sin(ab, ac)
            a1c1 = self.vect_copy(a1b1n)
            a1c1.multiply(cosalp * ac.Length)
            v = self.vect_copy(a1b1on)
            v.multiply(sinalp * ac.Length)
            a1c1 = a1c1.add(v)
            oa1 = self.vect_copy(devl1xy_l[j-1])
            oc1 = oa1.add(a1c1)
            devl1xy_l.append(oc1)

            ## B1D1
            cosalp = self.vect_cos(ab, bd)
            sinalp = self.vect_sin(ab, bd)
            b1d1 = self.vect_copy(a1b1n)
            b1d1.multiply(cosalp * bd.Length)
            v = self.vect_copy(a1b1on)
            v.multiply(sinalp * bd.Length)
            b1d1 = b1d1.add(v)
            ob1 = self.vect_copy(devl2xy_l[j-1])
            od1 = ob1.add(b1d1)
            devl2xy_l.append(od1)

            ## Draw generatrice
            #self.draw_line(devl1xy_l[j], devl2xy_l[j])
            c1d1 = devl2xy_l[j].sub(devl1xy_l[j])
            if ab.Length != 0:
                abl = ab.Length
                errormax = max(errormax, abs(abl - c1d1.Length) / abl)

        msg = "UnrollRuledSurface Error cd,c1d1: {:.1f} %\n"
        PrintMessage(msg.format(errormax*100))

        ## Close polygone
        devlxy_l = devl1xy_l
        devl2xy_l.reverse()
        devlxy_l.extend(devl2xy_l)
        v = Vector()
        devlxy_l.append(v)

        return devlxy_l


    def draw_line(self, vect0, vect1):
        """draws a Part.Line between vect0 & vect1"""
        l = Part.LineSegment()
        l.StartPoint = vect0
        l.EndPoint = vect1
        self.doc.addObject("Part::Feature", "Line").Shape = l.toShape()


class Scale:
    """keeps autoscaling to integers"""
    def __init__(self, scale):
        self.scale = scale if scale >= 1 else 1 / scale
        self.scale = int(self.scale)
        self.inverted = scale >= 1

    def get(self):
        if self.inverted:
            return self.scale, '{}:1'.format(self.scale)
        else:
            return 1/self.scale, '1:{}'.format(self.scale)


class Drawing2d:
    """
    TechDraw wb layout engine, diffeent logic compared to drawing wb layout.
    serves basic purpose...
    """
    def __init__(self, scale, scale_auto, a3, cartridge):

        self.a3 = a3
        self.scale = scale
        self.scale_auto = scale_auto
        self.cartridge = cartridge
        if a3:
            self.WH = 420, 297
        else:
            self.WH = 297, 210
        self.doc = FreeCAD.ActiveDocument


    def _mkquadrants(self, nbr):
        """centers of quadrants w/o margin"""
        w, h = self.WH
        w2, h2 = w/2, h/2
        w4, h4 = w/4, h/4

        q = {1: [[w, h], [[w2, h2]]],
             2: [[w2, h], [[w2 - w4, h2], [w2 + w4, h2]]],
             3: [[w2, h2], [[w2 - w4, h2 + h4], [w2 + w4, h2 + h4],
                            [w2 - w4, h2 - h4]]],
             4: [[w2, h2], [[w2 - w4, h2 + h4], [w2 + w4, h2 + h4],
                            [w2 - w4, h2 - h4], [w2 + w4, h2 - h4]]]}
        return q.get(nbr)


    def newPage(self, doc, page_name):
        freecad_dir = os.path.join(FreeCAD.getResourceDir(), dwgtpl)
        page = doc.addObject('TechDraw::DrawPage', page_name)
        template = self.doc.addObject('TechDraw::DrawSVGTemplate', 'Template')
        size = 'A3' if self.a3 else 'A4'
        frame = 'TD' if self.cartridge else '_blank'
        template.Template = freecad_dir + '/{}_Landscape{}.svg'.format(size, frame)
        page.Template = template
        return page

    def drawpage(self, faces, page_name):
        """max 4 per page, simple layout with halfs or quadrants"""
        page = self.newPage(self.doc, page_name)
        [W, H], ll = self._mkquadrants(len(faces))
        for [face, name], [x0, y0] in zip(faces, ll):
            bb = face.Shape.BoundBox
            xr, yr = W / bb.XLength, H / bb.YLength
            adjust = 0.7 if self.cartridge else 0.85
            scale = min(xr, yr) * adjust if self.scale_auto else self.scale
            scale, scr = Scale(scale).get()
            bb.scale(scale, scale, 1) # unrolled faces in xy-plane

            TopView = self.doc.addObject('TechDraw::DrawViewPart', name + ' view')
            page.addView(TopView)
            TopView.Source = face
            TopView.Direction = (0, 0, 1)
            TopView.XDirection = (1, 0, 0)
            TopView.Scale = scale
            TopView.X = x0
            TopView.Y = y0

            Text = self.doc.addObject('TechDraw::DrawViewAnnotation', name + ' txt')
            page.addView(Text)
            Text.Text = '{} [{}]'.format(name, scr)
            Text.recompute() # for size
            Text.X = x0
            yt = y0 - bb.YLength / 2 - Text.TextSize.Value * 1.5
            Text.Y = max(1, yt)

        self.doc.recompute()
        page.ViewObject.doubleClicked()
        FreeCADGui.runCommand('TechDraw_ToggleFrame', 0)


class Drawing2dLegacy:
    """
    makes 2d drawing with Drawing wb (original layout engine, now legacy)
    - obj_l: list of objects
    """
    ## untouched logic in v1.1
    def __init__(self, scale, scale_auto, a3, cartridge, onedrawing, page_str):
        self.TopX_H = self.TopY_H = 0
        self.TopX_V = self.TopY_V = 0
        self.TopX_Hmax = self.TopY_Hmax = 0
        self.TopX_Vmax = self.TopY_Vmax = 0
        self.a3 = a3
        self.scale = scale
        self.scale_auto = scale_auto
        self.cartridge = cartridge
        self.onedrawing = onedrawing
        self.marge = 6
        if self.a3:
            self.L, self.H = 420, 297
        else:
            self.L, self.H = 297, 210
        self.name = page_str

    def newPage(self):
        freecad_dir = os.path.join(FreeCAD.getResourceDir(), dwgtpllegacy)
        doc = FreeCAD.ActiveDocument
        page = doc.addObject('Drawing::FeaturePage', self.name)
        size = 'A3' if self.a3 else 'A4'
        frame = '' if self.cartridge else '_plain'
        page.Template = freecad_dir + '/{}_Landscape{}.svg'.format(size, frame)
        return page


    def all2d(self, objname_l):
        obj1_l = []
        for objid in range(len(objname_l)):
            if objid == 0 or not self.onedrawing:
                page = self.newPage()
            obj1_l.extend(self.done(objid, objname_l[objid]))
        return obj1_l

    def done(self, idx, objname):
        obj_l = []
        obj, objname = objname
        marge = self.marge
        bb = obj.Shape.BoundBox
        xmax = bb.XMax - bb.XMin
        ymax = bb.YMax - bb.YMin

        if ymax > xmax:
            Draft.rotate(obj, 90)
        Draft.move(obj, Vector(-bb.XMin, -bb.YMin, 0))
        xmax = bb.XMax - bb.XMin
        ymax = bb.YMax - bb.YMin

        scale = min((self.L-4 * marge) / xmax, (self.H-4 * marge) / ymax)

        if (not self.scale_auto) or self.onedrawing:
            scale = self.scale

        PrintMessage("UnrollRuledSurface.drawing: scale= {:.2f}\n".format(scale))


        if idx == 0 or not self.onedrawing:
            PrintMessage("Drawing2d: init\n")
            TopX = self.TopX_H = marge * 2
            TopY = self.TopY_H = marge * 2
            self.TopX_H = self.TopX_H + xmax * scale + marge
            self.TopY_H = self.TopY_H
            self.TopX_Hmax = max(self.TopX_Hmax, self.TopX_H)
            self.TopY_Hmax = max(self.TopY_Hmax,
                                 self.TopY_H + ymax * scale + marge)
            self.TopX_Vmax = max(self.TopX_Vmax, self.TopX_Hmax)
            self.TopX_V = max(self.TopX_Vmax, self.TopX_V)
            self.TopY_V = marge * 2

        elif self.onedrawing:
            if self.TopX_H + xmax * scale < self.L:
                if self.TopY_H + ymax * scale + marge * 2 < self.H:
                    ## H Add at right on same horizontal line
                    PrintMessage("Drawing2d: horizontal\n")
                    TopX, TopY = self.TopX_H, self.TopY_H
                    self.TopX_H = self.TopX_H + xmax * scale + marge
                    self.TopX_Hmax = max(self.TopX_Hmax, self.TopX_H)
                    self.TopY_Hmax = max(self.TopY_Hmax,
                                         self.TopY_H + ymax * scale + marge)
                    self.TopX_Vmax = max(self.TopX_Hmax, self.TopX_Vmax)
                    self.TopX_Vmax = max(self.TopX_Vmax, self.TopX_Hmax)
                    self.TopX_V = max(self.TopX_Vmax, self.TopX_V)

                else:
                    ## V Add at right on same horizontal line
                    PrintMessage("Drawing2d: vertival\n")
                    if (self.TopX_V + ymax * scale + 2 * marge < self.L
                        and self.TopY_V + xmax * scale + 2 * marge < self.H):
                        Draft.rotate(obj, 90)
                        Draft.move(obj, Vector(-bb.XMin, -bb.YMin, 0))
                        x0 = xmax; xmax = ymax; ymax = x0
                        self.TopX_V = max(self.TopX_Vmax, self.TopX_V)
                        TopX, TopY = self.TopX_V, self.TopY_V
                        self.TopX_V = self.TopX_V + xmax * scale + marge
                        self.TopY_Vmax = max(self.TopY_Vmax,
                                             self.TopY_V + ymax * scale + marge)

                    else:
                        obj_l.append([obj, self.name])
                        return obj_l

            else:
                ## H Carriage return
                if self.TopY_Hmax + ymax * scale + self.marge*2 < self.H:
                    msg = "Drawing2d: carriage return: {} > {}\n"
                    PrintMessage(msg.format(self.TopY_H + ymax * scale, self.H))
                    TopX = self.marge * 2
                    TopY = self.TopY_Hmax
                    self.TopX_H = TopX + xmax * scale + self.marge
                    self.TopY_H = TopY
                    self.TopX_Hmax = max(self.TopX_Hmax, self.TopX_H)
                    self.TopY_Hmax = self.TopY_Hmax + ymax * scale + self.marge
                    self.TopX_Vmax = max(self.TopX_Vmax, self.TopX_Hmax)
                    self.TopX_V = max(self.TopX_Vmax, self.TopX_V)
    
                else:
                    ## V Add at right on same horizontal line
                    msg = "Drawing2d: vertival: {} , {}\n"
                    PrintMessage(msg.format(self.TopX_V, self.TopX_Vmax))
                    if (self.TopX_V + ymax * scale + 2 * marge < self.L
                        and self.TopY_V + xmax * scale + 2 * marge < self.H):
                        Draft.rotate(obj, 90)
                        Draft.move(obj, Vector(-bb.XMin, -bb.YMin, 0))
                        x0 = xmax; xmax = ymax; ymax = x0
                        TopX, TopY = self.TopX_V, self.TopY_V
                        self.TopX_V = self.TopX_V + xmax * scale + marge
                        self.TopY_Vmax = max(self.TopY_Vmax,
                                             self.TopY_V + ymax * scale + marge)
    
                    else:
                        obj_l.append([obj, objname])
                        return obj_l

        doc = FreeCAD.ActiveDocument
        page = doc.getObject(self.name)

        Text = doc.addObject('Drawing::FeatureViewAnnotation', f"{objname}_txt")
        Text.Text = objname
        Text.X = TopX + xmax * scale / 2
        Text.Y = TopY + ymax * scale / 2
        Text.Scale = 2

        TopView = doc.addObject('Drawing::FeatureViewPart', objname)
        TopView.Source = obj
        TopView.Direction = (0, 0, 1)
        TopView.Rotation = 0
        TopView.X = TopX
        TopView.Y = TopY
        TopView.ShowHiddenLines = False
        TopView.Scale = scale
        page.addObject(TopView)
        page.addObject(Text)
        doc.recompute()
        page.ViewObject.doubleClicked()
        return obj_l



#####################################
###   Dialog Box
#####################################

DialogBox = QtGui.QDialog()
DialogBox.setWindowTitle("UnrollRuledSurface")
la = QtGui.QGridLayout(DialogBox)
la.setSpacing(7)
buttonGrpEdge = QtGui.QButtonGroup(DialogBox)
buttonGrpFormat = QtGui.QButtonGroup(DialogBox)

cols = 4
la.addWidget(QtGui.QLabel("File Name"), 0, 0, 1, cols)
fname = QtGui.QLineEdit("UnrollSurface")
la.addWidget(fname, 1, 0, 1, cols)

la.addWidget(QtGui.QLabel("Discretization Points Nbr"), 2, 0, 1, cols)
dpts = QtGui.QLineEdit("30")
la.addWidget(dpts, 3, 0, 1, 2)

###
la.addWidget(QtGui.QLabel("Generatrices from edge:"), 4, 0, 1, 2)
edgezero = QtGui.QRadioButton("0 to 3")
la.addWidget(edgezero, 4, 2)
edgeone = QtGui.QRadioButton("1 to 4")
la.addWidget(edgeone, 4, 3)
buttonGrpEdge.addButton(edgezero); buttonGrpEdge.addButton(edgeone)
edgezero.setChecked(True)

###
mkdwg = QtGui.QCheckBox("Make drawing")
mkdwg.setChecked(True)
la.addWidget(mkdwg, 5, 0, 1, 2)

legacylayout = QtGui.QCheckBox("Legacy layout")
legacylayout.setChecked(False)
la.addWidget(legacylayout, 5, 2, 1, 2)


###
autoscale = QtGui.QCheckBox("Auto scale")
autoscale.setChecked(True)
la.addWidget(autoscale, 6, 0, 1, 2)

la.addWidget(QtGui.QLabel("Scale"), 6, 2)
scale = QtGui.QLineEdit("1")
la.addWidget(scale, 6, 3)

###
la.addWidget(QtGui.QLabel("Paper size:"), 7, 0, 1, 2)
rba4 = QtGui.QRadioButton("A4")
la.addWidget(rba4, 7, 2)
rba3 = QtGui.QRadioButton("A3")
la.addWidget(rba3, 7, 3)
buttonGrpFormat.addButton(rba4); buttonGrpFormat.addButton(rba3)
rba4.setChecked(True)

###
cartridge = QtGui.QCheckBox("Cartridge")
cartridge.setChecked(False)
la.addWidget(cartridge, 8, 0, 1, 2)

groupdwg = QtGui.QCheckBox("Group drawings in page")
groupdwg.setChecked(True)
la.addWidget(groupdwg, 8, 2, 1, 2)

###
box = QtGui.QDialogButtonBox(DialogBox)
box.setOrientation(QtCore.Qt.Horizontal)
box.setStandardButtons(QtGui.QDialogButtonBox.Cancel | QtGui.QDialogButtonBox.Ok)
la.addWidget(box, 9, 0, 1, cols)

###
settings.update(dict(fname=fname, dpts=dpts, edge=buttonGrpEdge,
                     mkdwg=mkdwg, legacylayout=legacylayout,
                     autoscale=autoscale, scale=scale,
                     papersize=buttonGrpFormat,
                     cartridge=cartridge, groupdwg=groupdwg))

QtCore.QObject.connect(box, QtCore.SIGNAL("accepted()"), proceed)
QtCore.QObject.connect(box, QtCore.SIGNAL("rejected()"), close)
QtCore.QMetaObject.connectSlotsByName(DialogBox)
DialogBox.show()