diff --git a/server_patch/ai_3d_model/ai-solid-wood-cost/VALIDATION_LEDGER.md b/server_patch/ai_3d_model/ai-solid-wood-cost/VALIDATION_LEDGER.md index 4187a49..160f17a 100644 --- a/server_patch/ai_3d_model/ai-solid-wood-cost/VALIDATION_LEDGER.md +++ b/server_patch/ai_3d_model/ai-solid-wood-cost/VALIDATION_LEDGER.md @@ -290,3 +290,12 @@ - 本地新增/改写 12 项五金专项验证并通过;最终完整回归 226 项全部通过(253.672 秒),Python 语法、`git diff --check` 与技能结构校验均通过。四个运行文件已同步到服务器 `/tmp/dalongxia-cad-v85-eI8HeF` 隔离目录,SHA-256 与本机完全一致且服务器静态编译通过。 - 同步时服务器只读状态为 `active=0 waiting=0`,但另有 1 个得物1688持久任务执行中;因此本轮没有启动模型推理、没有覆盖生产文件、没有重启服务。真实 STEP/GLB 与 B-rep 五金模板构建仍须在服务器完全空闲后验证。 - 真实多产品盲测、原图与组装/拆件独立验收、公司实际五金 SKU 批准,以及冻结估价后逐项和总成本误差不超过 5%仍未完成;候选不能部署或标记成功。 + +## 2026-09-08 v114 五金复合实体 B-rep 验收(候选,未上线) + +- 服务器等待门严格等到真实 Chat API 活动、排队、得物1688未终态批次和持久任务全为 0;两个残留浏览器 busy 标记经连续闲置检查被确认为 stale。之后只在 `/tmp/dalongxia-cad-v85-eI8HeF` 运行隔离验证,没有修改生产目录、重启或部署服务。 +- 第一次真实 STEP 验收失败:结构编译和单测都通过,但导出器把同一只铰链的铰杯、底座和两段连接臂作为四个独立实体;OCCT 因此将连接臂与自身底座的正常搭接报为 2 组干涉(511.9999mm³ 与 319.9999mm³)。这一失败没有被算作通过,也没有通过放宽干涉容差规避。 +- 导出器现在会先将一个采购五金模板内的几何基元布尔合并为单一实体,再与门板、侧板等家具实体做权威干涉检查。结构层同时将铰链臂放在门板 Y=18mm 和侧板 X=18mm 的安装面外侧,仅允许铰杯进入已生成的 35mm 铰杯孔。 +- 第二次服务器真实内核验收通过:铰链生成 113053 字节 STEP 和 61632 字节 GLB,孔工序数为 5;旋钮生成 30274 字节 STEP 和 21168 字节 GLB,孔工序数为 1。两个样例的 B-rep 都为零干涉,报告在服务器 `hardware-template-brep-v113-fused/report.json`。 +- 本地结构图谱与制造层 91 项专项回归通过;最终完整回归 227 项全部通过(233.019秒,其中 1 项真实内核用例按设计仅在服务器开关启用时执行),Python 语法和 `git diff --check` 通过。 +- 仍未完成公司实际五金 SKU 模板批准、原始多图到 v2 图谱的新识别、多产品冻结后 BOM/成本盲测和总成本误差不超过 5%。本版仍是未部署候选,不得用以上样例通过替代真实多产品验收。 diff --git a/server_patch/ai_3d_model/ai-solid-wood-cost/probe_structure_graph.py b/server_patch/ai_3d_model/ai-solid-wood-cost/probe_structure_graph.py index 44cba1d..baa8b66 100644 --- a/server_patch/ai_3d_model/ai-solid-wood-cost/probe_structure_graph.py +++ b/server_patch/ai_3d_model/ai-solid-wood-cost/probe_structure_graph.py @@ -941,8 +941,14 @@ def cad_source(compiled, manufacturing_plan=None): f"{float(primitive['length_mm'])!r}, align=(Align.CENTER, Align.CENTER, Align.CENTER))" f"{rotate}.moved(Pos({center[0]!r}, {center[1]!r}, {center[2]!r}))" ) + # These primitives are regions of one purchased fitting. Fuse + # them before export so the authoritative B-rep oracle compares + # the fitting with the furniture, rather than reporting the + # hinge arm's intentional overlap with its own mounting plate. + lines.append(f" part = {primitive_names[0]}") + for primitive_name in primitive_names[1:]: + lines.append(f" part = part + {primitive_name}") lines.extend([ - f" part = Compound(children=[{', '.join(primitive_names)}])", f" part.label = {str(inst['id'])!r}", f" part.color = Color{colors[index % len(colors)]!r}", " parts.append(part)", diff --git a/server_patch/ai_3d_model/ai-solid-wood-cost/structure_graph.py b/server_patch/ai_3d_model/ai-solid-wood-cost/structure_graph.py index 27818e5..33663b3 100644 --- a/server_patch/ai_3d_model/ai-solid-wood-cost/structure_graph.py +++ b/server_patch/ai_3d_model/ai-solid-wood-cost/structure_graph.py @@ -781,7 +781,11 @@ def _build_hardware_template_geometry( # envelope, but it preserves the real L-shaped connection topology. arm_thickness = float(visual["arm_thickness_mm"]) cup_point = [float(value) for value in cup["mounting_center_mm"]] - plate_point = [float(value) for value in plate_center] + cup_outward = -1.0 if cup["face"].endswith("-") else 1.0 + cup_point[cup_axis] += cup_outward * arm_thickness / 2.0 + plate_point = [float(value) for value in plate["mounting_center_mm"]] + plate_outward = -1.0 if plate["face"].endswith("-") else 1.0 + plate_point[plate_axis] += plate_outward * arm_thickness / 2.0 bend = list(cup_point) bend[plate_axis] = plate_point[plate_axis] for role, start, end in ( diff --git a/server_patch/ai_3d_model/ai-solid-wood-cost/test_manufacturing_oracle.py b/server_patch/ai_3d_model/ai-solid-wood-cost/test_manufacturing_oracle.py index e143995..616e26f 100644 --- a/server_patch/ai_3d_model/ai-solid-wood-cost/test_manufacturing_oracle.py +++ b/server_patch/ai_3d_model/ai-solid-wood-cost/test_manufacturing_oracle.py @@ -1,6 +1,7 @@ """General manufacturing-feature tests; no SKU/name answer constants.""" import ast import copy +import json import os import pathlib import tempfile @@ -9,7 +10,8 @@ from unittest import mock from manufacturing_oracle import plan_manufacturing_features from probe_structure_graph import ( - authoritative_interference_feedback, bind_scope_facts, cad_source, + authoritative_interference_feedback, bind_scope_facts, + build_and_authoritatively_validate, cad_source, compile_lower_bound_can_improve, dead_end_question_focus, deterministic_dead_end_question, diagnostic_mounting_clearance_cuts, has_minimum_axis_packing_dead_end, @@ -171,7 +173,9 @@ class ManufacturingOracleTest(unittest.TestCase): self.assertEqual([], diagnostic_mounting_clearance_cuts(compiled)["audit"]) source = cad_source(compiled, plan) self.assertIn("Cylinder(17.5, 12.8", source) - self.assertIn("Compound(children=[hardware_2_1", source) + self.assertIn("part = hardware_2_1", source) + self.assertIn("part = part + hardware_2_2", source) + self.assertNotIn("Compound(children=[hardware_2_1", source) self.assertNotIn("Box(1.0, 1.0, 1.0", source) ast.parse(source) @@ -192,6 +196,62 @@ class ManufacturingOracleTest(unittest.TestCase): self.assertEqual(1, plan["cost_drivers"]["hardware_instances"]) self.assertNotIn("fitting-instance", plan["manufacturing_geometry_by_instance"]) + @unittest.skipUnless( + os.environ.get("DALONGXIA_RUN_REAL_CAD") == "1", + "requires the isolated build123d/OCCT runtime", + ) + def test_real_cad_kernel_builds_template_hardware_without_interference(self): + """Optional server acceptance; unit assertions alone cannot prove B-rep output.""" + from ai_3d_model_engine import AI3DModelEngine + from test_structure_graph import ( + anchored_door_with_hardware, anchored_hinged_door_with_template, + ) + + root = pathlib.Path( + os.environ.get("DALONGXIA_REAL_CAD_OUTPUT") + or tempfile.mkdtemp(prefix="dalongxia-hardware-template-") + ) + root.mkdir(parents=True, exist_ok=True) + os.environ["DALONGXIA_AI_3D_STATE_DIR"] = str(root / "state") + engine = AI3DModelEngine() + reports = [] + for name, graph in ( + ("hinge", anchored_hinged_door_with_template()), + ("knob", anchored_door_with_hardware()), + ): + compiled = compile_graph(graph, source_image_ids={"original-front"}) + self.assertTrue(compiled["ok"], compiled["issues"]) + plan = plan_manufacturing_features(compiled) + self.assertTrue(plan["ok"], plan["issues"]) + build_dir = root / name + build_dir.mkdir(parents=True, exist_ok=True) + source = build_dir / "assembly.step.py" + source.write_text(cad_source(compiled, plan), encoding="utf-8") + engine._validate_source(source.read_text(encoding="utf-8")) + built = build_and_authoritatively_validate( + engine, build_dir, source.name, requested_exports=["glb"] + ) + step = pathlib.Path(str(built["step_path"])) + glbs = sorted(build_dir.glob("*.glb")) + self.assertTrue(step.is_file()) + self.assertGreater(step.stat().st_size, 0) + self.assertTrue(glbs) + self.assertTrue(all(path.stat().st_size > 0 for path in glbs)) + reports.append({ + "case": name, + "step_path": str(step), + "step_bytes": step.stat().st_size, + "glb_paths": [str(path) for path in glbs], + "glb_bytes": [path.stat().st_size for path in glbs], + "hardware_template_instances": plan["cost_drivers"]["hardware_template_instances"], + "hole_operations": plan["cost_drivers"]["hole_operations"], + "release_status": plan["release_status"], + "interference_status": "passed_zero_pairs", + }) + (root / "report.json").write_text( + json.dumps(reports, ensure_ascii=False, indent=2), encoding="utf-8" + ) + def test_probe_resolves_semantic_role_filename_aliases(self): with tempfile.TemporaryDirectory() as folder: root = pathlib.Path(folder) diff --git a/server_patch/ai_3d_model/ai-solid-wood-cost/test_structure_graph.py b/server_patch/ai_3d_model/ai-solid-wood-cost/test_structure_graph.py index cd863bb..aae34b0 100644 --- a/server_patch/ai_3d_model/ai-solid-wood-cost/test_structure_graph.py +++ b/server_patch/ai_3d_model/ai-solid-wood-cost/test_structure_graph.py @@ -286,6 +286,15 @@ class StructureGraphTest(unittest.TestCase): {row["role"] for row in geometry["primitives"]}, ) self.assertIn("cylinder", {row["kind"] for row in geometry["primitives"]}) + arms = [row for row in geometry["primitives"] if row["role"].startswith("hinge_arm_")] + # Door interior face is Y=18 and side interior face is X=18. The arm + # may touch those planes, but only the cup cylinder may enter a board. + self.assertTrue(all( + row["center_mm"][1] - row["size_mm"][1] / 2 >= 18 for row in arms + )) + self.assertTrue(all( + row["center_mm"][0] - row["size_mm"][0] / 2 >= 18 for row in arms + )) self.assertTrue(all(row.get("accepted_as_mounting") for row in result["collision_audit"])) self.assertEqual([], result["derived_contacts"])