File size: 25,520 Bytes
985c397 | 1 2 3 4 5 6 7 8 9 10 11 12 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 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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 | # SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2009, 2010 Yorik van Havre <yorik@uncreated.net> *
# * Copyright (c) 2009, 2010 Ken Cline <cline@frii.com> *
# * Copyright (c) 2020 Eliud Cabrera Castillo <e.cabrera-castillo@tum.de> *
# * Copyright (c) 2025 The FreeCAD Project Association *
# * *
# * 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 *
# * *
# ***************************************************************************
"""Provides functions to upgrade objects by different methods.
See also the `downgrade` function.
"""
## @package downgrade
# \ingroup draftfunctions
# \brief Provides functions to upgrade objects by different methods.
import math
import re
import lazy_loader.lazy_loader as lz
import FreeCAD as App
from draftfunctions import draftify
from draftgeoutils.geometry import is_straight_line
from draftmake import make_block
from draftmake import make_wire
from draftutils import gui_utils
from draftutils import params
from draftutils import utils
from draftutils.groups import is_group
from draftutils.messages import _msg
from draftutils.translate import translate
# Delay import of module until first use because it is heavy
Part = lz.LazyLoader("Part", globals(), "Part")
DraftGeomUtils = lz.LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils")
Arch = lz.LazyLoader("Arch", globals(), "Arch")
_DEBUG = False
## \addtogroup draftfunctions
# @{
def upgrade(objects, delete=False, force=None):
"""Upgrade the given objects.
This is a counterpart to `downgrade`.
Parameters
----------
objects: Part::Feature or list
A single object to upgrade or a list
containing various such objects.
delete: bool, optional
It defaults to `False`.
If it is `True`, the old objects are deleted, and only the resulting
object is kept.
force: str, optional
It defaults to `None`.
Its value can be used to force a certain method of upgrading.
It can be any of: `'makeCompound'`, `'closeGroupWires'`,
`'makeSolid'`, `'closeWire'`, `'turnToParts'`, `'makeFusion'`,
`'makeShell'`, `'makeFaces'`, `'draftify'`, `'joinFaces'`,
`'makeSketchFace'`, `'makeWires'`.
Returns
-------
tuple
A tuple containing two lists, a list of new objects
and a list of objects to be deleted.
See Also
--------
downgrade
"""
# definitions of actions to perform
def makeCompound(objects):
"""Return a compound object made from the given objects."""
newobj = make_block.make_block(objects)
format(objects[0], [newobj])
add_to_parent(objects[0], [newobj])
add_list.append(newobj)
return True
def closeGroupWires(groups):
"""Close every open wire in the given groups."""
result = False
for grp in groups:
if any([closeWire(obj) for obj in grp.Group]):
result = True
return result
def makeSolid(obj):
"""Turn an object into a solid, if possible."""
if obj.Shape.Solids:
return False
try:
solid = Part.makeSolid(obj.Shape)
except Part.OCCError:
return False
if not solid.isClosed():
return False
newobj = doc.addObject("Part::Feature", "Solid")
newobj.Shape = solid
format(obj, [newobj])
add_to_parent(obj, [newobj])
add_list.append(newobj)
delete_list.append(obj)
return True
def closeWire(obj):
"""Close a wire object, if possible."""
if obj.Shape.Faces:
return False
if len(obj.Shape.Wires) != 1:
return False
if len(obj.Shape.Edges) == 1:
return False
if is_straight_line(obj.Shape):
return False
if utils.get_type(obj) == "Wire":
obj.Closed = True
return True
wire = obj.Shape.Wires[0]
if wire.isClosed():
return False
verts = wire.OrderedVertexes
p0 = verts[0].Point
p1 = verts[-1].Point
edges = wire.Edges
edges.append(Part.LineSegment(p1, p0).toShape())
wire = Part.Wire(Part.__sortEdges__(edges))
newobj = doc.addObject("Part::Feature", "Wire")
newobj.Shape = wire
format(obj, [newobj])
add_to_parent(obj, [newobj])
add_list.append(newobj)
delete_list.append(obj)
return True
def turnToParts(meshes):
"""Turn given meshes to parts."""
result = False
for mesh in meshes:
shp = Arch.getShapeFromMesh(mesh.Mesh)
if shp:
newobj = doc.addObject("Part::Feature", shp.ShapeType)
newobj.Shape = shp
format(mesh, [newobj])
add_to_parent(mesh, [newobj])
add_list.append(newobj)
delete_list.append(mesh)
result = True
return result
def makeFusion(objects):
"""Make a Draft or Part fusion between 2 given objects."""
newobj = doc.addObject("Part::Fuse", "Fusion")
newobj.Base = objects[0]
newobj.Tool = objects[1]
format(objects[0], [newobj])
add_to_parent(objects[0], [newobj])
add_list.append(newobj)
return True
def makeShell(objects):
"""Make a shell or compound with the given objects."""
faces = []
done_list = []
for obj in objects:
if obj.Shape.Faces:
faces.extend(obj.Shape.Faces)
done_list.append(obj)
if not faces:
return None
shp = Part.makeShell(faces)
if shp.isNull():
return None
newobj = doc.addObject("Part::Feature", shp.ShapeType)
newobj.Shape = shp
# Format before applying diffuse color:
format(done_list[0], [newobj])
add_to_parent(done_list[0], [newobj])
add_list.append(newobj)
delete_list.extend(done_list)
if App.GuiUp and params.get_param("preserveFaceColor"):
# Must happen after add_to_parent for correct CenterOfMass.
colors = gui_utils.get_diffuse_color(done_list)
if len(faces) != len(colors):
newobj.ViewObject.DiffuseColor = [colors[0]]
else:
# The ordering of shp.Faces may be different. Since we cannot
# compare via hashCode(), we have to iterate and use different
# criteria to find the correct color.
old_data = []
for face, color in zip(faces, colors):
old_data.append([face.Area, face.CenterOfMass, color])
new_colors = []
for new_face in shp.Faces:
new_area = new_face.Area
new_cen = new_face.CenterOfMass
for old_area, old_cen, old_color in old_data:
if math.isclose(new_area, old_area, abs_tol=1e-7) and new_cen.isEqual(
old_cen, 1e-7
):
new_colors.append(old_color)
break
newobj.ViewObject.DiffuseColor = new_colors
if params.get_param("preserveFaceNames"):
firstName = done_list[0].Label
nameNoTrailNumbers = re.sub(r"\d+$", "", firstName)
newobj.Label = "{} {}".format(newobj.Label, nameNoTrailNumbers)
return newobj
def joinFaces(objects):
"""Make one big face from the given objects, if possible."""
faces = []
done_list = []
for obj in objects:
if obj.Shape.Faces:
faces.extend(obj.Shape.Faces)
done_list.append(obj)
if not faces:
return False
if not DraftGeomUtils.is_coplanar(faces, 1e-3):
return False
fuse_face = faces.pop(0)
for face in faces:
fuse_face = fuse_face.fuse(face)
face = DraftGeomUtils.concatenate(fuse_face)
# check if concatenate failed
if face.isEqual(fuse_face):
return False
# several coplanar and non-curved faces, they can become a Draft Wire
if len(face.Wires) == 1 and not DraftGeomUtils.hasCurves(face):
newobj = make_wire.make_wire(face.Wires[0], closed=True, face=True)
# if not possible, we do a non-parametric union
else:
newobj = doc.addObject("Part::Feature", "Union")
newobj.Shape = face
format(done_list[0], [newobj])
add_to_parent(done_list[0], [newobj])
add_list.append(newobj)
delete_list.extend(done_list)
return True
def makeSketchFace(obj):
"""Make a face from a sketch."""
face = Part.makeFace(obj.Shape.Wires, "Part::FaceMakerBullseye")
if not face:
return False
newobj = doc.addObject("Part::Feature", "Face")
newobj.Shape = face
format(obj, [newobj])
add_to_parent(obj, [newobj])
add_list.append(newobj)
delete_list.append(obj)
return True
def makeFaces(objects):
"""Make a face from every closed wire in the given objects."""
result = False
for obj in objects:
new_list = []
for wire in obj.Shape.Wires:
try:
face = Part.Face(wire)
except Part.OCCError:
continue
newobj = doc.addObject("Part::Feature", "Face")
newobj.Shape = face
new_list.append(newobj)
if not new_list:
continue
format(obj, new_list)
add_to_parent(obj, new_list)
add_list.extend(new_list)
delete_list.append(obj)
result = True
return result
def makeWires(objects):
"""Join edges in the given objects into wires."""
edges = []
done_list = []
for obj in objects:
if obj.Shape.Edges:
edges.extend(obj.Shape.Edges)
done_list.append(obj)
if not edges:
return False
try:
sorted_edges = Part.sortEdges(edges)
if _DEBUG:
for cluster in sorted_edges:
for edge in cluster:
print("Curve: {}".format(edge.Curve))
print(
"first: {}, last: {}".format(
edge.Vertexes[0].Point, edge.Vertexes[-1].Point
)
)
wires = [Part.Wire(cluster) for cluster in sorted_edges]
except Part.OCCError:
return False
if len(objects) > 1 and len(wires) == len(objects):
# we still have the same number of objects, we actually didn't join anything!
return False
new_list = []
for wire in wires:
newobj = doc.addObject("Part::Feature", "Wire")
newobj.Shape = wire
new_list.append(newobj)
# We don't know which wire came from which obj, we format them the same:
format(done_list[0], new_list)
add_to_parent(done_list[0], new_list)
add_list.extend(new_list)
delete_list.extend(done_list)
return True
def _draftify(obj):
"""Wrapper for draftify."""
new_list = draftify.draftify(obj, delete=False)
if not new_list:
return False
if not isinstance(new_list, list):
new_list = [new_list]
format(obj, new_list)
add_to_parent(obj, new_list)
add_list.extend(new_list)
delete_list.append(obj)
return True
# helper functions (same as in downgrade.py)
def get_parent(obj):
# Problem with obj.getParent():
# https://github.com/FreeCAD/FreeCAD/issues/19600
parent = obj.getParentGroup()
if parent is not None:
return parent
return obj.getParentGeoFeatureGroup()
def can_be_deleted(obj):
if not obj.InList:
return True
for other in obj.InList:
if is_group(other):
continue
if other.TypeId == "App::Part":
continue
return False
return True
def delete_object(obj):
if utils.is_deleted(obj):
return
parent = get_parent(obj)
if parent is not None and parent.TypeId == "PartDesign::Body":
obj = parent
if not can_be_deleted(obj):
# Make obj invisible instead:
obj.Visibility = False
return
if obj.TypeId == "PartDesign::Body":
obj.removeObjectsFromDocument()
doc.removeObject(obj.Name)
def add_to_parent(obj, new_list):
parent = get_parent(obj)
if parent is None:
if doc.getObject("Draft_Construction"):
# This cludge is required because the make_* commands may
# put new objects in the construction group.
constr_group = doc.getObject("Draft_Construction")
for newobj in new_list:
constr_group.removeObject(newobj)
return
if parent.TypeId == "PartDesign::Body":
# We don't add to a PD Body. We process its placement and
# add to its parent instead.
for newobj in new_list:
newobj.Placement = parent.Placement.multiply(newobj.Placement)
add_to_parent(parent, new_list)
return
for newobj in new_list:
# Using addObject is different from just changing the Group property.
# With addObject the object will be added to the parent group, but if
# that is a normal group, also to that group's parent GeoFeatureGroup,
# if available.
parent.addObject(newobj)
def format(obj, new_list):
for newobj in new_list:
gui_utils.format_object(newobj, obj, ignore_construction=True)
doc = App.ActiveDocument
add_list = []
delete_list = []
result = False
if not isinstance(objects, list):
objects = [objects]
if not objects:
return add_list, delete_list
# analyzing objects
faces = []
wires = []
openwires = []
facewires = []
edges = []
loneedges = []
groups = []
meshes = []
parts = []
for obj in objects:
if obj.TypeId == "App::DocumentObjectGroup":
groups.append(obj)
elif hasattr(obj, "Shape"):
parts.append(obj)
faces.extend(obj.Shape.Faces)
wires.extend(obj.Shape.Wires)
edges.extend(obj.Shape.Edges)
for face in obj.Shape.Faces:
facewires.extend(face.Wires)
wirededges = []
for wire in obj.Shape.Wires:
if len(wire.Edges) > 1:
for edge in wire.Edges:
wirededges.append(edge.hashCode())
if not wire.isClosed():
openwires.append(wire)
for edge in obj.Shape.Edges:
if not edge.hashCode() in wirededges and not edge.isClosed():
loneedges.append(edge)
elif obj.isDerivedFrom("Mesh::Feature"):
meshes.append(obj)
objects = parts
if _DEBUG:
print("objects: {}, edges: {}".format(objects, edges))
print("wires: {}, openwires: {}".format(wires, openwires))
print("faces: {}".format(faces))
print("groups: {}".format(groups))
print("facewires: {}, loneedges: {}".format(facewires, loneedges))
if not (groups or objects or meshes):
result = False
elif force:
if force == "closeGroupWires":
result = closeGroupWires(groups)
elif force == "turnToParts":
result = turnToParts(meshes)
else:
# functions that work on a single object:
single_funcs = {
"closeWire": closeWire,
"draftify": _draftify,
"makeSketchFace": makeSketchFace,
"makeSolid": makeSolid,
}
# functions that work on multiple objects:
multi_funcs = {
"joinFaces": joinFaces,
"makeCompound": makeCompound,
"makeFaces": makeFaces,
"makeFusion": makeFusion,
"makeShell": makeShell,
"makeWires": makeWires,
}
if force in single_funcs:
result = any([single_funcs[force](obj) for obj in objects])
elif force in multi_funcs:
result = multi_funcs[force](objects)
else:
_msg(translate("draft", "Upgrade: Unknown force method:") + " " + force)
result = False
# if we have a group: close each wire inside
elif groups:
result = closeGroupWires(groups)
if result:
_msg(translate("draft", "Found groups: closing open wires inside"))
# if we have meshes, we try to turn them into shapes
elif meshes:
result = turnToParts(meshes)
if result:
_msg(translate("draft", "Found meshes: turning them into Part shapes"))
else:
# checking faces coplanarity
# The precision needed in Part.makeFace is 1e-7. Here we use a
# higher value to let that function throw the exception when
# joinFaces is called if the precision is insufficient.
if faces:
faces_coplanarity = DraftGeomUtils.is_coplanar(faces, 1e-3)
parent = get_parent(objects[0])
same_parent = True
same_parent_type = getattr(parent, "TypeId", "") # "" for global space.
if len(objects) > 1:
for obj in objects[1:]:
if get_parent(obj) != parent:
same_parent = False
same_parent_type = None
break
# we have only faces
if faces and len(facewires) == len(wires) and not openwires and not loneedges:
# we have one shell: we try to make a solid
# this also handles PD Bodies and PD features with solids (result will be False)
if len(objects) == 1 and len(faces) > 3 and not faces_coplanarity:
result = makeSolid(objects[0])
if result:
_msg(translate("draft", "Found 1 solidifiable object: solidifying it"))
# we have exactly 2 objects: we fuse them
elif (
len(objects) == 2
and not faces_coplanarity
and same_parent
and same_parent_type != "PartDesign::Body"
):
result = makeFusion(objects)
if result:
_msg(translate("draft", "Found 2 objects: fusing them"))
# we have many separate faces: we try to make a shell or compound
elif (
len(objects) > 1
and len(faces) > 1
and same_parent
and same_parent_type != "PartDesign::Body"
):
result = makeShell(objects)
if result:
_msg(
translate(
"draft", "Found several objects: creating a " + result.Shape.ShapeType
)
)
# we have faces: we try to join them if they are coplanar
elif len(objects) == 1 and len(faces) > 1 and faces_coplanarity:
result = joinFaces(objects)
if result:
_msg(
translate(
"draft", "Found object with several coplanar faces: refining them"
)
)
# only one object: if not parametric, we "draftify" it
elif (
len(objects) == 1
and not objects[0].isDerivedFrom("Part::Part2DObjectPython")
and not utils.get_type(objects[0]) in ["BezCurve", "BSpline", "Wire"]
):
result = _draftify(objects[0])
if result:
_msg(
translate(
"draft",
"Found 1 non-parametric object: replacing it with a Draft object",
)
)
# in the following cases there are no faces
elif not faces:
# we have only closed wires
if wires and not openwires and not loneedges:
# we have a sketch: extract a face
if len(objects) == 1 and objects[0].isDerivedFrom("Sketcher::SketchObject"):
result = makeSketchFace(objects[0])
if result:
_msg(
translate(
"draft", "Found 1 closed sketch object: creating a face from it"
)
)
# only closed wires
else:
result = makeFaces(objects)
if result:
_msg(translate("draft", "Found closed wires: creating faces"))
# wires or edges: we try to join them
elif len(objects) > 1 and len(edges) > 1 and same_parent:
result = makeWires(objects)
if result:
_msg(translate("draft", "Found several wires or edges: wiring them"))
else:
result = makeCompound(objects)
if result:
_msg(
translate(
"draft", "Found several non-treatable objects: creating compound"
)
)
# special case, we have only one open wire. We close it, unless it has only 1 edge!
elif len(objects) == 1 and len(openwires) == 1:
result = closeWire(objects[0])
if result:
_msg(translate("draft", "Found 1 open wire: closing it"))
# only one object: if not parametric, we "draftify" it
elif (
len(objects) == 1
and len(edges) == 1
and not objects[0].isDerivedFrom("Part::Part2DObjectPython")
and not utils.get_type(objects[0]) in ["BezCurve", "BSpline", "Wire"]
):
edge_type = DraftGeomUtils.geomType(objects[0].Shape.Edges[0])
# currently only support Line and Circle
if edge_type in ("Line", "Circle"):
result = _draftify(objects[0])
if result:
_msg(
translate(
"draft",
"Found 1 non-parametric object: replacing it with a Draft object",
)
)
# only points, no edges
elif not edges and len(objects) > 1:
result = makeCompound(objects)
if result:
_msg(translate("draft", "Found points: creating compound"))
# all other cases, if more than 1 object, make a compound
elif len(objects) > 1:
result = makeCompound(objects)
if result:
_msg(translate("draft", "Found several non-treatable objects: creating compound"))
# no result has been obtained
if not result:
_msg(translate("draft", "Unable to upgrade these objects"))
if delete:
for obj in delete_list:
delete_object(obj)
gui_utils.select(add_list)
return add_list, delete_list
## @}
|