feature/完善待装配设备和布线-zwl-0618
parent
e861c2408a
commit
f78f30509f
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,134 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _bootstrap_windows_freecad_runtime():
|
||||
if os.name != "nt":
|
||||
return
|
||||
|
||||
runtime_json = os.environ.get("QET_FREECAD_RUNTIME_JSON")
|
||||
if not runtime_json:
|
||||
runtime_json = os.path.join(os.environ.get("LOCALAPPDATA", ""), "QETDeps", "runtime.json")
|
||||
if not runtime_json or not os.path.exists(runtime_json):
|
||||
return
|
||||
|
||||
with open(runtime_json, "r", encoding="utf-8-sig") as handle:
|
||||
runtime = json.load(handle)
|
||||
|
||||
roots = [str(item) for item in runtime.get("path_prefix", []) if item]
|
||||
freecad_root = runtime.get("freecad_root", "")
|
||||
if freecad_root:
|
||||
roots.extend(
|
||||
[
|
||||
os.path.join(freecad_root, "build", "Mod", "Material"),
|
||||
os.path.join(freecad_root, "build", "Mod", "Part"),
|
||||
os.path.join(freecad_root, "build", "Mod", "Import"),
|
||||
os.path.join(freecad_root, "build", "Mod"),
|
||||
]
|
||||
)
|
||||
roots.append(os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "downlevel"))
|
||||
|
||||
for root in roots:
|
||||
if root and os.path.isdir(root):
|
||||
try:
|
||||
os.add_dll_directory(root)
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
if root not in sys.path:
|
||||
sys.path.append(root)
|
||||
|
||||
|
||||
_bootstrap_windows_freecad_runtime()
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
OUT_DIR = Path(__file__).resolve().parent
|
||||
FCSTD_PATH = OUT_DIR / "qet_panel_assembly.FCStd"
|
||||
STEP_PATH = OUT_DIR / "qet_panel_assembly.step"
|
||||
TOLERANCE = 0.01
|
||||
|
||||
|
||||
def _bbox(doc, name):
|
||||
obj = doc.getObject(name)
|
||||
if obj is None:
|
||||
raise AssertionError("missing object: {0}".format(name))
|
||||
bbox = obj.Shape.BoundBox
|
||||
return bbox
|
||||
|
||||
|
||||
def _assert_close(label, actual, expected):
|
||||
if abs(actual - expected) > TOLERANCE:
|
||||
raise AssertionError("{0}: expected {1:.3f}, got {2:.3f}".format(label, expected, actual))
|
||||
|
||||
|
||||
def main():
|
||||
if not FCSTD_PATH.exists():
|
||||
raise AssertionError("missing FCStd: {0}".format(FCSTD_PATH))
|
||||
if not STEP_PATH.exists():
|
||||
raise AssertionError("missing STEP: {0}".format(STEP_PATH))
|
||||
if "ISO-10303-21" not in STEP_PATH.read_text(encoding="utf-8", errors="ignore")[:256]:
|
||||
raise AssertionError("STEP output is missing ISO-10303-21 header")
|
||||
|
||||
doc = App.openDocument(str(FCSTD_PATH))
|
||||
backbox = _bbox(doc, "Panel_BackBox")
|
||||
left_face = _bbox(doc, "Panel_LeftDoorFace")
|
||||
mount_plate = _bbox(doc, "Panel_RightMountPlate")
|
||||
object_names = {obj.Name for obj in doc.Objects}
|
||||
|
||||
removed_objects = sorted(
|
||||
name
|
||||
for name in object_names
|
||||
if name.startswith("WireFrame_")
|
||||
or "_Screw_R" in name
|
||||
or name in {"Panel_HingeTop", "Panel_HingeBottom"}
|
||||
)
|
||||
if removed_objects:
|
||||
raise AssertionError("removed guide/hinge objects are still present: {0}".format(removed_objects))
|
||||
|
||||
_assert_close("left thin face touches main body", left_face.XMax, backbox.XMin)
|
||||
_assert_close("right mounting plate touches rear of main body", mount_plate.YMin, backbox.YMax)
|
||||
_assert_close(
|
||||
"right mounting plate is centered on main body's wide face",
|
||||
(mount_plate.XMin + mount_plate.XMax) / 2.0,
|
||||
(backbox.XMin + backbox.XMax) / 2.0,
|
||||
)
|
||||
if mount_plate.XMin < backbox.XMin - TOLERANCE or mount_plate.XMax > backbox.XMax + TOLERANCE:
|
||||
raise AssertionError(
|
||||
"right mounting plate must sit within main body's wide face: "
|
||||
"plate X=({0:.3f}, {1:.3f}), body X=({2:.3f}, {3:.3f})".format(
|
||||
mount_plate.XMin,
|
||||
mount_plate.XMax,
|
||||
backbox.XMin,
|
||||
backbox.XMax,
|
||||
)
|
||||
)
|
||||
|
||||
for name in (
|
||||
"ConnectorBank_Left_Body",
|
||||
"ConnectorBank_Right_Body",
|
||||
"AccessoryConnector_LowerLeft",
|
||||
"AccessoryConnector_LowerRight",
|
||||
):
|
||||
bbox = _bbox(doc, name)
|
||||
_assert_close("{0} touches mounting plate".format(name), bbox.YMin, mount_plate.YMax)
|
||||
if bbox.XMin < backbox.XMin - TOLERANCE or bbox.XMax > backbox.XMax + TOLERANCE:
|
||||
raise AssertionError(
|
||||
"{0} must sit within main body's wide face: object X=({1:.3f}, {2:.3f}), body X=({3:.3f}, {4:.3f})".format(
|
||||
name,
|
||||
bbox.XMin,
|
||||
bbox.XMax,
|
||||
backbox.XMin,
|
||||
backbox.XMax,
|
||||
)
|
||||
)
|
||||
|
||||
print("verified qet_panel_assembly contact constraints")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,36 @@
|
||||
# NAU03 Split Cabinet Asset
|
||||
|
||||
This directory contains a simplified NAU03-style electrical cabinet STEP asset for QET / FreeCAD assembly tests.
|
||||
|
||||
## Outputs
|
||||
|
||||
- `nau03_test_cabinet_split.step`: STEP model with cabinet faces split into independently hideable parts.
|
||||
- `nau03_test_cabinet_split.FCStd`: FreeCAD source document generated for inspection and regeneration.
|
||||
- `nau03_test_cabinet_split_report.json`: Generated metadata.
|
||||
- `create_nau03_split_cabinet.py`: FreeCAD Python generator.
|
||||
- `verify_nau03_split_cabinet.py`: Import verification script.
|
||||
|
||||
## Hideable Parts
|
||||
|
||||
The STEP is intentionally exported at medium granularity:
|
||||
|
||||
- `NAU03_Cabinet_Frame`
|
||||
- `NAU03_Left_Side_Panel`
|
||||
- `NAU03_Right_Side_Panel`
|
||||
- `NAU03_Rear_Panel`
|
||||
- `NAU03_Front_Left_Door`
|
||||
- `NAU03_Front_Right_Door`
|
||||
- `NAU03_Top_Roof`
|
||||
- `NAU03_Bottom_Base`
|
||||
- `NAU03_Interior_Mounting_Plate`
|
||||
|
||||
This avoids the original behavior where hiding the imported cabinet hides a large assembly at once, while also avoiding hundreds of tiny screw and hinge tree objects.
|
||||
|
||||
## Regenerate
|
||||
|
||||
```powershell
|
||||
$runtime = Get-Content -LiteralPath 'C:\Users\ng123\AppData\Local\QETDeps\runtime.json' -Raw | ConvertFrom-Json
|
||||
$env:QET_FREECAD_RUNTIME_JSON = 'C:\Users\ng123\AppData\Local\QETDeps\runtime.json'
|
||||
& $runtime.freecad_python 'D:\LightWork3D\data\examples\qet_split_cabinet\create_nau03_split_cabinet.py'
|
||||
& $runtime.freecad_python 'D:\LightWork3D\data\examples\qet_split_cabinet\verify_nau03_split_cabinet.py'
|
||||
```
|
||||
@ -0,0 +1,262 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _bootstrap_windows_freecad_runtime() -> None:
|
||||
if os.name != "nt":
|
||||
return
|
||||
|
||||
runtime_json = os.environ.get("QET_FREECAD_RUNTIME_JSON")
|
||||
if not runtime_json:
|
||||
runtime_json = os.path.join(os.environ.get("LOCALAPPDATA", ""), "QETDeps", "runtime.json")
|
||||
if not runtime_json or not os.path.exists(runtime_json):
|
||||
return
|
||||
|
||||
with open(runtime_json, "r", encoding="utf-8-sig") as handle:
|
||||
runtime = json.load(handle)
|
||||
|
||||
roots = [str(item) for item in runtime.get("path_prefix", []) if item]
|
||||
freecad_root = runtime.get("freecad_root", "")
|
||||
roots.extend(
|
||||
[
|
||||
os.path.join(freecad_root, "build", "Mod", "Material"),
|
||||
os.path.join(freecad_root, "build", "Mod", "Part"),
|
||||
os.path.join(freecad_root, "build", "Mod", "Import"),
|
||||
os.path.join(freecad_root, "build", "Mod"),
|
||||
os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "downlevel"),
|
||||
]
|
||||
)
|
||||
|
||||
for root in roots:
|
||||
if root and os.path.isdir(root):
|
||||
try:
|
||||
os.add_dll_directory(root)
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
if root not in sys.path:
|
||||
sys.path.append(root)
|
||||
|
||||
|
||||
_bootstrap_windows_freecad_runtime()
|
||||
|
||||
import FreeCAD as App
|
||||
import Part
|
||||
|
||||
|
||||
OUT_DIR = Path(__file__).resolve().parent
|
||||
FCSTD_PATH = OUT_DIR / "nau03_test_cabinet_split.FCStd"
|
||||
STEP_PATH = OUT_DIR / "nau03_test_cabinet_split.step"
|
||||
REPORT_PATH = OUT_DIR / "nau03_test_cabinet_split_report.json"
|
||||
|
||||
|
||||
def _style(obj, color, transparency=0) -> None:
|
||||
if not hasattr(obj, "ViewObject") or obj.ViewObject is None:
|
||||
return
|
||||
obj.ViewObject.ShapeColor = color
|
||||
obj.ViewObject.Transparency = transparency
|
||||
|
||||
|
||||
def _box(dx, dy, dz, x, y, z):
|
||||
return Part.makeBox(dx, dy, dz, App.Vector(x, y, z))
|
||||
|
||||
|
||||
def _part(doc, name, shape, color, transparency=0):
|
||||
obj = doc.addObject("Part::Feature", name)
|
||||
obj.Label = name
|
||||
obj.Shape = shape
|
||||
_style(obj, color, transparency)
|
||||
return obj
|
||||
|
||||
|
||||
def _fuse(shapes):
|
||||
valid_shapes = [shape for shape in shapes if shape and not shape.isNull()]
|
||||
if not valid_shapes:
|
||||
raise ValueError("expected at least one shape to fuse")
|
||||
result = valid_shapes[0]
|
||||
for shape in valid_shapes[1:]:
|
||||
result = result.fuse(shape)
|
||||
try:
|
||||
result = result.removeSplitter()
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
def _windowed_door(x, y, z, width, thickness, height, window_x, window_z, window_w, window_h):
|
||||
panel = _box(width, thickness, height, x, y, z)
|
||||
void = _box(window_w, thickness + 6.0, window_h, x + window_x, y - 3.0, z + window_z)
|
||||
panel = panel.cut(void)
|
||||
|
||||
# Small raised border around the viewing window, matching the reference cabinet silhouette.
|
||||
border = 12.0
|
||||
border_y = y - 4.0
|
||||
border_t = 8.0
|
||||
border_shapes = [
|
||||
_box(window_w + border * 2.0, border_t, border, x + window_x - border, border_y, z + window_z - border),
|
||||
_box(window_w + border * 2.0, border_t, border, x + window_x - border, border_y, z + window_z + window_h),
|
||||
_box(border, border_t, window_h, x + window_x - border, border_y, z + window_z),
|
||||
_box(border, border_t, window_h, x + window_x + window_w, border_y, z + window_z),
|
||||
]
|
||||
handle_w = 16.0
|
||||
handle_h = 120.0
|
||||
handle = _box(handle_w, 14.0, handle_h, x + width - 42.0, y - 12.0, z + height * 0.48)
|
||||
return _fuse([panel, handle] + border_shapes)
|
||||
|
||||
|
||||
def _frame_shape(width, depth, height):
|
||||
post = 45.0
|
||||
rail = 55.0
|
||||
x0 = -width / 2.0
|
||||
y0 = -depth / 2.0
|
||||
shapes = []
|
||||
|
||||
for x in (x0, width / 2.0 - post):
|
||||
for y in (y0, depth / 2.0 - post):
|
||||
shapes.append(_box(post, post, height, x, y, 0.0))
|
||||
|
||||
for z in (0.0, height - rail):
|
||||
shapes.extend(
|
||||
[
|
||||
_box(width, rail, rail, x0, y0, z),
|
||||
_box(width, rail, rail, x0, depth / 2.0 - rail, z),
|
||||
_box(rail, depth, rail, x0, y0, z),
|
||||
_box(rail, depth, rail, width / 2.0 - rail, y0, z),
|
||||
]
|
||||
)
|
||||
return _fuse(shapes)
|
||||
|
||||
|
||||
def _vent_slots(x, y, z, width, thickness, height, count):
|
||||
panel = _box(width, thickness, height, x, y, z)
|
||||
slot_w = width * 0.62
|
||||
slot_h = 16.0
|
||||
pitch = 42.0
|
||||
start_z = z + height * 0.22
|
||||
for index in range(count):
|
||||
sx = x + (width - slot_w) / 2.0
|
||||
sz = start_z + index * pitch
|
||||
panel = panel.cut(_box(slot_w, thickness + 6.0, slot_h, sx, y - 3.0, sz))
|
||||
return panel
|
||||
|
||||
|
||||
def _export_step(objects) -> None:
|
||||
try:
|
||||
import Import
|
||||
|
||||
Import.export(objects, str(STEP_PATH))
|
||||
except Exception:
|
||||
import ImportGui
|
||||
|
||||
ImportGui.export(objects, str(STEP_PATH))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
doc = App.newDocument("NAU03_Test_Cabinet_Split")
|
||||
|
||||
width = 750.0
|
||||
depth = 1300.0
|
||||
height = 2300.0
|
||||
side_t = 32.0
|
||||
door_t = 36.0
|
||||
roof_h = 75.0
|
||||
base_h = 110.0
|
||||
|
||||
metal = (0.62, 0.68, 0.69)
|
||||
dark = (0.08, 0.09, 0.10)
|
||||
inner = (0.34, 0.38, 0.40)
|
||||
|
||||
x_min = -width / 2.0
|
||||
x_max = width / 2.0
|
||||
y_front = -depth / 2.0
|
||||
y_back = depth / 2.0
|
||||
|
||||
door_gap = 8.0
|
||||
door_width = (width - 30.0 - door_gap) / 2.0
|
||||
door_z = 55.0
|
||||
door_h = height - 105.0
|
||||
left_door = _windowed_door(
|
||||
x_min + 12.0,
|
||||
y_front - door_t,
|
||||
door_z,
|
||||
door_width,
|
||||
door_t,
|
||||
door_h,
|
||||
95.0,
|
||||
1260.0,
|
||||
185.0,
|
||||
470.0,
|
||||
)
|
||||
right_door = _windowed_door(
|
||||
x_min + 12.0 + door_width + door_gap,
|
||||
y_front - door_t,
|
||||
door_z,
|
||||
door_width,
|
||||
door_t,
|
||||
door_h,
|
||||
70.0,
|
||||
1260.0,
|
||||
185.0,
|
||||
470.0,
|
||||
)
|
||||
|
||||
rear_panel = _box(width - 36.0, side_t, height - 90.0, x_min + 18.0, y_back, 45.0)
|
||||
left_panel = _vent_slots(x_min - side_t, y_front + 42.0, 50.0, side_t, depth - 84.0, height - 100.0, 10)
|
||||
right_panel = _vent_slots(x_max, y_front + 42.0, 50.0, side_t, depth - 84.0, height - 100.0, 10)
|
||||
|
||||
roof = _fuse(
|
||||
[
|
||||
_box(width + 70.0, depth + 100.0, roof_h, x_min - 35.0, y_front - 50.0, height),
|
||||
_box(width + 30.0, 58.0, 45.0, x_min - 15.0, y_front - 10.0, height - 45.0),
|
||||
_box(width + 30.0, 58.0, 45.0, x_min - 15.0, y_back - 48.0, height - 45.0),
|
||||
]
|
||||
)
|
||||
base = _fuse(
|
||||
[
|
||||
_box(width + 35.0, depth + 30.0, base_h, x_min - 17.5, y_front - 15.0, -base_h),
|
||||
_box(width + 85.0, 95.0, 58.0, x_min - 42.5, y_front - 40.0, -base_h - 58.0),
|
||||
_box(width + 85.0, 95.0, 58.0, x_min - 42.5, y_back - 55.0, -base_h - 58.0),
|
||||
]
|
||||
)
|
||||
mounting_plate = _box(width - 165.0, 14.0, height - 360.0, x_min + 82.5, y_back - 82.0, 180.0)
|
||||
|
||||
objects = [
|
||||
_part(doc, "NAU03_Cabinet_Frame", _frame_shape(width, depth, height), dark, 0),
|
||||
_part(doc, "NAU03_Left_Side_Panel", left_panel, metal, 0),
|
||||
_part(doc, "NAU03_Right_Side_Panel", right_panel, metal, 0),
|
||||
_part(doc, "NAU03_Rear_Panel", rear_panel, metal, 0),
|
||||
_part(doc, "NAU03_Front_Left_Door", left_door, metal, 0),
|
||||
_part(doc, "NAU03_Front_Right_Door", right_door, metal, 0),
|
||||
_part(doc, "NAU03_Top_Roof", roof, metal, 0),
|
||||
_part(doc, "NAU03_Bottom_Base", base, dark, 0),
|
||||
_part(doc, "NAU03_Interior_Mounting_Plate", mounting_plate, inner, 8),
|
||||
]
|
||||
|
||||
doc.recompute()
|
||||
doc.saveAs(str(FCSTD_PATH))
|
||||
_export_step(objects)
|
||||
|
||||
report = {
|
||||
"source_reference": r"D:\downloadWX\xwechat_files\wxid_pv577xuccot722_5d4a\msg\file\2026-04\MCCB CABINET ASS'Y.STEP",
|
||||
"outputs": {"fcstd": str(FCSTD_PATH), "step": str(STEP_PATH)},
|
||||
"dimensions_mm": {
|
||||
"width": width,
|
||||
"depth": depth,
|
||||
"height_without_roof_base": height,
|
||||
"overall_height": height + roof_h + base_h + 58.0,
|
||||
},
|
||||
"hideable_parts": [obj.Label for obj in objects],
|
||||
"object_count": len(objects),
|
||||
}
|
||||
REPORT_PATH.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
print("Generated FCStd: {0}".format(FCSTD_PATH))
|
||||
print("Generated STEP: {0}".format(STEP_PATH))
|
||||
print("Generated report: {0}".format(REPORT_PATH))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
||||
{
|
||||
"source_reference": "D:\\downloadWX\\xwechat_files\\wxid_pv577xuccot722_5d4a\\msg\\file\\2026-04\\MCCB CABINET ASS'Y.STEP",
|
||||
"outputs": {
|
||||
"fcstd": "D:\\LightWork3D\\data\\examples\\qet_split_cabinet\\nau03_test_cabinet_split.FCStd",
|
||||
"step": "D:\\LightWork3D\\data\\examples\\qet_split_cabinet\\nau03_test_cabinet_split.step"
|
||||
},
|
||||
"dimensions_mm": {
|
||||
"width": 750.0,
|
||||
"depth": 1300.0,
|
||||
"height_without_roof_base": 2300.0,
|
||||
"overall_height": 2543.0
|
||||
},
|
||||
"hideable_parts": [
|
||||
"NAU03_Cabinet_Frame",
|
||||
"NAU03_Left_Side_Panel",
|
||||
"NAU03_Right_Side_Panel",
|
||||
"NAU03_Rear_Panel",
|
||||
"NAU03_Front_Left_Door",
|
||||
"NAU03_Front_Right_Door",
|
||||
"NAU03_Top_Roof",
|
||||
"NAU03_Bottom_Base",
|
||||
"NAU03_Interior_Mounting_Plate"
|
||||
],
|
||||
"object_count": 9
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _bootstrap_windows_freecad_runtime() -> None:
|
||||
if os.name != "nt":
|
||||
return
|
||||
runtime_json = os.environ.get("QET_FREECAD_RUNTIME_JSON")
|
||||
if not runtime_json:
|
||||
runtime_json = os.path.join(os.environ.get("LOCALAPPDATA", ""), "QETDeps", "runtime.json")
|
||||
if not runtime_json or not os.path.exists(runtime_json):
|
||||
return
|
||||
with open(runtime_json, "r", encoding="utf-8-sig") as handle:
|
||||
runtime = json.load(handle)
|
||||
roots = [str(item) for item in runtime.get("path_prefix", []) if item]
|
||||
freecad_root = runtime.get("freecad_root", "")
|
||||
roots.extend(
|
||||
[
|
||||
os.path.join(freecad_root, "build", "Mod", "Import"),
|
||||
os.path.join(freecad_root, "build", "Mod", "Part"),
|
||||
os.path.join(freecad_root, "build", "Mod"),
|
||||
os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "downlevel"),
|
||||
]
|
||||
)
|
||||
for root in roots:
|
||||
if root and os.path.isdir(root):
|
||||
try:
|
||||
os.add_dll_directory(root)
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
if root not in sys.path:
|
||||
sys.path.append(root)
|
||||
|
||||
|
||||
_bootstrap_windows_freecad_runtime()
|
||||
|
||||
import FreeCAD as App
|
||||
import Import
|
||||
|
||||
|
||||
OUT_DIR = Path(__file__).resolve().parent
|
||||
STEP_PATH = Path(os.environ.get("NAU03_SPLIT_CABINET_STEP", OUT_DIR / "nau03_test_cabinet_split.step"))
|
||||
|
||||
EXPECTED_LABELS = {
|
||||
"NAU03_Cabinet_Frame",
|
||||
"NAU03_Left_Side_Panel",
|
||||
"NAU03_Right_Side_Panel",
|
||||
"NAU03_Rear_Panel",
|
||||
"NAU03_Front_Left_Door",
|
||||
"NAU03_Front_Right_Door",
|
||||
"NAU03_Top_Roof",
|
||||
"NAU03_Bottom_Base",
|
||||
"NAU03_Interior_Mounting_Plate",
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not STEP_PATH.exists():
|
||||
raise AssertionError(f"missing STEP output: {STEP_PATH}")
|
||||
|
||||
header = STEP_PATH.read_text(encoding="utf-8", errors="ignore")[:256]
|
||||
if "ISO-10303-21" not in header:
|
||||
raise AssertionError("STEP output is missing ISO-10303-21 header")
|
||||
|
||||
doc = App.newDocument("verify_nau03_split")
|
||||
Import.insert(str(STEP_PATH), doc.Name)
|
||||
doc.recompute()
|
||||
|
||||
shape_objects = [
|
||||
obj
|
||||
for obj in doc.Objects
|
||||
if hasattr(obj, "Shape") and not obj.Shape.isNull() and len(obj.Shape.Solids) > 0
|
||||
]
|
||||
labels = {obj.Label for obj in shape_objects}
|
||||
missing = sorted(EXPECTED_LABELS - labels)
|
||||
if missing:
|
||||
raise AssertionError(f"missing expected independently hideable labels: {missing}")
|
||||
|
||||
if len(shape_objects) > 16:
|
||||
raise AssertionError(f"too many top-level shape objects: {len(shape_objects)}")
|
||||
|
||||
for obj in shape_objects:
|
||||
if obj.Shape.Volume <= 0:
|
||||
raise AssertionError(f"{obj.Label} has no positive volume")
|
||||
|
||||
print("verified NAU03 split cabinet STEP")
|
||||
print("shape_objects=", len(shape_objects))
|
||||
print("labels=", sorted(labels))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,309 @@
|
||||
# FreeCADExchange GUI panel for QET pending device assembly.
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
try:
|
||||
import FreeCADGui as Gui
|
||||
except ImportError:
|
||||
Gui = None
|
||||
|
||||
try:
|
||||
from PySide6 import QtCore, QtWidgets
|
||||
except ImportError:
|
||||
try:
|
||||
from PySide2 import QtCore, QtWidgets
|
||||
except ImportError:
|
||||
try:
|
||||
from PySide import QtCore
|
||||
from PySide import QtGui as QtWidgets
|
||||
except ImportError:
|
||||
QtCore = None
|
||||
QtWidgets = None
|
||||
|
||||
import DeviceImport
|
||||
|
||||
|
||||
COMMAND_NAME = "QET_Exchange_OpenPendingDevicePanel"
|
||||
|
||||
|
||||
class PendingDeviceAssemblyPanelError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def pending_device_rows(doc):
|
||||
rows = []
|
||||
for item in DeviceImport.list_pending_devices(doc):
|
||||
model_path = (item.get("resolved_model_path", "") or "").strip()
|
||||
model_name = Path(model_path).name if model_path else "未绑定模型"
|
||||
display_tag = (item.get("display_tag", "") or "").strip()
|
||||
label = display_tag or (item.get("label", "") or "").strip()
|
||||
instance_id = (item.get("instance_id", "") or "").strip()
|
||||
title = label or instance_id or "未命名设备"
|
||||
rows.append(
|
||||
{
|
||||
"device": item.get("device"),
|
||||
"display_tag": display_tag,
|
||||
"instance_id": instance_id,
|
||||
"element_uuid": (item.get("element_uuid", "") or "").strip(),
|
||||
"resolved_model_path": model_path,
|
||||
"display_text": "{0} {1}".format(title, model_name),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def _document():
|
||||
doc = getattr(App, "ActiveDocument", None)
|
||||
if doc is None:
|
||||
raise PendingDeviceAssemblyPanelError("请先打开 FreeCAD 工程。")
|
||||
return doc
|
||||
|
||||
|
||||
def _selected_objects():
|
||||
if Gui is None:
|
||||
return []
|
||||
try:
|
||||
return list(Gui.Selection.getSelection() or [])
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def selected_mount_target(exclude_device=None):
|
||||
excluded = DeviceImport._find_device_group_from_object(exclude_device)
|
||||
for obj in _selected_objects():
|
||||
candidate_device = DeviceImport._find_device_group_from_object(obj)
|
||||
if excluded is not None and candidate_device is excluded:
|
||||
continue
|
||||
return obj
|
||||
return None
|
||||
|
||||
|
||||
def _vector_from_tuple(value):
|
||||
try:
|
||||
if value is None:
|
||||
return None
|
||||
return App.Vector(value[0], value[1], value[2])
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _face_anchor_point(face):
|
||||
if face is None:
|
||||
return None
|
||||
for attr_name in ("CenterOfMass", "Center"):
|
||||
point = getattr(face, attr_name, None)
|
||||
if point is not None:
|
||||
return point
|
||||
try:
|
||||
return face.valueAt(0.0, 0.0)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _face_normal(face):
|
||||
if face is None:
|
||||
return None
|
||||
for attr_name in ("normalAt", "NormalAt"):
|
||||
normal_getter = getattr(face, attr_name, None)
|
||||
if callable(normal_getter):
|
||||
try:
|
||||
return normal_getter(0.0, 0.0)
|
||||
except Exception:
|
||||
pass
|
||||
return getattr(face, "Normal", None)
|
||||
|
||||
|
||||
def selected_mount_context(exclude_device=None):
|
||||
excluded = DeviceImport._find_device_group_from_object(exclude_device)
|
||||
if Gui is not None:
|
||||
try:
|
||||
selection_ex = list(Gui.Selection.getSelectionEx() or [])
|
||||
except Exception:
|
||||
selection_ex = []
|
||||
for selected in selection_ex:
|
||||
obj = getattr(selected, "Object", None)
|
||||
candidate_device = DeviceImport._find_device_group_from_object(obj)
|
||||
if excluded is not None and candidate_device is excluded:
|
||||
continue
|
||||
picked_points = list(getattr(selected, "PickedPoints", []) or [])
|
||||
point = picked_points[0] if picked_points else None
|
||||
if point is None:
|
||||
for sub_object in list(getattr(selected, "SubObjects", []) or []):
|
||||
if (getattr(sub_object, "ShapeType", "") or "").lower() == "face":
|
||||
point = _face_anchor_point(sub_object)
|
||||
normal = _face_normal(sub_object)
|
||||
break
|
||||
else:
|
||||
normal = None
|
||||
for sub_object in list(getattr(selected, "SubObjects", []) or []):
|
||||
if (getattr(sub_object, "ShapeType", "") or "").lower() == "face":
|
||||
normal = _face_normal(sub_object)
|
||||
break
|
||||
if point is not None:
|
||||
rotation = getattr(getattr(obj, "Placement", None), "Rotation", None)
|
||||
return {
|
||||
"target": obj,
|
||||
"placement": App.Placement(point, rotation or App.Rotation()),
|
||||
"normal": normal,
|
||||
}
|
||||
if obj is not None:
|
||||
return {"target": obj, "placement": None, "normal": None}
|
||||
|
||||
target = selected_mount_target(exclude_device=exclude_device)
|
||||
return {"target": target, "placement": None, "normal": None}
|
||||
|
||||
|
||||
def _set_status(label, message, error=False):
|
||||
try:
|
||||
label.setText(message)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if error:
|
||||
App.Console.PrintError("[FreeCADExchange] {0}\n".format(message))
|
||||
else:
|
||||
App.Console.PrintMessage("[FreeCADExchange] {0}\n".format(message))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class PendingDeviceAssemblyTaskPanel:
|
||||
def __init__(self):
|
||||
if QtWidgets is None:
|
||||
raise PendingDeviceAssemblyPanelError("Qt widgets are not available.")
|
||||
|
||||
self.rows = []
|
||||
self.form = QtWidgets.QWidget()
|
||||
self.form.setWindowTitle("QET待装配设备")
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self.form)
|
||||
self.device_list = QtWidgets.QListWidget()
|
||||
layout.addWidget(self.device_list)
|
||||
|
||||
self.refresh_button = QtWidgets.QPushButton("刷新清单")
|
||||
self.insert_button = QtWidgets.QPushButton("插入设备")
|
||||
self.insert_to_target_button = QtWidgets.QPushButton("插入到选中目标")
|
||||
layout.addWidget(self.refresh_button)
|
||||
layout.addWidget(self.insert_button)
|
||||
|
||||
offset_row = QtWidgets.QHBoxLayout()
|
||||
offset_row.addWidget(QtWidgets.QLabel("贴合间距"))
|
||||
self.mount_offset_input = QtWidgets.QDoubleSpinBox()
|
||||
self.mount_offset_input.setRange(-10000.0, 10000.0)
|
||||
self.mount_offset_input.setDecimals(1)
|
||||
self.mount_offset_input.setSingleStep(1.0)
|
||||
self.mount_offset_input.setSuffix(" mm")
|
||||
self.mount_offset_input.setValue(0.0)
|
||||
offset_row.addWidget(self.mount_offset_input)
|
||||
layout.addLayout(offset_row)
|
||||
|
||||
layout.addWidget(self.insert_to_target_button)
|
||||
|
||||
self.status_label = QtWidgets.QLabel("")
|
||||
self.status_label.setWordWrap(True)
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
self.refresh_button.clicked.connect(self.refresh)
|
||||
self.insert_button.clicked.connect(self.insert_selected_device)
|
||||
self.insert_to_target_button.clicked.connect(self.insert_selected_device_to_target)
|
||||
|
||||
self.refresh()
|
||||
|
||||
def _selected_device(self):
|
||||
item = self.device_list.currentItem()
|
||||
if item is None:
|
||||
raise PendingDeviceAssemblyPanelError("请先在清单中选择一个待装配设备。")
|
||||
row_index = self.device_list.row(item)
|
||||
if row_index < 0 or row_index >= len(self.rows):
|
||||
raise PendingDeviceAssemblyPanelError("待装配设备清单已变化,请刷新后重试。")
|
||||
device = self.rows[row_index].get("device")
|
||||
if device is None:
|
||||
raise PendingDeviceAssemblyPanelError("待装配设备无效,请刷新后重试。")
|
||||
return device
|
||||
|
||||
def refresh(self):
|
||||
try:
|
||||
self.rows = pending_device_rows(getattr(App, "ActiveDocument", None))
|
||||
self.device_list.clear()
|
||||
for row in self.rows:
|
||||
self.device_list.addItem(row["display_text"])
|
||||
if self.rows:
|
||||
self.device_list.setCurrentRow(0)
|
||||
_set_status(self.status_label, "待装配设备:{0} 个".format(len(self.rows)))
|
||||
except Exception as exc:
|
||||
_set_status(self.status_label, str(exc), error=True)
|
||||
|
||||
def insert_selected_device(self):
|
||||
try:
|
||||
result = DeviceImport.insert_pending_device(_document(), self._selected_device())
|
||||
self.refresh()
|
||||
_set_status(
|
||||
self.status_label,
|
||||
"已插入设备:{0}".format(getattr(result["device"], "Label", "") or getattr(result["device"], "Name", "")),
|
||||
)
|
||||
except Exception as exc:
|
||||
_set_status(self.status_label, str(exc), error=True)
|
||||
|
||||
def insert_selected_device_to_target(self):
|
||||
try:
|
||||
device = self._selected_device()
|
||||
context = selected_mount_context(exclude_device=device)
|
||||
target = context.get("target")
|
||||
if target is None:
|
||||
raise PendingDeviceAssemblyPanelError("请先在 3D 视图中选择安装板、导轨、线槽或柜体安装面。")
|
||||
result = DeviceImport.insert_pending_device(
|
||||
_document(),
|
||||
device,
|
||||
mount_target=target,
|
||||
mount_placement=context.get("placement"),
|
||||
mount_normal=context.get("normal"),
|
||||
mount_offset_mm=self.mount_offset_input.value(),
|
||||
)
|
||||
self.refresh()
|
||||
_set_status(
|
||||
self.status_label,
|
||||
"已插入设备到选中目标:{0}".format(
|
||||
getattr(result["device"], "Label", "") or getattr(result["device"], "Name", "")
|
||||
),
|
||||
)
|
||||
except Exception as exc:
|
||||
_set_status(self.status_label, str(exc), error=True)
|
||||
|
||||
def accept(self):
|
||||
return True
|
||||
|
||||
def reject(self):
|
||||
return True
|
||||
|
||||
|
||||
class CommandOpenPendingDevicePanel:
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "待装配设备",
|
||||
"ToolTip": "打开 QET 待装配设备清单,并将设备插入到当前 3D 场景",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return getattr(App, "ActiveDocument", None) is not None and Gui is not None
|
||||
|
||||
def Activated(self):
|
||||
if Gui is None or not hasattr(Gui, "Control"):
|
||||
return
|
||||
if hasattr(Gui.Control, "activeDialog") and Gui.Control.activeDialog():
|
||||
Gui.Control.closeDialog()
|
||||
Gui.Control.showDialog(PendingDeviceAssemblyTaskPanel())
|
||||
|
||||
|
||||
_COMMANDS_REGISTERED = False
|
||||
|
||||
|
||||
def register_commands():
|
||||
global _COMMANDS_REGISTERED
|
||||
if _COMMANDS_REGISTERED:
|
||||
return
|
||||
if Gui is None or not hasattr(Gui, "addCommand"):
|
||||
return
|
||||
Gui.addCommand(COMMAND_NAME, CommandOpenPendingDevicePanel())
|
||||
_COMMANDS_REGISTERED = True
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,148 @@
|
||||
r"""Smoke test for QET pending-device scene persistence.
|
||||
|
||||
Run with FreeCADCmd.exe, not system Python:
|
||||
|
||||
D:\fc\run-FreeCAD-1.1.1\bin\FreeCADCmd.exe tests\manual\freecad_pending_device_scene_smoke.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
MODULE_DIR = REPO_ROOT / "src" / "Mod" / "FreeCADExchange"
|
||||
if str(MODULE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(MODULE_DIR))
|
||||
|
||||
import DeviceImport # noqa: E402
|
||||
|
||||
|
||||
def _close_doc(doc):
|
||||
if doc is None:
|
||||
return
|
||||
try:
|
||||
App.closeDocument(doc.Name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _make_source_model(path):
|
||||
doc = App.newDocument("SmokeSourceModel")
|
||||
try:
|
||||
body = doc.addObject("Part::Box", "Body")
|
||||
body.Length = 10
|
||||
body.Width = 8
|
||||
body.Height = 6
|
||||
doc.recompute()
|
||||
doc.saveAs(str(path))
|
||||
finally:
|
||||
_close_doc(doc)
|
||||
|
||||
|
||||
def _assert_close(actual, expected, label):
|
||||
if abs(float(actual) - float(expected)) > 1e-6:
|
||||
raise AssertionError("{0}: expected {1}, got {2}".format(label, expected, actual))
|
||||
|
||||
|
||||
def main():
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="qet_pending_device_smoke_"))
|
||||
try:
|
||||
source_model = temp_dir / "device.FCStd"
|
||||
scene_file = temp_dir / "QETScene.FCStd"
|
||||
_make_source_model(source_model)
|
||||
|
||||
scene = App.newDocument("QETSceneSmoke")
|
||||
root = DeviceImport._ensure_root_group(scene, None, "project-smoke")
|
||||
device_group, created = DeviceImport._ensure_device_group(
|
||||
scene,
|
||||
root,
|
||||
"element-smoke",
|
||||
"instance-smoke",
|
||||
str(source_model),
|
||||
"N600",
|
||||
0,
|
||||
)
|
||||
if not created:
|
||||
raise AssertionError("smoke device group should be newly created")
|
||||
DeviceImport._set_device_assembly_state(
|
||||
device_group,
|
||||
DeviceImport.ASSEMBLY_STATE_PENDING,
|
||||
)
|
||||
|
||||
mount_target = scene.addObject("App::Part", "MountingPlate")
|
||||
mount_target.Label = "安装板"
|
||||
mount_target.Placement = App.Placement(App.Vector(100, 200, 300), App.Rotation())
|
||||
DeviceImport._ensure_string_property(
|
||||
mount_target,
|
||||
"QetCarrierKind",
|
||||
"QET Mount",
|
||||
"Smoke mount target kind",
|
||||
"mounting_plate",
|
||||
)
|
||||
|
||||
DeviceImport.insert_pending_device(
|
||||
scene,
|
||||
device_group,
|
||||
mount_target=mount_target,
|
||||
mount_placement=App.Placement(App.Vector(10, 20, 30), App.Rotation()),
|
||||
mount_normal=App.Vector(0, 0, 1),
|
||||
mount_offset_mm=5.0,
|
||||
)
|
||||
device_group_name = device_group.Name
|
||||
scene.recompute()
|
||||
scene.saveAs(str(scene_file))
|
||||
_close_doc(scene)
|
||||
|
||||
reopened = App.openDocument(str(scene_file))
|
||||
try:
|
||||
reopened_device = reopened.getObject(device_group_name)
|
||||
if reopened_device is None:
|
||||
raise AssertionError("reopened scene does not contain device group")
|
||||
if getattr(reopened_device, "QetAssemblyState", "") != DeviceImport.ASSEMBLY_STATE_PLACED:
|
||||
raise AssertionError("reopened device is not marked Placed")
|
||||
_assert_close(reopened_device.Placement.Base.x, 10.0, "placement x")
|
||||
_assert_close(reopened_device.Placement.Base.y, 20.0, "placement y")
|
||||
_assert_close(reopened_device.Placement.Base.z, 35.0, "placement z")
|
||||
if getattr(reopened_device, "QetMountHostName", "") != "MountingPlate":
|
||||
raise AssertionError("mount host name was not persisted")
|
||||
if getattr(reopened_device, "QetMountHostKind", "") != "mounting_plate":
|
||||
raise AssertionError("mount host kind was not persisted")
|
||||
if getattr(reopened_device, "QetMountOffsetMm", "") != "5.000000":
|
||||
raise AssertionError("mount offset was not persisted")
|
||||
normal_payload = json.loads(getattr(reopened_device, "QetMountHostNormalJson", "{}") or "{}")
|
||||
_assert_close(normal_payload.get("z", 0.0), 1.0, "normal z")
|
||||
finally:
|
||||
_close_doc(reopened)
|
||||
|
||||
result_path = os.environ.get("QET_PENDING_DEVICE_SMOKE_RESULT", "").strip()
|
||||
if result_path:
|
||||
Path(result_path).write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"scene": str(scene_file),
|
||||
"device": device_group_name,
|
||||
"placement": {"x": 10.0, "y": 20.0, "z": 35.0},
|
||||
"assembly_state": DeviceImport.ASSEMBLY_STATE_PLACED,
|
||||
"mount_host": "MountingPlate",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print("SMOKE_OK scene={0}".format(scene_file))
|
||||
return 0
|
||||
finally:
|
||||
if os.environ.get("QET_KEEP_SMOKE_OUTPUT", "").strip() != "1":
|
||||
shutil.rmtree(str(temp_dir), ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue