Скрипт Freecad Python для создания головоломки — предохранители работают, но разрезы работают только частичноPython

Программы на Python
Ответить
Anonymous
 Скрипт Freecad Python для создания головоломки — предохранители работают, но разрезы работают только частично

Сообщение Anonymous »

Я пытаюсь создать головоломку в FreeCad. Если вы вставите первый блок кода в интерпретатор Python FreeCad v1.1, он создаст сетку блоков. Это демо-пример среды. Цель состоит в том, чтобы соединить каждый квадрат сетки.
Затем, когда я вставляю второй блок кода, он создает формы «ласточкин хвост», которые будут вырезаны из женских блоков и слиты с мужскими блоками.
Это все правильно и выглядит идеально.
Но проблема в третьем блоке. Все «ласточкины хвосты» срастаются правильно, но режутся неправильно. Лишь некоторые из них режут правильно. Один или два будут обрезаны правильно, но остальные - неудачно.
Я не уверен, это мой код или проблема с FreeCAD... Есть идеи, это известная ошибка или я делаю что-то не так?
Спасибо!
Первый код - создайте демонстрационную среду

Код: Выделить всё

import FreeCAD as App
import Part

# Create a new document
doc = App.activeDocument()
if not doc:
doc = App.newDocument("GridDocument")

# Dimensions of each box
size = 10.0

# Grid settings: 3 columns, 2 rows (total 6 boxes)
columns = 3
rows = 3

for i in range(columns):
for j in range(rows):
# Create the box shape
box_shape = Part.makeBox(size, size, size)

# Create a document object to hold the shape
box_obj = doc.addObject("Part::Feature", f"Box_{i}_{j}")
box_obj.Shape = box_shape

# Calculate position (all touching)
x_pos = i * size
y_pos = j * size

# Apply the position
box_obj.Placement = App.Placement(App.Vector(x_pos, y_pos, 0), App.Rotation(0, 0, 0))

# Refresh the 3D view
doc.recompute()
Второй код — создает формы «ласточкин хвост»

Код: Выделить всё

import FreeCAD as App
import FreeCADGui as Gui
import Part

# ==============================================================================
# PART 1: GEOMETRY & PLACEMENT HELPERS
# ==============================================================================

def get_absolute_placement(obj):
"""Recursive calculation to handle nested containers/LinkGroups."""
global_plm = obj.Placement
current_obj = obj
while True:
parents = current_obj.InList
if not parents:
break
found_parent = False
for parent in parents:
if hasattr(parent, "Placement"):
global_plm = parent.Placement.multiply(global_plm)
current_obj = parent
found_parent = True
break
if not found_parent:
break
return global_plm

def get_global_corners(obj):
"""
Returns a SET of rounded global corner tuples for an object.
"""
# 1. Determine Global Placement
is_link_related = False
if obj.isDerivedFrom("App::Link") or obj.isDerivedFrom("App::LinkGroup"):
is_link_related = True
else:
for parent in obj.InList:
if parent.isDerivedFrom("App::LinkGroup"):
is_link_related = True
break

if is_link_related:
global_plm = get_absolute_placement(obj)
elif hasattr(obj, "getGlobalPlacement"):
global_plm = obj.getGlobalPlacement()
else:
global_plm = obj.Placement

# 2. Calculate Effective Transform (Global * Inverse Local)
effective_transform = global_plm.multiply(obj.Placement.inverse())

if not hasattr(obj, "Shape"):
return set()

bbox = obj.Shape.BoundBox

# 3. Local Corners
corners_local = [
App.Vector(bbox.XMin, bbox.YMin, bbox.ZMin),
App.Vector(bbox.XMax, bbox.YMin, bbox.ZMin),
App.Vector(bbox.XMin, bbox.YMax, bbox.ZMin),
App.Vector(bbox.XMax, bbox.YMax, bbox.ZMin),
App.Vector(bbox.XMin, bbox.YMin, bbox.ZMax),
App.Vector(bbox.XMax, bbox.YMin, bbox.ZMax),
App.Vector(bbox.XMin, bbox.YMax, bbox.ZMax),
App.Vector(bbox.XMax, bbox.YMax, bbox.ZMax),
]

# 4.  Transform and Round
corners_global = set()
tolerance = 4

for c_local in corners_local:
c_global = effective_transform.multVec(c_local)
pt = (round(c_global.x, tolerance),
round(c_global.y, tolerance),
round(c_global.z, tolerance))
corners_global.add(pt)

return corners_global

# ==============================================================================
# PART 2: TOOL GENERATION
# ==============================================================================

def make_dovetail_tool(center_point, is_vertical, height_z, interface_length):
"""
Creates the dovetail cutter shape.
"""
# Proportional Dimensions
joint_depth = interface_length * 0.2
joint_narrow = interface_length * 0.2
joint_wide = interface_length * 0.4

# 1. Add Oversize (for clearance/overlap)
oversize = 0.2
tool_height = height_z + oversize

# Create points centered on Y-axis (for horizontal tool)
p1 = App.Vector(0,           -(joint_narrow/2), 0)
p2 = App.Vector(joint_depth, -(joint_wide/2),   0)
p3 = App.Vector(joint_depth,  (joint_wide/2),   0)
p4 = App.Vector(0,            (joint_narrow/2), 0)
p5 = p1

wire = Part.makePolygon([p1, p2, p3, p4, p5])
face = Part.Face(wire)
solid = face.extrude(App.Vector(0, 0, tool_height))

# 2. Center the tool vertically (Z-axis)
# Move it down by half the oversize so it extends slightly above and below
solid.translate(App.Vector(0, 0, -(oversize/2.0)))

# 3. Rotate if vertical orientation is required
if is_vertical:
solid.rotate(App.Vector(0,0,0), App.Vector(0,0,1), 90)

# 4. Move to global position
solid.translate(center_point)

return solid

def create_preview_object(doc, tool_shape, name_a, name_b):
"""
Creates a visual object in the document instead of performing boolean ops.
"""
label = f"Preview_Tool_{name_a}_vs_{name_b}"
obj = doc.addObject("Part::Feature", label)
obj.Shape = tool_shape

# Visual properties: Red and 50% transparent
obj.ViewObject.ShapeColor = (1.0, 0.0, 0.0)
obj.ViewObject.Transparency = 50
return obj

# ==============================================================================
# PART 3: MAIN EXECUTION
# ==============================================================================

def main():
selection = Gui.Selection.getSelection()
if not selection or len(selection) < 2:
print("Error: Select at least 2 panels.")
return

doc = App.activeDocument()
print(f"Generating PREVIEW tools for {len(selection)} panels...")

panel_data = {}

for obj in selection:
c_set = get_global_corners(obj)
if c_set:
# Convert to list for indexing coordinates
c_list = list(c_set)
xs = [p[0] for p in c_list]
ys = [p[1] for p in c_list]
zs = [p[2] for p in c_list]

panel_data[obj.Name] = {
"obj": obj,
"corners": c_set, # Keep set for intersection check
"cx": sum(xs) / len(xs),
"cy": sum(ys) / len(ys),
"sx": max(xs) - min(xs),
"sy": max(ys) - min(ys),
"z_base": min(zs),
"height": max(zs) - min(zs)
}

keys = sorted(list(panel_data.keys()))
tool_count = 0

# 2.  Iterate unique pairs
for i in range(len(keys)):
for j in range(i + 1, len(keys)):
name_a = keys[i]
name_b = keys[j]
data_a = panel_data[name_a]
data_b = panel_data[name_b]

# Check for shared corners
shared = data_a["corners"].intersection(data_b["corners"])

if len(shared) >= 4:
# Neighbor detected
dx = data_b["cx"] - data_a["cx"]
dy = data_b["cy"] - data_a["cy"]

mid_x = (data_a["cx"] + data_b["cx"]) / 2.0
mid_y = (data_a["cy"] + data_b["cy"]) / 2.0
z_pos = data_a["z_base"]
joint_center = App.Vector(mid_x, mid_y, z_pos)
h_val = data_a["height"]

# Determine orientation and generate tool
if abs(dx) > abs(dy):
# Neighbors are left/right of each other (Horizontal difference)
# Joint runs along Y, so interface length is Y size (sy)
ref_size = min(data_a["sy"], data_b["sy"])
tool = make_dovetail_tool(joint_center, False, h_val, ref_size)
create_preview_object(doc, tool, name_a, name_b)
tool_count += 1
else:
# Neighbors are above/below each other (Vertical difference)
# Joint runs along X, so interface length is X size (sx)
ref_size = min(data_a["sx"], data_b["sx"])
# is_vertical=True rotates the tool to face Y
tool = make_dovetail_tool(joint_center, True, h_val, ref_size)
create_preview_object(doc, tool, name_a, name_b)
tool_count += 1

doc.recompute()
print(f"Done. {tool_count} preview tools created.")

if __name__ == "__main__":
main()
Третий код — соедините и разрежьте фигуры «ласточкин хвост»

Код: Выделить всё

```py

import FreeCAD as App
import Part

def get_absolute_placement(obj):
"""Calculates global placement to handle nested containers."""
global_plm = obj.Placement
current_obj = obj
while True:
parents = current_obj.InList
if not parents:
break
found_parent = False
for parent in parents:
if hasattr(parent, "Placement"):
global_plm = parent.Placement.multiply(global_plm)
current_obj = parent
found_parent = True
break
if not found_parent:
break
return global_plm

def get_global_center(obj):
"""Fast approximation of global center using BoundingBox center."""
if not hasattr(obj, "Shape"):
return App.Vector(0,0,0)

# Get global placement
plm = get_absolute_placement(obj)

# Get local center
bb = obj.Shape.BoundBox
local_center = bb.Center

# Transform to global
return plm.multVec(local_center)

def apply_existing_tool(tool_obj, name_a, name_b):
doc = App.activeDocument()

# 1. Find the actual panel objects
obj_a = doc.getObject(name_a)
obj_b = doc.getObject(name_b)

if not obj_a or not obj_b:
print(f"Skipping {tool_obj.Name}: Could not find panels '{name_a}' or '{name_b}'")
return False

# 2.  Determine Orientation (Who is Male/Pin? Who is Female/Hole?)
# We re-evaluate the relative positions to ensure consistency with the standard logic:
# Left = Male, Right = Female
# Bottom = Male, Top = Female

pos_a = get_global_center(obj_a)
pos_b = get_global_center(obj_b)

dx = pos_b.x - pos_a.x
dy = pos_b.y - pos_a.y

male_obj = None
female_obj = None

if abs(dx) > abs(dy):
# Horizontal Interface
if dx > 0: # B is right of A
male_obj, female_obj = obj_a, obj_b
else:      # A is right of B
male_obj, female_obj = obj_b, obj_a
else:
# Vertical Interface
if dy > 0: # B is above A
male_obj, female_obj = obj_a, obj_b
else:      # A is above B
male_obj, female_obj = obj_b, obj_a

# 3. Apply the Tool
# The Tool is already in Global Space. We must transform it to Local Space.

tool_shape_global = tool_obj.Shape.copy()

# --- Fuse to Male ---
male_global_plm = get_absolute_placement(male_obj)
to_male_local = male_global_plm.inverse().toMatrix()

tool_for_male = tool_shape_global.copy()
tool_for_male.transformShape(to_male_local)

male_obj.Shape = male_obj.Shape.fuse(tool_for_male)

# --- Cut from Female ---
female_global_plm = get_absolute_placement(female_obj)
to_female_local = female_global_plm.inverse().toMatrix()

tool_for_female = tool_shape_global.copy()
tool_for_female.transformShape(to_female_local)

female_obj.Shape = female_obj.Shape.cut(tool_for_female)

return True

def main():
doc = App.activeDocument()
if not doc:
print("No active document.")
return

# Find all preview tools
preview_tools = []
for obj in doc.Objects:
if obj.Name.startswith("Preview_Tool_"):
preview_tools.append(obj)

if not preview_tools:
print("No 'Preview_Tool' objects found. Run the preview script first.")
return

print(f"Found {len(preview_tools)} preview tools. Applying operations...")

doc.openTransaction("Apply Preview Tools")

processed_count = 0

for tool in preview_tools:
# Parse name: Preview_Tool_{NameA}_vs_{NameB}
# We split by "_vs_" to separate the two panels
# The prefix is "Preview_Tool_" (length 13)

raw_name = tool.Name[13:] # Strip "Preview_Tool_"
if "_vs_" not in raw_name:
print(f"Skipping {tool.Name}: Naming format incorrect.")
continue

parts = raw_name.split("_vs_")
if len(parts) != 2:
print(f"Skipping {tool.Name}: Could not parse panel names.")
continue

name_a = parts[0]
name_b = parts[1]

success = apply_existing_tool(tool, name_a, name_b)
if success:
processed_count += 1
# Delete the tool after use to clean up
doc.removeObject(tool.Name)

doc.commitTransaction()
doc.recompute()
print(f"Done. Applied {processed_count} joints and removed preview tools.")

if __name__ == "__main__":
main()
После запуска третьего кода я получаю следующее:
Изображение
Но вы увидите, что если я начну скрывать блоки, большинства слотов там не будет. Некоторые из них есть, но большинства нет. Здесь я скрываю нижний левый угол, и вы видите, что в соседних блоках отсутствуют слоты.
Изображение


Подробнее здесь: https://stackoverflow.com/questions/798 ... ially-work
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Python»