2 Commits

Author SHA1 Message Date
likang e3b3558350 update 2026-08-14 13:51:19 +08:00
likang 5fbcf2c0b8 优化skill,添加特征树、编辑窗口 2026-08-07 17:44:12 +08:00
70 changed files with 13157 additions and 467 deletions
+17 -1
View File
@@ -4,6 +4,11 @@
**/.vscode/
*.swp
*.tmp
*~
*.orig
*.rej
*.bak
*.bak-*
# Python environments and caches
**/.venv/
@@ -11,6 +16,7 @@
**/__pycache__/
**/.pytest_cache/
**/.mypy_cache/
**/.ruff_cache/
*.py[cod]
# JavaScript dependencies and generated builds
@@ -18,9 +24,19 @@
**/.next/
**/.cache/
**/dist/
**/coverage/
**/.turbo/
**/.vercel/
**/playwright-report/
**/test-results/
*.tsbuildinfo
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
*.log
# Local frontend runtime data
cad-agent-studio/config/llm.config.yaml
cad-agent-studio/data/
cad-agent-studio/models/
@@ -0,0 +1,152 @@
# SimpleCADAPI Agent Capability Guide
This document is the required backend capability brief for CAD Agent Studio
when considering the `simplecadapi` route. It is intentionally an Agent-facing
decision document, not a geometry template.
## Role
Use this route when the Agent will author backend-native SimpleCADAPI Python
source and the server will only execute, validate, normalize to DesignIR 3.0,
and publish artifacts.
SimpleCADAPI is strongest when its standard factories or replayable model graph
capabilities materially improve correctness, editability, or validation.
## Strong Fits
- The primary requested object is a supported standard mechanical family:
spur, helical, herringbone, ring, or bevel gears; racks; cycloidal discs;
ball bearings; bolts; nuts; roller-chain sprockets; planetary or cycloidal
reducers; joint actuator assemblies.
- The task benefits from `@model`, `ModelResult`, `capture_result`, model JSON,
graph replay, semantic tags, topology lineage, source mapping, QL inspection,
unit checking, tolerance chains, scene packages, or CAD translators.
- The task needs strict STEP/B-Rep diagnostics, target/candidate comparison,
multi-view diagnostics, or slice XOR diagnostics.
- The model is a reusable mechanical product family whose formulas should be a
deterministic SDK workflow rather than one-off source construction.
## Weak Fits
- The request is a custom machined part, flange, hub adapter, housing, bracket,
plate, or fixture where no SimpleCADAPI standard factory directly represents
the primary object.
- Standard part words appear only as feature context. Examples: bolt holes,
screw clearances, nut pockets, bearing seats, gear-mounting holes. These are
features of another part, not requests to generate the standard part itself.
- The requested shape needs freeform or multi-rail surface work outside the
documented SimpleCADAPI vocabulary.
- The current source of truth is an existing build123d source and the user did
not request backend conversion.
## Studio Documentation Discipline
For CAD Agent Studio backend selection, this document is sufficient. Decide
from the capability boundary here: what SimpleCADAPI is strong at, what it is
weak at, and whether the primary requested object matches those strengths.
Do not read SimpleCADAPI API manuals, SDK indexes, or per-function pages during
ordinary backend selection. The selection task only needs to know what the tool
can generate, not the exact API signatures.
Prefer standard-library functions only when the standard component is the
primary requested object and the factory actually covers it.
## Agent Source Contract
When choosing this backend, submit raw Python source to `generate_cad` with:
- `selectedBackend = "simplecadapi"`
- `sourceKind = "simplecadapi_python"`
- A script that accepts `--step`, `--metadata`, and `--model-json`
- STEP export written exactly to the `--step` path
- Model JSON written exactly to the `--model-json` path
- Metadata JSON written exactly to the `--metadata` path
- stdout JSON is optional diagnostic output; file artifacts are the source of
truth for execution success
The source should use one replayable `@model` entry point when model JSON or
graph replay is part of the task. Capture explicit outputs with
`capture_result(...)` and write the returned `ModelResult.model_json`.
Do not submit diagnostic, API-probing, topology-probing, radius-sweep, or smoke
test scripts as `nativeSource`. Do not use `inspect.signature(...)`, broad
`dir(...)` dumps, trial loops, or intentional exceptions to discover the SDK or
geometry at execution time. The source submitted to `generate_cad` must be the
final model generator for the user's part and must write the requested file
artifacts in one execution. If this compact reference is insufficient, report
the missing reference instead of using `generate_cad` as an exploration tool.
If exposing editable parameters, include a top-level block:
```python
# CAD_AGENT_PARAMETERS_START
PARAMETERS = {
"example": 1.0
}
# CAD_AGENT_PARAMETERS_END
```
Each editable metadata parameter must include `name`, `value`, `unit`,
`editable`, `binding_kind`, `parameter_path`, and `regenerate_adapter`.
Use `binding_kind = "python_constant"` for parameters bound to the
`PARAMETERS` block, or `model_graph_parameter` only when the parameter is
actually replayable through the model graph. Use
`regenerate_adapter = "simplecadapi"`.
## Minimal Native Source Reference
Use this compact API surface for Studio generation. It is included here so the
Agent can write production source without reading API indexes during backend
selection.
```python
import argparse
import json
from pathlib import Path
import simplecadapi as scad
@scad.model(graph_id="model")
def build_model():
shape = scad.std.gear.make_spur_gear_rsolid(
n_teeth=24,
module=2.0,
pressure_angle=20.0,
gear_height=8.0,
)
scad.capture_result(value=shape)
return shape
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--step", required=True)
parser.add_argument("--metadata", required=True)
parser.add_argument("--model-json", required=True)
args = parser.parse_args()
result = build_model()
Path(args.model_json).write_text(result.model_json, encoding="utf-8")
scad.export_step(shapes=result.value, filename=args.step)
Path(args.metadata).write_text(json.dumps({"backend": "simplecadapi"}), encoding="utf-8")
```
Replace the body of `build_model()` with the requested standard component or
workflow. Keep the CLI/output protocol unchanged.
Common standard gear factories:
- `scad.std.gear.make_spur_gear_rsolid(n_teeth: int, module: float, pressure_angle: float = 20.0, gear_height: float = 6.0, *, addendum_factor: float = 1.0, clearance_factor: float = 0.25, backlash: float = 0.0)`
- `scad.std.gear.make_helical_gear_rsolid(n_teeth: int, module: float, pressure_angle: float = 20.0, helix_angle: float = 30.0, gear_height: float = 8.0, *, addendum_factor: float = 1.0, clearance_factor: float = 0.25, backlash: float = 0.0)`
- `scad.std.gear.make_herringbone_gear_rsolid(n_teeth: int, module: float, pressure_angle: float = 20.0, helix_angle: float = 32.0, gear_height: float = 10.0, *, addendum_factor: float = 1.0, clearance_factor: float = 0.25, backlash: float = 0.0)`
- `scad.std.gear.make_spur_ring_gear_rsolid(n_teeth: int, module: float, pressure_angle: float = 20.0, gear_height: float = 6.0, rim_thickness: float = 3.0, backlash: float = 0.0, *, addendum_factor: float = 1.0, clearance_factor: float = 0.25)`
- `scad.std.gear.make_helical_ring_gear_rsolid(n_teeth: int, module: float, pressure_angle: float = 20.0, helix_angle: float = 25.0, gear_height: float = 8.0, rim_thickness: float = 3.0, backlash: float = 0.0, *, addendum_factor: float = 1.0, clearance_factor: float = 0.25)`
- `scad.std.gear.make_straight_bevel_gear_rsolid(n_teeth: int, module: float, pitch_angle: float = 45.0, pressure_angle: float = 20.0, face_width: float = 8.0, *, addendum_factor: float = 1.0, clearance_factor: float = 0.25, backlash: float = 0.0)`
## Decision Rule
Choose SimpleCADAPI only when the primary requested model or required workflow
matches its documented standard factories or graph/semantic capabilities. Do
not select SimpleCADAPI merely because the prompt mentions a standard part word
inside another feature. For example, a wheel hub adapter with bolt holes is a
custom flange-like part unless the user asks to generate a bolt as the object.
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Validate submitted Studio source against the imported SimpleCADAPI runtime.
This is a server-side guard only. Agent-facing API documentation remains the
original SimpleCADAPI skill and its API Markdown pages.
"""
from __future__ import annotations
import argparse
import ast
import importlib
import json
from pathlib import Path
from typing import Any
import simplecadapi as scad
STDLIB_MODULES = ("gear", "bearing", "chain", "fastener")
# Make the package's public ``scad.std.<module>`` paths available before AST
# resolution. Model source may use any of these documented standard factories.
for _module_name in STDLIB_MODULES:
importlib.import_module(f"simplecadapi.std.{_module_name}")
def resolve_simplecad_path(parts: list[str]) -> Any:
value: Any = scad
for part in parts:
value = getattr(value, part)
return value
class SourceValidator(ast.NodeVisitor):
def __init__(self) -> None:
self.package_aliases: set[str] = set()
self.imported_names: dict[str, tuple[str, list[str]]] = {}
self.errors: list[dict[str, object]] = []
def error(self, node: ast.AST, message: str) -> None:
self.errors.append({
"line": getattr(node, "lineno", None),
"column": getattr(node, "col_offset", None),
"message": message,
})
def visit_Import(self, node: ast.Import) -> None:
for alias in node.names:
if alias.name == "simplecadapi":
self.package_aliases.add(alias.asname or "simplecadapi")
self.generic_visit(node)
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
module = node.module or ""
if module == "simplecadapi" or module.startswith("simplecadapi."):
module_parts = module.split(".")[1:]
for alias in node.names:
if alias.name == "*":
self.error(node, "Wildcard imports from SimpleCADAPI are not allowed in submitted source.")
continue
try:
resolve_simplecad_path(module_parts + [alias.name])
except AttributeError:
self.error(alias, f"SimpleCADAPI import does not exist: {module}.{alias.name}")
self.imported_names[alias.asname or alias.name] = (module, module_parts + [alias.name])
self.generic_visit(node)
def attribute_path(self, node: ast.Attribute) -> tuple[str, list[str]] | None:
parts: list[str] = [node.attr]
current: ast.AST = node.value
while isinstance(current, ast.Attribute):
parts.append(current.attr)
current = current.value
if isinstance(current, ast.Name) and current.id in self.package_aliases:
return current.id, list(reversed(parts))
return None
def visit_Attribute(self, node: ast.Attribute) -> None:
resolved = self.attribute_path(node)
if resolved:
_, parts = resolved
try:
resolve_simplecad_path(parts)
except AttributeError:
self.error(node, f"SimpleCADAPI attribute does not exist: scad.{'.'.join(parts)}")
self.generic_visit(node)
def visit_Call(self, node: ast.Call) -> None:
if isinstance(node.func, ast.Name) and node.func.id in {"getattr", "setattr", "hasattr", "dir", "inspect"}:
self.error(node, "Dynamic or introspective SDK access is not allowed in submitted SimpleCADAPI source.")
self.generic_visit(node)
def validate_source(source_path: Path) -> dict[str, object]:
try:
source = source_path.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(source_path))
except (OSError, SyntaxError) as exc:
return {"valid": False, "errors": [{"message": f"Cannot parse Python source: {exc}"}]}
validator = SourceValidator()
validator.visit(tree)
if not validator.package_aliases:
validator.errors.append({"message": "Submitted SimpleCADAPI source must import simplecadapi as scad."})
return {"valid": not validator.errors, "errors": validator.errors}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--source", type=Path, required=True)
args = parser.parse_args()
print(json.dumps(validate_source(args.source), ensure_ascii=True))
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+17 -110
View File
@@ -12,6 +12,9 @@
"@ai-sdk/openai-compatible": "3.0.14",
"@assistant-ui/react": "0.14.28",
"@assistant-ui/react-ai-sdk": "1.4.0",
"@radix-ui/react-collapsible": "1.1.20",
"@radix-ui/react-dropdown-menu": "2.1.24",
"@radix-ui/react-slider": "1.4.7",
"ai": "7.0.37",
"animejs": "^4.5.0",
"clsx": "^2.1.1",
@@ -267,6 +270,7 @@
"resolved": "https://registry.npmmirror.com/@assistant-ui/store/-/store-0.2.21.tgz",
"integrity": "sha512-on2xIqLRsMij1J9WXBjKHW2XeVckypicXo7z3Fam6C9DVThZvWIevwJl962uargqq8DKNQxMySQ6Z7Z83+I5nA==",
"license": "MIT",
"peer": true,
"dependencies": {
"use-effect-event": "^2.0.3"
},
@@ -286,6 +290,7 @@
"resolved": "https://registry.npmmirror.com/@assistant-ui/tap/-/tap-0.9.5.tgz",
"integrity": "sha512-xmj8pbZD3QX6VB1kSq1WGIhZOpaSUbD1HuAtYDJFN/oj/Oae9uEs7c5MeoGUrlB7rytxVXKovsqh9hxtNt4u2g==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "*",
"react": "^18 || ^19"
@@ -305,10 +310,11 @@
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -888,9 +894,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -907,9 +910,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -926,9 +926,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -945,9 +942,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -964,9 +958,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -983,9 +974,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1002,9 +990,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1021,9 +1006,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1040,9 +1022,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1065,9 +1044,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1090,9 +1066,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1115,9 +1088,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1140,9 +1110,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1165,9 +1132,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1190,9 +1154,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1215,9 +1176,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1404,9 +1362,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1423,9 +1378,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1442,9 +1394,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1461,9 +1410,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3149,9 +3095,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3169,9 +3112,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3189,9 +3129,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3209,9 +3146,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3251,27 +3185,6 @@
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.11.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.11.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"dev": true,
@@ -3381,6 +3294,7 @@
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3391,6 +3305,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -3833,9 +3748,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3857,9 +3769,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3881,9 +3790,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3905,9 +3811,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -4244,6 +4147,7 @@
"resolved": "https://registry.npmmirror.com/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4253,6 +4157,7 @@
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -4512,7 +4417,8 @@
"version": "0.160.0",
"resolved": "https://registry.npmmirror.com/three/-/three-0.160.0.tgz",
"integrity": "sha512-DLU8lc0zNIPkM7rH5/e1Ks1Z8tWCGRq6g8mPowdDJpw1CFBJMU7UoJjC6PefXW7z//SSl0b2+GCw14LB+uDhng==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/three-mesh-bvh": {
"version": "0.8.3",
@@ -4707,6 +4613,7 @@
"resolved": "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+3
View File
@@ -14,6 +14,9 @@
"@ai-sdk/openai-compatible": "3.0.14",
"@assistant-ui/react": "0.14.28",
"@assistant-ui/react-ai-sdk": "1.4.0",
"@radix-ui/react-collapsible": "1.1.20",
"@radix-ui/react-dropdown-menu": "2.1.24",
"@radix-ui/react-slider": "1.4.7",
"ai": "7.0.37",
"animejs": "^4.5.0",
"clsx": "^2.1.1",
@@ -13,6 +13,7 @@ export async function POST(request: NextRequest) {
provider: body.provider,
model: body.model,
selectedTaskId: body.selectedTaskId,
conversationId: body.conversationId,
});
} catch (error) {
return NextResponse.json({
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from "next/server";
import { readConversation, updateConversation } from "@/lib/conversation-store";
export const runtime = "nodejs";
export async function GET(_request: NextRequest, context: { params: Promise<{ conversationId: string }> }) {
const { conversationId } = await context.params;
try {
const conversation = await readConversation(conversationId);
if (!conversation) {
return NextResponse.json({ error: "Conversation was not found." }, { status: 404 });
}
return NextResponse.json(conversation);
} catch (error) {
return NextResponse.json({
error: error instanceof Error ? error.message : "Failed to read conversation.",
}, { status: 400 });
}
}
export async function PATCH(request: NextRequest, context: { params: Promise<{ conversationId: string }> }) {
const { conversationId } = await context.params;
const body = await request.json().catch(() => ({}));
try {
return NextResponse.json(await updateConversation({
conversationId,
currentTaskId: String(body.currentTaskId || ""),
attachments: Array.isArray(body.attachments) ? body.attachments : [],
}));
} catch (error) {
return NextResponse.json({
error: error instanceof Error ? error.message : "Failed to update conversation.",
}, { status: 400 });
}
}
@@ -0,0 +1,94 @@
import { NextRequest, NextResponse } from "next/server";
import { editDesignIRParameter } from "@/lib/cad-generator";
import { updateConversation } from "@/lib/conversation-store";
import { readManifest } from "@/lib/task-store";
export const runtime = "nodejs";
function cadResultPayload(result: Awaited<ReturnType<typeof editDesignIRParameter>>) {
return {
taskId: result.taskId,
sourcePath: result.sourcePath,
sourceUrl: result.sourceUrl,
parameters: result.parameters,
editableParameters: result.editableParameters,
artifactPath: result.artifactPath,
artifactUrl: result.artifactUrl,
featureTreePath: result.featureTreePath,
featureTreeUrl: result.featureTreeUrl,
parameterCatalogPath: result.parameterCatalogPath,
parameterCatalogUrl: result.parameterCatalogUrl,
previewPath: result.previewPath,
previewUrl: result.previewUrl,
viewerAssetPath: result.viewerAssetPath,
viewerAssetUrl: result.viewerAssetUrl,
summary: result.summary,
};
}
async function updateConversationTask(conversationId: unknown, taskId: string) {
const id = String(conversationId || "").trim();
if (!id) return;
try {
await updateConversation({ conversationId: id, currentTaskId: taskId });
} catch (error) {
console.error("Failed to update CAD conversation after parameter edit", error);
}
}
export async function POST(
request: NextRequest,
context: { params: Promise<{ taskId: string }> },
) {
const { taskId } = await context.params;
const body = await request.json().catch(() => ({}));
const parameter = String(body.parameter || body.name || "").trim();
try {
const manifest = await readManifest(taskId);
if (!manifest) {
return NextResponse.json({ error: "CAD task was not found." }, { status: 404 });
}
const values = (
body.values && typeof body.values === "object" && !Array.isArray(body.values)
? Object.entries(body.values as Record<string, unknown>)
: []
)
.map(([name, rawValue]) => [String(name).trim(), Number(rawValue)] as const)
.filter(([name, value]) => name && Number.isFinite(value));
if (values.length) {
let sourceTaskId = taskId;
let result: Awaited<ReturnType<typeof editDesignIRParameter>> | null = null;
for (const [name, value] of values) {
result = await editDesignIRParameter({
prompt: `参数 ${name} 恢复为默认值 ${value}`,
sourceTaskId,
parameter: name,
value,
});
sourceTaskId = result.taskId;
}
if (!result) {
throw new Error("No parameter values were supplied.");
}
await updateConversationTask(body.conversationId, result.taskId);
return NextResponse.json(cadResultPayload(result));
}
const value = Number(body.value);
if (!parameter || !Number.isFinite(value)) {
throw new Error("Parameter and finite numeric value are required.");
}
const result = await editDesignIRParameter({
prompt: `参数 ${parameter} 修改为 ${value}`,
sourceTaskId: taskId,
parameter,
value,
});
await updateConversationTask(body.conversationId, result.taskId);
return NextResponse.json(cadResultPayload(result));
} catch (error) {
const message = error instanceof Error
? error.message
: "Failed to edit CAD parameter.";
return NextResponse.json({ error: message }, { status: 400 });
}
}
+381 -150
View File
@@ -1,6 +1,6 @@
"use client";
import type { DragEvent } from "react";
import type { CSSProperties, DragEvent } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
AssistantRuntimeProvider,
@@ -9,24 +9,28 @@ import {
MessagePrimitive,
ThreadPrimitive,
useMessage,
useAuiState,
type DataMessagePartProps,
type ToolCallMessagePartProps,
} from "@assistant-ui/react";
import { AssistantChatTransport, useChatRuntime } from "@assistant-ui/react-ai-sdk";
import type { UIMessage } from "ai";
import { CadViewerPreview } from "@/components/cad-viewer-preview";
import { ParameterPanel } from "@/components/parameter-panel";
import { AiSelectionMode } from "@/lib/cad-edit-tools";
import { cn } from "@/lib/utils";
import {
Bot,
Box,
Check,
CircleAlert,
FileImage,
FileJson2,
FileUp,
ChevronsRight,
Loader2,
MessageSquare,
Paperclip,
SlidersHorizontal,
Send,
Settings2,
Sparkles,
@@ -62,9 +66,14 @@ type CadResult = {
taskId: string;
sourcePath?: string;
sourceUrl?: string;
parameters?: Array<Record<string, unknown>>;
editableParameters?: Array<Record<string, unknown>>;
artifactPath?: string;
artifactUrl?: string;
featureTreePath?: string;
featureTreeUrl?: string;
parameterCatalogPath?: string;
parameterCatalogUrl?: string;
previewPath?: string;
previewUrl?: string;
viewerAssetPath?: string;
@@ -79,6 +88,19 @@ type CadProgress = {
message?: string;
};
function editableParameterRecords(
primary?: Array<Record<string, unknown>>,
fallback?: Array<Record<string, unknown>>,
) {
const primaryEditable = Array.isArray(primary)
? primary.filter((parameter) => parameter.editable === true)
: [];
if (primaryEditable.length) return primaryEditable;
return Array.isArray(fallback)
? fallback.filter((parameter) => parameter.editable === true)
: [];
}
type CadError = {
stage?: string;
message: string;
@@ -112,6 +134,21 @@ type PublicConfig = {
};
};
type ConversationRecord = {
conversationId: string;
currentTaskId: string;
attachments: Attachment[];
messages: UIMessage[];
};
type ConversationBootstrap = {
conversationId: string;
currentTaskId: string;
attachments: Attachment[];
messages: UIMessage[];
error?: string;
};
const initialMessages: UIMessage[] = [{
id: "welcome",
role: "assistant",
@@ -121,12 +158,23 @@ const initialMessages: UIMessage[] = [{
}],
}];
const DEEPSEEK_PROVIDER_ID = "deepseek";
const DEFAULT_PROVIDER_ID = "deepseek";
function uniqueId(prefix: string) {
return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
}
function newConversationId() {
return `conv_${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
}
function isSafeId(value: string, prefix: "cad" | "conv") {
const expression = prefix === "cad"
? /^[a-zA-Z0-9_-]{4,80}$/
: /^conv_[a-zA-Z0-9_-]{4,75}$/;
return expression.test(value);
}
function formatBytes(size: number) {
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
@@ -207,6 +255,10 @@ function CadArtifactCard({ data }: DataMessagePartProps<CadResult>) {
: [];
const sourceDownloadUrl = data.sourceUrl
|| taskArtifactUrl(data.taskId, data.sourcePath || "");
const featureTreeDownloadUrl = data.featureTreeUrl
|| (data.featureTreePath ? taskArtifactUrl(data.taskId, data.featureTreePath) : "");
const parameterCatalogDownloadUrl = data.parameterCatalogUrl
|| (data.parameterCatalogPath ? taskArtifactUrl(data.taskId, data.parameterCatalogPath) : "");
return (
<div className="my-2 pl-1 text-[12px]">
<div className="flex items-center gap-2 font-medium text-[#dff5f6]">
@@ -229,6 +281,16 @@ function CadArtifactCard({ data }: DataMessagePartProps<CadResult>) {
DesignIR JSON
</a>
) : null}
{featureTreeDownloadUrl ? (
<a className="inline-flex text-[11px] text-[#9cd67a] underline-offset-2 hover:underline" href={featureTreeDownloadUrl} download>
JSON
</a>
) : null}
{parameterCatalogDownloadUrl ? (
<a className="inline-flex text-[11px] text-[#c6b7ff] underline-offset-2 hover:underline" href={parameterCatalogDownloadUrl} download>
JSON
</a>
) : null}
</div>
{editableParameters.length ? (
<div className="ml-5 mt-2 rounded border border-[#2d3339] bg-[#15181b] p-2">
@@ -384,25 +446,140 @@ function AnyMessage() {
return null;
}
function ChatRunControl() {
const isRunning = useAuiState((state) => state.thread.isRunning);
const buttonClassName = "inline-flex h-8 items-center gap-1.5 rounded px-3 text-[12px] font-semibold disabled:cursor-not-allowed disabled:opacity-50";
if (isRunning) {
return (
<ComposerPrimitive.Cancel
className={cn(
buttonClassName,
"border border-[#7dc8cf]/45 bg-[#20272a] text-[#cdeff2] hover:bg-[#273135]",
)}
title="停止生成"
aria-label="停止生成"
>
<Square className="size-3.5 fill-current" />
</ComposerPrimitive.Cancel>
);
}
return (
<ComposerPrimitive.Send
className={cn(
buttonClassName,
"bg-[#7dc8cf] text-[#101315] hover:bg-[#91d4da]",
)}
title="发送消息"
aria-label="发送消息"
>
<Send className="size-3.5" />
</ComposerPrimitive.Send>
);
}
export function AgentStudio() {
const [bootstrap, setBootstrap] = useState<ConversationBootstrap | null>(null);
useEffect(() => {
const url = new URL(window.location.href);
const requestedConversationId = String(url.searchParams.get("conversationId") || "").trim();
const requestedTaskId = String(url.searchParams.get("taskId") || "").trim();
const conversationId = isSafeId(requestedConversationId, "conv")
? requestedConversationId
: newConversationId();
const currentTaskId = isSafeId(requestedTaskId, "cad") ? requestedTaskId : "";
const finalize = (conversation?: ConversationRecord, error?: string) => {
const nextTaskId = currentTaskId || String(conversation?.currentTaskId || "");
url.searchParams.set("conversationId", conversationId);
if (nextTaskId) url.searchParams.set("taskId", nextTaskId);
else url.searchParams.delete("taskId");
window.history.replaceState({}, "", url);
setBootstrap({
conversationId,
currentTaskId: nextTaskId,
attachments: Array.isArray(conversation?.attachments) ? conversation.attachments : [],
messages: Array.isArray(conversation?.messages) ? conversation.messages : [],
error,
});
};
if (!requestedConversationId) {
void fetch(`/api/conversations/${encodeURIComponent(conversationId)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ currentTaskId }),
})
.then(async (response) => {
if (!response.ok) {
const payload = await response.json().catch(() => ({}));
throw new Error(payload.error || "会话创建失败");
}
finalize(await response.json() as ConversationRecord);
})
.catch((error) => {
finalize(undefined, error instanceof Error ? error.message : "会话创建失败");
});
return;
}
if (!isSafeId(requestedConversationId, "conv")) {
finalize(undefined, "会话链接无效,已创建新的会话。");
return;
}
void fetch(`/api/conversations/${encodeURIComponent(conversationId)}`)
.then(async (response) => {
if (response.status === 404) {
finalize(undefined, "会话记录不存在,将在发送下一条消息时创建。");
return;
}
if (!response.ok) {
const payload = await response.json().catch(() => ({}));
throw new Error(payload.error || "会话加载失败");
}
finalize(await response.json() as ConversationRecord);
})
.catch((error) => {
finalize(undefined, error instanceof Error ? error.message : "会话加载失败");
});
}, []);
if (!bootstrap) {
return (
<main className="flex h-screen items-center justify-center bg-[#111315] text-[12px] text-[#969b9f]">
CAD ...
</main>
);
}
return <AgentStudioSession key={bootstrap.conversationId} bootstrap={bootstrap} />;
}
function AgentStudioSession({ bootstrap }: { bootstrap: ConversationBootstrap }) {
const [config, setConfig] = useState<PublicConfig | null>(null);
const [provider, setProvider] = useState(DEEPSEEK_PROVIDER_ID);
const [provider, setProvider] = useState(DEFAULT_PROVIDER_ID);
const [model, setModel] = useState("");
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [attachments, setAttachments] = useState<Attachment[]>(bootstrap.attachments);
const [pendingViewerContexts, setPendingViewerContexts] = useState<ViewerContext[]>([]);
const [currentTaskId, setCurrentTaskId] = useState("");
const [currentTaskId, setCurrentTaskId] = useState(bootstrap.currentTaskId);
const [previewUrl, setPreviewUrl] = useState("");
const [artifactUrl, setArtifactUrl] = useState("");
const [sourceUrl, setSourceUrl] = useState("");
const [featureTreeUrl, setFeatureTreeUrl] = useState("");
const [parameterCatalogUrl, setParameterCatalogUrl] = useState("");
const [parameterPanelParameters, setParameterPanelParameters] = useState<Array<Record<string, unknown>>>([]);
const [editableParameters, setEditableParameters] = useState<Array<Record<string, unknown>>>([]);
const [robotExporting, setRobotExporting] = useState<RobotExportFormat | "">("");
const [parameterPanelOpen, setParameterPanelOpen] = useState(true);
const [parameterEditPending, setParameterEditPending] = useState("");
const [parameterEditError, setParameterEditError] = useState("");
const [artifactPath, setArtifactPath] = useState("");
const [viewerAssetUrl, setViewerAssetUrl] = useState("");
const [activeEditToolId, setActiveEditToolId] = useState("");
const [aiSelectionMode, setAiSelectionMode] = useState<AiSelectionMode>("point");
const [uploading, setUploading] = useState(false);
const [draggingUpload, setDraggingUpload] = useState(false);
const [error, setError] = useState("");
const [error, setError] = useState(bootstrap.error || "");
const dragDepthRef = useRef(0);
const fileInputRef = useRef<HTMLInputElement | null>(null);
@@ -418,13 +595,22 @@ export function AgentStudio() {
result.sourcePath || "",
),
);
setIfChanged(
setFeatureTreeUrl,
result.featureTreeUrl
|| (result.featureTreePath ? taskArtifactUrl(result.taskId, result.featureTreePath) : ""),
);
setIfChanged(
setParameterCatalogUrl,
result.parameterCatalogUrl
|| (result.parameterCatalogPath ? taskArtifactUrl(result.taskId, result.parameterCatalogPath) : ""),
);
setIfChanged(setPreviewUrl, result.previewUrl || "");
setIfChanged(setViewerAssetUrl, result.viewerAssetUrl || "");
setEditableParameters(
Array.isArray(result.editableParameters)
? result.editableParameters
: [],
);
const nextEditableParameters = editableParameterRecords(result.editableParameters, result.parameters);
setEditableParameters(nextEditableParameters);
setParameterPanelParameters(nextEditableParameters);
setParameterEditError("");
setActiveEditToolId("");
setAiSelectionMode("point");
}, []);
@@ -448,44 +634,79 @@ export function AgentStudio() {
}
}, [applyCadResult]);
const handleRobotExport = useCallback(async (format: RobotExportFormat) => {
if (!currentTaskId || robotExporting) return;
const handleParameterCommit = useCallback(async (parameter: string, value: number) => {
if (!currentTaskId || parameterEditPending) return;
setError("");
setRobotExporting(format);
setParameterEditError("");
setParameterEditPending(parameter);
try {
const response = await fetch(
`/api/tasks/${encodeURIComponent(currentTaskId)}/exports/${format}`,
{ method: "POST" },
`/api/tasks/${encodeURIComponent(currentTaskId)}/parameters`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ parameter, value, conversationId: bootstrap.conversationId }),
},
);
const result = await response.json() as RobotExportResult & { error?: string };
if (!response.ok || !result.packageUrl) {
throw new Error(result.error || `${format.toUpperCase()} 转换失败`);
const result = await response.json() as CadResult & { error?: string };
if (!response.ok || !result.taskId) {
throw new Error(result.error || "参数修改失败");
}
triggerArtifactDownload(result.packageUrl);
applyCadResult(result);
setPendingViewerContexts([]);
} catch (nextError) {
setError(
nextError instanceof Error
? nextError.message
: `${format.toUpperCase()} 转换失败`,
);
const message = nextError instanceof Error ? nextError.message : "参数修改失败";
setParameterEditError(message);
setError(message);
} finally {
setRobotExporting("");
setParameterEditPending("");
}
}, [currentTaskId, robotExporting]);
}, [applyCadResult, currentTaskId, parameterEditPending]);
const handleParameterReset = useCallback(async (values: Record<string, number>) => {
if (!currentTaskId || parameterEditPending || !Object.keys(values).length) return;
setError("");
setParameterEditError("");
setParameterEditPending("__reset__");
try {
const response = await fetch(
`/api/tasks/${encodeURIComponent(currentTaskId)}/parameters`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ values, conversationId: bootstrap.conversationId }),
},
);
const result = await response.json() as CadResult & { error?: string };
if (!response.ok || !result.taskId) {
throw new Error(result.error || "恢复默认参数失败");
}
applyCadResult(result);
setPendingViewerContexts([]);
} catch (nextError) {
const message = nextError instanceof Error ? nextError.message : "恢复默认参数失败";
setParameterEditError(message);
setError(message);
} finally {
setParameterEditPending("");
}
}, [applyCadResult, currentTaskId, parameterEditPending]);
const transport = useMemo(() => new AssistantChatTransport<UIMessage>({
api: "/api/chat",
body: {
provider: DEEPSEEK_PROVIDER_ID,
provider,
model,
attachments,
viewerContext: pendingViewerContexts,
selectedTaskId: currentTaskId,
conversationId: bootstrap.conversationId,
},
}), [attachments, currentTaskId, model, pendingViewerContexts]);
}), [attachments, bootstrap.conversationId, currentTaskId, model, pendingViewerContexts, provider]);
const runtime = useChatRuntime({
messages: initialMessages,
id: bootstrap.conversationId,
messages: bootstrap.messages.length ? bootstrap.messages : initialMessages,
transport,
onData: handleData,
onError: (nextError) => {
@@ -498,9 +719,12 @@ export function AgentStudio() {
.then((response) => response.json())
.then((nextConfig: PublicConfig) => {
setConfig(nextConfig);
setProvider(DEEPSEEK_PROVIDER_ID);
const deepseekProvider = nextConfig.providers[DEEPSEEK_PROVIDER_ID];
setModel(deepseekProvider?.models?.default || "");
const defaultProvider = nextConfig.providers[DEFAULT_PROVIDER_ID]
? DEFAULT_PROVIDER_ID
: nextConfig.defaultProvider;
setProvider(defaultProvider);
const providerConfig = nextConfig.providers[defaultProvider];
setModel(providerConfig?.models?.default || "");
})
.catch((nextError) => {
setError(nextError instanceof Error ? nextError.message : "配置加载失败");
@@ -508,27 +732,38 @@ export function AgentStudio() {
}, []);
useEffect(() => {
const taskId = new URLSearchParams(window.location.search)
.get("taskId")
?.trim();
if (taskId && /^[a-zA-Z0-9_-]{4,80}$/.test(taskId)) {
setCurrentTaskId(taskId);
const storedPanelState = window.localStorage.getItem("cad-agent-studio.parameters-panel-open");
if (storedPanelState === "0") {
setParameterPanelOpen(false);
}
}, []);
useEffect(() => {
window.localStorage.setItem(
"cad-agent-studio.parameters-panel-open",
parameterPanelOpen ? "1" : "0",
);
}, [parameterPanelOpen]);
useEffect(() => {
if (!currentTaskId) return;
const url = new URL(window.location.href);
if (url.searchParams.get("conversationId") !== bootstrap.conversationId) {
url.searchParams.set("conversationId", bootstrap.conversationId);
}
if (url.searchParams.get("taskId") !== currentTaskId) {
url.searchParams.set("taskId", currentTaskId);
window.history.replaceState({}, "", url);
}
}, [currentTaskId]);
window.history.replaceState({}, "", url);
}, [bootstrap.conversationId, currentTaskId]);
useEffect(() => {
if (!currentTaskId) {
setEditableParameters([]);
setParameterPanelParameters([]);
setSourceUrl("");
setFeatureTreeUrl("");
setParameterCatalogUrl("");
return;
}
void fetch(`/api/tasks/${encodeURIComponent(currentTaskId)}`)
@@ -540,9 +775,17 @@ export function AgentStudio() {
: {}
) as Record<string, any>;
const sourcePath = String(manifest.source?.path || "");
if (sourcePath) {
setSourceUrl(taskArtifactUrl(currentTaskId, sourcePath));
}
setSourceUrl(sourcePath ? taskArtifactUrl(currentTaskId, sourcePath) : "");
const artifacts = Array.isArray(task?.artifacts) ? task.artifacts : [];
const featureTreeArtifact = artifacts.find((artifact: Record<string, unknown>) => (
artifact.role === "feature_tree" && typeof artifact.url === "string"
));
setFeatureTreeUrl(featureTreeArtifact?.url ? String(featureTreeArtifact.url) : "");
const parameterArtifact = artifacts.find((artifact: Record<string, unknown>) => (
(artifact.role === "editable_parameters" || artifact.role === "validated_parameters")
&& typeof artifact.url === "string"
));
setParameterCatalogUrl(parameterArtifact?.url ? String(parameterArtifact.url) : "");
const latestArtifact = task?.latestArtifact;
if (latestArtifact?.path && latestArtifact?.url) {
setArtifactPath(String(latestArtifact.path));
@@ -553,11 +796,9 @@ export function AgentStudio() {
if (latestViewerAsset?.url) {
setViewerAssetUrl(String(latestViewerAsset.url));
}
setEditableParameters(
Array.isArray(manifest.parameters?.parameters)
? manifest.parameters.parameters
: [],
);
const nextEditableParameters = editableParameterRecords(manifest.parameters?.parameters);
setEditableParameters(nextEditableParameters);
setParameterPanelParameters(nextEditableParameters);
})
.catch(() => {
// The generation result still supplies the artifact links when task
@@ -566,9 +807,27 @@ export function AgentStudio() {
}, [currentTaskId]);
const providerModels = useMemo(() => {
const models = config?.providers?.[DEEPSEEK_PROVIDER_ID]?.models || {};
const models = config?.providers?.[provider]?.models || {};
return Object.entries(models);
}, [config]);
}, [config, provider]);
const parameterPanelDownloads = useMemo(() => [
...(artifactUrl ? [{ label: "STEP", description: "CAD 交换文件", url: artifactUrl }] : []),
...(sourceUrl ? [{ label: "JSON", description: "DesignIR 源文件", url: sourceUrl }] : []),
...(parameterCatalogUrl ? [{ label: "PARAMS", description: "参数契约", url: parameterCatalogUrl }] : []),
...(featureTreeUrl ? [{ label: "TREE", description: "特征树", url: featureTreeUrl }] : []),
], [artifactUrl, featureTreeUrl, parameterCatalogUrl, sourceUrl]);
const hasModelPanel = Boolean(
currentTaskId
&& (
viewerAssetUrl
|| artifactUrl
|| sourceUrl
|| parameterCatalogUrl
|| featureTreeUrl
|| parameterPanelParameters.length
),
);
const pushViewerContext = useCallback((type: string, payload: Record<string, unknown>) => {
if (!currentTaskId) {
@@ -615,6 +874,8 @@ export function AgentStudio() {
if (!files?.length) return;
setUploading(true);
setError("");
const uploaded: Attachment[] = [];
let nextTaskId = currentTaskId;
try {
for (const file of Array.from(files)) {
const form = new FormData();
@@ -631,10 +892,11 @@ export function AgentStudio() {
if (!response.ok) {
throw new Error(attachment.error || `${file.name} 上传失败`);
}
setAttachments((current) => [attachment, ...current]);
uploaded.push(attachment);
if (attachment.kind === "step") {
continue;
}
nextTaskId = attachment.taskId;
setCurrentTaskId(attachment.taskId);
if (attachment.artifactUrl) {
setArtifactUrl(attachment.artifactUrl);
@@ -650,6 +912,17 @@ export function AgentStudio() {
setError(`STEP 已上传,但预览网格生成失败:${attachment.previewError}`);
}
}
if (uploaded.length) {
setAttachments((current) => {
const next = [...uploaded.reverse(), ...current];
void fetch(`/api/conversations/${encodeURIComponent(bootstrap.conversationId)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ currentTaskId: nextTaskId, attachments: next }),
});
return next;
});
}
} catch (nextError) {
setError(nextError instanceof Error ? nextError.message : "上传失败");
} finally {
@@ -658,7 +931,7 @@ export function AgentStudio() {
fileInputRef.current.value = "";
}
}
}, [currentTaskId]);
}, [bootstrap.conversationId, currentTaskId]);
const handleDragEnter = useCallback((event: DragEvent<HTMLElement>) => {
event.preventDefault();
@@ -697,7 +970,7 @@ export function AgentStudio() {
return (
<AssistantRuntimeProvider runtime={runtime}>
<main className="flex h-screen min-h-0 flex-col bg-[#111315] text-[#e8e8e3]">
<main className="flex h-screen min-h-0 flex-col overflow-x-hidden bg-[#111315] text-[#e8e8e3]">
<header className="flex h-12 shrink-0 items-center justify-between border-b border-[#2d3339] bg-[#15181b] px-3">
<div className="flex min-w-0 items-center gap-2">
<Box className="size-4 text-[#7dc8cf]" aria-hidden="true" />
@@ -711,7 +984,7 @@ export function AgentStudio() {
<div className="flex items-center gap-2">
<div className="flex h-8 items-center gap-1.5 rounded border border-[#2d3339] bg-[#1e2226] px-2 text-[11px] text-[#c9cccf]">
<Settings2 className="size-3.5" aria-hidden="true" />
<span>DeepSeek</span>
<span>{provider === "openai" ? "OpenAI" : provider}</span>
</div>
<label className="hidden h-8 items-center gap-1.5 rounded border border-[#2d3339] bg-[#1e2226] px-2 text-[11px] text-[#c9cccf] md:flex">
<Sparkles className="size-3.5" aria-hidden="true" />
@@ -728,9 +1001,14 @@ export function AgentStudio() {
</div>
</header>
<div className="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[420px_minmax(0,1fr)]">
<div
className="flex min-h-0 flex-1 flex-col md:flex-row"
style={{
"--parameter-panel-width": parameterPanelOpen && hasModelPanel ? "460px" : "0px",
} as CSSProperties}
>
<section
className="relative flex min-h-0 flex-col border-r border-[#2d3339] bg-[#171a1d]"
className="relative flex min-h-0 flex-col border-r border-[#2d3339] bg-[#171a1d] md:w-[420px] md:shrink-0"
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
@@ -761,13 +1039,6 @@ export function AgentStudio() {
<ThreadPrimitive.Root className="flex min-h-0 flex-1 flex-col">
<ThreadPrimitive.Viewport className="scrollbar-thin min-h-0 flex-1 overflow-y-auto px-3 py-3">
{error ? (
<div className="mb-3 flex gap-2 rounded border border-[#7a3438] bg-[#2a171a] p-2 text-[12px] text-[#f2b0b2]">
<CircleAlert className="mt-0.5 size-4 shrink-0" />
<span>{error}</span>
</div>
) : null}
{attachments.length ? (
<div className="mb-3 border-b border-[#252b30] pb-2">
{attachments.slice(0, 5).map((attachment) => (
@@ -805,10 +1076,7 @@ export function AgentStudio() {
{uploading ? <Loader2 className="size-3.5 animate-spin" /> : <Paperclip className="size-3.5" />}
</button>
<ComposerPrimitive.Send className="inline-flex h-8 items-center gap-1.5 rounded bg-[#7dc8cf] px-3 text-[12px] font-semibold text-[#101315] hover:bg-[#91d4da] disabled:cursor-not-allowed disabled:opacity-50">
<Send className="size-3.5" />
</ComposerPrimitive.Send>
<ChatRunControl />
</div>
</div>
</ComposerPrimitive.Root>
@@ -816,7 +1084,7 @@ export function AgentStudio() {
</ThreadPrimitive.Root>
</section>
<section className="relative min-h-[42vh] bg-[#0d0f11] md:min-h-0">
<section className="relative min-h-[42vh] min-w-0 flex-1 bg-[#0d0f11] md:min-h-0">
{viewerAssetUrl ? (
<CadViewerPreview
taskId={currentTaskId}
@@ -824,6 +1092,7 @@ export function AgentStudio() {
viewerAssetUrl={viewerAssetUrl}
activeEditToolId={activeEditToolId}
aiSelectionMode={aiSelectionMode}
isParameterUpdating={Boolean(parameterEditPending)}
onSelectTool={setActiveEditToolId}
onSelectionModeChange={setAiSelectionMode}
onEditIntent={handleEditIntent}
@@ -840,89 +1109,51 @@ export function AgentStudio() {
</div>
</div>
)}
{artifactUrl || sourceUrl ? (
<div className="absolute right-4 top-4 z-30 flex items-center gap-1">
{sourceUrl ? (
<a
className="inline-flex h-7 items-center gap-1 rounded-md border border-[#2d3339] bg-[#15181b]/95 px-2 text-[11px] text-[#c9cccf] hover:bg-[#20252a]"
href={sourceUrl}
download
title="下载可独立重建和参数修改的 DesignIR JSON"
>
<FileJson2 className="size-3.5" />
JSON
</a>
) : null}
{artifactUrl ? (
<a
className="inline-flex h-7 items-center gap-1 rounded-md border border-[#2d3339] bg-[#15181b]/95 px-2 text-[11px] text-[#c9cccf] hover:bg-[#20252a]"
href={artifactUrl}
download
title="下载当前 STEP"
>
<FileUp className="size-3.5" />
STEP
</a>
) : null}
<button
className="inline-flex h-7 items-center gap-1 rounded-md border border-[#2d3339] bg-[#15181b]/95 px-2 text-[11px] text-[#c9cccf] hover:bg-[#20252a] disabled:cursor-wait disabled:opacity-60"
disabled={!currentTaskId || Boolean(robotExporting)}
onClick={() => void handleRobotExport("urdf")}
title="点击后按需生成并下载包含网格的 URDF 包"
type="button"
>
{robotExporting === "urdf"
? <Loader2 className="size-3.5 animate-spin" />
: <FileUp className="size-3.5" />}
URDF
</button>
<button
className="inline-flex h-7 items-center gap-1 rounded-md border border-[#2d3339] bg-[#15181b]/95 px-2 text-[11px] text-[#c9cccf] hover:bg-[#20252a] disabled:cursor-wait disabled:opacity-60"
disabled={!currentTaskId || Boolean(robotExporting)}
onClick={() => void handleRobotExport("mjcf")}
title="点击后按需生成并下载包含网格的 MJCF 包"
type="button"
>
{robotExporting === "mjcf"
? <Loader2 className="size-3.5 animate-spin" />
: <FileUp className="size-3.5" />}
MJCF
</button>
</div>
{hasModelPanel ? (
<button
className="absolute right-4 top-4 z-50 inline-flex h-7 items-center gap-1 rounded-md border border-[#2d3339] bg-[#15181b]/95 px-2 text-[11px] text-[#c9cccf] hover:bg-[#20252a] md:hidden"
onClick={() => setParameterPanelOpen((current) => !current)}
title={parameterPanelOpen ? "隐藏参数面板" : "显示参数面板"}
type="button"
>
<SlidersHorizontal className="size-3.5" />
{parameterPanelOpen ? "HIDE" : "PARAM"}
</button>
) : null}
{editableParameters.length ? (
<details className="absolute right-4 top-14 z-30 w-[310px] rounded-md border border-[#2d3339] bg-[#15181b]/95 text-[11px] shadow-xl">
<summary className="cursor-pointer px-3 py-2 text-[#dff5f6]">
{editableParameters.length}
</summary>
<div className="max-h-64 overflow-auto border-t border-[#2d3339] px-3 py-2 text-[#a9b0b6]">
{editableParameters.map((parameter) => {
const range = (
parameter.validated_range
&& typeof parameter.validated_range === "object"
? parameter.validated_range
: {}
) as Record<string, unknown>;
const minimum = range.minimum_tested_inclusive;
const maximum = range.maximum_tested_inclusive;
const unit = String(parameter.unit || "");
return (
<div className="py-0.5" key={String(parameter.name)}>
<span className="text-[#dff5f6]">{String(parameter.name)}</span>
{` = ${String(parameter.value)}${unit ? ` ${unit}` : ""}`}
{typeof minimum === "number" && typeof maximum === "number"
? `,范围 ${minimum}${maximum}${unit ? ` ${unit}` : ""}`
: ""}
</div>
);
})}
<div className="mt-2 border-t border-[#2d3339] pt-2 text-[10px] leading-4 text-[#777e84]">
DesignIR JSON
</div>
</div>
</details>
{hasModelPanel && !parameterPanelOpen ? (
<button
aria-label="显示参数面板"
className="absolute right-0 top-1/2 z-50 hidden h-[140px] w-9 -translate-y-1/2 flex-col items-center rounded-l-lg border border-r-0 border-gray-200/20 bg-[#17191b] px-1.5 py-2 text-[#f2f4f4] shadow-lg transition-colors hover:bg-[#101316] md:flex"
onClick={() => setParameterPanelOpen(true)}
type="button"
>
<ChevronsRight className="mb-3 size-5 rotate-180 text-white" />
<span className="min-w-[100px] -rotate-90 text-center text-base font-semibold text-white">
Parameters
</span>
</button>
) : null}
</section>
{hasModelPanel ? (
<div
className={cn(
"min-h-0 w-full transform-gpu overflow-hidden transition-[opacity,transform,width] duration-300 ease-out will-change-[width,transform,opacity] md:w-[var(--parameter-panel-width)] md:shrink-0",
parameterPanelOpen
? "translate-x-0 opacity-100"
: "translate-x-full opacity-0 pointer-events-none",
)}
>
<ParameterPanel
downloads={parameterPanelDownloads}
error={parameterEditError}
parameters={parameterPanelParameters}
pendingParameter={parameterEditPending}
onClose={() => setParameterPanelOpen(false)}
onCommit={handleParameterCommit}
onReset={handleParameterReset}
/>
</div>
) : null}
</div>
</main>
</AssistantRuntimeProvider>
@@ -638,6 +638,7 @@ export function CadViewerPreview({
viewerAssetUrl,
activeEditToolId,
aiSelectionMode,
isParameterUpdating = false,
onSelectTool,
onSelectionModeChange,
onEditIntent,
@@ -648,6 +649,7 @@ export function CadViewerPreview({
viewerAssetUrl: string;
activeEditToolId: string;
aiSelectionMode: AiSelectionMode;
isParameterUpdating?: boolean;
onSelectTool: (toolId: string) => void;
onSelectionModeChange: (mode: AiSelectionMode) => void;
onEditIntent: (payload: Record<string, unknown>) => void;
@@ -955,11 +957,11 @@ export function CadViewerPreview({
onHoverReferenceChange={handleHoverReferenceChange}
onActivateReference={handleActivateReference}
/>
{loadState.isRefreshing ? (
<div className="pointer-events-none absolute inset-0 z-20 grid place-items-center bg-[#0d0f11]/35">
{isParameterUpdating || loadState.isRefreshing ? (
<div className="absolute inset-0 z-20 grid place-items-center bg-[#0d0f11]/35" aria-hidden="true">
<div className="flex items-center gap-2 rounded border border-[#2d3339] bg-[#15181b]/95 px-3 py-2 text-[12px] text-[#c9cccf]">
<Loader2 className="size-4 animate-spin text-[#7dc8cf]" />
CAD Viewer ...
{isParameterUpdating ? "正在重建参数化模型..." : "加载 CAD Viewer 资产..."}
</div>
</div>
) : null}
@@ -0,0 +1,395 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import * as Collapsible from "@radix-ui/react-collapsible";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { ChevronDown, ChevronUp, Download, Loader2, RefreshCcw, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { CadamSlider } from "@/components/ui/cadam-slider";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
type EditableParameter = Record<string, unknown>;
type NormalizedParameter = {
name: string;
displayName: string;
group: string;
groupDisplayName: string;
value: number;
defaultValue: number;
min: number | null;
max: number | null;
step: number;
precision: number;
unit: string;
editable: boolean;
editState: string;
};
type DownloadFormat = {
label: string;
description: string;
url: string;
};
function numberValue(value: unknown) {
const numeric = typeof value === "number" ? value : Number(value);
return Number.isFinite(numeric) ? numeric : null;
}
function parameterToView(parameter: EditableParameter): NormalizedParameter | null {
const name = String(parameter.name || parameter.id || "").trim();
const value = numberValue(parameter.value);
const declaredRange = Array.isArray(parameter.range) ? parameter.range : [];
const min = numberValue(parameter.min ?? declaredRange[0]);
const max = numberValue(parameter.max ?? declaredRange[1]);
if (!name || value === null) return null;
return {
name,
displayName: String(parameter.display_name || parameter.displayName || parameter.label || name),
group: String(parameter.group || "dimensions"),
groupDisplayName: String(parameter.group_display_name || parameter.groupDisplayName || "尺寸"),
value,
defaultValue: numberValue(parameter.default_value ?? parameter.defaultValue) ?? value,
min,
max,
step: numberValue(parameter.step) ?? 1,
precision: Math.max(0, numberValue(parameter.precision) ?? 2),
unit: String(parameter.unit || ""),
editable: parameter.editable === true || parameter.edit_state === "declared_unvalidated",
editState: String(parameter.edit_state || "unvalidated"),
};
}
function formatValue(value: number, precision: number) {
if (!Number.isFinite(value)) return "";
if (Number.isInteger(value) && precision === 0) return String(value);
return Number(value.toFixed(precision)).toString();
}
function clampValue(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max);
}
function visualRange(parameter: NormalizedParameter) {
if (parameter.min !== null && parameter.max !== null && parameter.max > parameter.min) {
return { min: parameter.min, max: parameter.max };
}
const span = Math.max(Math.abs(parameter.value), 1);
return {
min: Math.max(0, parameter.value - span),
max: parameter.value + span,
};
}
function sectionDisplayName(group: { id: string; displayName: string }) {
return group.id === "dimensions" || group.displayName === "尺寸"
? "Dimensions"
: group.displayName;
}
export function ParameterPanel({
parameters,
downloads = [],
pendingParameter,
error,
onClose,
onCommit,
onReset,
}: {
parameters: EditableParameter[];
downloads?: DownloadFormat[];
pendingParameter?: string;
error?: string;
onClose: () => void;
onCommit: (parameter: string, value: number) => void;
onReset: (values: Record<string, number>) => void;
}) {
const normalizedParameters = useMemo(
() => parameters.map(parameterToView).filter((value): value is NormalizedParameter => Boolean(value)),
[parameters],
);
const [drafts, setDrafts] = useState<Record<string, string>>({});
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({});
const [selectedDownload, setSelectedDownload] = useState(downloads[0]?.label || "");
useEffect(() => {
setDrafts(Object.fromEntries(
normalizedParameters.map((parameter) => [
parameter.name,
formatValue(parameter.value, parameter.precision),
]),
));
setOpenGroups((current) => {
const next = { ...current };
for (const parameter of normalizedParameters) {
if (next[parameter.group] === undefined) next[parameter.group] = true;
}
return next;
});
}, [normalizedParameters]);
useEffect(() => {
if (!downloads.some((download) => download.label === selectedDownload)) {
setSelectedDownload(downloads[0]?.label || "");
}
}, [downloads, selectedDownload]);
const grouped = useMemo(() => {
const groups = new Map<string, { id: string; displayName: string; parameters: NormalizedParameter[] }>();
for (const parameter of normalizedParameters) {
const group = groups.get(parameter.group) || {
id: parameter.group,
displayName: parameter.groupDisplayName,
parameters: [],
};
group.parameters.push(parameter);
groups.set(parameter.group, group);
}
return Array.from(groups.values());
}, [normalizedParameters]);
const resetParameters = () => {
const values = Object.fromEntries(
normalizedParameters
.filter((parameter) => parameter.editable && parameter.value !== parameter.defaultValue)
.map((parameter) => [parameter.name, parameter.defaultValue]),
);
if (Object.keys(values).length) {
onReset(values);
return;
}
setDrafts(Object.fromEntries(
normalizedParameters.map((parameter) => [
parameter.name,
formatValue(parameter.defaultValue, parameter.precision),
]),
));
};
const selectedDownloadItem = downloads.find((download) => download.label === selectedDownload) || downloads[0] || null;
const commitValue = (parameter: NormalizedParameter, rawValue: string | number) => {
const numeric = numberValue(rawValue);
if (!parameter.editable) {
setDrafts((current) => ({
...current,
[parameter.name]: formatValue(parameter.value, parameter.precision),
}));
return;
}
if (numeric === null) {
setDrafts((current) => ({
...current,
[parameter.name]: formatValue(parameter.value, parameter.precision),
}));
return;
}
const clamped = parameter.min !== null && parameter.max !== null
? clampValue(numeric, parameter.min, parameter.max)
: numeric;
const formatted = formatValue(clamped, parameter.precision);
setDrafts((current) => ({ ...current, [parameter.name]: formatted }));
if (Number(formatted) !== parameter.value) {
onCommit(parameter.name, Number(formatted));
}
};
return (
<aside
className="flex h-full min-h-0 w-full flex-col overflow-hidden border-l border-gray-200/20 bg-[#1f1f1f] text-[#f2f4f4] shadow-lg shadow-black/25"
data-viewer-interaction-overlay="true"
role="dialog"
aria-label="可编辑参数"
onPointerDown={(event) => event.stopPropagation()}
>
<div className="flex h-14 shrink-0 items-center justify-between border-b border-[#3a3a3a] bg-[#1f1f1f] px-6">
<div className="flex items-center gap-2">
<div className="text-lg font-semibold tracking-tight text-[#f2f4f4]">Parameters</div>
</div>
<div className="flex items-center gap-1">
<Button
aria-label="恢复全部默认参数"
size="icon-sm"
variant="ghost"
className="size-8 rounded-full text-[#f2f4f4] hover:bg-[#2a2a2a]"
disabled={Boolean(pendingParameter) || !normalizedParameters.length}
onClick={resetParameters}
>
<RefreshCcw className="size-4" />
</Button>
<Button
aria-label="隐藏参数面板"
size="icon-sm"
variant="ghost"
className="size-8 rounded-full text-[#f2f4f4] hover:bg-[#2a2a2a]"
onClick={onClose}
>
<X className="size-4" />
</Button>
</div>
</div>
<div className="min-h-0 flex-1 overflow-auto px-6 py-6 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{error ? (
<div className="mb-3 rounded border border-[#7e3d42] bg-[#35191c] px-3 py-2 text-[11px] leading-4 text-[#f2b0b2]">
{error}
</div>
) : null}
{normalizedParameters.length ? (
<div className="flex flex-col gap-3">
{grouped.map((group) => (
<Collapsible.Root
key={group.id}
open={openGroups[group.id] ?? true}
onOpenChange={(open) => setOpenGroups((current) => ({ ...current, [group.id]: open }))}
>
<Collapsible.Trigger className="group flex w-full items-center justify-between gap-2 rounded-md py-1 text-left text-xs font-semibold text-[#f2f4f4] transition-colors focus:outline-none">
<span className="flex items-center gap-2">
{sectionDisplayName(group)}
<span className="text-[10px] text-[#777e84]">{group.parameters.length}</span>
</span>
<ChevronDown
className={cn(
"size-3.5 text-[#777e84] transition-all duration-200 group-hover:text-[#f2f4f4]",
openGroups[group.id] !== false && "rotate-180",
)}
/>
</Collapsible.Trigger>
<Collapsible.Content className="mt-3 flex flex-col gap-3">
{group.parameters.map((parameter) => {
const draft = drafts[parameter.name] ?? formatValue(parameter.value, parameter.precision);
const numericDraft = numberValue(draft) ?? parameter.value;
const disabled = Boolean(pendingParameter) || !parameter.editable;
const range = visualRange(parameter);
const pending = pendingParameter === parameter.name;
return (
<div
className="grid w-full grid-cols-[80px_minmax(0,1fr)] items-center gap-3"
key={parameter.name}
>
<label
className="min-w-0 overflow-hidden text-ellipsis text-xs font-normal leading-4 text-[#b2b5b8]"
htmlFor={`parameter-${parameter.name}`}
title={parameter.displayName}
>
<span className="block truncate">{parameter.displayName}</span>
</label>
<div className="flex w-full min-w-0 items-center gap-3">
<CadamSlider
id={`${parameter.name}-slider`}
name={parameter.name}
min={range.min}
max={range.max}
step={parameter.step}
value={[clampValue(numericDraft, range.min, range.max)]}
defaultValue={[clampValue(parameter.defaultValue, range.min, range.max)]}
disabled={disabled}
visualOnly={false}
defaultMarkerStyle="line"
onValueChange={([nextValue]) => {
setDrafts((current) => ({
...current,
[parameter.name]: formatValue(nextValue, parameter.precision),
}));
}}
onValueCommit={([nextValue]) => commitValue(parameter, nextValue)}
/>
<div className="flex shrink-0 items-center gap-2">
<div className="relative">
<Input
id={`parameter-${parameter.name}`}
autoComplete="off"
className="h-6 w-14 rounded-lg border-0 bg-[#26282b] px-2 pr-2 text-left text-xs text-[#f2f4f4] transition-colors selection:bg-[#7dc8cf]/50 selection:text-white focus-visible:ring-0 hover:bg-[#33363a]"
disabled={disabled}
inputMode="decimal"
max={range.max}
min={range.min}
step={parameter.step}
type="number"
value={draft}
onBlur={() => commitValue(parameter, draft)}
onChange={(event) => setDrafts((current) => ({
...current,
[parameter.name]: event.target.value,
}))}
onFocus={(event) => event.target.select()}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.currentTarget.blur();
}
if (event.key === "Escape") {
setDrafts((current) => ({
...current,
[parameter.name]: formatValue(parameter.value, parameter.precision),
}));
}
}}
/>
{pending ? (
<Loader2 className="absolute right-1.5 top-1.5 size-3 animate-spin text-[#7dc8cf]" />
) : null}
</div>
<span className="ml-1 w-6 text-left text-xs text-[#b2b5b8]">
{parameter.unit}
</span>
</div>
</div>
</div>
);
})}
</Collapsible.Content>
</Collapsible.Root>
))}
</div>
) : (
<div className="rounded border border-[#3a3a3a] bg-[#26282b] px-3 py-3 text-xs leading-5 text-[#b2b5b8]">
</div>
)}
</div>
<div className="flex shrink-0 flex-col gap-4 border-t border-[#3a3a3a] px-6 py-6">
<div className="flex">
<a
aria-disabled={!selectedDownloadItem}
className={cn(
"inline-flex h-12 flex-1 items-center justify-center rounded-l-lg rounded-r-none bg-[#f2f4f4] text-sm font-semibold text-[#22252a] transition-colors hover:bg-white",
!selectedDownloadItem && "pointer-events-none opacity-50",
)}
href={selectedDownloadItem?.url || "#"}
download
>
<Download className="mr-2 size-4" />
{selectedDownloadItem?.label || "STEP"}
</a>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<Button
aria-label="选择下载格式"
className="h-12 w-12 rounded-l-none rounded-r-lg border-l border-[#c4c7cb] bg-[#f2f4f4] p-0 text-[#22252a] hover:bg-white"
disabled={!downloads.length}
>
<ChevronUp className="size-4" />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content
align="end"
className="z-50 w-64 rounded-md border border-[#2d3339] bg-[#26282b] p-1 shadow-md"
>
{downloads.map((download) => (
<DropdownMenu.Item
key={download.label}
className="flex cursor-pointer items-center rounded px-3 py-2 text-[#f2f4f4] outline-none hover:bg-[#33363a]"
onSelect={() => setSelectedDownload(download.label)}
>
<span className="text-sm">.{download.label}</span>
<span className="ml-3 text-xs text-[#f2f4f4]/60">{download.description}</span>
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div>
</aside>
);
}
@@ -0,0 +1,147 @@
"use client";
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils";
type CadamSliderProps = React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
defaultMarkerStyle?: "dot" | "line";
hideDefaultMarker?: boolean;
visualOnly?: boolean;
};
function roundToStep(value: number, step: number) {
if (step <= 0) return value;
const decimals = step >= 1 ? 0 : Math.max(0, -Math.floor(Math.log10(step)));
const snapped = Math.round(value / step) * step;
return Math.round(snapped * Math.pow(10, decimals)) / Math.pow(10, decimals);
}
const CadamSlider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
CadamSliderProps
>(({
className,
value,
defaultValue,
min = 0,
max = 100,
step = 1,
disabled,
visualOnly = false,
onValueChange,
onValueCommit,
defaultMarkerStyle = "line",
hideDefaultMarker = false,
...props
}, ref) => {
const trackRef = React.useRef<HTMLDivElement>(null);
const lastValueRef = React.useRef(Array.isArray(value) ? value[0] ?? min : min);
const [isDragging, setIsDragging] = React.useState(false);
const currentValue = Array.isArray(value) ? value[0] ?? min : min;
const defaultVal = Array.isArray(defaultValue) ? defaultValue[0] ?? currentValue : currentValue;
const range = Math.max(max - min, 1);
const currentPosition = ((currentValue - min) / range) * 100;
const defaultPosition = ((defaultVal - min) / range) * 100;
React.useEffect(() => {
lastValueRef.current = currentValue;
}, [currentValue]);
const valueFromPointer = React.useCallback((clientX: number) => {
const rect = trackRef.current?.getBoundingClientRect();
if (!rect) return currentValue;
const ratio = rect.width > 0
? (Math.min(Math.max(clientX, rect.left), rect.right) - rect.left) / rect.width
: 0;
const nextValue = min + ratio * (max - min);
return Math.min(Math.max(roundToStep(nextValue, step), min), max);
}, [currentValue, max, min, step]);
const publish = React.useCallback((nextValue: number, commit = false) => {
lastValueRef.current = nextValue;
onValueChange?.([nextValue]);
if (commit) onValueCommit?.([nextValue]);
}, [onValueChange, onValueCommit]);
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
if (disabled || visualOnly) return;
setIsDragging(true);
(event.currentTarget as Element).setPointerCapture(event.pointerId);
publish(valueFromPointer(event.clientX));
};
const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
if (disabled || visualOnly || !isDragging) return;
publish(valueFromPointer(event.clientX));
};
const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
if (disabled || visualOnly || !isDragging) return;
setIsDragging(false);
publish(lastValueRef.current, true);
(event.currentTarget as Element).releasePointerCapture(event.pointerId);
};
const handleDefaultMarkerClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (disabled || visualOnly) return;
event.stopPropagation();
publish(defaultVal, true);
};
return (
<SliderPrimitive.Root
ref={ref}
className={cn("group relative flex h-8 w-full touch-none select-none items-center", className)}
disabled={disabled}
min={min}
max={max}
step={step}
value={[currentValue]}
onValueChange={() => {}}
{...props}
>
<SliderPrimitive.Track
ref={trackRef}
className={cn(
"relative h-6 w-full grow cursor-pointer overflow-hidden rounded-lg bg-sky-500/20 transition-all",
isDragging && "h-7",
(disabled || visualOnly) && "cursor-default",
)}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={() => setIsDragging(false)}
>
<SliderPrimitive.Range
className={cn(
"absolute h-full rounded-l-lg bg-sky-300/20 transition-colors",
!disabled && "group-hover:bg-sky-100/50",
isDragging && "!bg-sky-200/50",
)}
style={{ width: `${Math.min(Math.max(currentPosition, 0), 100)}%` }}
/>
{!hideDefaultMarker && currentValue !== defaultVal ? (
defaultMarkerStyle === "dot" ? (
<div
className="absolute top-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2 cursor-pointer rounded-full bg-white/70 shadow-[0_0_8px_rgba(0,0,0,0.5)] transition-all hover:h-2.5 hover:w-2.5 hover:bg-white"
style={{ left: `${Math.min(Math.max(defaultPosition, 0), 100)}%` }}
onClick={handleDefaultMarkerClick}
/>
) : (
<div
className="absolute bottom-[2px] top-[2px] w-[2px] -translate-x-1/2 cursor-pointer rounded-full bg-white/45 shadow-[0_0_8px_rgba(0,0,0,0.5)] transition-all hover:w-1 hover:bg-white/80"
style={{ left: `${Math.min(Math.max(defaultPosition, 0), 100)}%` }}
onClick={handleDefaultMarkerClick}
/>
)
) : null}
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="hidden" />
</SliderPrimitive.Root>
);
});
CadamSlider.displayName = "CadamSlider";
export { CadamSlider };
@@ -0,0 +1,394 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import { build123dCallCandidates, inspectBuild123dApi, readSimpleCadApiDocumentation, readTextToCadDocumentation } from "./chat";
import { executeBackendNativeGeneration, parseParameterBlock, replaceParameterInSource } from "./cad-generator";
const sourcePath = path.join(process.cwd(), "src", "lib", "cad-generator.ts");
const chatSourcePath = path.join(process.cwd(), "src", "lib", "chat.ts");
test("new CAD generation rejects missing backend-native source", async () => {
await assert.rejects(
() => executeBackendNativeGeneration({
request: "生成一个法兰盘",
sourceKind: "build123d_python",
nativeSource: "",
}),
/backend-native Python source/,
);
});
test("new CAD generation rejects runtime API probe scripts", async () => {
await assert.rejects(
() => executeBackendNativeGeneration({
request: "生成一个齿轮",
sourceKind: "simplecadapi_python",
nativeSource: [
"import inspect",
"import simplecadapi as scad",
"TOP_SIGS = inspect.signature(scad.model)",
"raise RuntimeError(str(TOP_SIGS))",
].join("\n"),
}),
/runtime API\/signature\/geometry probe/,
);
});
test("new CAD generation rejects topology diagnostic probe scripts", async () => {
await assert.rejects(
() => executeBackendNativeGeneration({
request: "生成一个带齿根圆角的齿轮",
sourceKind: "simplecadapi_python",
nativeSource: [
'"""Diagnose root fillet failure on factory spur gear."""',
"import simplecadapi as sc",
"def main():",
" msg = []",
" msg.append('ROOT_COUNT=0')",
" msg.append('TIP_COUNT=0')",
" raise RuntimeError(' | '.join(msg))",
].join("\n"),
}),
/runtime API\/signature\/geometry probe/,
);
});
test("backend-native generation validates actual artifacts instead of source-text patterns", () => {
const source = fs.readFileSync(sourcePath, "utf8");
assert.equal(source.includes("validateNativeSourceArtifactContract"), false);
assert.equal(source.includes("Backend native source did not produce a non-empty STEP"), true);
assert.equal(source.includes("Backend native source did not produce metadata JSON"), true);
assert.equal(source.includes("Backend metadata JSON is not readable"), true);
assert.equal(source.includes("SimpleCADAPI native source did not produce model JSON"), true);
});
test("cad-generator has no request-keyword model templates in the direct generation path", () => {
const source = fs.readFileSync(sourcePath, "utf8");
assert.equal(source.includes("build123dParametersForRequest"), false);
assert.equal(source.includes("simpleCadParametersForRequest"), false);
assert.equal(source.includes("make_bolt_rsolid"), false);
assert.equal(source.includes("make_spur_gear_rsolid"), false);
assert.equal(source.includes("text.includes(\"螺栓\")"), false);
});
test("direct backend-native generation requires a previously bound CAD Router decision", () => {
const source = fs.readFileSync(sourcePath, "utf8");
const functionStart = source.indexOf("export async function executeBackendNativeGeneration");
const legacyStart = source.indexOf("export async function executeDesignIR");
assert.notEqual(functionStart, -1);
assert.notEqual(legacyStart, -1);
const directGenerationSource = source.slice(functionStart, legacyStart);
assert.equal(directGenerationSource.includes("generationMode === \"new_model\" && !routeDecision"), true);
assert.equal(directGenerationSource.includes("routeNewCadRequest(request)"), false);
assert.equal(directGenerationSource.includes("submittedBackend !== selectedBackend"), true);
});
test("agent uses CAD Router instead of backend capability documentation", () => {
const source = fs.readFileSync(chatSourcePath, "utf8");
const toolStart = source.indexOf("route_cad_request: tool");
const nextToolStart = source.indexOf("inspect_uploaded_file: tool", toolStart);
assert.notEqual(toolStart, -1);
assert.notEqual(nextToolStart, -1);
const toolSource = source.slice(toolStart, nextToolStart);
assert.equal(toolSource.includes("routeNewCadRequest"), true);
assert.equal(source.includes("read_cad_backend_docs: tool"), false);
assert.equal(source.includes("cadBackendCapabilityContext"), false);
});
test("backend-native source execution uses file artifacts instead of stdout JSON", () => {
const source = fs.readFileSync(sourcePath, "utf8");
const functionStart = source.indexOf("async function runNativeSource");
const nextFunctionStart = source.indexOf("async function textToCadAdapter", functionStart);
assert.notEqual(functionStart, -1);
assert.notEqual(nextFunctionStart, -1);
const functionSource = source.slice(functionStart, nextFunctionStart);
assert.equal(functionSource.includes("runJson("), false);
assert.equal(functionSource.includes("execFileAsync("), true);
assert.equal(functionSource.includes("parseOptionalJsonReport(stdout)"), true);
assert.equal(functionSource.includes("removeIfExists(stepPath)"), true);
assert.equal(functionSource.includes("appendGenerationAttempt"), true);
});
test("internal worker failures retain structured JSON diagnostics", () => {
const source = fs.readFileSync(sourcePath, "utf8");
const functionStart = source.indexOf("async function runJson");
const nextFunctionStart = source.indexOf("export async function routeNewCadRequest", functionStart);
assert.notEqual(functionStart, -1);
assert.notEqual(nextFunctionStart, -1);
const functionSource = source.slice(functionStart, nextFunctionStart);
assert.equal(functionSource.includes("parseJsonOutput(stdout)"), true);
assert.equal(functionSource.includes("CAD worker failed:"), true);
});
test("editable parameter block parser accepts generated Python-style numeric maps", () => {
assert.deepEqual(
parseParameterBlock([
"{",
" '外径': 160,",
" \"厚度\": 12,",
" hole_count: 6,",
"}",
].join("\n")),
{
"外径": 160,
"厚度": 12,
hole_count: 6,
},
);
});
test("editable parameter block parser accepts strict JSON with trailing commas", () => {
assert.deepEqual(
parseParameterBlock([
"{",
" \"outer_diameter\": 160,",
" \"thickness\": 12,",
"}",
].join("\n")),
{
outer_diameter: 160,
thickness: 12,
},
);
});
test("backend parameter editing accepts native bindings as Python source bindings", () => {
const source = fs.readFileSync(sourcePath, "utf8");
const taskStoreSource = fs.readFileSync(path.join(process.cwd(), "src", "lib", "task-store.ts"), "utf8");
assert.equal(source.includes("\"native\""), true);
assert.equal(source.includes("\"parameter\""), true);
assert.equal(source.includes("\"native_python\""), true);
assert.equal(source.includes("\"source_variable\""), true);
assert.equal(taskStoreSource.includes("\"native\""), true);
assert.equal(taskStoreSource.includes("\"parameter\""), true);
assert.equal(taskStoreSource.includes("\"native_python\""), true);
assert.equal(taskStoreSource.includes("\"source_variable\""), true);
assert.equal(source.includes("EDITABLE_BACKEND_BINDING_KINDS.has(bindingKind)"), true);
});
test("backend parameter editing updates CAD_AGENT_PARAMETERS value dictionaries", () => {
const source = [
"CAD_AGENT_PARAMETERS = {",
" \"num_bolt_holes\": {\"display_name\": \"螺栓孔数量\", \"value\": 8, \"unit\": \"个\"},",
"}",
"count = int(CAD_AGENT_PARAMETERS[\"num_bolt_holes\"][\"value\"])",
].join("\n");
const edited = replaceParameterInSource(source, "num_bolt_holes", 10, {
binding_kind: "native_python",
parameter_path: "CAD_AGENT_PARAMETERS.num_bolt_holes.value",
});
assert.match(edited, /"num_bolt_holes": \{"display_name": "螺栓孔数量", "value": 10, "unit": "个"\}/);
});
test("backend parameter editing updates top-level numeric assignments", () => {
const source = [
"MODULE = 3.0",
"TEETH = 30",
"FACE_WIDTH = 40.0 # mm",
].join("\n");
const edited = replaceParameterInSource(source, "face_width", 45, {
binding_kind: "native_python",
parameter_path: "gear.face_width",
});
assert.match(edited, /^FACE_WIDTH = 45\.0\s+# mm$/m);
});
test("backend parameter editing accepts source_parameter top-level bindings", () => {
const source = [
"flange_od = 120.0 # 法兰外径 mm",
"bolt_hole_count = 8 # 螺栓孔数量",
].join("\n");
const edited = replaceParameterInSource(source, "flange_od", 130, {
binding_kind: "source_parameter",
parameter_path: "flange_od",
});
assert.match(edited, /^flange_od = 130\.0\s+# 法兰外径 mm$/m);
const generatorSource = fs.readFileSync(sourcePath, "utf8");
assert.equal(generatorSource.includes("\"source_parameter\""), true);
});
test("backend parameter editing accepts source_variable top-level bindings", () => {
const source = [
"outer_diameter = 140.0 # 外径 mm",
"bolt_count = 8 # 安装孔数量",
].join("\n");
const edited = replaceParameterInSource(source, "outer_diameter", 150, {
binding_kind: "source_variable",
parameter_path: "outer_diameter",
regenerate_adapter: "regenerate_native_source",
});
assert.match(edited, /^outer_diameter = 150\.0\s+# 外径 mm$/m);
});
test("new backend-native generation creates a fresh task and records explicit revisions", () => {
const source = fs.readFileSync(sourcePath, "utf8");
const functionStart = source.indexOf("export async function executeBackendNativeGeneration");
const legacyStart = source.indexOf("export async function executeDesignIR");
assert.notEqual(functionStart, -1);
assert.notEqual(legacyStart, -1);
const directGenerationSource = source.slice(functionStart, legacyStart);
assert.equal(directGenerationSource.includes("const task = await ensureTask();"), true);
assert.equal(directGenerationSource.includes("const task = await ensureTask(selectedTaskId"), false);
assert.equal(directGenerationSource.includes("generationMode === \"backend_conversion\""), true);
assert.equal(directGenerationSource.includes("Structural edits must keep the current backend"), true);
});
test("generate_cad exposes generationMode and treats stdout JSON as optional", () => {
const source = fs.readFileSync(chatSourcePath, "utf8");
const toolStart = source.indexOf("generate_cad: tool");
assert.notEqual(toolStart, -1);
const toolSource = source.slice(toolStart);
assert.equal(toolSource.includes("generationMode"), true);
assert.equal(toolSource.includes("stdout JSON is optional diagnostic output"), true);
assert.equal(toolSource.includes("selectedBackend:"), false);
assert.equal(toolSource.includes("capabilityEvidence"), false);
});
test("generate_cad backend failures are returned to the model for retry", () => {
const source = fs.readFileSync(chatSourcePath, "utf8");
const toolStart = source.indexOf("generate_cad: tool");
const nextToolStart = source.indexOf("const formatCurrentModelError", toolStart);
assert.notEqual(toolStart, -1);
assert.notEqual(nextToolStart, -1);
const toolSource = source.slice(toolStart, nextToolStart);
assert.equal(toolSource.includes("retryable: true"), true);
assert.equal(toolSource.includes("call generate_cad again in the same turn"), true);
assert.equal(toolSource.includes("failAgentStream(message);\n throw error;"), false);
});
test("SimpleCADAPI generation reads original docs and preflights source", () => {
const generatorSource = fs.readFileSync(sourcePath, "utf8");
const chatSource = fs.readFileSync(chatSourcePath, "utf8");
assert.equal(generatorSource.includes("simpleCadApiContractScript()"), true);
assert.equal(generatorSource.includes("validateSimpleCadApiSource(sourcePath, cwd)"), true);
assert.equal(chatSource.includes("read_simplecadapi_docs: tool"), true);
assert.equal(chatSource.includes("SIMPLECADAPI_REQUIRED_DOCS"), true);
assert.equal(chatSource.includes("read_simplecadapi_api_contract"), false);
assert.equal(chatSource.includes("scad.primitives"), false);
});
test("SimpleCADAPI documentation resolves namespace-shaped paths to the original API page", async () => {
const result = await readSimpleCadApiDocumentation("ql/value.md");
assert.equal(result.ok, true);
if (!result.ok) return;
assert.equal(result.document, "api/value.md");
assert.equal(result.resolvedFrom, "ql/value.md");
assert.equal(result.content.includes("simplecadapi.ql.value"), true);
});
test("SimpleCADAPI documentation misses return a tool result instead of throwing", async () => {
const result = await readSimpleCadApiDocumentation("ql/not-a-real-page.md");
assert.deepEqual(result, {
ok: false,
document: "ql/not-a-real-page.md",
error: "SimpleCADAPI document was not found. Read api/README.md or stdlib/README.md and use its linked canonical page path.",
suggestedDocuments: [],
});
});
test("text-to-cad build123d guidance is available from the original skill", async () => {
const result = await readTextToCadDocumentation("references/build123d-modeling.md");
assert.equal(result.ok, true);
if (!result.ok) return;
assert.equal(result.content.includes("build123d modeling patterns"), true);
});
test("text-to-cad CAD-SkillX references are available through documentation lookup", async () => {
const result = await readTextToCadDocumentation("references/cad-skillx/planning/mounting-plate.planning.md", {
cadSkillxEnabled: true,
});
assert.equal(result.ok, true);
if (!result.ok) return;
assert.equal(result.content.includes("Mounting Plate Planning Skill"), true);
});
test("text-to-cad CAD-SkillX references are disabled by the code switch", async () => {
const result = await readTextToCadDocumentation("references/cad-skillx/planning/mounting-plate.planning.md");
assert.equal(result.ok, false);
if (result.ok) return;
assert.equal(result.error.includes("disabled"), true);
const skill = await readTextToCadDocumentation("skill");
assert.equal(skill.ok, true);
if (!skill.ok) return;
assert.equal(skill.content.includes("skill-pack-curated-20260807"), false);
assert.equal(skill.content.includes("CAD-SkillX generated optimization references are disabled"), true);
});
test("text-to-cad documentation lookup rejects paths outside CAD-SkillX references", async () => {
const result = await readTextToCadDocumentation("references/cad-skillx/../build123d-modeling.md");
assert.equal(result.ok, false);
});
test("build123d API lookup returns the active chamfer signature", async () => {
const result = await inspectBuild123dApi(["chamfer"]);
assert.equal(result.symbols.length, 1);
assert.equal(result.symbols[0].ok, true);
assert.match(result.symbols[0].signature || "", /^\(objects: .*?, length: float/);
});
test("new build123d generation is bound to the router token and exact API docs", () => {
const source = fs.readFileSync(chatSourcePath, "utf8");
assert.equal(source.includes("read_text_to_cad_docs: tool"), true);
assert.equal(source.includes("read_build123d_api: tool"), true);
assert.equal(source.includes("loadRequiredTextToCadDocumentation({ cadSkillxEnabled: useCadSkillx })"), true);
assert.equal(source.includes("Studio-loaded text-to-cad/build123d base documentation"), true);
assert.equal(source.includes("build123d generation requires text-to-cad docs"), false);
assert.equal(source.includes("routeToken: z.string().optional()"), true);
assert.equal(source.includes("routeDecisions.get(String(routeToken || \"\"))"), true);
assert.equal(source.includes("build123d_api_preflight"), true);
assert.equal(source.includes("服务端已自动补读缺失 API"), true);
});
test("CAD-SkillX code switch is off and not exposed as a UI toggle", () => {
const uiSource = fs.readFileSync(path.join(process.cwd(), "src", "components", "agent-studio.tsx"), "utf8");
const routeSource = fs.readFileSync(path.join(process.cwd(), "src", "app", "api", "chat", "route.ts"), "utf8");
const chatSource = fs.readFileSync(chatSourcePath, "utf8");
assert.equal(uiSource.includes("cad-agent-studio.cad-skillx-enabled"), false);
assert.equal(uiSource.includes("cadSkillxEnabled"), false);
assert.equal(routeSource.includes("cadSkillxEnabled"), false);
assert.equal(chatSource.includes("const CAD_SKILLX_ENABLED = false;"), true);
assert.equal(chatSource.includes("const useCadSkillx = CAD_SKILLX_ENABLED;"), true);
assert.equal(chatSource.includes("CAD-SkillX generated optimization references are disabled"), true);
assert.equal(chatSource.includes("readTextToCadDocumentation(document, { cadSkillxEnabled: useCadSkillx })"), true);
});
test("build123d API preflight ignores non-build123d imported helpers", () => {
const candidates = build123dCallCandidates([
"from pathlib import Path",
"from math import cos, sin",
"from build123d import Align, BuildPart, Cylinder, Hole, export_step",
"",
"def make_model():",
" angle = cos(0) + sin(0)",
" with BuildPart() as part:",
" Cylinder(radius=10, height=5, align=(Align.CENTER, Align.CENTER, Align.CENTER))",
" Hole(radius=2)",
" Path('metadata.json').write_text('{}')",
" export_step(part.part, 'model.step')",
].join("\n"));
assert.deepEqual(candidates, ["BuildPart", "Cylinder", "Hole", "export_step", "Align"]);
});
test("agent stream does not forward model reasoning deltas to the UI", () => {
const source = fs.readFileSync(chatSourcePath, "utf8");
const streamStart = source.indexOf("result.toUIMessageStream({");
assert.notEqual(streamStart, -1);
const streamSource = source.slice(streamStart, source.indexOf("}));", streamStart));
assert.equal(streamSource.includes("sendReasoning: false"), true);
});
test("agent stream progress remains visible while the model is running", () => {
const source = fs.readFileSync(chatSourcePath, "utf8");
const transientStart = source.indexOf("const transientProgressSteps");
assert.notEqual(transientStart, -1);
const transientSource = source.slice(transientStart, source.indexOf(";", transientStart));
assert.equal(transientSource.includes("agent_stream"), false);
});
test("agent prompt requires Chinese editable parameter labels", () => {
const source = fs.readFileSync(chatSourcePath, "utf8");
assert.equal(source.includes("Parameter names shown to the user must be concise Chinese labels"), true);
assert.equal(source.includes("Chinese user-facing names/display_name"), true);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,188 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
catalogEditableParameters,
createParameterCatalogFromDesignIR,
normalizeParameterCatalog,
validateParameterEdit,
} from "./cad-parameters";
test("declared semantic DesignIR parameters without backend binding are readonly", () => {
const catalog = createParameterCatalogFromDesignIR(
{
semantic_layer: {
parameters: {
outer_diameter: {
value: 160,
unit: "mm",
editable: true,
range: [80, 240],
},
},
},
},
{ taskId: "cad_test", sourceDesignIRPath: "flange.designir.json" },
);
assert.equal(catalog.parameters.length, 1);
assert.equal(catalog.parameters[0].display_name, "外径");
assert.equal(catalog.parameters[0].editable, false);
assert.equal(catalog.parameters[0].min, 80);
assert.equal(catalog.parameters[0].max, 240);
assert.equal(catalogEditableParameters(catalog).length, 0);
});
test("backend-bound semantic DesignIR parameters are exported as editable", () => {
const catalog = createParameterCatalogFromDesignIR(
{
semantic_layer: {
parameters: {
outer_diameter: {
value: 160,
unit: "mm",
editable: true,
range: [80, 240],
backend_binding: {
backend: "build123d",
source_path: "model.build123d.py",
binding_kind: "python_constant",
parameter_path: "PARAMETERS.outer_diameter",
regenerate_adapter: "build123d",
},
},
},
},
},
{ taskId: "cad_test", sourceDesignIRPath: "flange.designir.json" },
);
assert.equal(catalog.parameters.length, 1);
assert.equal(catalog.parameters[0].display_name, "外径");
assert.equal(catalog.parameters[0].editable, true);
assert.equal(catalog.parameters[0].backend_binding?.backend, "build123d");
assert.equal(catalogEditableParameters(catalog).length, 1);
});
test("validated catalog parameters are exposed for editing", () => {
const catalog = normalizeParameterCatalog(
{
parameters: [{
name: "bolt_count",
display_name: "螺栓数量",
value: 8,
unit: "",
editable: true,
edit_state: "validated_executable_binding",
validated_range: {
minimum_tested_inclusive: 4,
maximum_tested_inclusive: 12,
},
}],
},
{ taskId: "cad_test", sourceDesignIRPath: "flange.designir.json" },
);
const [parameter] = catalogEditableParameters(catalog);
assert.equal(parameter.name, "bolt_count");
assert.equal(parameter.min, 4);
assert.equal(parameter.max, 12);
assert.equal(parameter.step, 1);
});
test("parameter edit validation accepts backend-bound parameters and rejects out-of-range values", () => {
const catalog = normalizeParameterCatalog(
{
parameters: [{
name: "thickness",
value: 20,
unit: "mm",
editable: true,
edit_state: "validated_executable_binding",
validated_range: {
minimum_tested_inclusive: 10,
maximum_tested_inclusive: 30,
},
backend_binding: {
backend: "build123d",
source_path: "model.build123d.py",
binding_kind: "python_constant",
parameter_path: "PARAMETERS.thickness",
regenerate_adapter: "build123d",
},
}, {
name: "draft_only",
value: 1,
editable: true,
range: [0, 3],
}],
},
{ taskId: "cad_test", sourceDesignIRPath: "flange.designir.json" },
);
assert.equal(validateParameterEdit(catalog, "thickness", 25).value, 25);
assert.throws(
() => validateParameterEdit(catalog, "thickness", 40),
/declared range/,
);
assert.throws(
() => validateParameterEdit(catalog, "draft_only", 2),
/not declared editable/,
);
});
test("backend-bound parameters without a range remain editable", () => {
const catalog = normalizeParameterCatalog(
{
parameters: [{
name: "offset",
value: 4,
editable: true,
backend_binding: {
backend: "simplecadapi",
source_path: "model.simplecadapi.py",
binding_kind: "python_constant",
parameter_path: "PARAMETERS.offset",
regenerate_adapter: "simplecadapi",
},
}],
},
{ taskId: "cad_test", sourceDesignIRPath: "model.designir.json" },
);
assert.equal(validateParameterEdit(catalog, "offset", 12).value, 12);
});
test("parameter catalog exposes Chinese display names for generated parameters", () => {
const catalog = normalizeParameterCatalog(
{
parameters: [{
name: "jaw_clearance",
display_name: "Jaw Clearance",
value: 4,
editable: true,
backend_binding: {
backend: "build123d",
source_path: "model.build123d.py",
binding_kind: "python_constant",
parameter_path: "PARAMETERS.jaw_clearance",
regenerate_adapter: "build123d",
},
}, {
name: "孔距",
value: 18,
editable: true,
backend_binding: {
backend: "build123d",
source_path: "model.build123d.py",
binding_kind: "python_constant",
parameter_path: "PARAMETERS.孔距",
regenerate_adapter: "build123d",
},
}],
},
{ taskId: "cad_test", sourceDesignIRPath: "model.designir.json" },
);
assert.equal(catalog.parameters[0].display_name, "间隙");
assert.equal(catalog.parameters[1].display_name, "孔距");
});
+368
View File
@@ -0,0 +1,368 @@
export type ParameterCatalogParameter = Record<string, unknown> & {
id: string;
name: string;
display_name: string;
group: string;
type: "number";
value: number;
default_value: number;
min?: number;
max?: number;
step: number;
precision: number;
unit: string;
control: "slider+number" | "number";
editable: boolean;
edit_state: string;
validation: {
validated: boolean;
state: string;
message?: string;
};
backend_binding?: {
backend: string;
source_path: string;
binding_kind: string;
parameter_path: string;
regenerate_adapter: string;
};
};
export type ParameterCatalog = Record<string, unknown> & {
schema_version: "1.0";
task_id: string;
source_designir_path: string;
generated_at: string;
groups: Array<{ id: string; display_name: string; count: number }>;
parameters: ParameterCatalogParameter[];
};
const DISPLAY_NAME_HINTS: Array<[RegExp, string]> = [
[/outer.*diameter|outside.*diameter|od$/i, "外径"],
[/inner.*diameter|inside.*diameter|hole.*diameter|id$/i, "内径"],
[/counterbore.*diameter/i, "沉孔直径"],
[/countersink.*diameter/i, "锥孔直径"],
[/diameter/i, "直径"],
[/radius/i, "半径"],
[/thickness/i, "厚度"],
[/clearance/i, "间隙"],
[/offset/i, "偏移量"],
[/spacing/i, "间距"],
[/width/i, "宽度"],
[/height/i, "高度"],
[/length/i, "长度"],
[/depth/i, "深度"],
[/pitch/i, "节距"],
[/bolt.*count|bolt.*number/i, "螺栓数量"],
[/hole.*count|hole.*number/i, "孔数量"],
[/tooth.*count|teeth|tooth.*number/i, "齿数"],
[/count|number/i, "数量"],
[/angle/i, "角度"],
[/x$/i, "X向尺寸"],
[/y$/i, "Y向尺寸"],
[/z$/i, "Z向尺寸"],
];
function finiteNumber(value: unknown): number | undefined {
const numeric = typeof value === "number" ? value : Number(value);
return Number.isFinite(numeric) ? numeric : undefined;
}
function decimalPlaces(value: number) {
const text = String(value);
const dot = text.indexOf(".");
return dot >= 0 ? Math.min(Math.max(text.length - dot - 1, 0), 6) : 0;
}
function fallbackStep(value: number, min?: number, max?: number) {
if (Number.isInteger(value)) return 1;
if (typeof min === "number" && typeof max === "number" && max > min) {
const raw = (max - min) / 100;
if (raw >= 1) return 1;
if (raw >= 0.1) return 0.1;
if (raw >= 0.01) return 0.01;
}
return 0.1;
}
function titleCaseName(name: string) {
return name
.replace(/[_-]+/g, " ")
.replace(/([a-z])([A-Z])/g, "$1 $2")
.trim()
.replace(/\b\w/g, (value) => value.toUpperCase());
}
function containsChinese(value: string) {
return /[\u3400-\u9fff]/.test(value);
}
function displayNameFor(name: string, record: Record<string, unknown>) {
for (const key of ["display_name", "displayName", "label", "title"]) {
const value = record[key];
if (typeof value === "string" && containsChinese(value.trim())) return value.trim();
}
const hint = DISPLAY_NAME_HINTS.find(([pattern]) => pattern.test(name));
if (hint) return hint[1];
if (containsChinese(name)) return name;
const fallback = titleCaseName(name);
return containsChinese(fallback) ? fallback : "参数";
}
function groupFor(record: Record<string, unknown>) {
const raw = String(record.group || record.category || "dimensions").trim() || "dimensions";
const normalized = raw.toLowerCase();
const displayName = normalized === "dimensions" || raw === "尺寸"
? "尺寸"
: raw === "features" || raw === "特征"
? "特征"
: raw;
return { id: normalized.replace(/[^a-z0-9_-]+/g, "_") || "dimensions", display_name: displayName };
}
function rangeFor(record: Record<string, unknown>) {
const validatedRange = (
record.validated_range && typeof record.validated_range === "object"
? record.validated_range
: {}
) as Record<string, unknown>;
const declaredRange = Array.isArray(record.range) ? record.range : [];
const min = finiteNumber(
record.min
?? record.minimum
?? validatedRange.minimum_tested_inclusive
?? validatedRange.min
?? declaredRange[0],
);
const max = finiteNumber(
record.max
?? record.maximum
?? validatedRange.maximum_tested_inclusive
?? validatedRange.max
?? declaredRange[1],
);
return { min, max };
}
export function normalizeParameterRecord(
rawName: string,
rawRecord: unknown,
): ParameterCatalogParameter | null {
const record = (
rawRecord && typeof rawRecord === "object"
? rawRecord
: { value: rawRecord }
) as Record<string, unknown>;
const name = String(record.name || record.id || rawName || "").trim();
const value = finiteNumber(record.value);
if (!name || value === undefined) return null;
const { min, max } = rangeFor(record);
const step = finiteNumber(record.step) ?? fallbackStep(value, min, max);
const precision = Math.max(
0,
finiteNumber(record.precision) ?? decimalPlaces(step) ?? decimalPlaces(value),
);
const editState = String(record.edit_state || record.editState || "").trim();
const rawBinding = (
record.backend_binding && typeof record.backend_binding === "object"
? record.backend_binding
: null
) as Record<string, unknown> | null;
const topLevelBackendBinding = (
!rawBinding
&& (record.binding_kind || record.parameter_path || record.regenerate_adapter)
)
? {
backend: String(record.backend || record.regenerate_adapter || ""),
source_path: String(record.source_path || ""),
binding_kind: String(record.binding_kind || ""),
parameter_path: String(record.parameter_path || name),
regenerate_adapter: String(record.regenerate_adapter || record.backend || ""),
}
: null;
const inferredSurfaceBinding = (
!rawBinding
&& !topLevelBackendBinding
&& record.editable === true
&& editState === "validated_executable_binding"
)
? {
backend: "surfaceir",
source_path: "",
binding_kind: "surfaceir_validated_parameter",
parameter_path: name,
regenerate_adapter: "surfaceir",
}
: null;
const backendBinding = rawBinding || topLevelBackendBinding || inferredSurfaceBinding;
const validated = (
record.editable === true
&& (editState === "validated_executable_binding" || Boolean(backendBinding))
&& typeof min === "number"
&& typeof max === "number"
);
const editable = record.editable === true && Boolean(backendBinding);
const group = groupFor(record);
return {
...record,
id: String(record.id || name),
name,
display_name: displayNameFor(name, record),
description: typeof record.description === "string" ? record.description : "",
group: group.id,
group_display_name: group.display_name,
type: "number",
value,
default_value: finiteNumber(record.default_value ?? record.defaultValue) ?? value,
min,
max,
step,
precision,
unit: String(record.unit || ""),
control: typeof min === "number" && typeof max === "number" ? "slider+number" : "number",
editable,
backend_binding: backendBinding
? {
backend: String(backendBinding.backend || ""),
source_path: String(backendBinding.source_path || ""),
binding_kind: String(backendBinding.binding_kind || ""),
parameter_path: String(backendBinding.parameter_path || name),
regenerate_adapter: String(backendBinding.regenerate_adapter || backendBinding.backend || ""),
}
: undefined,
edit_state: editState || (editable ? "declared_executable_binding" : "readonly"),
validation: {
validated,
state: validated ? "validated_executable_binding" : editable ? "declared_unvalidated" : "readonly",
message: validated
? "Parameter passed isolated perturbation acceptance."
: editable
? "Parameter is directly editable from its DesignIR declaration."
: "Parameter is marked readonly in DesignIR.",
},
};
}
export function normalizeParameterCatalog(
source: unknown,
{
taskId,
sourceDesignIRPath,
}: {
taskId: string;
sourceDesignIRPath: string;
},
): ParameterCatalog {
const sourceRecord = (
source && typeof source === "object"
? source
: {}
) as Record<string, unknown>;
const sourceParameters = Array.isArray(sourceRecord.parameters)
? sourceRecord.parameters.map((value, index) => normalizeParameterRecord(String(index), value))
: Object.entries(sourceRecord.parameters || {}).map(([name, value]) => normalizeParameterRecord(name, value));
const parameters = sourceParameters
.filter((value): value is ParameterCatalogParameter => Boolean(value));
const groupsById = new Map<string, { id: string; display_name: string; count: number }>();
for (const parameter of parameters) {
const id = parameter.group || "dimensions";
const displayName = String(parameter.group_display_name || "尺寸");
const current = groupsById.get(id) || { id, display_name: displayName, count: 0 };
current.count += 1;
groupsById.set(id, current);
}
return {
...sourceRecord,
schema_version: "1.0",
task_id: taskId,
source_designir_path: sourceDesignIRPath,
generated_at: new Date().toISOString(),
groups: Array.from(groupsById.values()),
parameters,
};
}
export function createParameterCatalogFromDesignIR(
designir: Record<string, any>,
options: { taskId: string; sourceDesignIRPath: string },
) {
return normalizeParameterCatalog(
{
source_kind: "designir_semantic_layer",
parameters: designir.semantic_layer?.parameters || {},
},
options,
);
}
export function catalogEditableParameters(
catalog: unknown,
): Array<Record<string, unknown>> {
return catalogParameters(catalog).filter(
(value) => value.editable === true,
);
}
export function catalogParameters(
catalog: unknown,
): Array<Record<string, unknown>> {
const parameters: unknown[] = (
catalog && typeof catalog === "object" && Array.isArray((catalog as Record<string, unknown>).parameters)
? (catalog as Record<string, unknown>).parameters
: []
) as unknown[];
return parameters.filter(
(value): value is Record<string, unknown> => (
value !== null
&& typeof value === "object"
),
);
}
export function updateCatalogParameterValue(
catalog: unknown,
parameterName: string,
value: number,
options: { taskId: string; sourceDesignIRPath: string },
) {
const normalized = normalizeParameterCatalog(catalog, options);
return {
...normalized,
parameters: normalized.parameters.map((parameter) => (
parameter.name === parameterName || parameter.id === parameterName
? { ...parameter, value }
: parameter
)),
};
}
export function validateParameterEdit(catalog: unknown, parameterName: string, value: unknown) {
const normalizedValue = finiteNumber(value);
if (normalizedValue === undefined) {
throw new Error("Parameter value must be a finite number.");
}
const parameters = (
catalog && typeof catalog === "object" && Array.isArray((catalog as Record<string, unknown>).parameters)
? (catalog as Record<string, unknown>).parameters
: []
) as Array<Record<string, unknown>>;
const parameter = parameters.find((item) => (
item.name === parameterName || item.id === parameterName
));
if (!parameter) {
throw new Error(`${parameterName} is not listed in parameters.json.`);
}
if (parameter.editable !== true) {
throw new Error(`${parameterName} is not declared editable in DesignIR.`);
}
const minimum = finiteNumber(parameter.min);
const maximum = finiteNumber(parameter.max);
if (
minimum !== undefined
&& maximum !== undefined
&& (normalizedValue < minimum || normalizedValue > maximum)
) {
throw new Error(`${parameterName} must remain within the declared range [${minimum}, ${maximum}].`);
}
return { parameter, value: normalizedValue };
}
+657 -70
View File
@@ -11,22 +11,48 @@ import {
type UserContent,
} from "ai";
import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { z } from "zod/v4";
import {
editDesignIR3Parameter,
executeDesignIR,
editDesignIRParameter,
executeBackendNativeGeneration,
reconstructUploadedStep,
routeNewCadRequest,
type CadGenerationResult,
type NewCadRouteDecision,
} from "@/lib/cad-generator";
import { loadLlmConfig, resolveProviderApiKey, selectedModelId, type LlmConfig } from "@/lib/config";
import {
appendConversationMessage,
conversationModelMessages,
ensureConversation,
safeConversationId,
} from "@/lib/conversation-store";
import { exportRobotDescription } from "@/lib/robot-export";
import { readManifest, taskDir } from "@/lib/task-store";
const execFileAsync = promisify(execFile);
function cadPythonExecutable() {
const configured = String(process.env.CAD_PYTHON || "").trim();
if (configured) return configured;
const workspacePython = path.join(process.cwd(), "..", "text-to-cad", ".venv", "bin", "python");
if (existsSync(workspacePython)) return workspacePython;
if (commandExists("python")) return "python";
if (commandExists("python3")) return "python3";
return "python3";
}
function commandExists(command: string) {
return String(process.env.PATH || "")
.split(path.delimiter)
.some((directory) => existsSync(path.join(directory, command)));
}
type ChatMessage = {
role: "user" | "assistant" | "system";
content: string;
@@ -61,11 +87,14 @@ function writeTextPart(writer: { write: (part: any) => void }, text: string) {
writer.write({ type: "text-end", id });
}
function uiTextResponse(text: string) {
function uiTextResponse(text: string, onEnd?: (message: UIMessage) => Promise<void>) {
const stream = createUIMessageStream<UIMessage>({
execute: ({ writer }) => {
writeTextPart(writer, text);
},
onEnd: async ({ responseMessage, isAborted }) => {
if (!isAborted) await onEnd?.(responseMessage);
},
});
return createUIMessageStreamResponse({
stream,
@@ -352,14 +381,347 @@ function attachmentSummary(attachments: unknown[]) {
].join("\n");
}
function buildSystemPrompt(viewerContext: unknown[], attachments: unknown[], taskContext = "") {
const SIMPLECADAPI_SKILL_ROOT = path.join(process.cwd(), "..", "SimpleCADAPI", "skills", "simplecadapi");
const SIMPLECADAPI_REFERENCE_ROOT = path.join(SIMPLECADAPI_SKILL_ROOT, "references");
const SIMPLECADAPI_REQUIRED_DOCS = ["skill", "api/README.md", "stdlib/README.md"];
const TEXT_TO_CAD_SKILL_ROOT = path.join(process.cwd(), "..", "text-to-cad", "skills", "cad");
const TEXT_TO_CAD_REQUIRED_DOCS = ["skill", "references/build123d-modeling.md", "references/step-generation.md"];
const CAD_SKILLX_ENABLED = false;
type SimpleCadApiDocumentationResult =
| {
ok: true;
document: string;
path: string;
content: string;
resolvedFrom?: string;
}
| {
ok: false;
document: string;
error: string;
suggestedDocuments: string[];
};
const SIMPLECADAPI_DOC_DIRECTORIES = ["api", "stdlib", "core"] as const;
function normalizeSimpleCadApiDocument(document: string) {
const requested = String(document || "").trim().replaceAll("\\", "/").replace(/^\.\//, "");
const normalized = path.posix.normalize(requested);
if (!requested || normalized === "." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) {
return null;
}
return normalized;
}
function normalizeTextToCadDocument(document: string) {
const requested = String(document || "").trim().replaceAll("\\", "/").replace(/^\.\//, "");
const normalized = path.posix.normalize(requested);
if (
!requested
|| requested.split("/").includes("..")
|| normalized === "."
|| normalized.startsWith("../")
|| path.posix.isAbsolute(normalized)
) {
return null;
}
return normalized;
}
async function matchingSimpleCadApiDocuments(filename: string) {
if (!filename.endsWith(".md")) return [];
const matches = await Promise.all(SIMPLECADAPI_DOC_DIRECTORIES.map(async (directory) => {
const root = path.join(SIMPLECADAPI_REFERENCE_ROOT, "docs", directory);
try {
const entries = await fs.readdir(root, { withFileTypes: true });
return entries
.filter((entry) => entry.isFile() && entry.name === filename)
.map((entry) => `${directory}/${entry.name}`);
} catch {
return [];
}
}));
return matches.flat();
}
export async function readSimpleCadApiDocumentation(document: string): Promise<SimpleCadApiDocumentationResult> {
const normalized = normalizeSimpleCadApiDocument(document);
if (!normalized) {
return {
ok: false,
document: String(document || ""),
error: "SimpleCADAPI document must be a listed base document or a relative Markdown page below references/docs/.",
suggestedDocuments: [],
};
}
const fixedDocuments: Record<string, string> = {
skill: path.join(SIMPLECADAPI_SKILL_ROOT, "SKILL.md"),
"api/README.md": path.join(SIMPLECADAPI_REFERENCE_ROOT, "docs", "api", "README.md"),
"stdlib/README.md": path.join(SIMPLECADAPI_REFERENCE_ROOT, "docs", "stdlib", "README.md"),
"SDK_SURFACES.md": path.join(SIMPLECADAPI_REFERENCE_ROOT, "SDK_SURFACES.md"),
"MODELING_WORKFLOWS.md": path.join(SIMPLECADAPI_REFERENCE_ROOT, "MODELING_WORKFLOWS.md"),
};
const fixedPath = fixedDocuments[normalized];
const referenceDocsRoot = path.resolve(SIMPLECADAPI_REFERENCE_ROOT, "docs");
let resolvedDocument = normalized;
let target = fixedPath || path.resolve(referenceDocsRoot, normalized);
const relativeTarget = path.relative(referenceDocsRoot, target);
const allowedDirectPath = fixedPath || (
normalized.endsWith(".md")
&& !relativeTarget.startsWith("..")
&& !path.isAbsolute(relativeTarget)
);
if (!allowedDirectPath || !target.endsWith(".md") || !existsSync(target)) {
const candidates = await matchingSimpleCadApiDocuments(path.posix.basename(normalized));
if (candidates.length === 1) {
resolvedDocument = candidates[0];
target = path.join(referenceDocsRoot, resolvedDocument);
} else {
return {
ok: false,
document: normalized,
error: candidates.length > 1
? "SimpleCADAPI document path is ambiguous. Use one of the suggested canonical paths."
: "SimpleCADAPI document was not found. Read api/README.md or stdlib/README.md and use its linked canonical page path.",
suggestedDocuments: candidates,
};
}
}
try {
const content = await fs.readFile(target, "utf8");
return {
ok: true,
document: resolvedDocument,
path: target,
content,
...(resolvedDocument !== normalized ? { resolvedFrom: normalized } : {}),
};
} catch (error) {
return {
ok: false,
document: normalized,
error: error instanceof Error ? error.message : "SimpleCADAPI document could not be read.",
suggestedDocuments: [],
};
}
}
type TextToCadDocumentationResult =
| { ok: true; document: string; path: string; content: string }
| { ok: false; document: string; error: string; suggestedDocuments: string[] };
type TextToCadDocumentationOptions = {
cadSkillxEnabled?: boolean;
};
function stripCadSkillxReferences(content: string) {
return content
.replace(/\n?<!-- cad-skillx:start:[\s\S]*?<!-- cad-skillx:end:[^>]*-->\n?/g, "\n\n")
.trimEnd();
}
export async function readTextToCadDocumentation(
document: string,
options: TextToCadDocumentationOptions = {},
): Promise<TextToCadDocumentationResult> {
const cadSkillxEnabled = options.cadSkillxEnabled ?? CAD_SKILLX_ENABLED;
const normalized = normalizeTextToCadDocument(document);
const fixedDocuments: Record<string, string> = {
skill: path.join(TEXT_TO_CAD_SKILL_ROOT, "SKILL.md"),
"references/build123d-modeling.md": path.join(TEXT_TO_CAD_SKILL_ROOT, "references", "build123d-modeling.md"),
"references/step-generation.md": path.join(TEXT_TO_CAD_SKILL_ROOT, "references", "step-generation.md"),
"references/inspection-and-validation.md": path.join(TEXT_TO_CAD_SKILL_ROOT, "references", "inspection-and-validation.md"),
"references/positioning.md": path.join(TEXT_TO_CAD_SKILL_ROOT, "references", "positioning.md"),
};
const cadSkillxRoot = path.resolve(TEXT_TO_CAD_SKILL_ROOT, "references", "cad-skillx");
const cadSkillxTarget = normalized
? path.resolve(TEXT_TO_CAD_SKILL_ROOT, normalized)
: "";
const cadSkillxRelative = cadSkillxTarget ? path.relative(cadSkillxRoot, cadSkillxTarget) : "";
const cadSkillxDocument = Boolean(
normalized
&& normalized.startsWith("references/cad-skillx/")
&& normalized.endsWith(".md")
&& cadSkillxRelative
&& !cadSkillxRelative.startsWith("..")
&& !path.isAbsolute(cadSkillxRelative),
);
const target = fixedDocuments[normalized || ""] || (cadSkillxDocument ? cadSkillxTarget : "");
if (cadSkillxDocument && !cadSkillxEnabled) {
return {
ok: false,
document: normalized || String(document || ""),
error: "CAD-SkillX optimization references are disabled for this request.",
suggestedDocuments: [
"skill",
...Object.keys(fixedDocuments).filter((key) => key !== "skill"),
],
};
}
if (!normalized || !target) {
return {
ok: false,
document: String(document || ""),
error: "Use a listed text-to-cad skill document key or a Markdown file below references/cad-skillx/.",
suggestedDocuments: [
"skill",
...Object.keys(fixedDocuments).filter((key) => key !== "skill"),
"references/cad-skillx/planning/mounting-plate.planning.md",
"references/cad-skillx/planning/flange.planning.md",
"references/cad-skillx/planning/bearing-housing-or-seat.planning.md",
],
};
}
try {
const content = await fs.readFile(target, "utf8");
return {
ok: true,
document: normalized,
path: target,
content: normalized === "skill" && !cadSkillxEnabled
? [
stripCadSkillxReferences(content),
"",
"## CAD-SkillX references",
"CAD-SkillX generated optimization references are disabled for this request. Do not read or use `references/cad-skillx/` documents.",
].join("\n")
: content,
};
} catch (error) {
return {
ok: false,
document: normalized,
error: error instanceof Error ? error.message : "text-to-cad document could not be read.",
suggestedDocuments: [],
};
}
}
async function loadRequiredTextToCadDocumentation(options: TextToCadDocumentationOptions = {}) {
const results = await Promise.all(
TEXT_TO_CAD_REQUIRED_DOCS.map((document) => readTextToCadDocumentation(document, options)),
);
const unavailable = results.filter((result) => !result.ok);
if (unavailable.length) {
throw new Error(`Studio could not load required text-to-cad documentation: ${unavailable.map((result) => result.document).join(", ")}.`);
}
return results.filter((result): result is Extract<TextToCadDocumentationResult, { ok: true }> => result.ok);
}
type Build123dApiSymbol = {
symbol: string;
ok: boolean;
signature?: string;
documentation?: string;
error?: string;
};
function parseJsonObjectFromStdout(stdout: string) {
const lines = stdout.split(/\r?\n/);
for (let index = lines.length - 1; index >= 0; index -= 1) {
if (!lines[index]?.trim().startsWith("{")) continue;
try {
return JSON.parse(lines.slice(index).join("\n")) as Record<string, unknown>;
} catch {
// Ignore non-JSON diagnostics preceding a final JSON report.
}
}
return null;
}
export async function inspectBuild123dApi(symbols: string[]) {
const requested = [...new Set(symbols.map((symbol) => String(symbol).trim()))]
.filter((symbol) => /^[A-Za-z_]\w*$/.test(symbol))
.slice(0, 32);
if (!requested.length) {
return { symbols: [] as Build123dApiSymbol[] };
}
const script = [
"import inspect, json, sys",
"import build123d as b123d",
"result = []",
"for name in sys.argv[1:] :",
" try:",
" value = getattr(b123d, name)",
" signature = str(inspect.signature(value))",
" documentation = inspect.getdoc(value) or ''",
" result.append({'symbol': name, 'ok': True, 'signature': signature, 'documentation': documentation[:4000]})",
" except Exception as error:",
" result.append({'symbol': name, 'ok': False, 'error': str(error)})",
"print(json.dumps({'symbols': result}, ensure_ascii=False))",
].join("\n");
const { stdout } = await execFileAsync(cadPythonExecutable(), ["-c", script, ...requested], {
cwd: process.cwd(),
timeout: 30_000,
maxBuffer: 1024 * 1024,
});
const payload = parseJsonObjectFromStdout(stdout);
const values = Array.isArray(payload?.symbols) ? payload.symbols : [];
return {
symbols: values.filter((value): value is Build123dApiSymbol => Boolean(value) && typeof value === "object"),
};
}
function importedPythonNames(source: string) {
const build123dImports = new Set<string>();
const externalImports = new Set<string>();
for (const match of source.matchAll(/^\s*from\s+([A-Za-z_][\w.]*)\s+import\s+(.+)$/gm)) {
const moduleName = match[1];
const names = match[2]
.split(",")
.map((part) => part.trim().replace(/\s+#.*$/, ""))
.filter(Boolean);
for (const namePart of names) {
const localName = (/\s+as\s+([A-Za-z_]\w*)$/.exec(namePart)?.[1] || namePart.split(/\s+as\s+/)[0]).trim();
if (!/^[A-Za-z_]\w*$/.test(localName)) continue;
if (moduleName === "build123d") {
build123dImports.add(localName);
} else {
externalImports.add(localName);
}
}
}
return { build123dImports, externalImports };
}
export function build123dCallCandidates(source: string) {
const { build123dImports, externalImports } = importedPythonNames(source);
const defined = new Set(
[...source.matchAll(/^\s*def\s+([A-Za-z_]\w*)\s*\(/gm)].map((match) => match[1]),
);
const candidates = [...source.matchAll(/(?<![\w.])([A-Za-z_]\w*)\s*\(/g)]
.map((match) => match[1])
.filter((name) => !defined.has(name))
.filter((name) => !externalImports.has(name))
.filter((name) => !build123dImports.size || build123dImports.has(name));
const memberReferences = [...source.matchAll(/(?<![\w.])([A-Za-z_]\w*)\.[A-Za-z_]\w*/g)]
.map((match) => match[1])
.filter((name) => build123dImports.has(name));
return [...new Set([...candidates, ...memberReferences])].slice(0, 64);
}
function buildSystemPrompt(
viewerContext: unknown[],
attachments: unknown[],
taskContext = "",
options: { cadSkillxEnabled?: boolean } = {},
) {
const cadSkillxEnabled = options.cadSkillxEnabled ?? CAD_SKILLX_ENABLED;
const build123dDocumentationInstruction = cadSkillxEnabled
? "When route_cad_request returns build123d_python, Studio automatically loads the text-to-cad skill, build123d-modeling, and step-generation documents into the route result. Do not repeat those reads. If the request matches a CAD-SkillX entry listed there, use read_text_to_cad_docs only for the relevant Planning/Functional/Atomic reference. Before writing source, call read_build123d_api once with every build123d symbol you plan to use. It reads the active build123d runtime's actual signature and docstring. Follow those signatures exactly: do not guess argument order, namespaces, or keyword names. Use build123d enum members exactly, for example Align.CENTER and Mode.SUBTRACT, not strings such as \"CENTER\"."
: "When route_cad_request returns build123d_python, Studio automatically loads the text-to-cad skill, build123d-modeling, and step-generation documents into the route result. Do not repeat those reads. CAD-SkillX generated optimization references are disabled for this request: do not call read_text_to_cad_docs for references/cad-skillx/... and do not use CAD-SkillX Planning/Functional/Atomic guidance. Before writing source, call read_build123d_api once with every build123d symbol you plan to use. It reads the active build123d runtime's actual signature and docstring. Follow those signatures exactly: do not guess argument order, namespaces, or keyword names. Use build123d enum members exactly, for example Align.CENTER and Mode.SUBTRACT, not strings such as \"CENTER\".";
return [
"You are the CAD Agent Studio assistant.",
"You help create and modify CAD models through a real server-side CAD generation tool.",
"Strict honesty rule: never claim that a CAD file/model was generated unless the generate_cad tool has succeeded and returned artifact URLs.",
"Never return only code when the user asks to generate CAD. Use the generate_cad tool instead.",
"Do not rely on hardcoded templates, canned examples, mock data, fake filenames, or imaginary viewer state.",
"The generate_cad tool validates executable DesignIR, selects a backend through CAD Router, and rebuilds STEP in a teacher-free sandbox.",
"For every new text/image CAD model, call route_cad_request once before writing native source. CAD Router selects SimpleCADAPI only for catalogued standard primary parts; every other new part is text-to-cad/build123d. Use its returned sourceKind in generate_cad. Do not choose the backend yourself and do not read backend documentation for routing.",
"When route_cad_request returns simplecadapi_python, first read the original SimpleCADAPI docs through read_simplecadapi_docs: skill, api/README.md, and stdlib/README.md. Then read the exact original API or stdlib Markdown page for every SimpleCADAPI function used in the source. Follow the documented signatures literally; do not invent namespaces or API names. Documentation paths come from the README links, not Python namespaces: for example simplecadapi.ql.value is documented as api/value.md, not ql/value.md. The server rejects source that calls a nonexistent SimpleCADAPI attribute.",
build123dDocumentationInstruction,
"When STEP files are attached, the source is teacher and acceptance truth only. Use reconstruct_uploaded_step; never import the teacher as model geometry.",
"All uploaded files are available to you. Images are attached directly to the latest user message when the selected provider/model supports vision. STEP/STP and binary files are available through inspect_uploaded_file.",
"When the user asks to model from an uploaded image, inspect the image directly from the image part. If the selected provider/model cannot process images, say that clearly and ask the user to switch to a vision-capable OpenAI-compatible model.",
@@ -367,16 +729,26 @@ function buildSystemPrompt(viewerContext: unknown[], attachments: unknown[], tas
"When viewer context is present, use its cad-edit-intent.v1 or cad-ai-geometry-selection.v1 payload as precise geometry context.",
"If viewer context selection.scope is selected_reference_only, modify only that selected topology/feature. Do not change a global pattern parameter or all repeated/symmetric features unless the user explicitly asks for all of them.",
"For selected holes, use the selected reference center/surface/bbox/adjacent selectors to identify the individual feature. If the editable source models a repeated pattern with one shared diameter variable, split out or override the selected instance instead of changing the shared variable.",
"Prefer editing the DesignIR recorded in cad-task.json before regenerating STEP and viewer assets.",
"If the current task is an uploaded-STEP DesignIR 3.0 surface_parametric reconstruction and the requested change matches a validated parameter, call edit_designir3_parameter.",
"Prefer the source of truth recorded in cad-task.json before regenerating STEP and viewer assets.",
"If the requested change matches a listed editable parameter, call edit_designir3_parameter. The server edits the backend-bound parameter without LLM participation: build123d/native Python, SimpleCADAPI source/model graph, or SurfaceIR validated parameter as recorded.",
"After uploaded-STEP reconstruction, list every perturbation-validated editable parameter by name, current value, unit, and validated range. Never report only the parameter count. Explain that unlisted inferred parameters remain in DesignIR but are not safely exposed until an executable binding and perturbation acceptance exist.",
"When the user asks to download or convert the current model to URDF or MJCF, call export_robot_description. Do not fabricate XML. The tool performs lazy conversion from the current DesignIR and returns a portable ZIP containing the robot description and mesh.",
"After export_robot_description succeeds, the UI starts the download and renders the exact package link. Say that the download has started; do not manually rewrite, shorten, or guess the returned URL.",
"A STEP-derived DesignIR without authoritative link/joint/material evidence exports honestly as one fixed base_link with inertial data omitted. Never invent articulated joints, limits, actuators, density, mass, or inertia.",
"If the current task is a text/image DesignIR 3.0 fully_semantic_parametric or hybrid_semantic_surface_parametric model, modify its authoritative semantic_layer and call generate_cad again; preserve unrelated parameters, constraints, and features.",
"For every new text/image CAD generation, author complete DesignIR 3.0 with designir_kind=independent_parametric_cad, reconstruction_mode=fully_semantic_parametric, authoring_mode=semantic_feature_program, and semantic_layer containing reconstruction_status, coordinate_system, datums, parameters, expressions, sketches, constraints, features, patterns, attachments, and construction_stages.",
"For semantic DesignIR 3.0, edit_interface must contain semantic_parameters, surface_parameter_groups=[], modification_levels=[semantic_feature], and preserved_interfaces. validation_contract must contain source_independence=true, geometry_checks, edit_checks, invariants, perturbations, and thresholds.",
"Use semantic parameter names that describe design intent, such as flange_thickness, shelf_depth, hole_spacing, rib_count, bore_diameter, and bolt_circle_diameter. Every editable parameter must directly drive at least one feature and have a perturbation test.",
"For structural edits to generated text/image models, read the current source of truth and submit the edited backend-native source to generate_cad; preserve the current backend unless the user explicitly requests backend conversion.",
"Do not author full DesignIR JSON for new text/image CAD generation. DesignIR 3.0 is the normalized contract layer generated after the backend has produced native source/graph and STEP.",
"DesignIR records backend, source-of-truth paths, parameters, feature tree, validation, and edit bindings; it is not the default modeling language for new geometry.",
"For generated backend-native models, never pretend backend_native_feature entries are replayable DesignIR compiler operations.",
"CAD Router may use its standard-part keyword catalog only to select a backend. The server never infers geometry, dimensions, or features from keywords. generate_cad requires final backend-native source.",
"Native source must be raw Python, not markdown. It must be a CLI program with argparse parse_args(), accept --step and --metadata, call export_step(..., args.step), and write backend-metadata.json to args.metadata; SimpleCADAPI source must also accept --model-json and write the requested model JSON file. A def gen_step() that only returns a part/object is an obsolete interface and will be rejected unless a __main__ CLI exports the files. stdout JSON is optional diagnostic output, not the source of truth.",
"Do not submit API/signature/geometry probe scripts to generate_cad. nativeSource must be the final model generator for the user request, not a script that introspects the SDK, sweeps trial parameters, diagnoses topology, or intentionally raises an exception to report facts.",
"Do not use generate_cad as an API discovery or exploration tool.",
"If generate_cad returns ok=false and retryable=true, read the returned error/artifact diagnostics, revise the native source, and call generate_cad again in the same turn. Do not ask the user whether to retry unless the failure is caused by missing user requirements.",
"Use generationMode=new_model for ordinary new CAD generation, structural_edit for modifying the current backend source, and backend_conversion only when the user explicitly asks to convert backend.",
"Every generated backend-native model must expose editable parameters as a normal part of generation. Define important dimensions, counts, spacings, radii, chamfers/fillets, angles, and offsets as named numeric variables in the native source instead of burying them as magic numbers.",
"For each generated model, include several useful editable parameters whenever the geometry has several meaningful dimensions. If the model is very simple, expose all meaningful dimensions. Do not leave backend-metadata.json parameters empty for a successfully generated model unless there are genuinely no numeric modeling choices.",
"Editable parameters must be real source-bound variables used by the geometry, not decorative metadata. Native source must include a CAD_AGENT_PARAMETERS block, and backend-metadata.json parameters must include name, display_name, value, unit, editable, binding_kind, parameter_path, and regenerate_adapter for each exposed parameter.",
"Parameter names shown to the user must be concise Chinese labels generated for the model, not raw English code identifiers. Keep code variable names stable and machine-friendly; keep display_name human-friendly and Chinese.",
"Uploaded STEP reconstruction also uses DesignIR 3.0, but in deterministic surface_parametric mode. Do not substitute one mode for the other.",
"Never embed STEP/B-Rep/mesh data or source topology references in DesignIR.",
"If CAD generation is requested and the generate_cad tool is available, call generate_cad.",
@@ -545,7 +917,7 @@ async function inspectStepAttachment(attachment: AttachmentRecord) {
"print(json.dumps(payload, ensure_ascii=False))",
].join("\n");
const { stdout } = await execFileAsync(
process.env.CAD_PYTHON || path.join(process.cwd(), "..", "text-to-cad", ".venv", "bin", "python"),
cadPythonExecutable(),
["-c", script, absolutePath],
{
timeout: 30_000,
@@ -599,6 +971,7 @@ export async function streamAgentResponse({
provider,
model,
selectedTaskId,
conversationId,
}: {
messages: unknown[];
attachments: unknown[];
@@ -606,14 +979,55 @@ export async function streamAgentResponse({
provider?: string;
model?: string;
selectedTaskId?: string;
conversationId?: string;
}) {
const config = loadLlmConfig();
const useCadSkillx = CAD_SKILLX_ENABLED;
const requestedConversationId = String(conversationId || "").trim();
const normalizedConversationId = requestedConversationId ? safeConversationId(requestedConversationId) : "";
const requestMessages = messages;
const uploadedAttachments = normalizedAttachments(attachments);
let chatMessages = normalizedMessages(requestMessages);
if (normalizedConversationId) {
const latestUserMessage = [...requestMessages].reverse().find((message) => {
const record = message && typeof message === "object" ? message as Record<string, unknown> : null;
return record?.role === "user";
});
if (!latestUserMessage) {
throw new Error("A user message is required to continue a conversation.");
}
const conversation = await ensureConversation(normalizedConversationId, selectedTaskId || "");
const persisted = await appendConversationMessage({
conversationId: normalizedConversationId,
message: latestUserMessage,
currentTaskId: selectedTaskId || conversation.currentTaskId,
attachments: uploadedAttachments,
});
chatMessages = conversationModelMessages(persisted.messages) as ChatMessage[];
selectedTaskId = String(selectedTaskId || persisted.currentTaskId || "").trim() || undefined;
}
const persistAssistantMessage = async (message: UIMessage) => {
if (!normalizedConversationId) return;
await appendConversationMessage({
conversationId: normalizedConversationId,
message,
currentTaskId: selectedTaskId || "",
attachments: uploadedAttachments,
});
};
let config: LlmConfig;
try {
config = loadLlmConfig();
} catch (error) {
const message = error instanceof Error ? error.message : "无法加载模型配置。";
return uiTextResponse(`Agent 请求没有成功。\n\n原因:${message}`, persistAssistantMessage);
}
const selected = selectedModelId(provider, model, config);
const apiKey = resolveProviderApiKey(selected.providerConfig);
const chatMessages = normalizedMessages(messages);
const uploadedAttachments = normalizedAttachments(attachments);
if (!apiKey) {
return uiTextResponse(localAssistantReply({ messages: chatMessages, attachments: uploadedAttachments, viewerContext, selectedTaskId }));
return uiTextResponse(
localAssistantReply({ messages: chatMessages, attachments: uploadedAttachments, viewerContext, selectedTaskId }),
persistAssistantMessage,
);
}
const languageModel = buildLanguageModel({
apiKey,
@@ -624,7 +1038,7 @@ export async function streamAgentResponse({
let cadGeneration: CadGenerationResult | null = null;
let streamWriter: { write: (part: any) => void } | null = null;
const transientProgressSteps = new Set(["analyze_request", "agent_stream"]);
const transientProgressSteps = new Set(["analyze_request"]);
const writeCadProgress = (payload: CadProgressPayload) => {
streamWriter?.write({
type: "data-cad-progress",
@@ -663,7 +1077,108 @@ export async function streamAgentResponse({
writeCadError("agent_stream", message);
};
const taskContext = await buildTaskContext(selectedTaskId);
const simpleCadApiDocsRead = new Set<string>();
const textToCadDocsRead = new Set<string>();
const build123dApiSymbolsRead = new Set<string>();
const routeDecisions = new Map<string, NewCadRouteDecision>();
const cadTools = {
route_cad_request: tool({
description: [
"Route one new CAD request before authoring native source.",
"The CAD Router uses its exact SimpleCADAPI standard-part catalog: catalogued primary parts use SimpleCADAPI; all other new parts use text-to-cad/build123d.",
"Do not use this for an uploaded STEP or a same-backend structural edit.",
].join(" "),
inputSchema: z.object({
request: z.string().describe("The full new-model request to classify."),
}),
execute: async ({ request }) => {
writeCadProgress({
step: "route_request",
label: "CAD Router 选路",
status: "running",
message: "正在按标准零件类型目录选择生成后端。",
});
const decision = await routeNewCadRequest(request || lastUserText(chatMessages));
simpleCadApiDocsRead.clear();
textToCadDocsRead.clear();
build123dApiSymbolsRead.clear();
routeDecisions.clear();
const build123dBaseDocs = decision.selectedBackend === "build123d"
? await loadRequiredTextToCadDocumentation({ cadSkillxEnabled: useCadSkillx })
: [];
for (const document of build123dBaseDocs) {
textToCadDocsRead.add(document.document);
}
const routeToken = randomUUID();
routeDecisions.set(routeToken, decision);
writeCadProgress({
step: "route_request",
label: "CAD Router 选路",
status: "success",
message: `${decision.selectedBackend}: ${decision.rationale}`,
});
return {
...decision,
routeToken,
...(build123dBaseDocs.length ? {
backendContext: {
source: "Studio-loaded text-to-cad/build123d base documentation",
cadSkillxEnabled: useCadSkillx,
documents: build123dBaseDocs.map(({ document, content }) => ({ document, content })),
},
} : {}),
};
},
}),
read_simplecadapi_docs: tool({
description: [
"Read original SimpleCADAPI skill or API documentation before writing SimpleCADAPI source.",
"Read skill, api/README.md, and stdlib/README.md first, then the exact API or stdlib Markdown page for every function used.",
"Use the canonical paths linked from api/README.md or stdlib/README.md. Do not derive a documentation directory from a Python namespace; ql/value.md, for example, resolves to api/value.md when it is unique.",
].join(" "),
inputSchema: z.object({
document: z.string().describe("Original skill/doc key or Markdown page path."),
}),
execute: async ({ document }) => {
const result = await readSimpleCadApiDocumentation(document);
if (result.ok) simpleCadApiDocsRead.add(result.document);
return result;
},
}),
read_text_to_cad_docs: tool({
description: [
"Read an additional original text-to-cad/build123d reference when the Studio-loaded base documentation identifies a need.",
"The base skill, build123d modeling, and STEP generation references are already returned by route_cad_request for build123d routes.",
useCadSkillx
? "Use this for relevant Markdown files below references/cad-skillx/ or optional positioning/inspection references."
: "CAD-SkillX optimization references are disabled; use this only for optional non-cad-skillx references such as positioning or inspection.",
].join(" "),
inputSchema: z.object({
document: z.string().describe("Listed text-to-cad skill document key or references/cad-skillx/... Markdown path."),
}),
execute: async ({ document }) => {
const result = await readTextToCadDocumentation(document, { cadSkillxEnabled: useCadSkillx });
if (result.ok) textToCadDocsRead.add(result.document);
return result;
},
}),
read_build123d_api: tool({
description: [
"Read exact function/class signatures and docstrings from the active build123d runtime before writing source.",
"Pass all planned build123d symbols in one call, for example BuildPart, Locations, Cylinder, Box, chamfer, and export_step.",
"This is runtime documentation only; it does not execute or generate geometry.",
].join(" "),
inputSchema: z.object({
symbols: z.array(z.string()).min(1).max(32).describe("build123d top-level symbols used by the planned native source."),
}),
execute: async ({ symbols }) => {
const result = await inspectBuild123dApi(symbols);
for (const symbol of result.symbols) {
if (symbol.ok) build123dApiSymbolsRead.add(symbol.symbol);
}
return result;
},
}),
inspect_uploaded_file: tool({
description: [
"Inspect a user-uploaded file before using it for CAD generation.",
@@ -800,9 +1315,14 @@ export async function streamAgentResponse({
taskId: cadGeneration.taskId,
sourcePath: cadGeneration.sourcePath,
sourceUrl: cadGeneration.sourceUrl,
parameters: cadGeneration.parameters,
editableParameters: cadGeneration.editableParameters,
artifactPath: cadGeneration.artifactPath,
artifactUrl: cadGeneration.artifactUrl,
featureTreePath: cadGeneration.featureTreePath,
featureTreeUrl: cadGeneration.featureTreeUrl,
parameterCatalogPath: cadGeneration.parameterCatalogPath,
parameterCatalogUrl: cadGeneration.parameterCatalogUrl,
previewPath: cadGeneration.previewPath,
previewUrl: cadGeneration.previewUrl,
viewerAssetPath: cadGeneration.viewerAssetPath,
@@ -816,26 +1336,26 @@ export async function streamAgentResponse({
}),
edit_designir3_parameter: tool({
description: [
"Edit one perturbation-validated parameter on the current DesignIR 3.0 reconstruction.",
"The requested value must be within the validated range shown in current cad-task.json.",
"The server reruns independent teacher-side semantic edit acceptance before publishing.",
"Edit one listed editable CAD parameter on the current task without LLM participation.",
"The requested value must be within the range shown in parameters.json.",
"The server uses the recorded backend binding: native Python/source for generated models or SurfaceIR acceptance for uploaded STEP reconstructions.",
].join(" "),
inputSchema: z.object({
parameter: z.string().describe("Validated parameter name from the current task."),
value: z.number().describe("New value within the parameter's validated range."),
parameter: z.string().describe("Editable parameter name from the current task."),
value: z.number().describe("New value within the parameter's declared range."),
}),
execute: async ({ parameter, value }) => {
if (!selectedTaskId) {
throw new Error("No current DesignIR 3.0 task is selected.");
throw new Error("No current CAD task is selected.");
}
writeCadProgress({
step: "execute_cad",
label: "修改 DesignIR 3.0 参数",
step: "backend_parameter_edit",
label: "修改后端绑定参数",
status: "running",
message: `${parameter} ${value},正在执行独立参数验收`,
message: `${parameter} -> ${value},正在使用记录的无 LLM adapter 重新生成`,
});
try {
cadGeneration = await editDesignIR3Parameter({
cadGeneration = await editDesignIRParameter({
prompt: lastUserText(chatMessages),
sourceTaskId: selectedTaskId,
parameter,
@@ -844,10 +1364,10 @@ export async function streamAgentResponse({
} catch (error) {
const message = error instanceof Error
? error.message
: "DesignIR 3.0 parameter edit failed.";
: "CAD parameter edit failed.";
writeCadProgress({
step: "execute_cad",
label: "修改 DesignIR 3.0 参数",
step: "backend_parameter_edit",
label: "修改后端绑定参数",
status: "error",
message,
});
@@ -855,8 +1375,8 @@ export async function streamAgentResponse({
throw error;
}
writeCadProgress({
step: "execute_cad",
label: "修改 DesignIR 3.0 参数",
step: "backend_parameter_edit",
label: "修改后端绑定参数",
status: "success",
message: cadGeneration.summary,
});
@@ -866,16 +1386,21 @@ export async function streamAgentResponse({
status: "success",
message: "参数修改后的模型已载入。",
});
completeAgentStream("DesignIR 3.0 参数修改和验收已完成。");
completeAgentStream("CAD 参数修改和重新生成已完成。");
streamWriter?.write({
type: "data-cad-result",
data: {
taskId: cadGeneration.taskId,
sourcePath: cadGeneration.sourcePath,
sourceUrl: cadGeneration.sourceUrl,
parameters: cadGeneration.parameters,
editableParameters: cadGeneration.editableParameters,
artifactPath: cadGeneration.artifactPath,
artifactUrl: cadGeneration.artifactUrl,
featureTreePath: cadGeneration.featureTreePath,
featureTreeUrl: cadGeneration.featureTreeUrl,
parameterCatalogPath: cadGeneration.parameterCatalogPath,
parameterCatalogUrl: cadGeneration.parameterCatalogUrl,
previewPath: cadGeneration.previewPath,
previewUrl: cadGeneration.previewUrl,
viewerAssetPath: cadGeneration.viewerAssetPath,
@@ -951,69 +1476,115 @@ export async function streamAgentResponse({
}),
generate_cad: tool({
description: [
"Generate a real CAD model from agent-authored semantic DesignIR 3.0.",
"Execute backend-native Python source submitted by the agent to generate a real CAD model.",
"Call this only when the user wants CAD generation or a model modification.",
"CAD Router selects the backend and the server rebuilds STEP without teacher geometry.",
"The server does not infer geometry, choose templates, or generate fallback models.",
"Do not call this for API discovery, topology diagnostics, radius sweeps, or smoke probes.",
"stdout JSON is optional diagnostic output; generated files are the execution source of truth.",
"If this returns ok=false and retryable=true, fix the nativeSource from the diagnostics and call generate_cad again immediately.",
].join(" "),
inputSchema: z.object({
summary: z.string().describe("Concise user-facing summary of the generated CAD model."),
designirFilename: z.string().describe("DesignIR filename, e.g. model.designir.json."),
stepFilename: z.string().describe("STEP output filename, e.g. model.step."),
designir: z.record(z.string(), z.unknown()).describe("Complete semantic DesignIR 3.0 JSON object in fully_semantic_parametric mode."),
teacherTaskId: z.string().optional().describe("Uploaded teacher STEP task id for independent acceptance only."),
teacherPath: z.string().optional().describe("Uploaded teacher STEP path for independent acceptance only."),
routeToken: z.string().optional().describe("Opaque token returned by route_cad_request. Required for new_model and binds the request/backend."),
sourceKind: z.enum(["build123d_python", "simplecadapi_python"]).describe("Native source kind returned by route_cad_request, or the existing source of truth for an edit."),
generationMode: z.enum(["new_model", "structural_edit", "backend_conversion"]).default("new_model").describe("Whether this is a new model, a same-backend structural edit, or an explicit backend conversion."),
nativeSource: z.string().describe("Raw backend-native Python source. No markdown fences. Must be the final CLI generator, not an API/topology/radius probe and not a gen_step-only return-object script. Must parse required CLI args, call export_step(..., args.step), and write STEP plus metadata. Editable parameter metadata must include Chinese user-facing names/display_name values."),
summary: z.string().optional().describe("Concise user-facing summary of the requested CAD model."),
targetName: z.string().optional().describe("Optional model/task artifact name stem, e.g. mounting_bracket."),
assumptions: z.array(z.string()).default([]).describe("Assumptions made because the user did not specify every dimension."),
}),
execute: async ({
routeToken,
sourceKind,
generationMode,
nativeSource,
summary,
designirFilename,
stepFilename,
designir,
teacherTaskId,
teacherPath,
targetName,
assumptions,
}) => {
const routeDecision = generationMode === "new_model"
? routeDecisions.get(String(routeToken || ""))
: undefined;
if (generationMode === "new_model" && !routeDecision) {
throw new Error("New CAD generation requires the routeToken returned by route_cad_request; do not rephrase or reroute the request in generate_cad.");
}
if (
generationMode === "new_model"
&& sourceKind === "simplecadapi_python"
&& SIMPLECADAPI_REQUIRED_DOCS.some((document) => !simpleCadApiDocsRead.has(document))
) {
throw new Error(
"SimpleCADAPI generation requires original docs: skill, api/README.md, and stdlib/README.md before native source is submitted.",
);
}
if (generationMode === "new_model" && sourceKind === "build123d_python") {
const apiSymbols = await inspectBuild123dApi(build123dCallCandidates(nativeSource));
const missing = apiSymbols.symbols
.filter((symbol) => symbol.ok && !build123dApiSymbolsRead.has(symbol.symbol))
.map((symbol) => symbol.symbol);
if (missing.length) {
for (const symbol of missing) {
build123dApiSymbolsRead.add(symbol);
}
writeCadProgress({
step: "build123d_api_preflight",
label: "补全 build123d API",
status: "success",
message: `服务端已自动补读缺失 API${missing.join(", ")}`,
});
}
}
writeCadProgress({
step: "generate_designir",
label: "生成 DesignIR",
status: "success",
message: designirFilename,
});
writeCadProgress({
step: "execute_cad",
label: "隔离重建 STEP",
step: "backend_generate",
label: "后端原生生成",
status: "running",
message: "正在进行防作弊检查、CAD Router 选路和无教师重建。",
message: "正在执行 agent 提交的原生 source,server 不生成模板或替代模型。",
});
try {
cadGeneration = await executeDesignIR({
prompt: lastUserText(chatMessages),
cadGeneration = await executeBackendNativeGeneration({
request: routeDecision?.request || lastUserText(chatMessages),
sourceKind,
generationMode,
routeDecision,
nativeSource,
summary,
designirFilename,
stepFilename,
designir,
teacherTaskId,
teacherPath,
targetName,
assumptions,
selectedTaskId,
});
} catch (error) {
const message = error instanceof Error ? error.message : "CAD generation failed.";
writeCadProgress({
step: "execute_cad",
label: "隔离重建 STEP",
step: "backend_generate",
label: "后端原生生成",
status: "error",
message,
});
failAgentStream(message);
throw error;
return {
ok: false,
retryable: true,
stage: "backend_generate",
error: message,
instruction: "Revise nativeSource using this diagnostic and call generate_cad again in the same turn. Do not ask the user for permission to retry.",
};
}
writeCadProgress({
step: "execute_cad",
label: "隔离重建 STEP",
step: "backend_generate",
label: "后端原生生成",
status: "success",
message: cadGeneration.artifactPath,
});
writeCadProgress({
step: "normalize_designir",
label: "归一化 DesignIR",
status: "success",
message: cadGeneration.sourcePath,
});
writeCadProgress({
step: "publish_artifacts",
label: "发布产物",
status: "success",
message: "特征树、参数列表和预览已准备完成。",
});
writeCadProgress({
step: "update_preview",
label: "更新右侧预览",
@@ -1027,9 +1598,14 @@ export async function streamAgentResponse({
taskId: cadGeneration.taskId,
sourcePath: cadGeneration.sourcePath,
sourceUrl: cadGeneration.sourceUrl,
parameters: cadGeneration.parameters,
editableParameters: cadGeneration.editableParameters,
artifactPath: cadGeneration.artifactPath,
artifactUrl: cadGeneration.artifactUrl,
featureTreePath: cadGeneration.featureTreePath,
featureTreeUrl: cadGeneration.featureTreeUrl,
parameterCatalogPath: cadGeneration.parameterCatalogPath,
parameterCatalogUrl: cadGeneration.parameterCatalogUrl,
previewPath: cadGeneration.previewPath,
previewUrl: cadGeneration.previewUrl,
viewerAssetPath: cadGeneration.viewerAssetPath,
@@ -1073,12 +1649,15 @@ export async function streamAgentResponse({
});
const result = streamText({
model: languageModel,
system: buildSystemPrompt(viewerContext, uploadedAttachments, taskContext),
system: buildSystemPrompt(viewerContext, uploadedAttachments, taskContext, {
cadSkillxEnabled: useCadSkillx,
}),
tools: cadTools,
stopWhen: isStepCount(3),
stopWhen: isStepCount(16),
messages: modelMessages,
});
writer.merge(result.toUIMessageStream({
sendReasoning: false,
onEnd: () => {
completeAgentStream(cadGeneration ? "模型和 CAD 工具执行已完成。" : "模型回复已完成。");
},
@@ -1095,6 +1674,14 @@ export async function streamAgentResponse({
}
},
onError: formatCurrentModelError,
onEnd: async ({ responseMessage, isAborted }) => {
if (isAborted) return;
try {
await persistAssistantMessage(responseMessage);
} catch (error) {
console.error("Failed to persist CAD conversation response", error);
}
},
});
return createUIMessageStreamResponse({
stream,
@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import test from "node:test";
import { conversationModelMessages, sanitizeConversationMessage } from "./conversation-store";
test("conversation persistence keeps final CAD UI parts and drops transient parts", () => {
const message = sanitizeConversationMessage({
id: "assistant-result",
role: "assistant",
parts: [
{ type: "text", text: "模型已生成。" },
{ type: "data-cad-progress", data: { status: "running" } },
{ type: "tool-generate_cad", state: "output-available" },
{ type: "data-cad-result", data: { taskId: "cad_123456789abc", summary: "完成" } },
{ type: "data-cad-error", data: { stage: "validation", message: "仅用于展示的最终错误" } },
],
});
assert.deepEqual(message, {
id: "assistant-result",
role: "assistant",
parts: [
{ type: "text", text: "模型已生成。" },
{ type: "data-cad-result", data: { taskId: "cad_123456789abc", summary: "完成" } },
{ type: "data-cad-error", data: { stage: "validation", message: "仅用于展示的最终错误" } },
],
});
});
test("conversation model context contains only completed text messages", () => {
const user = sanitizeConversationMessage({
id: "user-1",
role: "user",
parts: [{ type: "text", text: "把外径改成 120 mm" }],
});
const assistant = sanitizeConversationMessage({
id: "assistant-1",
role: "assistant",
parts: [
{ type: "data-cad-result", data: { taskId: "cad_123456789abc" } },
{ type: "text", text: "已生成新版本。" },
],
});
assert.ok(user);
assert.ok(assistant);
assert.deepEqual(conversationModelMessages([user, assistant]), [
{ role: "user", content: "把外径改成 120 mm" },
{ role: "assistant", content: "已生成新版本。" },
]);
});
test("conversation rejects invalid user messages without text", () => {
assert.equal(sanitizeConversationMessage({
id: "user-1",
role: "user",
parts: [{ type: "file", url: "https://example.test/model.step" }],
}), null);
});
@@ -0,0 +1,331 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { resolveTaskRoot } from "@/lib/config";
import { safeTaskId } from "@/lib/task-store";
export type ConversationAttachment = {
id: string;
taskId: string;
name: string;
kind: string;
path: string;
size: number;
sha256: string;
mime: string;
};
export type ConversationMessagePart =
| { type: "text"; text: string }
| { type: "data-cad-result"; data: Record<string, unknown> }
| { type: "data-cad-error"; data: { stage?: string; message: string } };
export type ConversationMessage = {
id: string;
role: "user" | "assistant";
parts: ConversationMessagePart[];
};
export type ConversationRecord = {
schema_version: "1.0";
conversationId: string;
createdAt: string;
updatedAt: string;
currentTaskId: string;
taskIds: string[];
attachments: ConversationAttachment[];
messages: ConversationMessage[];
};
const conversationLocks = new Map<string, Promise<void>>();
async function withConversationLock<T>(conversationId: string, operation: () => Promise<T>) {
const safeId = safeConversationId(conversationId);
const previous = conversationLocks.get(safeId) || Promise.resolve();
let release: (() => void) | undefined;
const current = previous.then(() => new Promise<void>((resolve) => {
release = resolve;
}));
conversationLocks.set(safeId, current);
await previous;
try {
return await operation();
} finally {
release?.();
if (conversationLocks.get(safeId) === current) {
conversationLocks.delete(safeId);
}
}
}
export function newConversationId() {
return `conv_${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
}
export function safeConversationId(value: string) {
const conversationId = String(value || "").trim();
if (!/^conv_[a-zA-Z0-9_-]{4,75}$/.test(conversationId)) {
throw new Error("Invalid conversation id");
}
return conversationId;
}
export function conversationRoot() {
return path.join(resolveTaskRoot(), "conversations");
}
export function conversationPath(conversationId: string) {
return path.join(conversationRoot(), safeConversationId(conversationId), "conversation.json");
}
function safeTaskIdOrEmpty(value: unknown) {
const taskId = String(value || "").trim();
if (!taskId) return "";
try {
return safeTaskId(taskId);
} catch {
return "";
}
}
function boundedText(value: unknown, limit = 120_000) {
return String(value || "").slice(0, limit);
}
function sanitizeCadResult(value: unknown) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const result = value as Record<string, unknown>;
const taskId = safeTaskIdOrEmpty(result.taskId);
if (!taskId) return null;
return JSON.parse(JSON.stringify({ ...result, taskId })) as Record<string, unknown>;
}
function sanitizeCadError(value: unknown) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const record = value as Record<string, unknown>;
const message = boundedText(record.message, 16_000).trim();
if (!message) return null;
const stage = boundedText(record.stage, 200).trim();
return stage ? { stage, message } : { message };
}
export function sanitizeConversationMessage(value: unknown): ConversationMessage | null {
if (!value || typeof value !== "object") return null;
const record = value as Record<string, unknown>;
const role = record.role === "assistant" ? "assistant" : record.role === "user" ? "user" : null;
const id = boundedText(record.id, 160).trim();
if (!role || !id || !Array.isArray(record.parts)) return null;
const parts: ConversationMessagePart[] = [];
for (const rawPart of record.parts) {
if (!rawPart || typeof rawPart !== "object") continue;
const part = rawPart as Record<string, unknown>;
if (part.type === "text") {
const text = boundedText(part.text).trim();
if (text) parts.push({ type: "text", text });
continue;
}
if (role !== "assistant" || part.type !== "data-cad-result" && part.type !== "data-cad-error") continue;
if (part.type === "data-cad-result") {
const data = sanitizeCadResult(part.data);
if (data) parts.push({ type: "data-cad-result", data });
continue;
}
const data = sanitizeCadError(part.data);
if (data) parts.push({ type: "data-cad-error", data });
}
if (!parts.length || (role === "user" && !parts.some((part) => part.type === "text"))) return null;
return { id, role, parts };
}
function sanitizeAttachment(value: unknown): ConversationAttachment | null {
if (!value || typeof value !== "object") return null;
const record = value as Record<string, unknown>;
const taskId = safeTaskIdOrEmpty(record.taskId);
const artifactPath = String(record.path || "").trim();
if (!taskId || !artifactPath || artifactPath.startsWith("/") || artifactPath.split(/[\\/]+/).includes("..")) {
return null;
}
return {
id: boundedText(record.id, 160).trim(),
taskId,
name: boundedText(record.name || path.basename(artifactPath), 500).trim(),
kind: boundedText(record.kind || "other", 80).trim() || "other",
path: artifactPath,
size: Math.max(0, Number(record.size) || 0),
sha256: boundedText(record.sha256, 200).trim(),
mime: boundedText(record.mime || "application/octet-stream", 200).trim() || "application/octet-stream",
};
}
function uniqueTaskIds(values: unknown[]) {
return [...new Set(values.map(safeTaskIdOrEmpty).filter(Boolean))];
}
function uniqueAttachments(values: unknown[]) {
const seen = new Set<string>();
const result: ConversationAttachment[] = [];
for (const value of values) {
const attachment = sanitizeAttachment(value);
if (!attachment) continue;
const key = [attachment.id, attachment.taskId, attachment.path].join(":");
if (seen.has(key)) continue;
seen.add(key);
result.push(attachment);
}
return result;
}
function normalizeConversation(value: unknown, expectedConversationId?: string): ConversationRecord | null {
if (!value || typeof value !== "object") return null;
const record = value as Record<string, unknown>;
const conversationId = safeConversationId(String(record.conversationId || expectedConversationId || ""));
if (expectedConversationId && conversationId !== safeConversationId(expectedConversationId)) return null;
const messages = Array.isArray(record.messages)
? record.messages.map(sanitizeConversationMessage).filter((message): message is ConversationMessage => Boolean(message))
: [];
const messageIds = new Set<string>();
const uniqueMessages = messages.filter((message) => {
if (messageIds.has(message.id)) return false;
messageIds.add(message.id);
return true;
});
const currentTaskId = safeTaskIdOrEmpty(record.currentTaskId);
const taskIds = uniqueTaskIds([
...(Array.isArray(record.taskIds) ? record.taskIds : []),
currentTaskId,
...uniqueMessages.flatMap((message) => message.parts.map((part) => (
part.type === "data-cad-result" ? part.data.taskId : ""
))),
]);
const now = new Date().toISOString();
return {
schema_version: "1.0",
conversationId,
createdAt: boundedText(record.createdAt, 80).trim() || now,
updatedAt: boundedText(record.updatedAt, 80).trim() || now,
currentTaskId,
taskIds,
attachments: uniqueAttachments(Array.isArray(record.attachments) ? record.attachments : []),
messages: uniqueMessages,
};
}
export function defaultConversation(conversationId: string, currentTaskId = ""): ConversationRecord {
const now = new Date().toISOString();
const safeCurrentTaskId = safeTaskIdOrEmpty(currentTaskId);
return {
schema_version: "1.0",
conversationId: safeConversationId(conversationId),
createdAt: now,
updatedAt: now,
currentTaskId: safeCurrentTaskId,
taskIds: safeCurrentTaskId ? [safeCurrentTaskId] : [],
attachments: [],
messages: [],
};
}
export async function readConversation(conversationId: string) {
const safeId = safeConversationId(conversationId);
try {
const source = await fs.readFile(conversationPath(safeId), "utf8");
return normalizeConversation(JSON.parse(source), safeId);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw error;
}
}
async function writeConversation(record: ConversationRecord) {
const directory = path.dirname(conversationPath(record.conversationId));
await fs.mkdir(directory, { recursive: true });
const target = conversationPath(record.conversationId);
const temporary = path.join(directory, `.conversation-${crypto.randomUUID()}.tmp`);
await fs.writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`);
await fs.rename(temporary, target);
return record;
}
async function readOrCreateConversation(conversationId: string, currentTaskId = "") {
const existing = await readConversation(conversationId);
if (existing) return existing;
return writeConversation(defaultConversation(conversationId, currentTaskId));
}
export async function ensureConversation(conversationId: string, currentTaskId = "") {
return withConversationLock(conversationId, () => readOrCreateConversation(conversationId, currentTaskId));
}
export async function updateConversation({
conversationId,
currentTaskId,
attachments,
}: {
conversationId: string;
currentTaskId?: string;
attachments?: unknown[];
}) {
return withConversationLock(conversationId, async () => {
const current = await readOrCreateConversation(conversationId, currentTaskId || "");
const nextTaskId = safeTaskIdOrEmpty(currentTaskId) || current.currentTaskId;
const next: ConversationRecord = {
...current,
updatedAt: new Date().toISOString(),
currentTaskId: nextTaskId,
taskIds: uniqueTaskIds([...current.taskIds, nextTaskId]),
attachments: uniqueAttachments([...current.attachments, ...(attachments || [])]),
};
return writeConversation(next);
});
}
export async function appendConversationMessage({
conversationId,
message,
currentTaskId,
attachments,
}: {
conversationId: string;
message: unknown;
currentTaskId?: string;
attachments?: unknown[];
}) {
const sanitized = sanitizeConversationMessage(message);
if (!sanitized) throw new Error("Conversation message has no persistable content.");
return withConversationLock(conversationId, async () => {
const current = await readOrCreateConversation(conversationId, currentTaskId || "");
const resultTaskIds = sanitized.parts.flatMap((part) => (
part.type === "data-cad-result" ? [safeTaskIdOrEmpty(part.data.taskId)] : []
));
const nextTaskId = resultTaskIds.find(Boolean) || safeTaskIdOrEmpty(currentTaskId) || current.currentTaskId;
const next: ConversationRecord = {
...current,
updatedAt: new Date().toISOString(),
currentTaskId: nextTaskId,
taskIds: uniqueTaskIds([...current.taskIds, nextTaskId, ...resultTaskIds]),
attachments: uniqueAttachments([...current.attachments, ...(attachments || [])]),
messages: current.messages.some((item) => item.id === sanitized.id)
? current.messages
: [...current.messages, sanitized],
};
return writeConversation(next);
});
}
export function conversationModelMessages(messages: ConversationMessage[]) {
const completeMessages = messages.map((message) => ({
role: message.role,
content: message.parts
.filter((part): part is Extract<ConversationMessagePart, { type: "text" }> => part.type === "text")
.map((part) => part.text)
.join("\n"),
})).filter((message) => message.content.trim());
const selected = [] as typeof completeMessages;
let characters = 0;
for (const message of [...completeMessages].reverse()) {
if (selected.length >= 48 || characters + message.content.length > 48_000) break;
selected.push(message);
characters += message.content.length;
}
return selected.reverse();
}
+14 -2
View File
@@ -1,5 +1,6 @@
import crypto from "node:crypto";
import { execFile } from "node:child_process";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
@@ -40,8 +41,19 @@ type ExportState = {
};
function pythonExecutable() {
return process.env.CAD_PYTHON
|| enginePath("text-to-cad", ".venv", "bin", "python");
const configured = String(process.env.CAD_PYTHON || "").trim();
if (configured) return configured;
const workspacePython = enginePath("text-to-cad", ".venv", "bin", "python");
if (existsSync(workspacePython)) return workspacePython;
if (commandExists("python")) return "python";
if (commandExists("python3")) return "python3";
return "python3";
}
function commandExists(command: string) {
return String(process.env.PATH || "")
.split(path.delimiter)
.some((directory) => existsSync(path.join(directory, command)));
}
function exporterScript() {
+104 -1
View File
@@ -201,9 +201,112 @@ export async function upsertManifest(taskId: string, patch: Partial<TaskManifest
return writeManifest(taskId, next);
}
function containsEditablePythonParameter(source: string, parameter: string, binding?: Record<string, unknown>) {
if (
/# CAD_AGENT_PARAMETERS_START\s*\nPARAMETERS\s*=\s*\{[\s\S]*?\}\s*\n# CAD_AGENT_PARAMETERS_END/.test(source)
&& new RegExp(`["']?${parameter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']?\\s*:`).test(source)
) {
return true;
}
if (
/CAD_AGENT_PARAMETERS\s*=\s*\{/.test(source)
&& new RegExp(`["']${parameter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']\\s*:\\s*\\{[\\s\\S]*?["']value["']\\s*:`).test(source)
) {
return true;
}
const parameterPath = String(binding?.parameter_path || "").trim();
const candidates = Array.from(new Set([
parameter,
parameterPath.split(".").filter(Boolean).at(-1) || "",
parameter.toUpperCase(),
parameterPath.split(".").filter(Boolean).at(-1)?.toUpperCase() || "",
].filter((candidate) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(candidate))));
return candidates.some((candidate) => (
new RegExp(`^\\s*${candidate.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=\\s*[-+]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][-+]?\\d+)?`, "m").test(source)
));
}
const DIRECT_EDITABLE_BINDING_KINDS = new Set([
"python_constant",
"native",
"parameter",
"native_python",
"native_source",
"source_parameter",
"source_variable",
"model_graph_parameter",
]);
async function parameterIsDirectlyEditable(taskRoot: string, manifest: TaskManifest, parameter: Record<string, unknown>) {
if (parameter.editable !== true) return false;
const binding = (
parameter.backend_binding && typeof parameter.backend_binding === "object"
? parameter.backend_binding
: null
) as Record<string, unknown> | null;
const uploadReconstruction = (
manifest.studio?.uploadReconstruction && typeof manifest.studio.uploadReconstruction === "object"
? manifest.studio.uploadReconstruction
: {}
) as Record<string, unknown>;
if (!binding) {
return Boolean(uploadReconstruction.teacherTaskId && uploadReconstruction.teacherPath);
}
const backend = String(binding.backend || binding.regenerate_adapter || "");
if (backend === "surfaceir") {
return Boolean(uploadReconstruction.teacherTaskId && uploadReconstruction.teacherPath);
}
const bindingKind = String(binding.binding_kind || "");
if (bindingKind && !DIRECT_EDITABLE_BINDING_KINDS.has(bindingKind)) {
return false;
}
const sourcePath = String(
binding.source_path
|| manifest.source?.native_source_path
|| manifest.source?.path
|| "",
);
if (!sourcePath.endsWith(".py")) return false;
const absoluteSourcePath = path.join(taskRoot, sourcePath);
try {
const source = await fs.readFile(absoluteSourcePath, "utf8");
return containsEditablePythonParameter(source, String(parameter.name || parameter.id || ""), binding);
} catch {
return false;
}
}
async function manifestWithDirectEditableParameters(taskId: string, manifest: TaskManifest | null) {
if (!manifest) return manifest;
const catalog = manifest.parameters;
const parameters = Array.isArray(catalog?.parameters)
? catalog.parameters.filter(
(parameter): parameter is Record<string, unknown> => parameter !== null && typeof parameter === "object",
)
: [];
if (!parameters.length) return manifest;
const root = taskDir(taskId);
const filtered = [];
for (const parameter of parameters) {
if (await parameterIsDirectlyEditable(root, manifest, parameter)) {
filtered.push(parameter);
}
}
return {
...manifest,
parameters: {
...catalog,
parameters: filtered,
groups: Array.isArray((catalog as Record<string, unknown>).groups)
? (catalog as Record<string, unknown>).groups
: [],
},
};
}
export async function readTask(taskId: string) {
const dir = taskDir(taskId);
const manifest = await readManifest(taskId);
const manifest = await manifestWithDirectEditableParameters(taskId, await readManifest(taskId));
const safeId = safeTaskId(taskId);
const artifactVersion = String(manifest?.studio?.latestVersion || "").trim();
return {
@@ -178,9 +178,6 @@ const DEFAULT_DAMPING_FACTOR = 0.14;
const DEFAULT_ZOOM_SPEED = 4.5;
const COARSE_POINTER_ZOOM_SPEED = 1.6;
const EXPLODED_VIEW_ANIMATION_DURATION_MS = 1000;
const ACCELERATED_WHEEL_ZOOM_SPEED = 10;
const TRACKPAD_PINCH_ZOOM_SPEED = 14;
const COARSE_POINTER_PINCH_ZOOM_SPEED = 2.4;
const KEYBOARD_ORBIT_NUDGE_RAD = Math.PI / 32;
const KEYBOARD_ORBIT_SPEED_RAD_PER_SEC = Math.PI * 0.42;
const KEYBOARD_POLAR_EPSILON = 0.02;
@@ -944,10 +941,6 @@ function updateStageEffects(runtime, viewerTheme, themeSettings, radius, floorZ
}
}
function isTrackpadLikeWheelEvent(event) {
return event.ctrlKey || (event.deltaMode === 0 && Math.abs(event.deltaY) < 20);
}
function normalizeViewportFrameInsets(value = {}) {
const normalizeInset = (inset) => {
const numericInset = Number(inset);
@@ -3362,7 +3355,6 @@ const CadViewer = forwardRef(function CadViewer({
getActiveViewPlaneFaceId,
cancelCameraTransition,
clearKeyboardOrbitState,
isTrackpadLikeWheelEvent,
getKeyboardOrbitCommand,
getKeyboardOrbitAxes,
applyOrbitDelta,
@@ -3385,9 +3377,6 @@ const CadViewer = forwardRef(function CadViewer({
INTERACTION_PIXEL_RATIO_CAP,
IDLE_PIXEL_RATIO_CAP,
INTERACTION_IDLE_DELAY_MS,
TRACKPAD_PINCH_ZOOM_SPEED,
COARSE_POINTER_PINCH_ZOOM_SPEED,
ACCELERATED_WHEEL_ZOOM_SPEED,
KEYBOARD_ORBIT_NUDGE_RAD,
defaultGridRadius,
sceneScaleMode: normalizedSceneScaleMode,
@@ -10,7 +10,10 @@ import {
import {
resolveInteractionPixelRatioCap
} from "cadjs/lib/viewer/renderQuality";
import { updateOrbitControls } from "../orbitControls.js";
import {
updateOrbitControls,
wheelZoomDistanceMultiplier
} from "../orbitControls.js";
function createWebGlRenderer(THREE) {
return createCadWebGlRenderer(THREE, {
@@ -36,7 +39,6 @@ export function useViewerRuntime({
getActiveViewPlaneFaceId,
cancelCameraTransition,
clearKeyboardOrbitState,
isTrackpadLikeWheelEvent,
getKeyboardOrbitCommand,
getKeyboardOrbitAxes,
applyOrbitDelta,
@@ -59,9 +61,6 @@ export function useViewerRuntime({
INTERACTION_PIXEL_RATIO_CAP,
IDLE_PIXEL_RATIO_CAP,
INTERACTION_IDLE_DELAY_MS,
TRACKPAD_PINCH_ZOOM_SPEED,
COARSE_POINTER_PINCH_ZOOM_SPEED,
ACCELERATED_WHEEL_ZOOM_SPEED,
KEYBOARD_ORBIT_NUDGE_RAD,
defaultGridRadius,
sceneScaleMode,
@@ -123,15 +122,7 @@ export function useViewerRuntime({
? window.matchMedia("(pointer: coarse)")
: null;
const prefersCoarsePointer = coarsePointerQuery?.matches ?? false;
const getOrbitControlsPixelRatioBucket = () => Math.max((window.devicePixelRatio || 1) | 0, 1);
const initialWheelPixelRatioBucket = getOrbitControlsPixelRatioBucket();
const getDefaultZoomSpeed = () => (prefersCoarsePointer ? COARSE_POINTER_ZOOM_SPEED : DEFAULT_ZOOM_SPEED);
const getPinchZoomSpeed = () => (prefersCoarsePointer ? COARSE_POINTER_PINCH_ZOOM_SPEED : TRACKPAD_PINCH_ZOOM_SPEED);
const getWheelZoomSpeed = (baseZoomSpeed) => {
// OrbitControls divides wheel deltas by a floored devicePixelRatio internally.
// Browser zoom changes that bucket, so scale our speed back to the initial bucket.
return baseZoomSpeed * (getOrbitControlsPixelRatioBucket() / initialWheelPixelRatioBucket);
};
const width = container.clientWidth || 800;
const height = container.clientHeight || 640;
@@ -173,7 +164,12 @@ export function useViewerRuntime({
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.setPixelRatio(getPixelRatioCap(IDLE_PIXEL_RATIO_CAP));
renderer.setSize(width, height);
// Keep the canvas coupled to its flex container. ResizeObserver updates the
// drawing buffer while CSS keeps the visible canvas at the current layout size.
renderer.setSize(width, height, false);
renderer.domElement.style.display = "block";
renderer.domElement.style.width = "100%";
renderer.domElement.style.height = "100%";
container.innerHTML = "";
container.appendChild(renderer.domElement);
@@ -428,11 +424,24 @@ export function useViewerRuntime({
}, INTERACTION_IDLE_DELAY_MS);
};
const onResize = () => {
let resizeRafId = 0;
let lastResizeWidth = width;
let lastResizeHeight = height;
const resizeAndRenderFrame = () => {
resizeRafId = 0;
const w = container.clientWidth || 800;
const h = container.clientHeight || 640;
if (w < 2 || h < 2) {
return;
}
if (Math.abs(w - lastResizeWidth) < 1 && Math.abs(h - lastResizeHeight) < 1) {
requestRender();
return;
}
lastResizeWidth = w;
lastResizeHeight = h;
applyRenderQuality(interactionState.pixelRatioCap);
renderer.setSize(w, h);
renderer.setSize(w, h, false);
syncCameraViewport(perspectiveCamera, w, h);
syncCameraViewport(orthographicCamera, w, h);
applyCameraFrameInsets?.(runtimeRef.current, frameInsetsRef?.current, { updateProjection: false });
@@ -440,8 +449,15 @@ export function useViewerRuntime({
syncDrawingCanvasSize(runtimeRef.current);
renderDrawingOverlay();
runtimeRef.current?.onViewportResize?.();
renderer.render(scene, runtimeRef.current?.camera || camera);
requestRender();
};
const onResize = () => {
if (resizeRafId) {
return;
}
resizeRafId = window.requestAnimationFrame(resizeAndRenderFrame);
};
window.addEventListener("resize", onResize);
const resizeObserver = typeof ResizeObserver === "function"
? new ResizeObserver(() => {
@@ -478,16 +494,67 @@ export function useViewerRuntime({
controlsStartDistance = null;
scheduleIdleQuality();
};
const applyWheelZoom = (event) => {
const activeRuntime = runtimeRef.current;
const activeCamera = activeRuntime?.camera;
const activeControls = activeRuntime?.controls;
if (!activeRuntime?.THREE || !activeCamera || !activeControls?.target) {
return false;
}
const distanceMultiplier = wheelZoomDistanceMultiplier(event);
if (!Number.isFinite(distanceMultiplier) || distanceMultiplier <= 0 || distanceMultiplier === 1) {
return false;
}
if (activeCamera.isOrthographicCamera) {
const minZoom = Number.isFinite(Number(activeControls.minZoom)) && Number(activeControls.minZoom) > 0
? Number(activeControls.minZoom)
: 0;
const maxZoom = Number.isFinite(Number(activeControls.maxZoom)) && Number(activeControls.maxZoom) > 0
? Number(activeControls.maxZoom)
: Number.POSITIVE_INFINITY;
activeCamera.zoom = Math.max(minZoom, Math.min(maxZoom, activeCamera.zoom / distanceMultiplier));
activeCamera.updateProjectionMatrix?.();
} else {
const offset = activeCamera.position.clone().sub(activeControls.target);
const currentDistance = offset.length();
if (!Number.isFinite(currentDistance) || currentDistance <= 1e-6) {
return false;
}
const minDistance = Number.isFinite(Number(activeControls.minDistance))
? Number(activeControls.minDistance)
: 0.01;
const maxDistance = Number.isFinite(Number(activeControls.maxDistance)) && Number(activeControls.maxDistance) > 0
? Number(activeControls.maxDistance)
: Number.POSITIVE_INFINITY;
const nextDistance = Math.max(minDistance, Math.min(maxDistance, currentDistance * distanceMultiplier));
activeCamera.position.copy(
activeControls.target.clone().add(offset.normalize().multiplyScalar(nextDistance))
);
activeCamera.zoom = 1;
activeCamera.updateProjectionMatrix?.();
}
activeCamera.lookAt(activeControls.target);
activeControls.update?.();
applyCameraFrameInsets?.(activeRuntime, frameInsetsRef?.current, { updateProjection: false });
emitPerspectiveChange(activeRuntime);
requestRender();
return true;
};
const handleWheel = (event) => {
event.preventDefault();
event.stopImmediatePropagation?.();
runtimeRef.current?.onManualCameraInteraction?.("wheel");
cancelCameraTransition(runtimeRef.current);
controls.enableDamping = false;
controls.zoomSpeed = getWheelZoomSpeed(isTrackpadLikeWheelEvent(event)
? getPinchZoomSpeed()
: ACCELERATED_WHEEL_ZOOM_SPEED);
beginInteraction();
if (applyWheelZoom(event)) {
scheduleIdleQuality();
}
};
const wheelListenerOptions = { passive: true, capture: true };
const wheelListenerOptions = { passive: false, capture: true };
controls.addEventListener("start", handleControlsStart);
controls.addEventListener("change", handleControlsChange);
@@ -661,6 +728,9 @@ export function useViewerRuntime({
window.clearTimeout(runtime.interactionState.renderFallbackTimerId);
}
cancelCameraTransition(runtime, { scheduleIdle: false });
if (resizeRafId) {
window.cancelAnimationFrame(resizeRafId);
}
window.cancelAnimationFrame(runtime.rafId);
window.removeEventListener("resize", runtime.onResize);
runtime.resizeObserver?.disconnect();
@@ -1,4 +1,9 @@
const MAX_ORBIT_DELTA_SECONDS = 1;
const WHEEL_PIXEL_STEP = 100;
const WHEEL_LINE_STEP = 3;
const WHEEL_PAGE_STEP = 9;
const WHEEL_ZOOM_STEP_SCALE = 0.08;
const MAX_WHEEL_ZOOM_STEPS = 3;
export const PREVIEW_ORBIT_SECONDS_PER_TURN = 60;
export const PREVIEW_AUTO_ROTATE_SPEED = 60 / PREVIEW_ORBIT_SECONDS_PER_TURN;
@@ -35,3 +40,25 @@ export function updateOrbitControls(controls, timestamp, state) {
}
return deltaSeconds === null ? controls.update() : controls.update(deltaSeconds);
}
export function normalizedWheelZoomSteps(event) {
const deltaY = Number(event?.deltaY);
if (!Number.isFinite(deltaY) || deltaY === 0) {
return 0;
}
let divisor = WHEEL_PIXEL_STEP;
if (event?.deltaMode === 1) {
divisor = WHEEL_LINE_STEP;
} else if (event?.deltaMode === 2) {
divisor = WHEEL_PAGE_STEP;
}
const steps = deltaY / divisor;
return Math.max(-MAX_WHEEL_ZOOM_STEPS, Math.min(MAX_WHEEL_ZOOM_STEPS, steps));
}
export function wheelZoomDistanceMultiplier(event) {
const steps = normalizedWheelZoomSteps(event);
return steps === 0 ? 1 : Math.exp(steps * WHEEL_ZOOM_STEP_SCALE);
}
@@ -2,10 +2,12 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
normalizedWheelZoomSteps,
orbitControlsDeltaSeconds,
PREVIEW_AUTO_ROTATE_SPEED,
PREVIEW_ORBIT_SECONDS_PER_TURN,
updateOrbitControls
updateOrbitControls,
wheelZoomDistanceMultiplier
} from "./orbitControls.js";
test("preview auto-rotate speed uses the configured full-turn duration", () => {
@@ -56,3 +58,19 @@ test("updateOrbitControls resets timing when auto-rotate is inactive", () => {
assert.deepEqual(updateArgs, [[]]);
assert.equal(state.orbitControlsLastTimestamp, 0);
});
test("normalizedWheelZoomSteps normalizes Chrome pixel wheel deltas", () => {
assert.equal(normalizedWheelZoomSteps({ deltaY: 100, deltaMode: 0 }), 1);
assert.equal(normalizedWheelZoomSteps({ deltaY: -100, deltaMode: 0 }), -1);
});
test("normalizedWheelZoomSteps caps very large wheel deltas", () => {
assert.equal(normalizedWheelZoomSteps({ deltaY: 1000, deltaMode: 0 }), 3);
assert.equal(normalizedWheelZoomSteps({ deltaY: -1000, deltaMode: 0 }), -3);
});
test("wheelZoomDistanceMultiplier keeps one wheel event away from zoom limits", () => {
assert.ok(wheelZoomDistanceMultiplier({ deltaY: 100, deltaMode: 0 }) < 1.1);
assert.ok(wheelZoomDistanceMultiplier({ deltaY: -100, deltaMode: 0 }) > 0.9);
assert.ok(wheelZoomDistanceMultiplier({ deltaY: 1000, deltaMode: 0 }) < 1.3);
});
@@ -0,0 +1,298 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://cad-agent.local/contracts/solidworks-feature-manager.schema.json",
"title": "SolidWorks FeatureManager style feature_tree.json",
"type": "object",
"required": [
"schema_version",
"tree_kind",
"model",
"source",
"feature_manager",
"rebuild_contract",
"validation"
],
"properties": {
"schema_version": {
"const": "1.0"
},
"tree_kind": {
"const": "solidworks_feature_manager"
},
"model": {
"type": "object",
"required": ["model_id", "units", "document_type"],
"properties": {
"model_id": {},
"family": {},
"units": {
"type": "string"
},
"document_type": {
"const": "part"
}
},
"additionalProperties": true
},
"source": {
"type": "object",
"required": [
"authority",
"reconstruction_mode",
"history_kind",
"claims_original_solidworks_history"
],
"properties": {
"authority": {
"const": "designir-3.0"
},
"designir_path": {},
"step_path": {},
"parameters_path": {},
"reconstruction_mode": {},
"history_kind": {
"enum": [
"authored_semantic",
"hybrid_semantic_surface",
"inferred_from_step",
"unknown"
]
},
"backend": {},
"claims_original_solidworks_history": {
"type": "boolean"
}
},
"additionalProperties": true
},
"feature_manager": {
"type": "object",
"required": ["root_id", "standard_root", "nodes"],
"properties": {
"root_id": {
"type": "string"
},
"standard_root": {
"type": "object"
},
"nodes": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/node"
}
}
},
"additionalProperties": false
},
"rebuild_contract": {
"type": "object",
"required": ["kind", "entrypoint"],
"properties": {
"kind": {
"enum": ["feature_manager_native", "surfaceir_imported_feature"]
},
"entrypoint": {
"type": "string"
},
"backend": {},
"payload_node": {
"type": "string"
},
"payload_encoding": {
"const": "gzip+base64+json"
},
"payload_sha256": {
"type": "string"
}
},
"additionalProperties": true
},
"validation": {
"type": "object"
}
},
"$defs": {
"node": {
"type": "object",
"required": [
"id",
"order",
"solidworks_type",
"display_name",
"english_name",
"children",
"inputs",
"parameters",
"definition",
"dimensions",
"references",
"selection_sets",
"operation_spec",
"result",
"rebuild",
"provenance",
"confidence",
"rebuildable"
],
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"order": {
"type": "integer",
"minimum": 0
},
"solidworks_type": {
"enum": [
"Part",
"HistoryFolder",
"OriginProfileFeature",
"RefPlane",
"RefAxis",
"ProfileFeature",
"BossExtrude",
"CutExtrude",
"Revolve",
"RevolvedCut",
"HoleWizard",
"Fillet",
"Chamfer",
"LinearPattern",
"CircularPattern",
"MirrorPattern",
"ImportedFeature",
"UnsupportedFeature"
]
},
"display_name": {
"type": "string"
},
"english_name": {
"type": "string"
},
"children": {
"type": "array",
"items": {
"type": "string"
}
},
"inputs": {
"type": "object"
},
"parameters": {
"type": "array",
"items": {
"$ref": "#/$defs/parameter"
}
},
"definition": {
"type": "object"
},
"dimensions": {
"type": "array",
"items": {
"$ref": "#/$defs/dimension"
}
},
"references": {
"type": "object",
"properties": {
"parent": {},
"sketch": {},
"plane": {},
"parent_features": {
"type": "array",
"items": {"type": "string"}
},
"child_features": {
"type": "array",
"items": {"type": "string"}
}
},
"additionalProperties": true
},
"selection_sets": {
"type": "object",
"properties": {
"selected_faces": {"type": "array"},
"selected_edges": {"type": "array"},
"selected_contours": {"type": "array"}
},
"additionalProperties": true
},
"operation_spec": {
"type": "object"
},
"result": {
"type": "object"
},
"rebuild": {
"type": "object",
"properties": {
"suppressed": {"type": "boolean"},
"rollback_order": {"type": "integer"},
"status": {"type": "string"}
},
"additionalProperties": true
},
"provenance": {
"type": "object"
},
"confidence": {
"type": ["number", "null"]
},
"rebuildable": {
"type": "boolean"
}
},
"additionalProperties": false
},
"dimension": {
"type": "object",
"required": ["id", "name", "display_name"],
"properties": {
"id": {"type": "string"},
"name": {"type": "string"},
"display_name": {"type": "string"},
"value": {},
"default_value": {},
"unit": {},
"parameter_binding": {},
"driven": {"type": "boolean"}
},
"additionalProperties": true
},
"parameter": {
"type": "object",
"required": ["id", "name", "display_name", "binding_id"],
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"display_name": {
"type": "string"
},
"value": {},
"default_value": {},
"unit": {},
"binding_id": {
"type": "string"
},
"editable": {
"type": "boolean"
},
"edit_state": {},
"source_pointer": {
"type": "string"
}
},
"additionalProperties": true
}
},
"additionalProperties": false
}
+541 -8
View File
@@ -231,7 +231,13 @@ def _validate_compiler_payload(payload: dict[str, Any]) -> dict[str, Any]:
name
for name in editable
if not parameters[name].get("editable")
or _parameter_reference_count(features, name) == 0
or (
_parameter_reference_count(features, name) == 0
and _parameter_expression_reference_count(
features, payload.get("expressions", {}), name
)
== 0
)
)
if disconnected:
raise DesignIRError(
@@ -307,12 +313,15 @@ def migrate_designir_2_to_3(payload: dict[str, Any]) -> dict[str, Any]:
def compiler_payload(payload: dict[str, Any]) -> dict[str, Any]:
"""Return the stable flat representation consumed by both CAD backends."""
version = payload.get("schema_version")
if version is None and payload.get("designir_kind") == DESIGNIR_KIND:
version = SCHEMA_VERSION
if version == LEGACY_SCHEMA_VERSION:
return _validate_compiler_payload(copy.deepcopy(payload))
if version != SCHEMA_VERSION or payload.get("designir_kind") != DESIGNIR_KIND:
raise DesignIRError(
"DesignIR must be legacy 2.0 or independent_parametric_cad 3.0"
)
payload = _normalize_tool_authored_designir(payload)
mode = payload.get("reconstruction_mode")
if mode == "surface_parametric":
raise DesignIRError(
@@ -331,8 +340,16 @@ def compiler_payload(payload: dict[str, Any]) -> dict[str, Any]:
semantic = payload.get("semantic_layer")
if not isinstance(semantic, dict):
raise DesignIRError("semantic_layer must be an object")
semantic = _normalize_tool_authored_semantic_layer(payload, semantic)
edit_interface = payload.get("edit_interface", {})
validation = payload.get("validation_contract", {})
validation = copy.deepcopy(payload.get("validation_contract", {}))
if isinstance(validation, dict):
validation["perturbations"] = _normalize_validation_list(
validation.get("perturbations", [])
)
validation["invariants"] = _normalize_validation_list(
validation.get("invariants", []), parameter_key="id"
)
flat = {
"schema_version": LEGACY_SCHEMA_VERSION,
"designir_kind": LEGACY_DESIGNIR_KIND,
@@ -366,6 +383,462 @@ def compiler_payload(payload: dict[str, Any]) -> dict[str, Any]:
return _validate_compiler_payload(flat)
def _map_object_to_list(value: Any) -> Any:
if isinstance(value, dict):
return [
{"id": str(key), **item}
if isinstance(item, dict) and "id" not in item
else item
for key, item in value.items()
]
return value
def _map_named_list_to_object(value: Any) -> Any:
if not isinstance(value, list):
return value
mapped: dict[str, Any] = {}
for index, item in enumerate(value):
if not isinstance(item, dict):
continue
name = item.get("name") or item.get("id") or f"item_{index + 1}"
mapped[str(name)] = {
key: child
for key, child in item.items()
if key not in {"name", "id"}
}
return mapped if mapped else value
def _normalize_expression_object(value: Any) -> Any:
if not isinstance(value, list):
return value
mapped: dict[str, Any] = {}
for item in value:
if not isinstance(item, dict):
continue
name = item.get("name") or item.get("id")
expression = item.get("expression") or item.get("value")
if name and expression is not None:
mapped[str(name)] = expression
return mapped if mapped else value
def _normalize_semantic_parameter_names(value: Any) -> list[str]:
if not isinstance(value, list):
return []
names = []
for item in value:
if isinstance(item, str):
names.append(item)
elif isinstance(item, dict):
name = item.get("name") or item.get("id")
if isinstance(name, str):
names.append(name)
return names
def _semantic_parameter_metadata(value: Any) -> dict[str, dict[str, Any]]:
if not isinstance(value, list):
return {}
metadata: dict[str, dict[str, Any]] = {}
for item in value:
if not isinstance(item, dict):
continue
name = item.get("name") or item.get("id")
if isinstance(name, str):
metadata[name] = item
return metadata
def _normalize_coordinate_system(value: Any) -> Any:
if not isinstance(value, dict):
return value
axes = value.get("axes")
if isinstance(axes, dict):
normalized = dict(value)
normalized.setdefault("x_axis", axes.get("x"))
normalized.setdefault("y_axis", axes.get("y"))
normalized.setdefault("z_axis", axes.get("z"))
normalized.pop("axes", None)
return normalized
return value
def _parameter_ref(name: str) -> dict[str, str]:
return {"parameter": name}
def _value_or_parameter_ref(parameters: dict[str, Any], name: str, fallback: Any) -> Any:
return _parameter_ref(name) if name in parameters else fallback
def _expression_or_value_ref(expressions: dict[str, Any], name: str, fallback: Any) -> Any:
return {"expression": name} if name in expressions else fallback
def _circle_profile(sketches: dict[str, Any], sketch_id: Any) -> dict[str, Any] | None:
sketch = sketches.get(str(sketch_id))
if not isinstance(sketch, dict):
return None
profile = sketch.get("profile")
if isinstance(profile, dict) and profile.get("type") == "circle":
return profile
return None
def _center3(profile: dict[str, Any], z: Any = 0) -> list[Any]:
center = profile.get("center", [0, 0])
if not isinstance(center, list):
center = [0, 0]
return [
center[0] if len(center) > 0 else 0,
center[1] if len(center) > 1 else 0,
z,
]
def _diameter_from_profile(
parameters: dict[str, Any],
expressions: dict[str, Any],
profile: dict[str, Any],
*,
diameter_parameter: str,
radius_expression: str,
) -> Any:
if diameter_parameter in parameters:
return _parameter_ref(diameter_parameter)
if radius_expression in expressions:
return {"expression": radius_expression}
radius = profile.get("radius")
if isinstance(radius, (int, float)):
return float(radius) * 2.0
return radius
def _radius_from_profile(
parameters: dict[str, Any],
expressions: dict[str, Any],
profile: dict[str, Any],
*,
radius_parameter: str,
radius_expression: str,
) -> Any:
if radius_parameter in parameters:
return _parameter_ref(radius_parameter)
if radius_expression in expressions:
return {"expression": radius_expression}
return profile.get("radius")
def _distance_xy(center: list[Any]) -> float | None:
if len(center) < 2:
return None
try:
return math.sqrt(float(center[0]) ** 2 + float(center[1]) ** 2)
except (TypeError, ValueError):
return None
def _normalize_common_llm_feature_program(
semantic: dict[str, Any],
) -> list[dict[str, Any]] | None:
raw_features = semantic.get("features")
if not isinstance(raw_features, dict):
return None
sketches = semantic.get("sketches", {})
if not isinstance(sketches, dict):
return None
patterns = semantic.get("patterns", {})
if not isinstance(patterns, dict):
patterns = {}
parameters = semantic.get("parameters", {})
if not isinstance(parameters, dict):
parameters = {}
expressions = semantic.get("expressions", {})
if not isinstance(expressions, dict):
expressions = {}
generated_bodies: list[dict[str, Any]] = []
generated_cuts: list[dict[str, Any]] = []
generated_patterns: list[dict[str, Any]] = []
converted_pattern_sources: set[str] = set()
host_feature_id: str | None = None
for pattern_id, pattern in patterns.items():
if not isinstance(pattern, dict) or pattern.get("type") not in {
"circular",
"polar",
}:
continue
source_feature_id = str(pattern.get("feature") or "")
source_feature = raw_features.get(source_feature_id)
if not isinstance(source_feature, dict):
continue
profile = _circle_profile(sketches, source_feature.get("sketch"))
if profile is None:
continue
center = profile.get("center", [0, 0])
pitch = _value_or_parameter_ref(
parameters,
"bolt_circle_diameter",
None,
)
if pitch is None:
radius = _distance_xy(center if isinstance(center, list) else [])
pitch = radius * 2.0 if radius is not None else pattern.get("pitch_diameter")
generated_patterns.append(
{
"id": str(pattern_id),
"operation": "polar_hole_pattern",
"count": _value_or_parameter_ref(
parameters,
"bolt_count",
pattern.get("count", len(pattern.get("instances", [])) or 1),
),
"diameter": _diameter_from_profile(
parameters,
expressions,
profile,
diameter_parameter="bolt_hole_diameter",
radius_expression="bolt_hole_radius",
),
"pitch_diameter": pitch,
"axis": "primary_axis",
"host": host_feature_id or "flange_body",
}
)
converted_pattern_sources.add(source_feature_id)
for feature_id, feature in raw_features.items():
if not isinstance(feature, dict) or feature_id in converted_pattern_sources:
continue
profile = _circle_profile(sketches, feature.get("sketch"))
if profile is None:
continue
feature_type = str(feature.get("type") or "").lower()
operation = str(feature.get("operation") or "").lower()
if feature_type == "extrude" or operation == "new_body":
generated_feature = {
"id": str(feature_id),
"operation": "extrude_circle",
"radius": _radius_from_profile(
parameters,
expressions,
profile,
radius_parameter="flange_outer_radius",
radius_expression="flange_outer_radius",
),
"height": _value_or_parameter_ref(
parameters,
"flange_thickness",
feature.get("depth", feature.get("height")),
),
"axis": "primary_axis",
"center": _center3(profile),
}
generated_bodies.append(generated_feature)
host_feature_id = str(feature_id)
elif feature_type == "cut_extrude" or operation == "cut":
generated_cuts.append(
{
"id": str(feature_id),
"operation": "through_hole",
"diameter": _diameter_from_profile(
parameters,
expressions,
profile,
diameter_parameter="bore_diameter",
radius_expression="bore_radius",
),
"axis": "primary_axis",
"center": _center3(profile),
"host": host_feature_id or "flange_body",
}
)
generated = generated_bodies + generated_cuts
for pattern_feature in generated_patterns:
if (
pattern_feature.get("host") == "flange_body"
and host_feature_id is not None
):
pattern_feature["host"] = host_feature_id
generated.append(pattern_feature)
if not generated:
return None
return generated
def _normalize_validation_list(value: Any, *, parameter_key: str = "parameter") -> Any:
if isinstance(value, dict):
rows = [
(
{
parameter_key: str(key),
**item,
}
if isinstance(item, dict)
else {parameter_key: str(key), "description": item}
)
for key, item in value.items()
]
elif isinstance(value, list):
rows = [
copy.deepcopy(item)
if isinstance(item, dict)
else {"description": item}
for item in value
]
else:
return value
if parameter_key == "parameter":
for row in rows:
row.setdefault(
"expected_change",
row.get("acceptance") or row.get("description") or "geometry_changes",
)
return rows
def _normalize_feature_aliases(feature: dict[str, Any]) -> dict[str, Any]:
normalized = copy.deepcopy(feature)
operation = normalized.get("operation")
if operation == "polar_hole_pattern":
if "diameter" not in normalized and "hole_diameter" in normalized:
normalized["diameter"] = normalized.pop("hole_diameter")
reference = normalized.get("reference")
if (
"host" not in normalized
and isinstance(reference, dict)
and isinstance(reference.get("feature"), str)
):
normalized["host"] = reference["feature"]
elif operation == "through_hole":
reference = normalized.get("reference")
if (
"host" not in normalized
and isinstance(reference, dict)
and isinstance(reference.get("feature"), str)
):
normalized["host"] = reference["feature"]
if operation in SUPPORTED_OPERATIONS:
normalized.setdefault("axis", "primary_axis")
return normalized
def _normalize_feature_list_aliases(value: Any) -> Any:
features = _map_object_to_list(value)
if not isinstance(features, list):
return features
return [
_normalize_feature_aliases(feature)
if isinstance(feature, dict)
else feature
for feature in features
]
def _normalize_tool_authored_designir(payload: dict[str, Any]) -> dict[str, Any]:
normalized = copy.deepcopy(payload)
normalized.setdefault("schema_version", SCHEMA_VERSION)
if normalized.get("units") is None:
normalized["units"] = "mm"
semantic = normalized.get("semantic_layer")
if isinstance(semantic, dict):
normalized["semantic_layer"] = _normalize_tool_authored_semantic_layer(
normalized, semantic
)
edit_interface = normalized.get("edit_interface")
if isinstance(edit_interface, dict):
edit_interface["semantic_parameters"] = _normalize_semantic_parameter_names(
edit_interface.get("semantic_parameters", [])
)
validation = normalized.get("validation_contract")
if isinstance(validation, dict):
validation["perturbations"] = _normalize_validation_list(
validation.get("perturbations", [])
)
validation["invariants"] = _normalize_validation_list(
validation.get("invariants", []), parameter_key="id"
)
return normalized
def _normalize_tool_authored_semantic_layer(
payload: dict[str, Any], semantic: dict[str, Any]
) -> dict[str, Any]:
normalized = copy.deepcopy(semantic)
if normalized.get("reconstruction_status") in {
"independent",
"fully_parametric",
"fully_semantic_parametric",
}:
normalized["reconstruction_status"] = "ready"
normalized["coordinate_system"] = _normalize_coordinate_system(
normalized.get("coordinate_system")
)
coordinate = normalized.get("coordinate_system")
if isinstance(coordinate, dict):
coordinate.setdefault("origin", [0, 0, 0])
coordinate.setdefault("x_axis", [1, 0, 0])
coordinate.setdefault("y_axis", [0, 1, 0])
coordinate.setdefault("z_axis", [0, 0, 1])
normalized["parameters"] = _map_named_list_to_object(
normalized.get("parameters", {})
)
normalized["expressions"] = _normalize_expression_object(
normalized.get("expressions", {})
)
normalized["datums"] = _map_named_list_to_object(
normalized.get("datums", {})
)
datums = normalized.setdefault("datums", {})
if isinstance(datums, dict):
datums.setdefault("primary_axis", {"kind": "axis", "axis": "z"})
edit_interface = payload.get("edit_interface", {})
editable_names = set(
_normalize_semantic_parameter_names(
edit_interface.get("semantic_parameters", [])
)
if isinstance(edit_interface, dict)
else []
)
edit_metadata = (
_semantic_parameter_metadata(edit_interface.get("semantic_parameters", []))
if isinstance(edit_interface, dict)
else {}
)
parameters = normalized.get("parameters")
if isinstance(parameters, dict):
for name, parameter in parameters.items():
if isinstance(parameter, dict):
parameter.setdefault("editable", name in editable_names)
metadata = edit_metadata.get(name, {})
if metadata.get("label") and not parameter.get("display_name"):
parameter["display_name"] = metadata["label"]
if metadata.get("range") and not parameter.get("range"):
parameter["range"] = metadata["range"]
converted_features = _normalize_common_llm_feature_program(normalized)
if converted_features is not None:
normalized["features"] = converted_features
else:
normalized["features"] = _normalize_feature_list_aliases(
normalized.get("features", [])
)
normalized["sketches"] = _map_object_to_list(normalized.get("sketches", []))
normalized["patterns"] = _map_object_to_list(normalized.get("patterns", []))
normalized["attachments"] = _map_object_to_list(normalized.get("attachments", []))
normalized["constraints"] = _normalize_validation_list(
normalized.get("constraints", []), parameter_key="id"
)
normalized["construction_stages"] = _map_object_to_list(
normalized.get("construction_stages", [])
)
return normalized
def validate_designir(payload: dict[str, Any]) -> dict[str, Any]:
"""Validate either supported version and return its compiler representation."""
return compiler_payload(payload)
@@ -423,7 +896,7 @@ def resolve_value(value: Any, values: dict[str, float], label: str) -> float:
if isinstance(value, dict) and set(value) == {"expression"}:
name = str(value["expression"])
if name not in values:
raise DesignIRError(f"{label} references unknown expression {name}")
return _safe_expression(name, values)
return values[name]
raise DesignIRError(f"{label} must be a number, parameter, or expression")
@@ -578,10 +1051,9 @@ def build_shape(payload: dict[str, Any]) -> Part:
if shape is None:
raise DesignIRError("DesignIR produced no geometry")
result = Part(shape.wrapped)
if not list(result.solids()) or result.volume <= 0:
if not list(shape.solids()) or shape.volume <= 0:
raise DesignIRError("DesignIR did not produce a valid solid")
return result
return shape
def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
@@ -920,9 +1392,26 @@ def _distance(first: list[float], second: list[float]) -> float:
return math.sqrt(sum((a - b) ** 2 for a, b in zip(first, second)))
def _boolean_result_volume(value: Any) -> float:
volume = getattr(value, "volume", None)
if isinstance(volume, (int, float)):
return float(volume)
try:
return sum(
float(getattr(item, "volume", 0.0))
for item in value
)
except TypeError as exc:
raise DesignIRError(
f"Boolean result has no measurable volume: {type(value).__name__}"
) from exc
def _symmetric_difference_volume(first: Part, second: Part) -> float:
try:
return float((first - second).volume + (second - first).volume)
return _boolean_result_volume(first - second) + _boolean_result_volume(
second - first
)
except Exception as exc: # OpenCascade failures become an explicit metric state.
raise DesignIRError(f"Symmetric-difference boolean failed: {exc}") from exc
@@ -939,6 +1428,50 @@ def _parameter_reference_count(value: Any, parameter: str) -> int:
return 0
def _expression_reference_names(value: Any) -> set[str]:
if isinstance(value, dict):
direct = {str(value["expression"])} if isinstance(value.get("expression"), str) else set()
return direct.union(
*(_expression_reference_names(child) for child in value.values())
)
if isinstance(value, list):
names: set[str] = set()
for child in value:
names.update(_expression_reference_names(child))
return names
return set()
def _expression_reference_values(value: Any) -> set[str]:
if isinstance(value, dict):
direct = {str(value["expression"])} if isinstance(value.get("expression"), str) else set()
values = set(direct)
for child in value.values():
values.update(_expression_reference_values(child))
return values
if isinstance(value, list):
values: set[str] = set()
for child in value:
values.update(_expression_reference_values(child))
return values
return set()
def _parameter_expression_reference_count(
value: Any, expressions: Any, parameter: str
) -> int:
if not isinstance(expressions, dict):
expressions = {}
count = 0
for expression_value in _expression_reference_values(value):
expression = expressions.get(expression_value, expression_value)
if isinstance(expression, str):
count += len(
re.findall(rf"\b{re.escape(parameter)}\b", expression)
)
return count
def acceptance_report(
teacher_path: Path,
rebuilt_path: Path,
@@ -1186,7 +1719,7 @@ def main(argv: list[str] | None = None) -> int:
migrated = (
migrate_designir_2_to_3(original)
if original.get("schema_version") == LEGACY_SCHEMA_VERSION
else original
else _normalize_tool_authored_designir(original)
)
compiler_payload(migrated)
write_json(args.output, migrated)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,340 @@
#!/usr/bin/env python3
"""Normalize backend-native CAD generation artifacts into DesignIR 3.0.
This script intentionally treats backend-native source or model graph files as
the editable authority. DesignIR records the contract, parameters, validation
evidence, and a non-authoritative SurfaceIR snapshot compiled from the generated
STEP.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
from pathlib import Path
from typing import Any
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from designir_codec import write_designir
import surfaceir_pipeline
def _safe_id(value: str, fallback: str) -> str:
token = re.sub(r"[^a-zA-Z0-9_.-]+", "_", value.strip()).strip("._-")
return token or fallback
def _read_json(path: Path | None) -> dict[str, Any]:
if path is None:
return {}
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
return {}
if not isinstance(payload, dict):
return {}
return payload
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _parameter_record(
item: dict[str, Any],
*,
backend: str,
source_path: str | None,
graph_path: str | None,
) -> tuple[str, dict[str, Any]] | None:
name = str(item.get("name") or item.get("id") or "").strip()
if not name:
return None
try:
value = float(item.get("value"))
except (TypeError, ValueError):
return None
binding_kind = str(item.get("binding_kind") or "python_constant")
binding_source = str(item.get("source_path") or source_path or "")
if binding_kind == "model_graph_parameter" and graph_path:
binding_source = str(item.get("source_path") or graph_path)
minimum = item.get("min", item.get("minimum"))
maximum = item.get("max", item.get("maximum"))
record: dict[str, Any] = {
"value": value,
"unit": str(item.get("unit") or "mm"),
"editable": bool(item.get("editable", True)),
"semantic_role": str(item.get("semantic_role") or name),
"display_name": item.get("display_name") or item.get("label") or name,
"description": item.get("description") or "",
"edit_state": str(item.get("edit_state") or "backend_bound_parameter"),
"backend_binding": {
"backend": backend,
"source_path": binding_source,
"binding_kind": binding_kind,
"parameter_path": str(item.get("parameter_path") or f"PARAMETERS.{name}"),
"regenerate_adapter": str(item.get("regenerate_adapter") or backend),
},
}
if minimum is not None:
record["min"] = minimum
if maximum is not None:
record["max"] = maximum
for key in ("step", "precision", "group", "validated_range"):
if key in item:
record[key] = item[key]
return name, record
def normalize_backend_result(
*,
request: str,
backend: str,
source_of_truth: str,
step_path: Path,
output_path: Path,
model_id: str,
family: str,
native_source_path: str | None,
backend_graph_path: str | None,
metadata_path: Path | None,
validation_paths: list[str],
) -> dict[str, Any]:
metadata = _read_json(metadata_path)
parameters: dict[str, Any] = {}
for item in metadata.get("parameters", []):
if isinstance(item, dict):
parsed = _parameter_record(
item,
backend=backend,
source_path=native_source_path,
graph_path=backend_graph_path,
)
if parsed:
name, record = parsed
parameters[name] = record
features = []
for index, item in enumerate(metadata.get("features", []), start=1):
if not isinstance(item, dict):
continue
feature_id = str(item.get("id") or f"backend_feature_{index}")
features.append(
{
**item,
"id": feature_id,
"operation": str(item.get("operation") or "backend_native_feature"),
"backend": backend,
"original_history_recovered": False,
"source_of_truth": source_of_truth,
}
)
if not features:
features.append(
{
"id": "backend_native_model",
"operation": "backend_native_feature",
"backend": backend,
"source_of_truth": source_of_truth,
"original_history_recovered": False,
"description": "Model was authored by the selected backend's native generation flow.",
}
)
surface_payload: dict[str, Any] | None = None
surface_snapshot: dict[str, Any] = {
"status": "materialized",
"source": "generated_step",
}
try:
surface_payload = surfaceir_pipeline.extract_surfaceir(step_path)
except Exception as exc:
# SurfaceIR is a non-authoritative snapshot for backend-native models.
# Unsupported STEP surface vocabulary must not discard a valid STEP,
# native source, or replayable model graph.
surface_snapshot = {
"status": "unavailable",
"source": "generated_step",
"error": str(exc),
}
has_surface_snapshot = surface_payload is not None
reconstruction_mode = (
"hybrid_semantic_surface_parametric"
if has_surface_snapshot
else "fully_semantic_parametric"
)
designir: dict[str, Any] = {
"schema_version": "3.0",
"designir_kind": "independent_parametric_cad",
"model_id": model_id,
"family": family,
"units": str(metadata.get("units") or "mm"),
"document_status": (
surface_payload.get("document_status", "geometry_present")
if surface_payload
else "geometry_present"
),
"reconstruction_mode": reconstruction_mode,
"authoring_mode": "semantic_feature_program",
"backend_hint": backend,
"source_of_truth": {
"kind": source_of_truth,
"backend": backend,
"native_source_path": native_source_path,
"backend_graph_path": backend_graph_path,
"primary_step_path": str(step_path),
"primary_step_sha256": _sha256(step_path),
},
"semantic_layer": {
"reconstruction_status": "ready" if has_surface_snapshot else "partial",
"coordinate_system": {
"origin": [0, 0, 0],
"x_axis": [1, 0, 0],
"y_axis": [0, 1, 0],
"z_axis": [0, 0, 1],
},
"datums": {
"primary_axis": {"kind": "axis", "axis": "z"},
},
"parameters": parameters,
"expressions": {},
"sketches": [],
"constraints": [],
"features": features,
"patterns": [],
"attachments": [
{
"id": "backend_native_authority",
"kind": "source_of_truth",
"backend": backend,
"source_of_truth": source_of_truth,
"native_source_path": native_source_path,
"backend_graph_path": backend_graph_path,
}
],
"construction_stages": [
{
"id": "backend_native_generation",
"kind": "backend_generation",
"backend": backend,
"validation_artifacts": validation_paths,
},
{
"id": "surfaceir_snapshot",
"kind": "non_authoritative_surface_snapshot",
**surface_snapshot,
},
],
},
"edit_interface": {
"semantic_parameters": [
name
for name, record in parameters.items()
if record.get("editable") is True and record.get("backend_binding")
],
"surface_parameter_groups": (
surface_payload.get("edit_interface", {}).get(
"surface_parameter_groups", []
)
if surface_payload
else []
),
"modification_levels": ["backend_native_parameter", "semantic_feature"],
"preserved_interfaces": [],
},
"validation_contract": {
"source_independence": True,
"geometry_checks": (
["backend_native_generation", "step_surfaceir_materialization"]
if has_surface_snapshot
else ["backend_native_generation"]
),
"edit_checks": ["backend_bound_parameter_regeneration"],
"invariants": [],
"perturbations": [],
"thresholds": {},
"backend_validation_artifacts": validation_paths,
"surface_snapshot": surface_snapshot,
},
}
if surface_payload:
designir["surface_layer"] = surface_payload["surface_layer"]
designir["reconstruction_strategy"] = surface_payload.get(
"reconstruction_strategy",
{"boundary_strategy": "exact_3d", "selection_status": "default"},
)
designir["compiled_surface_provenance"] = {
"source": "generated_step",
"teacher_geometry_used": False,
"semantic_layer_authoritative": True,
}
write_designir(output_path, designir)
return {
"valid": True,
"output": str(output_path.expanduser().resolve()),
"schema_version": "3.0",
"reconstruction_mode": designir["reconstruction_mode"],
"backend": backend,
"source_of_truth": source_of_truth,
"parameter_count": len(parameters),
"feature_count": len(features),
"surface_snapshot": surface_snapshot,
}
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--request", required=True)
parser.add_argument("--backend", choices=("build123d", "simplecadapi", "surfaceir"), required=True)
parser.add_argument(
"--source-of-truth",
choices=("native_source", "model_graph", "surfaceir_designir"),
required=True,
)
parser.add_argument("--step", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--model-id", required=True)
parser.add_argument("--family", default="backend_native_model")
parser.add_argument("--native-source-path")
parser.add_argument("--backend-graph-path")
parser.add_argument("--metadata", type=Path)
parser.add_argument("--validation-artifact", action="append", default=[])
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
result = normalize_backend_result(
request=args.request,
backend=args.backend,
source_of_truth=args.source_of_truth,
step_path=args.step.expanduser().resolve(),
output_path=args.output.expanduser().resolve(),
model_id=_safe_id(args.model_id, "backend_native_model"),
family=args.family,
native_source_path=args.native_source_path,
backend_graph_path=args.backend_graph_path,
metadata_path=args.metadata,
validation_paths=args.validation_artifact,
)
except Exception as exc:
print(json.dumps({"valid": False, "error": str(exc)}, ensure_ascii=False, indent=2))
return 2
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -15,6 +15,14 @@ MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader
SPEC.loader.exec_module(MODULE)
FEATURE_TREE_SCRIPT = ROOT / "scripts" / "feature_tree.py"
FEATURE_TREE_SPEC = importlib.util.spec_from_file_location(
"feature_tree", FEATURE_TREE_SCRIPT
)
FEATURE_TREE = importlib.util.module_from_spec(FEATURE_TREE_SPEC)
assert FEATURE_TREE_SPEC.loader
FEATURE_TREE_SPEC.loader.exec_module(FEATURE_TREE)
class DesignIRPipelineTests(unittest.TestCase):
def payload(self) -> dict:
@@ -66,6 +74,257 @@ class DesignIRPipelineTests(unittest.TestCase):
MODULE.compile_designir(hybrid, rebuilt)
self.assertTrue(rebuilt.is_file())
def test_feature_tree_is_derived_from_semantic_designir(self) -> None:
migrated = MODULE.migrate_designir_2_to_3(self.payload())
tree = FEATURE_TREE.generate_feature_tree(
migrated,
designir_path="flange.designir.json",
step_path="flange.step",
backend="build123d",
compiled=True,
validated=True,
)
self.assertEqual("solidworks_feature_manager", tree["tree_kind"])
self.assertEqual("1.0", tree["schema_version"])
serialized = json.dumps(tree)
self.assertNotIn('"designir_3"', serialized)
self.assertNotIn('"source_designir_feature"', serialized)
self.assertNotIn('"manufacturing"', serialized)
self.assertEqual(
"authored_semantic",
tree["source"]["history_kind"],
)
self.assertEqual(
"valid",
tree["validation"]["structure"]["status"],
)
nodes = {
node["id"]: node
for node in tree["feature_manager"]["nodes"]
}
self.assertIn("sw:front_plane", nodes)
self.assertIn("sketch:base_flange", nodes)
self.assertIn("feature:base_flange", nodes)
self.assertIn("feature:central_bore", nodes)
self.assertEqual(
"BossExtrude",
nodes["feature:base_flange"]["solidworks_type"],
)
self.assertEqual(
"BossExtrude",
nodes["feature:base_flange"]["definition"]["feature_type"],
)
self.assertTrue(nodes["feature:base_flange"]["dimensions"])
self.assertIn(
"feature:central_bore",
nodes["feature:base_flange"]["references"]["child_features"],
)
self.assertEqual(
"CutExtrude",
nodes["feature:central_bore"]["solidworks_type"],
)
self.assertEqual(
"ThroughAll",
nodes["feature:central_bore"]["definition"]["end_condition"],
)
self.assertEqual(
[],
nodes["feature:central_bore"]["selection_sets"]["selected_edges"],
)
self.assertFalse(nodes["feature:central_bore"]["rebuild"]["suppressed"])
self.assertIn(
"凸台-拉伸",
nodes["feature:base_flange"]["display_name"],
)
self.assertEqual(
["bore_diameter"],
[
parameter["binding_id"]
for parameter in nodes["feature:central_bore"]["parameters"]
],
)
replay_designir = FEATURE_TREE.designir_from_feature_tree(tree)
self.assertEqual("3.0", replay_designir["schema_version"])
self.assertEqual(
"feature_manager_tree_replay",
replay_designir["authoring_mode"],
)
with tempfile.TemporaryDirectory() as temporary:
original_step = Path(temporary) / "original.step"
replay_step = Path(temporary) / "replayed.step"
original = MODULE.compile_designir(migrated, original_step)
result = FEATURE_TREE.compile_feature_tree(tree, replay_step)
self.assertTrue(replay_step.is_file())
self.assertEqual("feature_manager_native", result["replay_kind"])
for key in ("solid_count", "face_count", "edge_count"):
self.assertEqual(original["facts"][key], result["facts"][key])
self.assertEqual(original["facts"]["size_mm"], result["facts"]["size_mm"])
self.assertAlmostEqual(
original["facts"]["volume_mm3"],
result["facts"]["volume_mm3"],
places=7,
)
def test_llm_flange_variant_normalizes_and_compiles(self) -> None:
payload = {
"designir_kind": "independent_parametric_cad",
"reconstruction_mode": "fully_semantic_parametric",
"authoring_mode": "semantic_feature_program",
"semantic_layer": {
"reconstruction_status": "ready",
"coordinate_system": {
"origin": [0, 0, 0],
"x_axis": [1, 0, 0],
"y_axis": [0, 1, 0],
"z_axis": [0, 0, 1],
},
"datums": [
{
"id": "datum_xy",
"plane": {"normal": [0, 0, 1], "offset": 0},
}
],
"parameters": [
{
"name": "flange_outer_diameter",
"value": 120,
"unit": "mm",
"editable": True,
},
{
"name": "flange_thickness",
"value": 16,
"unit": "mm",
"editable": True,
},
{
"name": "bore_diameter",
"value": 40,
"unit": "mm",
"editable": True,
},
{
"name": "bolt_circle_diameter",
"value": 90,
"unit": "mm",
"editable": True,
},
{
"name": "bolt_hole_diameter",
"value": 11,
"unit": "mm",
"editable": True,
},
{
"name": "bolt_count",
"value": 4,
"unit": "count",
"editable": True,
},
],
"expressions": [
{
"name": "flange_outer_radius",
"expression": "flange_outer_diameter / 2",
}
],
"sketches": [],
"constraints": [],
"features": [
{
"id": "feat_flange_body",
"operation": "extrude_circle",
"radius": {
"expression": "flange_outer_diameter / 2"
},
"height": {"parameter": "flange_thickness"},
},
{
"id": "feat_central_bore",
"operation": "through_hole",
"diameter": {"parameter": "bore_diameter"},
"reference": {"feature": "feat_flange_body"},
},
{
"id": "feat_bolt_holes",
"operation": "polar_hole_pattern",
"count": {"parameter": "bolt_count"},
"pitch_diameter": {
"parameter": "bolt_circle_diameter"
},
"hole_diameter": {
"parameter": "bolt_hole_diameter"
},
"reference": {"feature": "feat_flange_body"},
},
],
"patterns": [],
"attachments": [],
"construction_stages": [],
},
"edit_interface": {
"semantic_parameters": [
{"name": "flange_outer_diameter", "label": "法兰外径"},
{"name": "flange_thickness", "label": "法兰厚度"},
{"name": "bore_diameter", "label": "中心孔径"},
{"name": "bolt_circle_diameter", "label": "分布圆直径"},
{"name": "bolt_hole_diameter", "label": "螺栓孔径"},
{"name": "bolt_count", "label": "螺栓孔数量"},
],
"surface_parameter_groups": [],
"modification_levels": ["semantic_feature"],
"preserved_interfaces": [],
},
"validation_contract": {
"source_independence": True,
"geometry_checks": [],
"edit_checks": [],
"invariants": [],
"perturbations": [
{"parameter": "flange_outer_diameter", "delta": 10}
],
"thresholds": {},
},
"backend_hint": "build123d",
}
normalized = MODULE._normalize_tool_authored_designir(payload)
self.assertEqual("3.0", normalized["schema_version"])
self.assertEqual("mm", normalized["units"])
self.assertIsInstance(
normalized["semantic_layer"]["parameters"], dict
)
self.assertEqual(
["flange_outer_diameter", "flange_thickness", "bore_diameter",
"bolt_circle_diameter", "bolt_hole_diameter", "bolt_count"],
normalized["edit_interface"]["semantic_parameters"],
)
compiled = MODULE.validate_designir(payload)
self.assertEqual(
{"parameter": "bolt_hole_diameter"},
compiled["features"][2]["diameter"],
)
self.assertEqual("feat_flange_body", compiled["features"][1]["host"])
with tempfile.TemporaryDirectory() as temporary:
output = Path(temporary) / "flange.step"
result = MODULE.compile_designir(payload, output)
self.assertEqual(1, result["facts"]["solid_count"])
self.assertEqual([120.0, 120.0, 16.0], result["facts"]["size_mm"])
tree = FEATURE_TREE.generate_feature_tree(
normalized,
designir_path="flange.designir.json",
step_path="flange.step",
backend="build123d",
compiled=True,
validated=True,
)
serialized = json.dumps(tree)
self.assertNotIn('"designir_3"', serialized)
self.assertNotIn('"source_designir_feature"', serialized)
replay_step = Path(temporary) / "flange-replayed.step"
replay = FEATURE_TREE.compile_feature_tree(tree, replay_step)
self.assertEqual("feature_manager_native", replay["replay_kind"])
self.assertEqual(result["facts"]["size_mm"], replay["facts"]["size_mm"])
def test_teacher_dependency_is_rejected(self) -> None:
payload = self.payload()
payload["source_step"] = "teacher.step"
@@ -0,0 +1,75 @@
from __future__ import annotations
import importlib.util
import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "normalize_backend_result.py"
SPEC = importlib.util.spec_from_file_location("normalize_backend_result", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
MODULE = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
class NormalizeBackendResultTests(unittest.TestCase):
def test_unsupported_surfaceir_snapshot_does_not_block_native_publish(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
step_path = directory / "model.step"
metadata_path = directory / "backend-metadata.json"
output_path = directory / "model.designir.json"
step_path.write_bytes(b"ISO-10303-21;\nEND-ISO-10303-21;\n")
metadata_path.write_text(
json.dumps(
{
"parameters": [
{
"name": "module",
"value": 2.5,
"unit": "mm",
"editable": True,
"binding_kind": "native_python",
"parameter_path": "PARAMETERS.module",
"regenerate_adapter": "simplecadapi",
}
]
}
),
encoding="utf-8",
)
with patch.object(
MODULE.surfaceir_pipeline,
"extract_surfaceir",
side_effect=ValueError("Unsupported surface type: 8"),
):
result = MODULE.normalize_backend_result(
request="standard spur gear",
backend="simplecadapi",
source_of_truth="model_graph",
step_path=step_path,
output_path=output_path,
model_id="spur_gear",
family="simplecadapi_native_model",
native_source_path="model.simplecadapi.py",
backend_graph_path="model.simplecad.model.json",
metadata_path=metadata_path,
validation_paths=["backend-validation.json"],
)
payload = json.loads(output_path.read_text(encoding="utf-8"))
self.assertTrue(result["valid"])
self.assertEqual("fully_semantic_parametric", result["reconstruction_mode"])
self.assertEqual("unavailable", result["surface_snapshot"]["status"])
self.assertNotIn("surface_layer", payload)
self.assertEqual("partial", payload["semantic_layer"]["reconstruction_status"])
snapshot_stage = payload["semantic_layer"]["construction_stages"][1]
self.assertEqual("unavailable", snapshot_stage["status"])
self.assertEqual("Unsupported surface type: 8", snapshot_stage["error"])
@@ -26,6 +26,9 @@ SURFACEIR = load_module(
DESIGNIR = load_module(
"designir_pipeline_surface_test", ROOT / "scripts" / "designir_pipeline.py"
)
FEATURE_TREE = load_module(
"feature_tree_surface_test", ROOT / "scripts" / "feature_tree.py"
)
class SurfaceIRPipelineTests(unittest.TestCase):
@@ -128,6 +131,59 @@ class SurfaceIRPipelineTests(unittest.TestCase):
DESIGNIR._symmetric_difference_volume(expected, actual), 1e-8
)
def test_feature_tree_for_uploaded_step_is_inferred_summary(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
teacher = directory / "teacher.step"
DESIGNIR.export_step(DESIGNIR.Box(10, 20, 30), teacher)
payload = SURFACEIR.extract_surfaceir(teacher)
tree = FEATURE_TREE.generate_feature_tree(
payload,
designir_path="box.designir.json",
step_path="box.reconstructed.step",
parameters_path="parameters.json",
backend="surfaceir_occt",
compiled=True,
validated=True,
)
self.assertEqual(
"inferred_from_step",
tree["source"]["history_kind"],
)
self.assertEqual("solidworks_feature_manager", tree["tree_kind"])
self.assertFalse(
tree["source"]["claims_original_solidworks_history"]
)
imported = next(
node
for node in tree["feature_manager"]["nodes"]
if node["solidworks_type"] == "ImportedFeature"
)
self.assertEqual("导入1", imported["display_name"])
self.assertEqual(
"ImportedFeature",
imported["definition"]["feature_type"],
)
self.assertFalse(imported["definition"]["native_history_recovered"])
self.assertNotIn("manufacturing", imported)
self.assertEqual(1, imported["result"]["solid_count"])
self.assertEqual(
"gzip+base64+json",
imported["operation_spec"]["payload_encoding"],
)
self.assertEqual(
"fallback_imported_feature",
tree["validation"]["feature_recognition"]["status"],
)
replay_designir = FEATURE_TREE.designir_from_feature_tree(tree)
self.assertEqual(payload["surface_layer"], replay_designir["surface_layer"])
replayed = directory / "replayed_from_tree.step"
result = FEATURE_TREE.compile_feature_tree(tree, replayed)
self.assertTrue(replayed.is_file())
self.assertEqual("surfaceir_imported_feature", result["replay_kind"])
serialized = json.dumps(tree)
self.assertNotIn('"surface_layer": {"vertices"', serialized)
def test_empty_step_document_is_represented_honestly(self) -> None:
payload = {
"schema_version": "3.0",
+95
View File
@@ -0,0 +1,95 @@
# CAD Feature Tree Export
`feature_tree.json` is now a SolidWorks FeatureManager-style export. Semantic
models are replayed from the feature nodes themselves; uploaded STEP models use
a SolidWorks-style imported feature with an embedded SurfaceIR payload.
## Contract
- The artifact name stays `feature_tree.json`.
- `tree_kind` is `solidworks_feature_manager`.
- `schema_version` is `1.0`.
- The JSON Schema lives at
`designir-pipeline/contracts/feature_tree.schema.json`.
- Studio records the artifact with role `feature_tree`; no feature-tree UI is
required.
Top-level fields are:
- `model`: part metadata such as model id, family, units, and document type.
- `source`: DesignIR path, STEP path, reconstruction mode, backend, and the
native-history claim.
- `feature_manager`: root id, standard SolidWorks reference nodes, and ordered
feature nodes.
- `rebuild_contract`: the replay mode and compiler entrypoint.
- `validation`: structure validation, replay status, geometry/B-Rep status, and
feature-recognition coverage.
## FeatureManager Style
Every tree starts with a familiar part root and reference geometry:
- `历史`
- `原点`
- `前视基准面`
- `上视基准面`
- `右视基准面`
- `原点`
Semantic DesignIR features are normalized into SolidWorks-style nodes:
- `extrude_circle`, `extrude_rectangle`, `add_cylinder` -> `BossExtrude`
- `through_hole` -> `CutExtrude`
- `polar_hole_pattern` -> seed `CutExtrude` plus `CircularPattern`
- recognized future operations map to `Revolve`, `RevolvedCut`, `HoleWizard`,
`Fillet`, `Chamfer`, `LinearPattern`, and `MirrorPattern`
Parameters are attached to the owning feature node. They are not top-level
tree nodes. Each parameter keeps a `binding_id` that matches `parameters.json`
and the DesignIR semantic parameter name.
Semantic feature trees do not embed a full DesignIR copy. `BossExtrude`,
`CutExtrude`, and `CircularPattern` nodes store the reconstructable sketch,
depth, direction, hole, and pattern definitions needed by the
FeatureTreeCompiler.
Each node also includes SolidWorks-style inspection fields:
- `definition`: feature-specific definition such as end condition, depth,
direction, merge result, seed features, axis, or sketch profile.
- `dimensions`: SolidWorks-style dimension rows such as `D1@凸台-拉伸1`, with
parameter bindings when available.
- `references`: sketch, plane, parent features, and child features.
- `selection_sets`: selected faces, edges, and contours. These stay empty until
stable B-Rep selectors are available.
- `rebuild`: suppression and rollback/rebuild status.
## STEP And SurfaceIR
STEP does not contain original SolidWorks sketches or FeatureManager history.
For uploaded STEP/SurfaceIR models, the exporter creates one canonical
SolidWorks-style imported feature:
- `solidworks_type`: `ImportedFeature`
- `display_name`: `导入1`
- `english_name`: `Imported1`
The imported feature embeds compressed `gzip+base64+json` SurfaceIR. This keeps
the tree honest and independently replayable without pretending that the
original native CAD history was recovered.
## Replay
The tree can be replayed without sibling files:
```bash
python designir-pipeline/scripts/feature_tree.py compile \
feature_tree.json \
--output-step replayed.step \
--output-designir replayed.designir.json
```
Semantic trees replay through `feature_tree.compile_feature_manager`, which
builds DesignIR 3.0 from FeatureManager nodes and then calls
`designir_pipeline.compile_designir`. Imported SurfaceIR trees replay through
`surfaceir_pipeline.build_surfaceir`.
+315
View File
@@ -0,0 +1,315 @@
# DesignIR 3.0 支持能力分析与补全范围
本文档分析当前 CadSet 通过 `DesignIR 3.0 JSON -> text-to-cad / SimpleCADAPI -> STEP`
生成模型时的能力边界,并给出 DesignIR 3.0 JSON 应补全的支持范围。目标是把
DesignIR 从“少量语义特征可编译 + SurfaceIR 可重建”推进为能承接两个生成后端主要能力的
统一可编辑模型契约。
## 结论摘要
- 当前 DesignIR 3.0 的顶层契约已经统一了三种模式:纯语义参数化、STEP 推断的
SurfaceIR、以及语义层加编译后 SurfaceIR 快照的 hybrid 模式。
- STEP 上传重建路径相对完整:可抽取 analytic/B-spline surface、拓扑、边界、pcurve
再通过独立 rebuild、几何验收、SimpleCADAPI B-Rep 比较和参数扰动验收交付。
- 语义生成路径仍然很窄:`designir_pipeline.py` 目前只真正编译
`extrude_circle``add_cylinder``extrude_rectangle``through_hole`
`polar_hole_pattern`
- DesignIR 3.0 schema 对 `features``sketches``constraints` 的结构约束较弱,
允许任意 object,但 compiler 实际支持的 operation 很少。这导致“JSON 看似可写”,
但不能稳定生成模型。
- SimpleCADAPI 本身能力明显更强,覆盖 primitives、sketch、curve、revolve、sweep、
loft、twisted sweep、boolean、fillet、chamfer、shell、pattern、assembly constraint、
unit、tolerance、semantic tag、operation graph、translator 和标准机械件库。
- text-to-cad 当前主要负责路由、DesignIR 所有权、验证、SurfaceIR 重建、Viewer 交付;
对语义 DesignIR 的通用后端编译能力仍集中在 `designir_pipeline.py` 的小交集内。
## 支持等级定义
| 等级 | 含义 |
| --- | --- |
| L0 | 不支持,或只能作为自由文本/未定义 object 存在 |
| L1 | 路由或文档知道该能力,但 DesignIR 不能稳定表达或编译 |
| L2 | DesignIR 可表达,但缺少一个或多个后端 compiler adapter |
| L3 | 可由一个后端稳定生成并导出 STEP |
| L4 | build123d 和 SimpleCADAPI 路径都可生成,且有基础验证 |
| L5 | 有标准工厂/语义图/参数扰动/验收或专门测试覆盖 |
## 当前系统分层
| 层 | 当前职责 | 主要证据 |
| --- | --- | --- |
| cad-router | 根据需求选择 build123d 或 SimpleCADAPI,上传 STEP 时切到 SurfaceIR 执行 | `text-to-cad/skills/cad-router/scripts/route.py` |
| DesignIR 3.0 schema | 定义统一 envelope、语义层、SurfaceIR 层、编辑接口、验证契约 | `designir-pipeline/contracts/designir-3.0.schema.json` |
| semantic compiler | 把语义 DesignIR 编译为 build123d 或 SimpleCADAPI STEP | `designir-pipeline/scripts/designir_pipeline.py` |
| SurfaceIR runtime | 从 STEP 抽取/重建 surface-parametric DesignIR,验证几何和语义参数编辑 | `designir-pipeline/scripts/surfaceir_pipeline.py` |
| SimpleCADAPI | 标准机械件、语义图、单位、公差、assembly、translator、B-Rep 比较 | `SimpleCADAPI/src/simplecadapi` 与测试 |
| cad-agent-studio | 任务、参数、导出、预览 UI 集成 | `cad-agent-studio/src/lib` 与 API routes |
## 模型类型支持矩阵
| 模型/零件类型 | text-to-cad 路由 | DesignIR 语义 JSON | build123d 编译 | SimpleCADAPI 编译 | 当前等级 | 说明 |
| --- | --- | --- | --- | --- | --- | --- |
| 简单圆柱、圆盘、轴类 | 可识别 | `extrude_circle/add_cylinder` | 支持 | 支持 | L4 | 任意轴圆柱在 build123d 路径较好;SimpleCADAPI 路径圆柱支持任意轴 |
| 简单长方体、板、块 | 可识别 | `extrude_rectangle` | 支持 | 支持 | L4 | SimpleCADAPI box 当前要求正 Z 轴 |
| 法兰/轮毂/中心孔/螺栓孔阵列 | 可识别 | 圆柱 + 通孔 + polar hole | 支持 | 支持 | L4 | 当前最成熟的语义样例 |
| 单孔/通孔 | 可识别 | `through_hole` | 支持 | 支持 | L4 | 语义 compiler 中孔切割要求正 Z 轴 |
| 线性孔阵列 | SurfaceIR 可推断 | JSON 没有可编译语义 operation | 不支持 | 不支持 | L1 | SurfaceIR 可推断 `linear_hole_pattern`,语义 compiler 未实现 |
| 一般挤出草图 | 后端可做 | DesignIR 未定义稳定 profile vocab | 不支持 | 不支持 | L1 | 目前只规范化圆/矩形 |
| 旋转体、轴肩、锥台 | 后端可做 | 缺少 `revolve`/`cone` 语义 operation | 不支持 | 不支持 | L1 | SurfaceIR 可从 STEP 捕捉结果面,但语义生成不支持 |
| sweep/loft/twisted/helical sweep | 后端可做 | 缺少语义 operation | 不支持 | 不支持 | L1 | SimpleCADAPI 已有可回放操作 |
| fillet/chamfer/shell | 后端可做 | 缺少 modifier/selector 契约 | 不支持 | 不支持 | L1 | 最大缺口是稳定选择边/面 |
| 一般 boolean union/cut/intersect | 后端可做 | 只有隐式 union 和孔 cut | 部分 | 部分 | L2 | 需要把工具体、host、结果命名写入 JSON |
| 镜像/平移/旋转 | 后端可做 | 缺少 transform operation | 不支持 | 不支持 | L1 | SimpleCADAPI 支持表达式参数 |
| 线性/径向 pattern | 后端可做 | 缺少通用 pattern | 不支持 | 不支持 | L1 | 当前仅 polar hole special case |
| 齿轮、齿条、内齿圈、锥齿轮、摆线盘 | 路由到 SimpleCADAPI | 无标准工厂 feature | 不支持 | 不支持 | L1 | SimpleCADAPI stdlib 原生强,但 DesignIR 不会调用 |
| 螺栓、螺母、滚子链轮 | 路由到 SimpleCADAPI | 无标准工厂 feature | 不支持 | 不支持 | L1 | fastener/chain 工厂已有参数语义 |
| 滚珠轴承 | 路由到 SimpleCADAPI | 无 assembly/factory feature | 不支持 | 不支持 | L1 | SimpleCADAPI 返回 assemblyDesignIR part contract 未承接 |
| 行星/摆线减速器、关节执行器 | 路由倾向 SimpleCADAPI | 缺少 product/assembly/constraint JSON | 不支持 | 不支持 | L1 | examples 已证明后端能做复杂 product |
| 自由曲面/NURBS | 路由偏 build123d | SurfaceIR 可存 final surface;语义生成不足 | 低 | 低 | L1-L2 | 语义层缺少 profile/path/surface patch vocab |
| 上传 STEP 任意 B-Rep 重建 | route `--execute` | `surface_parametric` 完整 envelope | SurfaceIR 执行 | B-Rep 比较 | L5 | 这是重建路径,不等价于语义 authoring |
## 特征能力支持矩阵
| 特征类别 | 当前 DesignIR 语义支持 | 后端原生能力 | 主要差距 | 建议目标等级 |
| --- | --- | --- | --- | --- |
| 参数与表达式 | 数值参数、简单表达式、`mm/deg/count/ratio` | SimpleCADAPI 有 expression graph 和 units | 缺少 typed unit value、dimension checking、参数域/默认 UI hints | L5 |
| Datum/坐标系 | primary axis 和基本 axis | 两后端均可定位 | 缺少 datum plane、local frame、workplane、connector datum 的规范结构 | L4 |
| Sketch | 仅圆/矩形归一化 | SimpleCADAPI 支持 point/line/circle/arc/bspline 和约束 | schema 未定义 sketch entity/constraint vocab | L4 |
| Primitive | 圆柱/圆挤出、矩形盒 | box/cylinder/cone/sphere 等 | 缺少 cone、sphere、torus-like primitive、primitive metadata | L4 |
| Extrude | 圆/矩形 profile 的 add body | 一般 face extrude | 缺少 profile 引用、direction、extent、symmetric、cut/new/merge 模式 | L4 |
| Revolve | 无 | SimpleCADAPI/build123d 均可 | 缺少 axis/origin/angle/profile contract | L4 |
| Sweep/Loft | 无 | SimpleCADAPI 已有 sweep/loft/twisted/helical | 缺少 path/profile/section/ruled/twist contract | L3-L4 |
| Boolean | add body 隐式 unionhole cut special case | union/cut/intersect | 缺少 body graph、tool list、skip strategy、tracking policy | L4 |
| Hole feature | through hole、polar hole pattern | 后端可更多 | 缺少 blind/counterbore/countersink/threaded/tapped/slot hole | L5 |
| Edge/face modifiers | 无 | fillet/chamfer/shell | 缺少 stable selector 与作用范围 | L4 |
| Patterns | polar hole only | linear/radial/mirror pattern | 缺少通用 source feature、transform series、merge/cut policy | L4 |
| Standard factory | 无 | SimpleCADAPI stdlib 强 | 缺少 factory op、参数 schema、生成 graph/metadata 绑定 | L5 |
| Assembly/product | 另有 assembly schema,但未接入 DesignIR semantic compiler | SimpleCADAPI assembly/connector/constraint 完整 | DesignIR part envelope 与 product-level contract 分离 | L3-L5 |
| Semantic tags/lineage | feature id、preserved interface | SimpleCADAPI tag/topology lineage/source mapping | 缺少 output role、selector by semantic tag、lineage carry-over | L5 |
| Tolerance/material | validation_contract 字符串为主 | SimpleCADAPI units/tolerance/material | 缺少公差链、材料、表面处理的结构化字段 | L4 |
| Translators/scene package | 无 | SimpleCADAPI FreeCAD/Fusion/SolidWorks/scene.zip | DesignIR 没声明 translator intent 和 export contract | L3 |
| Validation/edit | perturbation 契约、SurfaceIR 参数验收 | 两路径均可扩展 | 语义 compiler 的 edit validation 只覆盖少数参数 | L5 |
## 当前 DesignIR 3.0 JSON 的关键不足
1. `features` 是松散 object array,缺少 machine-readable operation enum 和 per-operation schema。
2. semantic compiler 仍把 DesignIR 3.0 压平成 legacy 2.0 payload 再验证,实际 vocabulary 被
`SUPPORTED_OPERATIONS` 限制。
3. schema 层能容纳 `sketches/patterns/constraints/attachments`,但 compiler 只使用极少字段。
4. 缺少稳定 selector 模型。fillet/chamfer/shell/general boolean 需要选择边/面/feature 输出,
不能依赖 STEP face id 或后端临时拓扑 id。
5. SimpleCADAPI 的 `ModelResult`、operation graph、semantic tag、source mapping、units、
tolerance chain 没有映射回 DesignIR。
6. SimpleCADAPI 标准工厂已覆盖多个机械族,但 DesignIR 无 `standard_factory` feature
导致路由能选 SimpleCADAPIJSON compiler 却不会生成齿轮/轴承/紧固件等模型。
7. assembly/product 能力没有成为 DesignIR 3.0 的一等对象。复杂 reducers/actuators 只能靠
原生 Python generator,不能靠 DesignIR JSON 表达。
8. build123d 的广义建模能力没有在 DesignIR vocabulary 中体现;当前 DesignIR 对它也只开放
圆柱、盒子和孔阵列小子集。
## 补全后的 DesignIR 3.0 JSON 支持范围
补全应分为“统一语义词汇”和“后端 adapter 映射”两部分。DesignIR 不应直接复制后端 API
但必须覆盖足够稳定的建模意图。
### 1. 通用结构
建议新增或规范化以下结构:
- `operation_registry_version`: 语义 feature vocabulary 版本。
- `semantic_layer.bodies`: 命名 body graph,用于区分 source body、tool body、result body。
- `semantic_layer.feature_outputs`: feature 输出角色,例如 `body`, `top_face`, `side_faces`,
`created_edges`, `tool_body`
- `semantic_layer.selectors`: 以 semantic role/tag、feature output、datum relation、geometry rule
选择边/面/体。
- `backend_capabilities`: 每个 feature 对 `build123d``simplecadapi` 的支持状态、fallback、
最低版本和测试状态。
- `model_graph_artifacts`: SimpleCADAPI `*.simplecad.model.json`、operation graph、scene package
等派生产物引用。
### 2. 参数、表达式、单位
目标支持:
- typed parameter`length``angle``count``ratio``dimensionless``area``volume`
- `value_ref`: 支持 literal、parameter、expression、unit value。
- 参数域:`min``max``step``recommended_values``integer_only`
- 派生表达式 DAG:记录依赖、单位推断、是否可编辑。
- UI/edit hintsslider、number input、enum、toggle,但不替代验证。
### 3. Datum、Frame、Connector
目标支持:
- datum point/axis/plane/frame/workplane。
- local coordinate frame 与 parent frame。
- connector datumface/edge/vertex/placement connector,用于 assembly 和可编辑接口。
- preserved interface:用 connector 和 semantic selector 表示,而不是纯字符串。
### 4. Sketch 与 Profile
目标支持:
- sketch entitypoint、line、circle、arc、ellipse、polyline、bspline、construction geometry。
- sketch constraintscoincident、distance、distance_x/y、horizontal、vertical、parallel、
perpendicular、tangent、concentric、equal_length、equal_radius、diameter、radius、angle、
midpoint、symmetric、fix。
- profileclosed wire、outer loop、inner loops、face from sketch、face from wires。
### 5. Solid Feature Vocabulary
第一阶段应补齐两后端高确定性的共同能力:
- `primitive_box`
- `primitive_cylinder`
- `primitive_cone`
- `primitive_sphere`
- `extrude`
- `revolve`
- `boolean_union`
- `boolean_cut`
- `boolean_intersect`
- `through_hole`
- `blind_hole`
- `counterbore_hole`
- `countersink_hole`
- `linear_pattern`
- `radial_pattern`
- `mirror_feature`
- `translate_feature`
- `rotate_feature`
- `fillet`
- `chamfer`
- `shell`
第二阶段补齐曲面和复杂生成:
- `sweep`
- `loft`
- `twisted_sweep`
- `helical_sweep`
- `thread_external`
- `thread_internal`
- `knurl`
- `rib`
- `boss`
- `pocket`
- `slot`
- `draft`
- `surface_patch`
- `surface_trim`
### 6. Standard Factory Vocabulary
DesignIR 应提供稳定的 `standard_factory` feature,用于调用 SimpleCADAPI 标准库,同时保留
参数语义和 ModelResult graph
| family | factory operation | 关键参数 |
| --- | --- | --- |
| spur gear | `standard_factory.gear.spur` | `n_teeth`, `module`, `pressure_angle`, `gear_height`, `backlash`, bore |
| helical gear | `standard_factory.gear.helical` | spur 参数 + `helix_angle/handedness` |
| herringbone gear | `standard_factory.gear.herringbone` | helical 参数 + center gap/phase |
| ring gear | `standard_factory.gear.ring` | internal teeth、rim、bore、height |
| straight bevel gear | `standard_factory.gear.straight_bevel` | `n_teeth`, `module`, `pitch_angle`, `face_width` |
| rack | `standard_factory.gear.rack` | tooth count/length、module、height、width |
| cycloidal disc | `standard_factory.gear.cycloidal_disc` | lobe count、pin radius、eccentricity、disc thickness |
| bolt | `standard_factory.fastener.bolt` | diameter、length、head style、drive style、thread style/detail |
| nut | `standard_factory.fastener.nut` | diameter、width、height、nut style、hole style、thread detail |
| roller chain sprocket | `standard_factory.chain.roller_sprocket` | tooth count、chain pitch、roller diameter、thickness、bore radius |
| ball bearing | `standard_factory.bearing.ball_bearing` | inner/outer diameter、width、ball diameter/count、clearance |
这些 feature 对 build123d 的支持可以是 `unsupported``fallback_to_backend_native_python`
但 DesignIR 必须诚实记录;对 SimpleCADAPI 应达到 L5。
### 7. Assembly/Product Vocabulary
复杂机械产品需要 product-level DesignIR,而不只是 part-level DesignIR
- `components`: part、subassembly、standard factory component。
- `placements`: explicit placement、connector-to-connector placement。
- `connectors`: face/edge/vertex/placement connector。
- `constraints`: fixed、revolute、prismatic、gear、belt、rack-pinion。
- `solve_policy`: strict/non-strict、residual thresholds。
- `public_interfaces`: 对外暴露的轴线、安装面、孔系、连接器。
- `export_policy`: STEP compound、scene package、URDF/MJCF fixed-base 或显式机构导出。
短期可复用 `assembly-designir-1.0.schema.json`,但需要在 DesignIR 3.0 文档中明确
part 与 product 的边界和交付 artifact contract。
### 8. Validation Contract 补全
每个新增 feature 至少要有:
- geometry checkssolid count、volume、bounds、face/edge count、surface type mix。
- semantic checks:参数是否驱动目标 feature、selector 是否解析到非空对象。
- edit checks:参数扰动后目标变化、非目标 interface 保持。
- backend checksbuild123d/SimpleCADAPI 生成一致性,或明确单后端支持。
- graph checksSimpleCADAPI replay、model JSON roundtrip、semantic tag presence。
- translator checks:需要导出 FreeCAD/Fusion/SolidWorks 时才启用。
## 推荐实现阶段
### P0: 文档和注册表对齐
- 把本文档中的 feature vocabulary 拆成 machine-readable `designir-capabilities.json`
- 在 `designir-3.0.schema.json` 中给 `features` 增加 operation discriminators。
- 保留 `missing_capabilities`,但要求 unsupported feature 必须列明后端和原因。
### P1: 两后端共同核心
- 实现 cone、sphere、general extrude、revolve、boolean union/cut/intersect。
- 实现 linear/radial pattern、transform、fillet、chamfer、shell。
- 为 build123d 和 SimpleCADAPI 都加 compiler tests。
### P2: Sketch/Profile
- 定义 sketch entity 和 constraints schema。
- 支持 face from sketch/wire,并让 extrude/revolve/sweep/loft 引用 profile。
- 加入 selector by feature output role,避免 brittle topology refs。
### P3: SimpleCADAPI 标准机械件
- 增加 `standard_factory.*` operation。
- 输出 `.simplecad.model.json` 并在 `cad-task.json` 中登记。
- 对 gear、fastener、bearing、chain 四类先做端到端测试。
### P4: Product/Assembly
- 统一 part DesignIR 与 assembly DesignIR 的引用关系。
- 映射 SimpleCADAPI assembly、connector、constraint、solve report。
- 让 reducer/actuator 类模型可由 JSON 组织,而不是只能依赖示例 Python。
### P5: Surface/Freeform 与高级编辑
- 在 semantic layer 增加 surface patch、trim、loft continuity、NURBS control support。
- 将 SurfaceIR 的推断参数与语义 authoring 参数合流,但仍区分 inferred 与 authored。
- 对上传 STEP 的语义修复和新建自由曲面模型建立共同 vocabulary。
## 当前优先级建议
最高优先级不是继续扩大 router 关键词,而是扩展 DesignIR 语义 vocabulary 和 compiler adapter
1. 先补 `operation schema + compiler adapter + tests` 的闭环。
2. 然后补 `standard_factory.*`,因为 SimpleCADAPI 已经有成熟工厂,收益最大。
3. 再补 selector/feature output role,支撑 fillet/chamfer/shell/general boolean。
4. 最后补 product-level assembly,使 reducer、bearing、actuator 等复杂模型真正能由
DesignIR JSON 表达和编辑。
## 参考文件
- `text-to-cad/skills/cad-router/references/designir-3.0.md`
- `text-to-cad/skills/cad-router/references/capabilities.md`
- `text-to-cad/skills/cad-router/scripts/capabilities.json`
- `text-to-cad/skills/cad-router/scripts/route.py`
- `text-to-cad/skills/cad-router/scripts/reconstruct_step.py`
- `designir-pipeline/contracts/designir-3.0.schema.json`
- `designir-pipeline/scripts/designir_pipeline.py`
- `designir-pipeline/scripts/surfaceir_pipeline.py`
- `designir-pipeline/tests/test_designir_pipeline.py`
- `designir-pipeline/tests/test_surfaceir_pipeline.py`
- `SimpleCADAPI/src/simplecadapi/__init__.py`
- `SimpleCADAPI/src/simplecadapi/operations.py`
- `SimpleCADAPI/src/simplecadapi/std/gear.py`
- `SimpleCADAPI/src/simplecadapi/std/fastener.py`
- `SimpleCADAPI/src/simplecadapi/std/bearing.py`
- `SimpleCADAPI/src/simplecadapi/std/chain.py`
+4 -4
View File
@@ -3,11 +3,11 @@ defaultModel: deepseek:deepseek-v4-flash
providers:
openai:
type: openai
apiKeyEnv: OPENAI_API_KEY
apiKey: sk-6586c229d77de8c421ba98e7eb0d9c6bb10f08ebc796de946ed17cf8d0d7a229
baseURL: https://api.vip1129.cc/v1
models:
default: gpt-5.5
fast: gpt-5.4-mini
default: gpt-5.4-mini
fast: gpt-5.5
deepseek:
type: openai-compatible
baseURL: https://api.deepseek.com
@@ -15,7 +15,7 @@ providers:
default: deepseek-v4-flash
flash: deepseek-v4-flash
reasoner: deepseek-v4-pro
apiKey: sk-967ad5466ec94a23a73028fe3799b046
apiKey: sk-d3f8fe84bf9a4100a6563559e5d2fefd
uploads:
maxImageMB: 20
maxStepMB: 200
+11 -6
View File
@@ -15,7 +15,11 @@ Choose a backend before modeling, keep its source as the editable authority, and
only its promoted schema `2.0` generalized library.
Never read `cad-experience-plugin/parser/input`, `parser/output`, or private
case JSON during creation, modification, or reconstruction.
2. Run the deterministic router from this skill directory:
2. Run the deterministic router from this skill directory. For a new model it
matches only the requested primary part against the SimpleCADAPI standard
part catalog in `scripts/capabilities.json`. A catalog match selects
SimpleCADAPI; every unmatched request selects text-to-cad/build123d. It does
not read capability documents or let an LLM choose the backend:
```bash
python3 scripts/route.py "<request>" --explain
@@ -46,9 +50,9 @@ Choose a backend before modeling, keep its source as the editable authority, and
geometry acceptance, parameter perturbation acceptance, and artifact
publication. A route decision alone is not a completed reconstruction.
3. Follow the selected route unless a hard environmental constraint makes it unavailable. If overriding it, record the reason in the task manifest.
4. Load only the selected backend reference from `references/capabilities.md` and the matching installed Skill:
- `build123d`: use `$cad`; prefer it for custom brackets, housings, shafts, flanges, fixtures, source-level assemblies, and freeform/custom surface work.
- `simplecadapi`: use an installed SimpleCADAPI 2.0.2 Skill/runtime; prefer it when a standard factory exists, or when replayable ModelResult graphs, semantic lineage, physical units, tolerance chains, scene packages, or CAD translators materially improve the result. Its standard library includes gears, racks, ring and bevel gears, bearings, bolts, nuts, roller-chain sprockets, cycloidal parts, and reducer families.
4. Use the selected backend directly:
- `build123d`: use `$cad` for custom brackets, housings, shafts, flanges, fixtures, source-level assemblies, and every new part outside the catalog.
- `simplecadapi`: use the installed SimpleCADAPI 2.0.2 runtime only for catalogued standard primary parts: gears, ring gears, racks, cycloidal discs, ball bearings, roller-chain sprockets, bolts, and nuts.
5. When the route reports `requirement_refinement` or `visual_repair`, refine the brief, generate, execute, inspect multiple views, and repair no more than three visual mismatch rounds before asking the user.
6. Generate into a task-owned directory and start CAD Viewer with that task directory as `--dir`; do not expose the repository-wide fixture library for a task review. Write one DesignIR 3.0 `*.designir.json` per part and treat it as the editable geometry contract; keep `cad-task.json` as the execution record. Read `references/designir-3.0.md` for both semantic authoring and uploaded STEP reconstruction. `references/designir-2.0.md` is migration-only. Never change backend during an edit unless conversion is explicitly requested; modify the recorded authoritative layer.
For a supplied STEP/STP, use `scripts/route.py --execute`; do not manually
@@ -90,8 +94,9 @@ Choose a backend before modeling, keep its source as the editable authority, and
- Prefer an explicit user backend when it can produce the editable STEP contract.
- Preserve the backend of an explicitly supplied DesignIR or Python generator unless conversion is requested.
- Do not assign a permanent default backend. Score native-source continuity, factory coverage, semantic/replay needs, geometry vocabulary, assembly needs, and validation needs.
- Prefer SimpleCADAPI when a supported standard mechanical factory or its model graph, semantic topology, unit/tolerance, scene, or translator capabilities materially improve editability and verification.
- Prefer build123d for custom machined parts, source-driven assemblies, and freeform or surface-heavy work not covered by a SimpleCADAPI factory or tested operation vocabulary.
- For a new model, do not use semantic, graph, tolerance, assembly, or generic mechanical keywords to choose a backend. Use the standard-part catalog only.
- Select SimpleCADAPI only when the primary requested part matches an entry in `scripts/capabilities.json`. Standard-part words used only as features, such as bolt holes or bearing seats, are excluded from that match.
- Select build123d for every unmatched new request, including custom machined parts, source-driven assemblies, and freeform or surface-heavy work.
- For uploaded STEP, report SurfaceIR as the effective reconstruction engine. The selected authoring backend describes future semantic repair/edit work; it is not allowed to read the teacher STEP during reconstruction.
- Prefer source quality and future modification over the shortest first generation.
@@ -0,0 +1,90 @@
# text-to-cad / build123d Agent Capability Guide
This document is the required backend capability brief for CAD Agent Studio
when considering the `build123d` route through text-to-cad. It is intentionally
an Agent-facing decision document, not a geometry template.
## Role
Use this route when the Agent will author backend-native `build123d` Python
source and the server will only execute, validate, normalize to DesignIR 3.0,
and publish artifacts.
text-to-cad owns orchestration, artifact contracts, DesignIR normalization,
SurfaceIR reconstruction for uploaded STEP, validation, repair workflow, and
Viewer handoff. For direct new model generation, `build123d` source is the
editable source of truth.
## Studio Documentation Discipline
For CAD Agent Studio backend selection, this document is sufficient. Decide
from the capability boundary here: what text-to-cad/build123d is strong at,
what it is weak at, and whether the primary requested object matches those
strengths.
Do not read build123d API manuals, skill internals, or implementation guides
during ordinary backend selection. The selection task only needs to know what
the tool can generate, not the exact API signatures.
## Strong Fits
- Custom mechanical parts: brackets, flanges, wheel hub adapters, housings,
plates, fixtures, covers, shafts, bushings, levers, cranks, connecting rods.
- Feature-rich machined parts with cylinders, boxes, revolve-like stacks,
holes, through cuts, counterbores, pockets, bosses, ribs, shells, chamfers,
fillets, lofts, sweeps, and source-level construction logic.
- Requests where words such as bolt, nut, bearing, gear, or sprocket describe
holes, mounts, clearances, interfaces, or surrounding features rather than
asking for those standard parts as the primary object.
- Geometry where future edits should preserve readable Python parameters and
explicit construction order.
## Weak Fits
- The requested primary object is a standard mechanical component already
covered by a SimpleCADAPI standard factory, such as a gear, bearing, bolt,
nut, roller-chain sprocket, rack, or reducer.
- The request specifically requires SimpleCADAPI model graph replay, semantic
topology lineage, unit/tolerance chains, translator workflows, or strict
B-Rep comparison.
## Agent Source Contract
When choosing this backend, submit raw Python source to `generate_cad` with:
- `selectedBackend = "build123d"`
- `sourceKind = "build123d_python"`
- A script that accepts `--step` and `--metadata`
- STEP export written exactly to the `--step` path
- Metadata JSON written exactly to the `--metadata` path
- stdout JSON is optional diagnostic output; file artifacts are the source of
truth for execution success
If exposing editable parameters, include a top-level block:
```python
# CAD_AGENT_PARAMETERS_START
PARAMETERS = {
"example": 1.0
}
# CAD_AGENT_PARAMETERS_END
```
Each editable metadata parameter must include `name`, `value`, `unit`,
`editable`, `binding_kind`, `parameter_path`, and `regenerate_adapter`.
Use `binding_kind = "python_constant"` and `regenerate_adapter = "build123d"`
for parameters bound to the `PARAMETERS` block.
Do not submit diagnostic, API-probing, topology-probing, radius-sweep, or smoke
test scripts as `nativeSource`. The source must be the final model generator
for the user's part and must write the requested file artifacts in one
execution. If this compact reference is insufficient, report the missing
reference instead of using `generate_cad` as an exploration tool.
## Decision Rule
Choose build123d when the full requested part is a custom feature model and no
SimpleCADAPI standard factory directly represents the primary object. Do not
route to another backend merely because a feature name contains a standard part
word. For example, bolt holes or nut clearances in a flange remain features of
the flange, not a request to generate a bolt or nut.
@@ -16,6 +16,78 @@
"simplecadapi_brep_compare"
]
},
"routing_catalog": {
"selection_rule": "For a new model, choose SimpleCADAPI only when the requested primary object matches one catalog entry. Feature-context matches are excluded. Every other request chooses build123d.",
"source": "SimpleCADAPI/src/simplecadapi/std/{gear,bearing,chain,fastener}.py",
"simplecadapi_standard_part_types": [
{
"type": "spur_gear",
"factory": "simplecadapi.std.gear.make_spur_gear_rsolid",
"aliases": ["齿轮", "直齿轮", "正齿轮", "spur gear"],
"feature_context_exclusions": ["齿轮孔", "齿轮安装", "齿轮座", "齿轮箱", "gear hole", "gear mount", "gear housing", "gearbox"]
},
{
"type": "straight_bevel_gear",
"factory": "simplecadapi.std.gear.make_straight_bevel_gear_rsolid",
"aliases": ["直齿锥齿轮", "锥齿轮", "straight bevel gear", "bevel gear"],
"feature_context_exclusions": ["锥齿轮箱", "bevel gearbox", "锥齿轮座"]
},
{
"type": "helical_gear",
"factory": "simplecadapi.std.gear.make_helical_gear_rsolid",
"aliases": ["斜齿轮", "helical gear"],
"feature_context_exclusions": ["斜齿轮箱", "helical gearbox", "斜齿轮座"]
},
{
"type": "herringbone_gear",
"factory": "simplecadapi.std.gear.make_herringbone_gear_rsolid",
"aliases": ["人字齿轮", "herringbone gear"],
"feature_context_exclusions": ["人字齿轮箱", "herringbone gearbox", "人字齿轮座"]
},
{
"type": "ring_gear",
"factory": "simplecadapi.std.gear.make_spur_ring_gear_rsolid",
"aliases": ["内齿圈", "齿圈", "ring gear"],
"feature_context_exclusions": ["齿圈座", "ring gear housing", "ring gear mount"]
},
{
"type": "gear_rack",
"factory": "simplecadapi.std.gear.make_spur_rack_rsolid",
"aliases": ["齿条", "直齿条", "斜齿条", "人字齿条", "gear rack", "rack gear", "spur rack", "helical rack", "herringbone rack"],
"feature_context_exclusions": ["齿条安装", "齿条座", "rack mount", "rack housing"]
},
{
"type": "cycloidal_disc",
"factory": "simplecadapi.std.gear.make_cycloidal_disc_rsolid",
"aliases": ["摆线盘", "摆线轮", "cycloidal disc"],
"feature_context_exclusions": ["摆线减速器", "cycloidal reducer", "摆线针轮减速器"]
},
{
"type": "ball_bearing",
"factory": "simplecadapi.std.bearing.make_ball_bearing_rassembly",
"aliases": ["轴承", "滚动轴承", "球轴承", "深沟球轴承", "ball bearing"],
"feature_context_exclusions": ["轴承座", "轴承壳", "轴承孔", "轴承安装", "bearing housing", "bearing seat", "bearing hole", "bearing mount"]
},
{
"type": "roller_chain_sprocket",
"factory": "simplecadapi.std.chain.make_roller_chain_sprocket_rsolid",
"aliases": ["滚子链轮", "链轮", "roller chain sprocket", "sprocket"],
"feature_context_exclusions": ["链轮座", "链轮箱", "链轮安装", "sprocket housing", "sprocket mount"]
},
{
"type": "bolt",
"factory": "simplecadapi.std.fastener.make_bolt_rsolid",
"aliases": ["螺栓", "六角螺栓", "内六角螺栓", "螺栓本体", "bolt", "hex bolt", "socket head bolt"],
"feature_context_exclusions": ["螺栓孔", "螺栓圆", "螺栓阵列", "bolt hole", "bolt circle", "bolt pattern"]
},
{
"type": "nut",
"factory": "simplecadapi.std.fastener.make_nut_rsolid",
"aliases": ["螺母", "六角螺母", "螺母本体", "nut", "hex nut"],
"feature_context_exclusions": ["螺母槽", "螺母孔", "螺母避让", "nut pocket", "nut hole", "nut clearance"]
}
]
},
"backends": {
"build123d": {
"project": "text-to-cad",
+177 -10
View File
@@ -181,6 +181,49 @@ def contains_any(text: str, terms: tuple[str, ...]) -> bool:
return any(term in text for term in terms)
def simplecadapi_standard_part_match(
request: str,
registry: dict[str, object],
) -> dict[str, object] | None:
"""Match only a requested *primary* standard part, never a feature word.
The catalog is maintained beside the router from the actual SimpleCADAPI
``simplecadapi.std`` factories. This is intentionally deterministic: an
unmatched or ambiguous request is a build123d request.
"""
catalog = registry.get("routing_catalog", {})
rules = (
catalog.get("simplecadapi_standard_part_types", [])
if isinstance(catalog, dict)
else []
)
text = request.casefold()
best_match: tuple[int, dict[str, object], list[str]] | None = None
for raw_rule in rules:
if not isinstance(raw_rule, dict):
continue
aliases = [str(value).casefold() for value in raw_rule.get("aliases", [])]
exclusions = [str(value).casefold() for value in raw_rule.get("feature_context_exclusions", [])]
matched_aliases = [alias for alias in aliases if alias and alias in text]
if not matched_aliases:
continue
if any(exclusion and exclusion in text for exclusion in exclusions):
continue
# Generic terms such as "gear" must never hide a more specific
# standard-part type such as "herringbone gear".
specificity = max(len(alias) for alias in matched_aliases)
if best_match is None or specificity > best_match[0]:
best_match = (specificity, raw_rule, matched_aliases)
if best_match is None:
return None
_, rule, matched_aliases = best_match
return {
"type": rule.get("type"),
"factory": rule.get("factory"),
"matched_aliases": matched_aliases,
}
def infer_experience_query(request: str) -> tuple[str | None, list[str]]:
text = request.lower()
family = next(
@@ -559,6 +602,127 @@ def route(args: argparse.Namespace) -> dict[str, object]:
assembly=args.assembly,
source_backend=source_backend,
)
if not edit_context and args.backend == "auto":
registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
standard_part = simplecadapi_standard_part_match(request, registry)
if not standard_part:
requirements = [
requirement
for requirement in requirements
if requirement != "standard_mechanical_factory"
]
agent_experiences = (
experience_context.get("experiences", [])
if isinstance(experience_context, dict)
else []
)
agent_generalized_methods = [
{
key: item[key]
for key in (
"id",
"kind",
"guidance",
"check",
"repair",
"confidence",
"reconstruction_grammar",
)
if isinstance(item, dict) and key in item
}
for item in agent_experiences
if isinstance(item, dict)
]
agent_reconstruction_grammars = [
item["reconstruction_grammar"]
for item in agent_experiences
if isinstance(item, dict)
and item.get("kind") == "reconstruction_grammar"
and "reconstruction_grammar" in item
]
selected_name = "simplecadapi" if standard_part else "build123d"
backend_info = registry["backends"][selected_name]
source_of_truth = (
"simplecadapi model graph with native Python source"
if selected_name == "simplecadapi"
else "build123d native Python source"
)
selection_reason = (
"matched SimpleCADAPI standard-part catalog entry "
f"{standard_part['type']} ({standard_part['factory']})"
if standard_part
else "no SimpleCADAPI standard-part catalog entry matched; use the general text-to-cad/build123d backend"
)
return {
"schema_version": "1.0",
"request": request,
"selected_backend": selected_name,
"runner_skill": backend_info["runner_skill"],
"project": backend_info["project"],
"adapter": None,
"confidence": "high",
"routing_policy": {
"version": "4.0",
"selection_scope": "new_model_primary_part_type",
"requirements": requirements,
"decision_authority": "cad_router_standard_part_catalog",
"keyword_backend_scoring": True,
"fallback_policy": "unmatched_requests_use_build123d",
"standard_part_match": standard_part,
"server_side_template_generation": False,
"effective_execution_engine": selected_name,
"validation_engines": ["backend_geometry_validation"],
},
"backend_scores": [
{
"backend": selected_name,
"score": 100,
"reasons": [selection_reason],
},
{
"backend": "build123d" if selected_name == "simplecadapi" else "simplecadapi",
"score": 0,
"reasons": ["not selected by the standard-part catalog"],
},
],
"fallback_order": ["build123d"] if selected_name == "simplecadapi" else [],
"workflow_profiles": [],
"artifact_contract": {
"primary_format": "step",
"editable_contract": "DesignIR 3.0 normalized contract",
"editable_contract_role": "contract_after_backend_generation",
"authoritative_geometry_source": source_of_truth,
},
"source_of_truth": source_of_truth,
"experience_context": experience_context,
"design_plan": {
"plan_kind": "deterministic_standard_part_route",
"operation": "create",
"part_family": experience_family or "unclassified",
"requested_feature_roles": sorted(set(experience_features)),
"selected_backend": selected_name,
"source_of_truth": source_of_truth,
"standard_part_match": standard_part,
"generalized_methods": agent_generalized_methods,
"reconstruction_grammars": agent_reconstruction_grammars,
"experience_status": (
"matched"
if experience_context and experience_context.get("experiences")
else "no_promoted_method_matched"
),
"execution_steps": [
"CAD Router selects the backend from the standard-part catalog.",
"Agent writes backend-native Python source for the full request.",
"Server executes the submitted source and rejects missing artifacts.",
"Server normalizes backend output into DesignIR 3.0.",
],
},
"project_contributions": {
"text-to-cad": "default backend for every request outside the SimpleCADAPI standard-part catalog",
"SimpleCADAPI": "backend for the catalogued standard mechanical part types only",
},
"availability": None,
}
scores = {
"build123d": BackendScore(
"build123d",
@@ -692,12 +856,13 @@ def route(args: argparse.Namespace) -> dict[str, object]:
registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
backend_info = registry["backends"][selected.name]
editable_contract = "DesignIR 3.0"
source_of_truth = (
"per-part DesignIR 3.0 with an isolated SurfaceIR compiler"
if teacher_step_reconstruction
else "per-part semantic DesignIR 3.0 with an isolated backend compiler"
)
editable_contract = "DesignIR 3.0 normalized contract"
if teacher_step_reconstruction:
source_of_truth = "surfaceir_designir"
elif selected.name == "simplecadapi":
source_of_truth = "simplecadapi model graph with native Python source"
else:
source_of_truth = "build123d native Python source"
gap = selected.score - max((scores[name].score for name in scores if name != selected.name), default=0)
confidence = "high" if gap >= 40 else "medium" if gap >= 15 else "low"
result: dict[str, object] = {
@@ -740,6 +905,8 @@ def route(args: argparse.Namespace) -> dict[str, object]:
"artifact_contract": {
"primary_format": "step",
"editable_contract": editable_contract,
"editable_contract_role": "contract_after_backend_generation",
"authoritative_geometry_source": source_of_truth,
},
"source_of_truth": source_of_truth,
"experience_context": experience_context,
@@ -831,10 +998,10 @@ def route(args: argparse.Namespace) -> dict[str, object]:
if edit_context
else [
"Resolve explicit user dimensions and preserve them as authoritative parameters.",
"Instantiate matched reconstruction grammars as DesignIR parameters, datums, constraints, and canonical feature stages; never copy teacher geometry.",
"Translate requested feature roles and matched generalized methods into backend operations.",
"Generate the editable source of truth and STEP artifact.",
"Validate topology, dimensions, feature relationships, surface mix, and export integrity against the grammar validation roles.",
"Translate requested feature roles and matched generalized methods into the selected backend's native operations.",
"Generate backend-native source or model graph and export the primary STEP artifact.",
"Normalize backend metadata into DesignIR 3.0 for parameters, feature tree, validation, and future edit bindings.",
"Validate topology, dimensions, feature relationships, surface mix, and export integrity against the brief.",
]
),
},
+51
View File
@@ -98,5 +98,56 @@ Load these files only when their trigger applies:
- `references/parameters.md` — parameterizing or animating a STEP model: source parameters, `.step.js` sidecar modules, viewer controls, and animation design.
- `references/supported-exports.md` — secondary STL/3MF/native GLB sidecar workflows.
- `references/repair-loop.md` — diagnosis and repair procedures.
- `references/part-families/bearing-housing.md` — bearing housings and seats: bearing housing, bearing seat, bearing support, pillow block, flanged bearing housing, cartridge bearing holder, `轴承座`, `轴承支座`, `带座轴承`.
- `references/part-families/cup.md` — drinking vessels and mugs: cup, mug, water cup, tumbler, handled cup, travel mug, `水杯`, `杯子`, `马克杯`, `茶杯`.
<!-- cad-skillx:start:skill-pack-curated-20260807 -->
- Pack: `skill-pack-curated-20260807`
Use CAD-SkillX references only after the core build123d references are loaded.
For a matching request, read one Planning skill first, then add only the
Functional and Atomic skills required by the requested geometry. Do not read
the whole pack by default.
Planning skills:
- `references/cad-skillx/planning/bearing-housing-or-seat.planning.md` — bearing housing, bearing seat, bearing block, bushing seat, pillow block, `轴承座`, `轴承支座`, `带座轴承`.
- `references/cad-skillx/planning/flange.planning.md` — flange, circular flange, pipe flange, coupling flange, flanged sleeve, `法兰`, `法兰盘`.
- `references/cad-skillx/planning/hexagonal-nut.planning.md` — hex nut, hexagonal nut, threaded nut, internal threaded bore, `六角螺母`, `螺母`.
- `references/cad-skillx/planning/mounting-bracket.planning.md` — mounting bracket, support bracket, L/U/T bracket, bracket with holes, web/lug/tab/boss, `安装支架`, `支架`.
- `references/cad-skillx/planning/mounting-plate.planning.md` — mounting plate, base plate, adapter plate, fixture plate, flat plate with holes, `安装板`, `安装底板`, `安装版`.
- `references/cad-skillx/planning/simple-shaft-or-cylindrical-rod.planning.md` — simple shaft, cylindrical rod, pin, standoff, stepped shaft, end hole, `轴`, `阶梯轴`, `圆柱杆`, `销轴`.
Functional skills:
- `references/cad-skillx/functional/axisymmetric-revolve-strategy.functional.md` — read for axisymmetric/turned bodies, stepped shafts, bushings, washers, sleeves, flange hubs, grooves, or revolved cuts.
- `references/cad-skillx/functional/bearing-bore-seat.functional.md` — read with bearing housing/seat requests where a shaft clearance bore, bearing pocket, shoulder, or seat depth is required.
- `references/cad-skillx/functional/flange-bolt-circle.functional.md` — read with flange requests containing bolt circle, pitch circle, PCD, circular bolt pattern, or evenly spaced flange holes.
- `references/cad-skillx/functional/mounting-plate-hole-layout.functional.md` — read with mounting/base/adapter plates containing rectangular hole grids, edge margins, slots, threaded holes, counterbores, or countersinks.
- `references/cad-skillx/functional/slotted-adjustment-feature.functional.md` — read when the model needs slots, slotted holes, elongated holes, guide slots, or adjustment slots.
- `references/cad-skillx/functional/standard-hole-wizard.functional.md` — read whenever standard clearance holes, threaded/tapped holes, counterbores, countersinks, dowel holes, or screw-seat holes are required.
- `references/cad-skillx/functional/symmetric-feature-layout.functional.md` — read when holes, bosses, tabs, slots, cutouts, or ribs are mirrored, centered, or symmetric about a centerline/midplane.
Atomic skills:
- `references/cad-skillx/atomic/coaxial-bore-rule.atomic.md` — read when bore, pocket, hub, sleeve, bearing seat, shaft clearance, pilot diameter, or counterbore must share one axis.
- `references/cad-skillx/atomic/counterbored-hole-creation.atomic.md` — read for counterbored holes, socket head/cap screw seats, flush screw-head recesses.
- `references/cad-skillx/atomic/extrude-base-profile.atomic.md` — read when the base body is an extruded rectangle, circle, annulus, hexagon, block, plate blank, bracket base, or simple cylinder.
- `references/cad-skillx/atomic/fillet-chamfer-last.atomic.md` — read when finishing edges with chamfers, fillets, deburring, lead-ins, or stress-relief rounds.
- `references/cad-skillx/atomic/pattern-holes-from-datum.atomic.md` — read for repeated holes controlled by pitch, angle, symmetry, linear pattern, circular pattern, mirror pattern, or bolt pattern.
- `references/cad-skillx/atomic/revolve-profile-around-axis.atomic.md` — read for individual revolve/revolved-cut operations around a construction axis.
- `references/cad-skillx/atomic/threaded-hole-creation.atomic.md` — read for internal threaded/tapped holes such as M3/M4/M5/M6/M8/M10.
- `references/cad-skillx/atomic/through-hole-cut.atomic.md` — read for plain through holes, clearance holes, central holes, circular openings, or bore-through cuts.
Recommended CAD-SkillX combinations:
- Bearing housing / `轴承座`: planning bearing housing + functional bearing bore seat + atomic coaxial bore; add standard hole wizard, pattern holes, counterbore/threaded/through-hole, and fillet-chamfer-last only if those features are requested.
- Flange / `法兰`: planning flange + functional flange bolt circle + atomic coaxial bore + pattern holes from datum; add axisymmetric revolve when the flange has a hub, sleeve, steps, grooves, or turned profile.
- Mounting plate / `安装板`: planning mounting plate + functional mounting plate hole layout; add standard hole wizard, symmetric feature layout, slots, pattern holes, counterbored/threaded/through holes, and fillet-chamfer-last as needed.
- Mounting bracket / `安装支架`: planning mounting bracket; add standard hole wizard, symmetric feature layout, slotted adjustment, extrude base profile, pattern holes, and fillet-chamfer-last as needed.
- Simple shaft / cylindrical rod / `轴`: planning simple shaft or cylindrical rod; add axisymmetric revolve or revolve-profile-around-axis for steps/shoulders/grooves, threaded-hole creation for centered end holes, and fillet-chamfer-last for end edge breaks.
- Hexagonal nut / `六角螺母`: planning hexagonal nut + atomic threaded-hole creation + fillet-chamfer-last; add revolve-profile-around-axis only when using a revolved blank or revolved thread/counterbore.
<!-- cad-skillx:end:skill-pack-curated-20260807 -->
Final responses should include generated files, returned `$cad-viewer` viewer links, verification snapshots, validation actually run, assumptions, and caveats. Use `references/inspection-and-validation.md` for report structure.
@@ -0,0 +1,83 @@
# Coaxial Bore Atomic Skill
## When to use
- A model contains nested cylindrical features that must share one axis: bore, pocket, hub, sleeve, shaft clearance, bearing seat, pilot diameter, or counterbore.
- The request mentions concentric, coaxial, central bore, shaft hole, bearing pocket, or flanged cylindrical geometry.
## Do not use when
- Circular holes are independent mounting holes on a plate face.
- The circular features intentionally use different centers.
- The shape is decorative and has no functional axis.
## Recognition features
- Central bore or shaft clearance through a rotational or support body.
- Concentric circles in sketch evidence.
- Cylindrical faces nested around one axis.
- Bearing seat, flange hub, sleeve, bushing, washer, spacer, or shaft-like part.
## Core invariants
- Use one explicit construction axis or shared sketch origin for all central cylindrical features.
- Distinguish each diameter by role: clearance, seat, counterbore, outer diameter, pilot, groove.
- Cut through holes through all relevant material; cut seats and counterbores only to their functional depth.
- Do not create separate sketches whose centers can drift.
## Parameter roles
- `axis` is the shared centerline.
- `bore_diameter` controls the through opening.
- `seat_diameter` controls bearing/pilot/counterbore fit.
- `outer_diameter` controls surrounding material.
- `depth` controls blind pocket, counterbore, or groove depth.
- `lead_in_chamfer` controls assembly entry.
## Construction sequence
1. Define the shared axis using the sketch origin, construction line, or selected cylindrical face.
2. Sketch concentric circles or a revolved profile from that axis.
3. Cut the smallest through bore first when it establishes shaft clearance.
4. Cut larger seats, pockets, or counterbores to controlled depth.
5. Add grooves, chamfers, and fillets after the primary coaxial cuts are stable.
## Common failures
- Off-axis bore because each circle was sketched from a different center.
- Confusing through bore diameter with bearing seat diameter.
- Cutting the bearing pocket through all and deleting the shoulder.
- Treating unrelated bolt holes as coaxial features.
## Evidence summary
- Current library evidence: 50 reviewed SCAD58 flange and bearing-seat models.
- Latest run evidence: accepted insights for central bore alignment, concentric circles, bearing seats, flanges, washers, bushings, and axisymmetric bodies.
- Frequent operations in the latest run: Cut, RevCut, Revolution, Extrusion of concentric circles, Chamfer.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `coaxial bore`
- `central bore`
- `concentric`
- `shaft clearance`
Secondary triggers:
- `bearing seat`
- `pilot bore`
- `counterbore`
- `hub`
- `sleeve`
Operation triggers:
- `concentric sketch`
- `cut through bore`
- `counterbore to depth`
- `revolved cut`
Exclusions:
- `random face holes`
- `rectangular hole grid`
- `offset holes`
@@ -0,0 +1,89 @@
# Counterbored Hole Creation Atomic Skill
## When to use
- A fastener hole needs a cylindrical recess so a socket head, cap screw head, or flat seating surface sits flush or below the top face.
- The request mentions counterbore, counterbored hole, socket head screw, cap screw, recessed screw head, or flat-bottom screw seat.
## Do not use when
- The screw head requires a conical countersink rather than a cylindrical counterbore.
- The feature is a bearing pocket, large cylindrical recess, decorative ring, or simple through hole.
- The part has no fastener-seat function.
## Recognition features
- Through hole plus larger coaxial shallow cylindrical recess.
- HoleWzd evidence for counterbored screw holes.
- Descriptions mention recessed screw heads, socket head screws, counterbore depth, or flush mounting.
- Often appears on mounting plates, brackets, flanges, and covers.
## Core invariants
- Counterbore axis and through-hole axis are identical.
- Counterbore diameter must be larger than through-hole diameter.
- Counterbore depth must be enough for the screw head but not cut through the part unless requested.
- Create the hole as one standard feature when possible to preserve fastener intent.
- Pattern the complete counterbored hole feature when repeated.
## Parameter roles
- `through_hole_diameter` defines screw clearance.
- `counterbore_diameter` defines head clearance.
- `counterbore_depth` defines head seating depth.
- `fastener_size` defines standard screw selection.
- `placement_point` defines hole center.
- `end_condition` defines through, blind, or up-to-next behavior.
## Construction sequence
1. Select the mounting face where the screw head sits.
2. Place the hole center from datums or pattern references.
3. Create a counterbored hole feature with through diameter, counterbore diameter, and counterbore depth.
4. Confirm the counterbore opens on the correct face.
5. Pattern or mirror the full counterbored hole feature if repeated.
6. Add small entrance chamfers only after the counterbore is correct.
## Common failures
- Modeling only a shallow large circle without the through hole.
- Reversing the counterbore to the wrong side of the part.
- Making counterbore and through-hole diameters equal.
- Using countersink geometry for a socket head screw.
- Patterning only the through hole and forgetting the recess.
## Evidence summary
- Latest run evidence: 49 accepted atomic insights matched counterbored hole creation.
- Strong repeated names include `create_counterbored_hole`, `use_hole_wizard_for_counterbored_holes`, and `create_counterbore_hole_for_socket_head_screw`.
- Frequent operations in the latest run: HoleWzd, Cut, LinearPattern, MirrorPattern, Chamfer.
- Source model examples: 020555, 028735, 031527, 079425, 080632, 086114, 096301, 144289, 146696, 174675, 202122, 211326, 217613, 225627, 227232.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `counterbored hole`
- `counterbore`
- `socket head screw`
- `cap screw`
- `recessed screw head`
Secondary triggers:
- `flush screw`
- `counterbore depth`
- `counterbore diameter`
- `mounting hole`
- `fastener seat`
Operation triggers:
- `hole wizard counterbore`
- `cut counterbore`
- `pattern counterbored hole`
- `chamfer hole edge`
Exclusions:
- `countersink`
- `bearing pocket`
- `decorative ring`
- `plain through hole`
@@ -0,0 +1,87 @@
# Extrude Base Profile Atomic Skill
## When to use
- The first solid body is a simple prismatic shape made from a closed 2D profile: rectangle, rounded rectangle, circle, annulus, hexagon, or simple contour.
- The part is a plate, block, simple cylinder, bracket base, washer blank, spacer blank, or nut body made by extrusion.
## Do not use when
- The primary body has multiple coaxial diameter steps better made by revolve.
- The shape is freeform, lofted, swept, bent sheet metal, or surface-based.
- The extrusion profile is open or ambiguous.
## Recognition features
- SolidWorks evidence such as Extrusion/Boss-Extrude after a single sketch.
- STEP evidence dominated by planar faces for prismatic parts or cylindrical surfaces for simple cylinders.
- Descriptions mention rectangular block, plate, simple cylinder, extruded profile, or uniform thickness.
## Core invariants
- Start from a fully closed, constrained sketch.
- Extrude normal to the sketch plane by the intended thickness or length.
- Use symmetric or mid-plane extrusion when the part should stay centered on a datum.
- Keep the base body simple; add holes, pockets, bosses, slots, and edge treatments as later features.
- Use concentric circles for annular extrusions such as washers or tubes.
## Parameter roles
- `profile_type` selects rectangle, circle, annulus, hexagon, or custom closed contour.
- `length`, `width`, `radius`, `outer_radius`, `inner_radius`, and `across_flats` define the sketch.
- `extrude_depth` or `thickness` defines the solid depth.
- `extrude_direction` and `midplane` define placement relative to datums.
## Construction sequence
1. Select the reference plane or face for the base profile.
2. Draw and constrain the closed profile.
3. Apply dimensional constraints for width, length, radius, or diameter.
4. Extrude the profile to the required depth.
5. Keep the resulting body merged as the main solid unless separate bodies are explicitly requested.
6. Add secondary cuts and edge treatments afterward.
## Common failures
- Using an open sketch for solid extrusion.
- Combining holes and detailed cutouts into the first sketch when separate features would be clearer.
- Using multiple separate extrusions for what should be one base body.
- Extruding a stepped shaft when revolve would preserve axial intent better.
## Evidence summary
- Latest run evidence: 70 accepted atomic insights matched base extrusion from rectangular/circular profiles.
- Strong repeated names include `extrude_rectangle`, `extrude_circle`, `extrude_rectangle_from_sketch`, `extrude_rectangular_profile`, and `extrude_circular_profile`.
- Frequent operations in the latest run: Extrusion, Sketch, Cut, Chamfer.
- Source model examples: 000166, 026303, 050972, 053267, 088851, 114800, 148444, 157630, 162045, 188147, 209994, 220726, 237160, 238926.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `extrude base profile`
- `extrude rectangle`
- `extrude circle`
- `base extrusion`
- `simple cylinder`
- `rectangular block`
Secondary triggers:
- `plate blank`
- `bracket base`
- `washer blank`
- `annular extrusion`
- `uniform thickness`
Operation triggers:
- `sketch closed profile`
- `boss extrude`
- `extrude to thickness`
- `midplane extrusion`
Exclusions:
- `stepped shaft`
- `revolve profile`
- `loft`
- `sweep`
- `sheet metal bend`
@@ -0,0 +1,82 @@
# Fillet Chamfer Last Atomic Skill
## When to use
- A machined part needs edge breaks, deburring chamfers, assembly lead-ins, stress-relief fillets, or small finishing rounds.
- The edge treatment is secondary to the main body, holes, pockets, slots, and bores.
## Do not use when
- The rounded or chamfered profile is the primary defining shape, such as an O-ring groove, cam surface, ergonomic shell, or turbine blade.
- A sharp edge is explicitly required for sealing, locating, scraping, or mating.
- The chamfer/fillet would remove critical wall thickness around a hole, shoulder, or thin rib.
## Recognition features
- SolidWorks feature evidence such as Chamfer, Fillet, radius values, or angle-distance chamfers.
- STEP evidence with conical or toroidal surfaces near final edges.
- Descriptions mentioning chamfered edges, rounded corners, smooth transitions, edge relief, assembly ease, or stress reduction.
## Core invariants
- Apply edge treatments after primary solids, holes, bores, pockets, slots, and patterns are stable.
- Keep edge treatment small relative to nearby feature size and wall thickness.
- Use chamfer for lead-in/deburring and fillet for stress relief or smooth load transition.
- Select specific edges; do not blindly fillet or chamfer every edge.
- Preserve functional shoulders, datum faces, seal lands, and fastener seats.
## Parameter roles
- `chamfer_distance` defines bevel size.
- `chamfer_angle` defines bevel angle, often 45 degrees when not otherwise specified.
- `fillet_radius` defines round size.
- `edge_set` defines which edges receive treatment.
- `minimum_wall_after_edge_treatment` protects small holes, ribs, and pockets.
## Construction sequence
1. Complete the main body and all functional cuts.
2. Identify functional edges that need lead-in, deburring, stress relief, or handling safety.
3. Apply chamfers to hole entrances, exposed outer edges, or insertion edges.
4. Apply fillets to internal corners, web transitions, and load-bearing transitions.
5. Verify that holes, slots, shoulders, and bearing seats remain intact.
## Common failures
- Adding large fillets early and causing later cuts or patterns to fail.
- Chamfering a bearing shoulder, seal land, or datum face that should remain crisp.
- Using one global fillet radius across tiny holes and large outer edges.
- Treating decorative edge rounding as more important than functional geometry.
## Evidence summary
- Current library evidence: 58 reviewed SCAD58 machined parts with final chamfer/fillet operations.
- Latest run evidence: high-frequency accepted insights for `chamfer_edges`, `chamfer_edges_for_safety`, `apply_chamfer_to_edges`, `apply_fillet_to_edges`, and stress-relief fillets.
- Frequent operations in the latest run: Chamfer and Fillet after Extrusion, Cut, HoleWzd, Pattern, and Revolution.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `fillet chamfer last`
- `edge treatment`
- `chamfered edges`
- `filleted edges`
Secondary triggers:
- `deburr`
- `assembly lead in`
- `stress relief`
- `smooth transition`
- `edge break`
Operation triggers:
- `chamfer`
- `fillet`
- `apply edge treatment after cuts`
Exclusions:
- `primary rounded profile`
- `sharp datum edge`
- `seal land`
- `cam surface`
@@ -0,0 +1,86 @@
# Pattern Holes From Datum Atomic Skill
## When to use
- A model has repeated holes or repeated cut features that should be controlled by pitch, angle, symmetry, or a construction datum.
- The design intent is a hole row, hole grid, mirrored pair, circular bolt circle, or repeated mounting pattern.
## Do not use when
- There is only one custom non-repeated hole.
- Holes are intentionally irregular or individually dimensioned.
- The feature is decorative and not constrained by functional layout.
## Recognition features
- Multiple holes with equal spacing, equal angular spacing, or mirrored positions.
- SolidWorks evidence such as LinearPattern, CircularPattern, MirrorPattern, or repeated HoleWzd features.
- Datum clues such as centerlines, edge offsets, pitch dimensions, central axis, or symmetry planes.
## Core invariants
- Build one correct seed hole or seed cut feature first.
- Constrain the seed from a datum: edge offset, centerline, pitch circle, or axis.
- Pattern the feature/cut, not only sketch circles.
- Choose pattern type from geometry: linear for rows/grids, circular for flange bolt circles, mirror for symmetric pairs.
- Keep the pattern editable by count, pitch, angle, and axis rather than hard-coded coordinates.
## Parameter roles
- `seed_position` defines the first hole from the datum.
- `pitch` defines linear spacing.
- `count` defines number of repeated instances.
- `pattern_direction` defines row/grid direction.
- `pattern_axis` defines circular pattern axis.
- `total_angle` is usually 360 degrees for full bolt circles.
- `mirror_plane` defines symmetric duplication.
## Construction sequence
1. Define the datum: edge offset, centerline, symmetry plane, or central axis.
2. Create and validate one seed hole or cut feature.
3. Select linear, circular, or mirror pattern based on the part family and layout.
4. Pattern the feature with explicit count and spacing/angle.
5. Add shared counterbore/countersink/thread detail before patterning when all instances match.
6. Verify clearance to edges, central bores, slots, and neighboring holes.
## Common failures
- Duplicating sketch circles without actual cuts.
- Using linear pattern for a flange bolt circle.
- Using circular pattern for rectangular plate holes.
- Patterning counterbore circles without patterning the underlying through hole.
- Missing the datum so later size changes break alignment.
## Evidence summary
- Current library evidence: 58 reviewed SCAD58 models with repeated hole patterns.
- Latest run evidence: accepted insights for linear repeated holes, mirror symmetry, symmetric hole placement, circular bolt patterns, and datum-driven construction geometry.
- Frequent operations in the latest run: HoleWzd, Cut, LinearPattern, MirrorPattern, CircularPattern.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `hole pattern`
- `pattern holes from datum`
- `repeated holes`
- `symmetric holes`
Secondary triggers:
- `linear pattern`
- `circular pattern`
- `mirror pattern`
- `bolt holes`
- `hole grid`
Operation triggers:
- `pattern cut feature`
- `linear pattern`
- `circular pattern`
- `mirror`
Exclusions:
- `single custom hole`
- `decorative circles`
- `irregular holes`
@@ -0,0 +1,87 @@
# Revolve Profile Around Axis Atomic Skill
## When to use
- A single feature is created by revolving a 2D profile around an axis: stepped cylinder, shaft shoulder, sleeve, bushing, washer groove, nut blank, knob, or circular recess.
- The feature has a rotational cross-section that is easier to define in side view.
## Do not use when
- A plain constant-diameter cylinder can be created more simply by extruding a circle.
- The profile is open, crosses the axis incorrectly, or represents non-axisymmetric geometry.
- The feature is a rectangular plate, bracket wall, or arbitrary cutout.
## Recognition features
- Revolution/Revolve/RevCut feature evidence.
- Sketch contains an axis line or construction centerline.
- Multiple coaxial cylindrical, conical, or toroidal surfaces are expected.
- Profile controls axial steps, shoulders, tapers, grooves, or rounded transitions.
## Core invariants
- The revolve axis must be explicit and stable.
- The profile must be closed for material-adding revolve.
- Keep the profile on the correct side of the axis to avoid self-intersection.
- Use 360 degrees for complete rotational solids unless a partial sector is requested.
- Use revolved cut, not boss revolve, when removing a groove or internal recess.
## Parameter roles
- `axis` defines the rotation center.
- `profile` defines the closed side-view outline.
- `revolve_angle` defines full or partial rotation.
- `diameter_steps`, `axial_lengths`, `groove_depth`, and `taper_angle` define shape.
- `operation_type` selects add or cut.
## Construction sequence
1. Create a sketch on a plane that contains the desired axis.
2. Draw a construction axis or select a stable centerline.
3. Draw the closed profile for added material, or the removal profile for a revolved cut.
4. Revolve the profile around the axis, usually 360 degrees.
5. Validate that the result is one solid and all intended circular features are coaxial.
6. Add later holes, flats, slots, patterns, chamfers, and fillets as separate features.
## Common failures
- Revolving around the wrong edge or temporary line.
- Letting the sketch cross the axis and causing invalid geometry.
- Making several extruded cylinders instead of a single coherent turned profile.
- Using revolve for non-axisymmetric bracket or plate features.
## Evidence summary
- Latest run evidence: 81 accepted atomic insights matched revolve, revolved profile, or revolved cut operations.
- Strong repeated names include `revolve_profile`, `create_revolved_cut`, `revolve_profile_around_axis`, and `revolved_cut_for_internal_thread`.
- Frequent operations in the latest run: Revolution, RevCut, Sketch, Cut, Chamfer, Fillet.
- Source model examples: 009934, 010137, 015133, 026624, 053895, 079633, 086237, 094412, 139073, 146478, 161210, 162241, 222550, 238044.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `revolve profile`
- `revolve around axis`
- `revolved cut`
- `axisymmetric profile`
Secondary triggers:
- `shaft shoulder`
- `turned groove`
- `bushing`
- `washer`
- `nut blank`
- `stepped diameter`
Operation triggers:
- `draw construction axis`
- `revolve 360 degrees`
- `revolved cut`
- `sketch half profile`
Exclusions:
- `plain extruded cylinder`
- `rectangular plate`
- `bracket wall`
- `loft`
@@ -0,0 +1,93 @@
# Threaded Hole Creation Atomic Skill
## When to use
- A part needs an internal thread, tapped hole, threaded mounting hole, or screw hole with a specified metric/imperial size.
- The request mentions M3, M4, M5, M6, M8, M10, tapped hole, threaded hole, or thread depth.
## Do not use when
- The feature is an external thread on a bolt, screw, or shaft.
- The hole is only a smooth clearance hole.
- The model should avoid thread geometry and only show a simple bore unless the prompt asks for thread detail.
## Recognition features
- SolidWorks evidence such as HoleWzd threaded hole or thread parameters.
- Descriptions mention fastening, tapped holes, internal thread, or screw mounting.
- Hole is located on a planar face, boss face, or shaft end face.
## Core invariants
- Use a standard threaded hole feature or thread callout when available.
- Keep the threaded hole axis normal to the placement face unless angled threading is requested.
- Separate tap drill diameter, nominal thread size, hole depth, and thread depth.
- Keep enough wall material around the threaded hole.
- Add lead-in chamfer after the thread/hole is placed.
## Parameter roles
- `thread_size` defines nominal size such as M3/M4/M5/M6/M8/M10.
- `thread_pitch` defines fine/coarse pitch when specified.
- `thread_depth` defines threaded length.
- `hole_depth` defines blind hole depth if not through.
- `placement_point` and `face_normal` define location and direction.
- `lead_in_chamfer` defines assembly entry.
## Construction sequence
1. Select the planar face or end face for the threaded hole.
2. Place the hole center from datums, centerlines, or axis references.
3. Create the hole using a threaded HoleWzd/hole feature with size and depth.
4. If the backend cannot model threads, create the correct pilot bore and preserve thread intent in naming/comments.
5. Pattern or mirror the threaded hole feature when repeated.
6. Add a small lead-in chamfer after the threaded feature is stable.
## Common failures
- Modeling a threaded hole as a plain through cylinder.
- Creating external thread geometry on an internal hole.
- Placing threaded holes too close to edges, slots, or pockets.
- Forgetting blind depth or thread depth.
- Adding visual helical threads that make the model heavy or fragile when a callout is enough.
## Evidence summary
- Latest run evidence: 87 accepted atomic insights matched threaded/tapped hole creation.
- Strong repeated names include `create_threaded_hole_with_hole_wizard`, `create_threaded_hole`, `threaded_hole_placement`, and `use_hole_wizard_for_threaded_holes`.
- Frequent operations in the latest run: HoleWzd, Cut, RevCut, Chamfer, Pattern.
- Source model examples: 000924, 011524, 017151, 047238, 077661, 080683, 095520, 105855, 145389, 160434, 174675, 200790, 206894, 211326, 225767.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `threaded hole`
- `tapped hole`
- `internal thread`
- `M3`
- `M4`
- `M5`
- `M6`
- `M8`
- `M10`
Secondary triggers:
- `thread depth`
- `tap drill`
- `screw mounting`
- `threaded boss`
- `end threaded hole`
Operation triggers:
- `hole wizard threaded hole`
- `create tapped hole`
- `add thread callout`
- `lead in chamfer`
Exclusions:
- `external thread`
- `bolt`
- `screw`
- `clearance hole only`
- `decorative helix`
@@ -0,0 +1,89 @@
# Through Hole Cut Atomic Skill
## When to use
- A part needs a clean circular hole or bore that passes fully through a plate, block, bracket wall, flange, washer, spacer, or boss.
- The request mentions through hole, clearance hole, central hole, circular opening, bore through, or cut through all.
## Do not use when
- The hole is threaded, counterbored, countersunk, or a bearing seat requiring special geometry.
- The circular feature is a blind pocket, groove, decorative recess, or non-through relief.
- The hole is part of a patterned bolt circle; combine with the pattern skill for repeated holes.
## Recognition features
- Circular sketch on a planar face followed by Cut-Extrude or HoleWzd through all.
- Cylindrical internal face runs through the full thickness.
- Used for clearance, alignment, lightening, shaft passage, or central opening.
## Core invariants
- Hole axis should be normal to the face or coaxial with the main part axis.
- Use through-all end condition when the hole must pass through the entire body.
- Keep hole diameter and placement explicit.
- For central holes, use the part axis or centerline as the placement reference.
- For repeated holes, make one seed through hole and pattern it.
## Parameter roles
- `hole_diameter` defines opening size.
- `placement_point` defines center on the start face.
- `axis` defines coaxial central placement when needed.
- `end_condition` is usually through all.
- `face` defines the start plane.
## Construction sequence
1. Select the face or reference plane where the hole starts.
2. Place a circle center from datums, centerlines, or the main axis.
3. Sketch the circle with the required diameter.
4. Cut through all material in the intended direction.
5. Pattern or mirror the through-hole cut if repeated.
6. Add hole edge chamfers only after the cut exists.
## Common failures
- Creating a shallow blind pocket when a through hole is required.
- Leaving a sketch circle without cutting material.
- Placing a central hole off-axis.
- Treating threaded/counterbored/bearing holes as plain through holes.
- Cutting through unintended bodies in an assembly-like model.
## Evidence summary
- Latest run evidence: 49 accepted atomic insights matched through-hole and circular-cut creation.
- Strong repeated names include `create_through_hole`, `cut_circular_hole`, `cut_circle`, `cut_central_bore_through_plate`, and `create_through_holes_with_cut`.
- Frequent operations in the latest run: Cut, HoleWzd, CircularPattern, LinearPattern, Chamfer.
- Source model examples: 001563, 001942, 046907, 054633, 059866, 080145, 086131, 096474, 142955, 148675, 153729, 160077, 222192, 230562, 241179.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `through hole`
- `clearance hole`
- `central hole`
- `circular opening`
- `cut through`
- `bore through`
Secondary triggers:
- `shaft passage`
- `alignment hole`
- `lightening hole`
- `hole through plate`
- `hole through boss`
Operation triggers:
- `cut extrude through all`
- `cut circle`
- `hole wizard through hole`
- `pattern through hole`
Exclusions:
- `threaded hole`
- `counterbored hole`
- `countersink`
- `bearing seat`
- `blind pocket`
@@ -0,0 +1,91 @@
# Axisymmetric Revolve Strategy Functional Skill
## When to use
- A part or major feature is rotationally symmetric around an axis: shaft, stepped shaft, flange hub, bushing, washer, spacer, sleeve, pulley-like blank, turned knob, or nut blank.
- The profile contains steps, shoulders, grooves, tapers, spherical/rounded transitions, or coaxial bores that are easier to define in a half-section.
## Do not use when
- The body is a simple constant-diameter cylinder that can be created by one circle extrusion.
- The part is primarily prismatic with only small cylindrical holes.
- The geometry is freeform, non-axisymmetric, or has asymmetric lugs as the dominant feature.
## Recognition features
- Dominant central axis and circular cross-sections.
- SolidWorks features such as Revolution, Revolve, RevCut, or revolved thread/groove cuts.
- STEP evidence with multiple cylindrical, conical, or toroidal surfaces sharing an axis.
- Functional roles include shafting, spacing, sealing, rotation, alignment, or fastening.
## Core invariants
- Sketch the axial cross-section on a plane that contains the rotation axis.
- Keep the revolved profile on one side of the axis unless the CAD kernel expects a centerline profile.
- Use one 360-degree revolve for the main axisymmetric body where possible.
- Use revolved cuts for coaxial grooves, internal thread reliefs, and circular recesses.
- Add asymmetric holes, flats, keyways, or bolt patterns after the revolved body is complete.
## Parameter roles
- `axis` defines the rotation centerline.
- `profile_points` define the half-section outline.
- `diameters` and `axial_lengths` define steps and shoulders.
- `revolve_angle` is usually 360 degrees for a complete solid.
- `groove_width`, `groove_depth`, `taper_angle`, and `fillet_radius` define turned details.
## Construction sequence
1. Define the central axis and an axial sketch plane.
2. Draw the half-section profile with all major diameters, steps, shoulders, and bore boundaries.
3. Revolve the closed profile 360 degrees to create the main body.
4. Use revolved cuts for grooves, recesses, or thread reliefs that are also axisymmetric.
5. Add non-axisymmetric secondary features such as flats, slots, keyways, or bolt holes.
6. Apply chamfers and fillets at ends and shoulders last.
## Common failures
- Extruding several cylinders separately and leaving seams or separate bodies.
- Revolving a profile that crosses the axis and creates invalid self-intersections.
- Forgetting the construction axis or using an arbitrary edge as axis.
- Adding asymmetric features before the revolve and making the base profile ambiguous.
- Using revolve for a simple plain cylinder when extrusion is clearer.
## Evidence summary
- Latest run evidence: 105 accepted functional insights matched axisymmetric revolve and revolved-cut strategies.
- Strong repeated names include `use_revolve_for_axisymmetric_parts`, `use_revolve_for_rotational_parts`, `use_revolve_for_axisymmetric_body`, and `use_revolution_for_axisymmetric_features`.
- Frequent operations in the latest run: Revolution, RevCut, Cut, Chamfer, Fillet, HoleWzd.
- Source model examples: 001563, 001957, 009934, 010137, 025245, 053895, 080683, 086232, 094412, 135604, 144887, 162241, 173716, 218803, 225038.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `axisymmetric`
- `revolve`
- `revolved body`
- `rotational part`
- `turned part`
Secondary triggers:
- `shaft`
- `bushing`
- `washer`
- `sleeve`
- `flange hub`
- `stepped diameter`
- `groove`
Operation triggers:
- `sketch half profile`
- `revolve profile`
- `revolved cut`
- `define construction axis`
- `chamfer shoulders`
Exclusions:
- `simple extruded cylinder`
- `rectangular plate`
- `mounting bracket`
- `freeform surface`
@@ -0,0 +1,90 @@
# Bearing Bore Seat Functional Skill
## When to use
- A part must locate a bearing, bushing, sleeve, or shaft using a coaxial bore, counterbore, pocket, shoulder, or retaining seat.
- The requested model is a housing/seat component, bracketed bearing support, bushing support, or bearing retainer.
## Do not use when
- The part only has generic decorative holes or a simple washer hole.
- The task is to create a full bearing assembly with rolling elements.
- The central hole is only a screw clearance hole and not a shaft/bearing datum.
## Recognition features
- Through bore for shaft clearance.
- Larger coaxial pocket for bearing outside diameter.
- Shoulder or depth stop for axial location.
- Optional retaining groove, cover recess, set-screw hole, lubrication hole, or mounting base.
- Bore entry chamfers for assembly.
## Core invariants
- Shaft clearance bore and bearing seat remain coaxial.
- Seat diameter is larger than shaft clearance diameter when a bearing pocket is present.
- Seat depth must be represented; a bearing seat is not just a surface ring.
- Shoulder geometry should remain after the pocket cut.
- Mounting holes must not break into the bearing seat unless explicitly designed.
## Parameter roles
- `axis` defines the bore centerline.
- `shaft_clearance_diameter` defines the through hole.
- `bearing_outer_diameter` defines the pocket/seat diameter.
- `seat_depth` defines bearing insertion depth.
- `shoulder_diameter` and `shoulder_height` define axial stop.
- `retaining_groove_width` and `retaining_groove_depth` define optional retention.
## Construction sequence
1. Create or select the main housing body.
2. Establish a construction axis through the intended shaft centerline.
3. Cut the shaft clearance bore through all relevant material.
4. Cut the bearing seat/counterbore to controlled depth from the assembly side.
5. Preserve or create the shoulder that stops the bearing.
6. Add retaining grooves, set-screw holes, lubrication holes, and mounting holes only after the bore/seat is correct.
7. Apply bore lead-in chamfers and small external fillets last.
## Common failures
- Making the seat blind depth ambiguous.
- Losing the shoulder by cutting the same diameter through the whole body.
- Off-axis bearing pocket relative to shaft bore.
- Treating bearing housing as complete bearing assembly.
- Adding mounting holes before the seat and accidentally breaking the pocket wall.
## Evidence summary
- Current library evidence: 24 reviewed SCAD58 bearing housing/seat models.
- Latest run evidence: accepted insights for central bore alignment, coaxial cylindrical features, counterbore/seat construction, and bore chamfering.
- Frequent operations in the latest run: Cut, RevCut, Revolution, HoleWzd, Chamfer, Fillet.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `bearing bore seat`
- `bearing pocket`
- `bearing seat`
- `bushing bore`
- `shaft clearance bore`
Secondary triggers:
- `coaxial bore`
- `counterbore`
- `shoulder`
- `retaining groove`
- `bearing housing`
Operation triggers:
- `cut through bore`
- `counterbore to depth`
- `preserve shoulder`
- `chamfer bore`
Exclusions:
- `flange only`
- `plain washer`
- `decorative circular hole`
- `complete rolling bearing`
@@ -0,0 +1,86 @@
# Flange Bolt Circle Functional Skill
## When to use
- A flange, circular cover, annular ring, coupling, or flanged sleeve needs fastener holes evenly spaced around a central bore.
- The request mentions bolt circle, pitch circle, PCD, evenly spaced flange holes, or circular pattern of screws.
## Do not use when
- Holes are in a rectangular grid, linear row, or mirrored pair on a plate or bracket.
- The part has no central axis or central bore.
- The holes are decorative circular cutouts rather than fastening holes.
## Recognition features
- Central bore or hub defines the rotation axis.
- Bolt holes lie on a pitch circle concentric with the bore.
- Angular spacing is equal: `360 / bolt_count`.
- Optional counterbores or countersinks remain concentric with each bolt hole.
## Core invariants
- Bolt-circle center, central bore, hub, and outer diameter share the same axis.
- Create one correct seed hole with the correct diameter and seat type.
- Circular-pattern the hole feature, not just sketch circles.
- Keep bolt count, pitch-circle diameter, and start angle explicit.
- Add counterbore/countersink to the seed hole before patterning when all holes share the same fastener type.
## Parameter roles
- `bolt_count` controls number of fastener holes.
- `bolt_circle_diameter` or `pitch_circle_diameter` controls radial placement.
- `bolt_hole_diameter` defines clearance or pilot size.
- `start_angle` controls angular orientation relative to a datum.
- `counterbore_diameter`, `counterbore_depth`, and `countersink_angle` define fastener seating.
## Construction sequence
1. Define the central axis from the flange bore or construction geometry.
2. Place one seed hole center at radius `bolt_circle_diameter / 2`.
3. Create the seed hole as a cut, HoleWzd hole, counterbore, or countersink according to the fastener.
4. Use circular pattern around the central axis for `bolt_count` instances over 360 degrees.
5. Verify that every patterned hole cuts through the flange and remains inside the outer diameter.
6. Add final chamfers and fillets after the bolt pattern is complete.
## Common failures
- Manually placing each hole and producing uneven angular spacing.
- Patterning a sketch without cutting material.
- Using a linear array on a circular flange.
- Choosing pitch circle too close to the bore or outer edge.
- Forgetting counterbore/countersink depth for flush fasteners.
## Evidence summary
- Current library evidence: 45 reviewed SCAD58 flange-like models.
- Latest run evidence: accepted atomic and functional insights for circular pattern bolt holes, central bore alignment, and axisymmetric flange bodies.
- Frequent operations in the latest run: CircularPattern, HoleWzd, Cut, Revolution, Chamfer.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `flange bolt circle`
- `bolt circle`
- `pitch circle`
- `PCD`
- `circular bolt pattern`
Secondary triggers:
- `flange holes`
- `evenly spaced holes`
- `central bore`
- `counterbored flange holes`
Operation triggers:
- `circular pattern`
- `hole wizard`
- `pattern cut feature`
- `cut bolt holes`
Exclusions:
- `linear hole grid`
- `mounting plate row`
- `random holes`
- `side tab holes`
@@ -0,0 +1,88 @@
# Mounting Plate Hole Layout Functional Skill
## When to use
- A plate or base block needs holes, slots, counterbores, countersinks, or threaded holes positioned from datum edges or centerlines.
- The layout is rectangular, row-based, symmetric, mirrored, or grid-like rather than circular around a flange axis.
- The hole layout controls mounting, alignment, adjustment, or fastening.
## Do not use when
- Holes are evenly spaced around a circular bolt circle on a flange.
- The holes are decorative, random, or not functionally related.
- The main operation is a bearing bore or central shaft seat rather than a mounting-hole layout.
## Recognition features
- Flat planar top face or side face used as the hole placement plane.
- Edge margins, centerline offsets, pitch values, or symmetric pairs.
- Repeated hole groups, slots, threaded holes, clearance holes, counterbores, countersinks, or dowel holes.
- SolidWorks evidence often appears as HoleWzd, Cut, LPattern, MirrorPattern, and Chamfer.
## Core invariants
- Define plate datums before hole placement.
- Keep hole centers constrained to margins, pitch, and symmetry instead of free coordinates.
- Select the hole type from fastening function, not from appearance.
- Pattern the cut or hole feature after the seed feature is correct.
- Preserve minimum material around holes and between holes.
## Parameter roles
- `datum_edge_x`, `datum_edge_y`, and `centerline` define references.
- `edge_margin_x`, `edge_margin_y`, `pitch_x`, `pitch_y`, `row_count`, and `column_count` define repeated layouts.
- `hole_diameter`, `thread_size`, `fit`, and `end_condition` define the hole itself.
- `counterbore_diameter`, `counterbore_depth`, `countersink_angle`, and `slot_length` define fastener seating or adjustability.
## Construction sequence
1. Select the plate face and define construction centerlines or edge-offset dimensions.
2. Create one seed hole or one seed hole group using HoleWzd or a through cut.
3. Use linear pattern for rows/grids and mirror for symmetric pairs.
4. Use slots when the design requires adjustment rather than fixed bolt location.
5. Add counterbore, countersink, or thread data to the seed hole before patterning where possible.
6. Add chamfers and deburring features after the functional holes are complete.
## Common failures
- Converting counterbored/threaded/countersunk holes into plain cylinders.
- Patterning sketch circles instead of the actual cut or hole feature.
- Mixing flange bolt-circle logic into rectangular plate layouts.
- Missing datum margins, causing holes to float.
- Placing slots too close to edges or other holes.
## Evidence summary
- Current library evidence: 207 reviewed SCAD58 plate-like models.
- Latest run evidence: high-frequency accepted insights for hole wizard use, standard hole types, symmetric hole placement, linear hole patterns, mirror patterns, slots, and edge chamfers.
- Frequent operations in the latest run: HoleWzd, Cut, LinearPattern, MirrorPattern, Chamfer.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `mounting plate hole layout`
- `base plate hole pattern`
- `rectangular hole grid`
- `symmetric mounting holes`
Secondary triggers:
- `edge margin`
- `linear pitch`
- `counterbored holes`
- `threaded holes`
- `slots`
- `dowel holes`
Operation triggers:
- `hole wizard`
- `linear pattern`
- `mirror holes`
- `cut through holes`
- `counterbore`
- `countersink`
Exclusions:
- `flange bolt circle`
- `bearing bore`
- `random decorative holes`
@@ -0,0 +1,87 @@
# Slotted Adjustment Feature Functional Skill
## When to use
- A plate, bracket, clamp, guide, or mounting part needs an elongated slot for positional adjustment, clearance, sliding, or alignment.
- The request mentions slot, slotted hole, adjustment slot, elongated hole, guide slot, keyway-like slot, or obround cutout.
## Do not use when
- The feature is a simple round hole with no adjustment function.
- The slot is actually a shaft keyway or turned groove; use a shaft/keyway-specific rule if available.
- The slot is decorative and not tied to fastening, travel, or clearance.
## Recognition features
- Elongated hole with two semicircular ends and straight sides.
- Slot located on a flat plate/bracket face or through a wall.
- Often paired, mirrored, or patterned.
- Can coexist with counterbored holes, threaded holes, ribs, or edge chamfers.
## Core invariants
- Slot width should match the fastener/shaft clearance role.
- Slot length defines adjustment travel and must be longer than the fastener diameter.
- Keep end radii equal to half the slot width for manufacturable milled/slot features.
- Dimension slots from datums and centerlines.
- Cut slots after the base body exists and before final chamfers/fillets.
## Parameter roles
- `slot_width` defines cutter or fastener clearance.
- `slot_length` defines adjustment range.
- `end_radius` is usually `slot_width / 2`.
- `slot_center`, `slot_angle`, and `edge_margin` locate the slot.
- `slot_depth` or `through_all` defines end condition.
## Construction sequence
1. Select the planar face or reference plane where the slot starts.
2. Sketch an obround slot from centerline, length, and width.
3. Cut through all or to the required depth.
4. Pattern or mirror the slot if repeated.
5. Add small chamfers to slot edges after the cut is complete.
## Common failures
- Modeling a slot as a rectangle with sharp internal corners.
- Making the slot length equal to the hole diameter, removing adjustment function.
- Placing slots without enough wall material at the ends.
- Confusing a bracket adjustment slot with a shaft keyway or revolved groove.
## Evidence summary
- Latest run evidence: 38 accepted functional insights matched slot, slotted adjustment, guide slot, or slot/cutout behavior.
- Strong repeated names include `use_slots_for_adjustment`, `use_cut_extrude_for_holes_and_slots`, `central_slot_for_alignment`, and `cut_slots_for_adjustment`.
- Frequent operations in the latest run: Cut, Extrusion, LinearPattern, MirrorPattern, Chamfer.
- Source model examples: 004764, 005640, 006514, 011853, 026940, 043134, 058682, 096676, 106216, 156844, 163197, 185756, 199739, 213507, 240416.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `slot`
- `slotted hole`
- `adjustment slot`
- `elongated hole`
- `guide slot`
Secondary triggers:
- `obround`
- `clearance slot`
- `mounting adjustment`
- `sliding`
- `alignment slot`
Operation triggers:
- `sketch slot`
- `cut extrude slot`
- `mirror slot`
- `linear pattern slot`
- `chamfer slot edge`
Exclusions:
- `round hole only`
- `shaft keyway`
- `revolved groove`
- `decorative cutout`
@@ -0,0 +1,92 @@
# Standard Hole Wizard Functional Skill
## When to use
- A part needs standard clearance holes, threaded/tapped holes, counterbored holes, countersunk holes, dowel holes, or screw-seat holes.
- The request mentions screw size, metric thread, socket head screw, flat head screw, fastener clearance, tapping, counterbore, countersink, or Hole Wizard-like behavior.
## Do not use when
- The circular feature is a bearing bore, shaft bore, decorative opening, large window, or non-standard milled pocket.
- The hole is part of a freeform surface where a standard hole feature cannot be cleanly placed.
- The user asks for detailed thread geometry and the CAD backend cannot represent it reliably.
## Recognition features
- Feature evidence such as HoleWzd, threaded hole, counterbored hole, countersunk hole, through hole, or standard metric sizes like M3, M4, M5, M6, M8, M10.
- Holes are used for fastening, alignment, or assembly rather than decoration.
- Hole positions are points on a planar face, often patterned or mirrored.
## Core invariants
- Select hole type before modeling: clearance, threaded, counterbore, countersink, or dowel.
- Keep hole axis normal to the placement face unless an angled hole is explicitly requested.
- Specify end condition: through all, blind depth, up to next, or thread depth.
- Preserve fastener seating geometry: counterbore/countersink is not a decorative ring.
- Pattern or mirror the completed hole feature when identical holes repeat.
## Parameter roles
- `hole_type` selects clearance, threaded, counterbore, countersink, dowel, or simple through hole.
- `standard` and `thread_size` define metric/imperial thread family when available.
- `hole_diameter`, `tap_drill_diameter`, `thread_depth`, and `hole_depth` define cut size.
- `counterbore_diameter`, `counterbore_depth`, and `countersink_angle` define screw head seating.
- `placement_points`, `edge_offsets`, and `pattern_count` define layout.
## Construction sequence
1. Identify the fastening or alignment function for each hole group.
2. Choose the matching standard hole type and size.
3. Place hole center points on the correct face using datums, centerlines, or coordinates.
4. Create the hole feature with diameter, depth, thread, and seating data.
5. Pattern or mirror the completed hole feature when the group repeats.
6. Add entrance chamfers only after the hole geometry is correct.
## Common failures
- Modeling all holes as plain cylinders and losing thread/counterbore/countersink meaning.
- Adding visual thread helixes where a thread callout or standard threaded hole is enough.
- Forgetting counterbore depth for socket head screws.
- Patterning hole center sketches without patterning the hole cut.
- Using non-standard sizes when the prompt specifies standard fasteners.
## Evidence summary
- Latest run evidence: 149 accepted functional insights matched standard hole creation and fastener-hole logic.
- Strong repeated names include `use_hole_wizard_for_standard_holes`, `use_counterbored_holes_for_flat_head_screws`, `use_hole_wizard_for_threaded_holes`, and `use_hole_wizard_for_counterbore_holes`.
- Frequent operations in the latest run: HoleWzd, Cut, LinearPattern, MirrorPattern, Chamfer.
- Source model examples: 001082, 001607, 003846, 011721, 014040, 029487, 041650, 061936, 074693, 094929, 102841, 145389, 160434, 211326, 227986.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `hole wizard`
- `standard holes`
- `threaded holes`
- `counterbored holes`
- `countersunk holes`
- `tapped holes`
Secondary triggers:
- `M3`
- `M4`
- `M5`
- `M6`
- `M8`
- `socket head screw`
- `flat head screw`
Operation triggers:
- `create hole wizard feature`
- `create threaded hole`
- `create counterbore`
- `create countersink`
- `pattern hole feature`
Exclusions:
- `bearing bore`
- `shaft bore`
- `decorative hole`
- `large cutout`
- `freeform pocket`
@@ -0,0 +1,85 @@
# Symmetric Feature Layout Functional Skill
## When to use
- A part has holes, cutouts, bosses, tabs, ribs, or slots arranged symmetrically about a centerline, midplane, or axis.
- The prompt mentions symmetric holes, mirrored features, balanced mounting, centered layout, equal offsets, or left/right duplicates.
## Do not use when
- Features are intentionally asymmetric for clearance, indexing, handedness, or mating orientation.
- The only symmetry is the natural circular symmetry of a plain shaft or washer with no repeated secondary features.
- The model requires unique hand-placed features with different sizes.
## Recognition features
- Hole pairs, mirrored slots, duplicated bosses, matching tabs, or opposite-side cutouts.
- SolidWorks features such as MirrorPattern or sketches with construction centerlines.
- Descriptions mention symmetry, balanced fastening, alignment, centered holes, or mirrored geometry.
## Core invariants
- Define the symmetry plane, centerline, or axis before placing the seed feature.
- Model one side or one quadrant carefully, then mirror/pattern it.
- Keep symmetric features tied to the same dimensions so edits preserve balance.
- Do not mirror features that are explicitly handed or direction-specific.
- Combine with hole-type rules when mirrored features are fastener holes.
## Parameter roles
- `symmetry_plane` or `centerline` defines the mirror reference.
- `seed_feature` defines the original hole, slot, boss, or cutout.
- `offset_from_center`, `edge_margin`, and `pitch` define placement.
- `mirror_count`, `pattern_count`, and `quadrant_count` define repetition intent.
## Construction sequence
1. Identify the natural datum: plate centerline, bracket midplane, flange axis, or body midplane.
2. Create construction geometry for the symmetry reference.
3. Build the seed feature on one side with full dimensions.
4. Mirror or pattern the completed feature across the reference.
5. Verify that all mirrored features remain inside material and keep correct hole type/depth.
6. Apply common edge treatments after mirrored features are complete.
## Common failures
- Manually placing mirrored holes and getting unequal offsets.
- Mirroring asymmetric features such as countersinks that should face a specific side.
- Losing thread/counterbore data during mirror or pattern.
- Creating a visually symmetric model with unconstrained feature positions.
## Evidence summary
- Latest run evidence: 228 accepted functional insights contained symmetry, mirror, centerline, or pattern alignment signals.
- Strong repeated names include `use_mirror_pattern_for_symmetry`, `position_holes_symmetrically`, `symmetrical_hole_placement`, and `use_mirror_for_symmetry`.
- Frequent operations in the latest run: MirrorPattern, LinearPattern, HoleWzd, Cut, construction lines, Chamfer.
- Source model examples: 000124, 002620, 008006, 012598, 029447, 032081, 059866, 097101, 125932, 134012, 145543, 154893, 166339, 173306, 202020.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `symmetric holes`
- `mirror pattern`
- `symmetry`
- `mirrored features`
- `centerline layout`
Secondary triggers:
- `balanced mounting`
- `equal offsets`
- `left right holes`
- `construction line`
- `midplane`
Operation triggers:
- `create construction centerline`
- `mirror feature`
- `pattern symmetric holes`
- `use equal constraints`
Exclusions:
- `asymmetric clearance`
- `handed part`
- `unique holes`
- `plain cylinder only`
@@ -0,0 +1,98 @@
# Bearing Housing Or Seat Planning Skill
## When to use
- The request describes a bearing housing, bearing seat, bearing support, bearing block, pillow-block-like support, bushing seat, shaft support, or retainer body.
- The central functional feature is a precise coaxial bore, pocket, sleeve, shoulder, or counterbore that locates a bearing or bushing.
- The part is a single machined component that supports a shaft or bearing, not the rolling bearing assembly itself.
## Do not use when
- The user asks for a complete bearing with balls, rollers, cage, inner race, and outer race.
- The bearing word is only speculative and there is no bore, seat, sleeve, pocket, or retaining geometry.
- The part is mainly a flange, washer, simple tube, or rectangular plate with unrelated holes.
- The task requires an assembly of shaft, bearing, seals, fasteners, and housing together.
## Recognition features
- Coaxial shaft clearance bore through the body.
- Larger bearing pocket or counterbore with controlled depth.
- Shoulder, step, retaining lip, snap-ring groove, or cover seat to locate the bearing axially.
- Mounting base, side lugs, flange, bolt holes, ribs, or bosses can appear, but they support the bearing axis rather than define the part family.
- Chamfers at bore entrances and seat edges guide assembly.
## Core invariants
- Bearing seat diameter, shaft clearance diameter, housing outside diameter, and cover features share one axis.
- Separate the bearing outer-diameter seat from the shaft clearance bore; they are not the same feature.
- Leave a shoulder or defined depth when the bearing must stop axially.
- Build enough housing material before cutting the bearing pocket.
- Add mounting holes and ribs after the main bore/seat relationship is established.
- Do not model balls or rollers unless the prompt explicitly asks for a bearing assembly.
## Parameter roles
- `bearing_outer_diameter` defines the pocket or seat diameter.
- `shaft_clearance_diameter` defines the through bore.
- `seat_depth` defines how far the bearing is inserted.
- `shoulder_height` or `shoulder_diameter` defines axial retention.
- `housing_wall_thickness` controls material around the bearing pocket.
- `mounting_hole_pattern`, `base_length`, `base_width`, and `base_thickness` define attachment to the frame.
- `rib_thickness` and `rib_height` define optional support ribs.
## Construction sequence
1. Define the bearing axis and choose the housing form: block, flanged seat, sleeve, pedestal, or bracketed support.
2. Create the main housing/body with enough wall thickness around the bearing axis.
3. Cut the through shaft clearance bore on the main axis.
4. Cut the bearing pocket/counterbore to its seat depth, leaving a clear shoulder where needed.
5. Add retaining grooves, cover recesses, set-screw holes, oil holes, or alignment features only if requested.
6. Add base mounting holes, bolt pads, ribs, and bosses using datum-driven positions.
7. Finish with bore lead-in chamfers, outside chamfers, and small fillets.
## Common failures
- Generating a bearing assembly instead of a housing or seat part.
- Making the bearing seat and shaft clearance the same diameter.
- Missing the shoulder/depth that locates the bearing.
- Off-axis mounting of bore, pocket, or outer sleeve.
- Turning every bearing support into a flange even when the evidence suggests a block or pedestal.
- Adding decorative ribs or fasteners that obscure the bearing pocket.
## Evidence summary
- Current library evidence: 24 previously reviewed SCAD58 bearing housing/seat descriptions.
- Latest run evidence: accepted insights for central bore alignment, bearing/bushing-like cylindrical seats, coaxial features, counterbores, mounting holes, and chamfered bore entries.
- Frequent operations in the latest run: Extrusion, Revolution, Cut, RevCut, HoleWzd, Chamfer, Fillet.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `bearing housing`
- `bearing seat`
- `bearing support`
- `bearing block`
- `bushing seat`
Secondary triggers:
- `bearing bore`
- `bearing pocket`
- `shaft clearance`
- `retaining shoulder`
- `coaxial seat`
- `pillow block`
Operation triggers:
- `cut coaxial bore`
- `counterbore bearing seat`
- `create shoulder`
- `add mounting base holes`
- `chamfer bore entry`
Exclusions:
- `complete rolling bearing`
- `plain washer`
- `flange only`
- `simple tube`
- `shaft only`
@@ -0,0 +1,93 @@
# Flange Planning Skill
## When to use
- The request describes a circular flange, coupling flange, pipe flange, gasket-like flange, flanged sleeve, or flanged cylindrical adapter.
- The part has a central axis, central bore, and fastener holes arranged around a pitch circle.
- The flange connects, spaces, seals, or mounts coaxial parts such as shafts, pipes, bearings, covers, or couplings.
## Do not use when
- The word flange only means a side tab, flat ear, or rectangular mounting plate extension.
- The hole layout is a rectangular grid, edge row, or arbitrary plate pattern.
- The model is primarily a plain shaft, washer, pulley, bearing assembly, or bracket without a true circular flange body.
## Recognition features
- Axisymmetric disc, annular ring, raised hub, sleeve, stepped boss, or cylindrical landing surface.
- Central through bore, pilot bore, counterbore, recess, seal land, or shaft clearance on the main axis.
- Bolt holes are evenly spaced by angle around the same center as the bore.
- Optional recesses, gasket grooves, chamfers, fillets, or counterbores are secondary to the coaxial flange body.
## Core invariants
- The outer diameter, bore, hub, sleeve, recesses, and bolt circle share one central construction axis.
- Build the rotational body before cutting the bolt holes.
- Define bolt-circle diameter separately from outer diameter and bore diameter.
- Create one correct bolt hole, then circular-pattern the cut feature over 360 degrees.
- Do not replace the bolt circle with a rectangular hole grid.
- Keep fillets, chamfers, and cosmetic edge breaks late in the sequence.
## Parameter roles
- `outer_diameter` defines the flange envelope.
- `thickness` defines the disc or flange plate depth.
- `bore_diameter` defines shaft or pipe clearance.
- `hub_diameter`, `hub_height`, and `pilot_diameter` define raised coaxial features.
- `bolt_circle_diameter`, `bolt_count`, `bolt_hole_diameter`, and `start_angle` define the fastener pattern.
- `counterbore_diameter`, `counterbore_depth`, `gasket_groove_width`, and `gasket_groove_depth` define secondary seats.
## Construction sequence
1. Establish the central axis and primary reference plane.
2. Create the flange disc and any coaxial hub or sleeve using revolve for stepped profiles, or extrude circles for simple flat discs.
3. Cut the central bore through the flange and hub on the same axis.
4. Add one bolt hole at the pitch radius using the correct fastener hole type.
5. Circular-pattern that cut around the central axis with equal angular spacing.
6. Add recesses, gasket grooves, counterbores, chamfers, and fillets after the bore and bolt pattern are stable.
## Common failures
- Off-axis bore relative to hub or outer diameter.
- Manual bolt hole placement that produces unequal angular spacing.
- Confusing a rectangular mounting plate with a flange because both have multiple holes.
- Forgetting the central bore and creating only a circular plate with holes.
- Applying large fillets before the bolt pattern and causing feature failure.
## Evidence summary
- Current library evidence: 45 previously reviewed SCAD58 flange-like models.
- Latest run evidence: accepted insights for circular flange bolt holes, circular pattern around a central bore, revolved axisymmetric bodies, and coaxial cylindrical features.
- Frequent operations in the latest run: Revolution, Extrusion, Cut, HoleWzd, CircularPattern, Chamfer, Fillet.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `flange`
- `circular flange`
- `pipe flange`
- `coupling flange`
- `flanged sleeve`
Secondary triggers:
- `bolt circle`
- `pitch circle`
- `central bore`
- `raised hub`
- `gasket groove`
- `pilot diameter`
Operation triggers:
- `revolve profile`
- `extrude circular disc`
- `cut central bore`
- `circular pattern`
- `counterbore bolt holes`
Exclusions:
- `mounting plate`
- `rectangular hole grid`
- `side tab`
- `plain shaft`
- `complete bearing`
@@ -0,0 +1,91 @@
# Hexagonal Nut Planning Skill
## When to use
- The request describes a hex nut, hexagonal nut, machined nut, nut-like fastener, or hexagonal threaded insert.
- The key features are wrench flats, a central threaded bore, and chamfered faces/edges.
## Do not use when
- The part is a bolt, screw, socket head cap screw, threaded rod, wing nut, cap nut, or flange nut unless the prompt explicitly asks for that variant.
- The hexagon is only a decorative grip on another part.
- The threaded feature is external rather than an internal bore.
## Recognition features
- Six flat wrench faces or hexagonal prism envelope.
- Central internal threaded hole coaxial with the nut thickness.
- Chamfers on top and bottom openings and on hex edges.
- Optional counterbore, lead-in chamfer, or relieved thread entry.
## Core invariants
- Keep the threaded bore centered through the hex body.
- Preserve the hex across-flats size separately from thread size and thickness.
- Model chamfers as functional lead-ins and edge breaks, not as random bevels.
- Represent internal thread intent using a threaded hole, thread callout, or helical/revolved cut only when required by output format.
- Do not generate the mating bolt unless requested.
## Parameter roles
- `across_flats` defines wrench size.
- `thickness` defines nut height.
- `thread_size`, `thread_diameter`, and `thread_depth` define the central bore.
- `bore_diameter` defines the pre-thread or clearance hole if thread geometry is abstracted.
- `face_chamfer` and `edge_chamfer` define lead-in and deburring.
## Construction sequence
1. Create the hexagonal body as a hex extrusion or by cutting flats from a revolved/chamfered blank.
2. Define the central axis through the nut.
3. Create the central bore or threaded hole through the body.
4. Add thread representation or thread callout if the CAD output supports it.
5. Add top/bottom lead-in chamfers and small edge chamfers.
6. Verify the hex flats remain planar and the bore remains coaxial.
## Common failures
- Producing a bolt or screw instead of a nut.
- Forgetting the central threaded bore.
- Placing the bore off-center relative to the hex body.
- Making the chamfers so large that wrench flats disappear.
- Treating a flange nut, cap nut, or wing nut as a plain hex nut.
## Evidence summary
- Latest run evidence: 25 accepted planning insights matched hex nut or nut-like threaded fastener descriptions.
- Strong repeated names include `plan_hex_nut`, `revolved_hex_nut_with_internal_thread`, `hex_nut_construction`, and `plan_hexagonal_nut_with_threaded_bore`.
- Frequent operations in the latest run: Revolution, Extrusion, Cut, RevCut, HoleWzd, Chamfer.
- Source model examples: 009934, 010137, 052867, 066148, 067249, 135604, 138372, 150622, 200147, 204805, 222550, 225767, 231397, 238840.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `hex nut`
- `hexagonal nut`
- `nut`
- `threaded nut`
- `hexagonal threaded insert`
Secondary triggers:
- `internal thread`
- `central threaded bore`
- `wrench flats`
- `across flats`
- `chamfered nut`
Operation triggers:
- `extrude hexagon`
- `cut central bore`
- `hole wizard threaded hole`
- `revolved cut thread`
- `chamfer faces`
Exclusions:
- `bolt`
- `screw`
- `threaded shaft`
- `wing nut`
- `cap nut`
- `flange nut`
@@ -0,0 +1,97 @@
# Mounting Bracket Planning Skill
## When to use
- The request describes a mounting bracket, L bracket, U bracket, T bracket, support bracket, clamp bracket, lugged bracket, or machined support with holes.
- The part has a load-bearing base plus one or more raised webs, tabs, arms, bosses, lugs, pockets, slots, or cutouts.
- The model is a single machined bracket component, not an assembly with screws or attached hardware.
## Do not use when
- The part is only a flat plate with holes and no bracket-like 3D support structure.
- The part is mainly a shaft, flange, bearing housing, complete hinge assembly, or sheet-metal bent bracket.
- The prompt asks for a decorative stand or enclosure rather than a functional machined support.
## Recognition features
- Base block or plate that mounts to another part.
- Vertical web, side wall, lug, tab, raised boss, or protruding arm.
- Mounting holes, threaded holes, counterbores, slots, or cutouts.
- Reinforcing ribs or fillets at transitions may appear when strength matters.
- Hole placement is usually datum-driven and often symmetric or mirrored.
## Core invariants
- Build the main load path first: base, web/tab/lug, and boss geometry before small holes.
- Keep bracket features as one coherent body unless the prompt explicitly asks for assembled parts.
- Place holes from functional datums, not visual guesses.
- Use standard hole features for threaded, counterbored, countersunk, and clearance holes.
- Add cutouts/slots for clearance or adjustment only where they make functional sense.
- Apply fillets/chamfers late, especially at web-to-base transitions and exposed edges.
## Parameter roles
- `base_length`, `base_width`, and `base_thickness` define the mounting base.
- `web_height`, `web_thickness`, `tab_width`, `lug_radius`, and `boss_diameter` define support geometry.
- `hole_diameter`, `thread_size`, `counterbore_diameter`, and `slot_length` define fastening and adjustment.
- `edge_margin`, `hole_pitch`, `symmetry_plane`, and `boss_offset` define layout.
- `fillet_radius` controls stress relief at transitions.
## Construction sequence
1. Create the base extrusion or main bracket profile.
2. Add vertical webs, tabs, lugs, bosses, or arms as secondary extrusions from clear datum faces.
3. Cut clearance pockets, U/L/T-shaped openings, slots, or lightening cutouts.
4. Add standard holes using HoleWzd or cut features, positioned from datums and symmetry references.
5. Pattern or mirror repeated holes/features when the design is symmetric.
6. Add structural fillets at web/base intersections, then finish with small chamfers on exposed edges.
## Common failures
- Collapsing a bracket into a simple flat plate and losing the vertical/web structure.
- Creating an assembly of screws, pins, or attached parts when only the bracket is requested.
- Adding holes before the base/web geometry and producing floating or misaligned holes.
- Mixing several unrelated bracket variants into one overloaded model.
- Using decorative cutouts that weaken the load path or overlap holes.
## Evidence summary
- Latest run evidence: 247 accepted planning insights matched bracket-like descriptions and operations.
- Strong repeated names include `plan_l_bracket_with_holes`, `bracket_with_holes_and_cutouts`, `bracket_with_holes_planning`, `mounting_bracket_with_holes`, and `plan_bracket_with_holes`.
- Frequent operations in the latest run: Extrusion, Cut, HoleWzd, LinearPattern, MirrorPattern, Chamfer, Fillet, Boss.
- Source model examples: 000780, 001607, 002620, 004421, 009414, 028735, 032081, 036242, 041396, 047048, 054507, 079425, 096301, 134801, 187546, 195869, 207737, 225658.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `mounting bracket`
- `support bracket`
- `L bracket`
- `U bracket`
- `T bracket`
- `bracket with holes`
Secondary triggers:
- `web`
- `lug`
- `tab`
- `boss`
- `slot`
- `cutout`
- `threaded holes`
Operation triggers:
- `extrude base`
- `extrude tab`
- `cut slot`
- `hole wizard`
- `mirror pattern`
- `fillet web transition`
Exclusions:
- `flat mounting plate only`
- `flange`
- `shaft`
- `bearing assembly`
- `sheet metal bend`
@@ -0,0 +1,97 @@
# Mounting Plate Planning Skill
## When to use
- The request describes a flat mounting plate, base plate, adapter plate, panel, fixture plate, or spacer plate.
- The dominant body is a prismatic plate with functional holes, slots, counterbores, countersinks, threaded holes, pockets, or a central clearance cutout.
- The plate locates, fastens, spaces, or supports other components from planar datum faces.
## Do not use when
- The primary geometry is an axisymmetric flange with a circular bolt pitch pattern.
- The plate is only a secondary base under a bearing housing, bracket, motor body, or cylindrical module.
- The part is a true 3D bracket with vertical webs, lugs, arms, or side walls that control the design.
- The request is for sheet-metal bends, formed panels, or decorative flat art rather than a machined plate.
## Recognition features
- Rectangular, square, rounded-rectangle, or simple profile plate with mostly uniform thickness.
- Holes are located from datum edges, centerlines, or symmetric offsets instead of arbitrary visual placement.
- Common hole types include clearance holes, threaded holes, counterbored holes, countersunk holes, dowel holes, and through holes.
- Cutouts can be central circular holes, rectangular windows, slots, pockets, or weight-reduction openings.
- Edge chamfers and small fillets are usually finishing operations, not the defining shape.
## Core invariants
- Establish the base plate envelope, thickness, top face, bottom face, and edge datum references before adding holes.
- Keep mounting holes fully constrained by edge margins, centerlines, pitch, symmetry, or functional datums.
- Model holes as real cuts or standard hole features; do not leave sketch circles on the face.
- Use one seed hole or hole group plus pattern/mirror when the layout is regular.
- Choose hole type from function: clearance for bolts, threaded/tapped for fastening into the plate, counterbore/countersink for flush screw heads, slot for adjustment.
- Apply chamfers and fillets after functional holes and cutouts so edge treatment does not distort hole geometry.
## Parameter roles
- `length`, `width`, and `thickness` define the blank.
- `corner_radius` or `corner_chamfer` controls perimeter treatment when requested.
- `edge_margin_x`, `edge_margin_y`, `pitch_x`, and `pitch_y` locate rectangular hole groups.
- `central_cutout_width`, `central_cutout_length`, or `central_bore_diameter` define clearance openings.
- `hole_diameter`, `thread_size`, `counterbore_diameter`, `counterbore_depth`, `countersink_angle`, and `slot_length` define fastener features.
## Construction sequence
1. Create a constrained base sketch for the plate outline and extrude it to thickness.
2. Define datum centerlines, edge offsets, and symmetry references on the main face.
3. Add the central clearance hole, rectangular cutout, or pocket if it controls component clearance.
4. Add the first functional hole using the correct hole type and end condition.
5. Replicate regular hole groups with linear pattern, circular pattern, or mirror according to layout intent.
6. Add slots, secondary pockets, bosses, or local relief cuts only after primary mounting references are stable.
7. Finish with perimeter chamfers, small fillets, and deburring details.
## Common failures
- Treating every round mark as the same generic hole and losing counterbore/thread/countersink meaning.
- Placing holes by eyeballing coordinates instead of datum margins and symmetry.
- Using a flange bolt-circle pattern for a rectangular plate grid.
- Adding chamfers before holes, causing holes or counterbores to be clipped.
- Over-modeling decorative bevels while missing the functional fastening layout.
- Combining unrelated bracket walls or cylindrical housings into the mounting plate body.
## Evidence summary
- Current library evidence: 207 previously reviewed SCAD58 mounting/base plate descriptions.
- Latest run evidence: repeated accepted insights for `mounting_plate_with_holes`, `plan_mounting_plate_with_holes`, `extrude_rectangular_plate`, `base_plate_with_mounting_holes`, central cutouts, threaded holes, counterbored holes, linear patterns, and edge chamfers.
- Frequent operations in the latest run: Extrusion, HoleWzd, Cut, LinearPattern, MirrorPattern, Chamfer, Fillet.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `mounting plate`
- `base plate`
- `adapter plate`
- `fixture plate`
- `flat plate with holes`
Secondary triggers:
- `hole grid`
- `edge margins`
- `counterbored holes`
- `threaded holes`
- `central cutout`
- `slots`
Operation triggers:
- `extrude base plate`
- `hole wizard`
- `linear pattern`
- `mirror holes`
- `cut central opening`
- `chamfer edges last`
Exclusions:
- `flange bolt circle`
- `bearing housing`
- `shaft`
- `3d bracket`
- `sheet metal bend`
@@ -0,0 +1,95 @@
# Simple Shaft Or Cylindrical Rod Planning Skill
## When to use
- The request describes a single cylindrical rod, simple shaft, spacer rod, cylindrical pin, standoff-like cylinder, or shaft-like machined part.
- The body is primarily a cylinder along one axis, optionally with end holes, chamfers, simple steps, flats, slots, or threaded holes.
- The task is to generate one part, not a spindle cartridge or shaft assembly.
## Do not use when
- The request describes a spindle module, motor rotor assembly, gearbox shaft assembly, bearings, seals, pulleys, keys, and fasteners together.
- The part is mainly a flange, washer, bushing, screw, bolt, or complex housing.
- The body is a hollow tube or bearing sleeve where inner/outer diameter relationship is the main design intent.
## Recognition features
- Long cylindrical body with uniform or stepped diameters.
- Central axis is the dominant datum.
- Optional end chamfers, end fillets, centered threaded holes, transverse holes, flats, slots, grooves, or keyway-like cuts.
- Turning and cylindrical surfaces dominate; milling may appear for flats, slots, or keyways.
## Core invariants
- Establish one main shaft axis and overall length first.
- Use extrude for a uniform simple cylinder; use revolve for stepped diameters, grooves, shoulders, tapers, or turned profiles.
- Keep all coaxial diameter changes, end holes, and grooves aligned to the shaft axis.
- Treat holes, slots, flats, keyways, and threads as secondary features.
- Do not invent bearings, couplings, gears, or housings unless requested.
## Parameter roles
- `length` defines the axial span.
- `diameter` defines a simple rod.
- `step_diameters`, `step_lengths`, `shoulder_positions`, and `groove_width` define turned features.
- `end_hole_diameter`, `thread_size`, and `thread_depth` define centered end holes.
- `flat_width`, `slot_width`, `slot_depth`, and `keyway_length` define milled secondary features.
- `chamfer_distance` and `fillet_radius` define edge finishing.
## Construction sequence
1. Define the shaft axis and overall length.
2. For a simple rod, sketch a circle and extrude along the axis.
3. For stepped or shouldered shafts, sketch the half-profile and revolve it around the axis.
4. Add centered end holes, threaded holes, grooves, flats, slots, or keyways as secondary cuts.
5. Keep secondary features referenced to the shaft axis or end-face datums.
6. Finish with end chamfers, shoulder fillets, and small edge breaks.
## Common failures
- Generating a spindle or bearing assembly when only a shaft/rod part was requested.
- Creating off-axis end holes.
- Using multiple separate bodies for diameter steps instead of one coherent solid.
- Treating a threaded shaft, bolt, or screw as the same as a plain shaft without preserving thread/head differences.
- Adding arbitrary decorative grooves that were not requested.
## Evidence summary
- Latest run evidence: 107 accepted planning insights matched shaft, rod, cylinder, and stepped-shaft construction.
- Strong repeated names include `plan_stepped_shaft`, `simple_cylinder_extrusion`, `stepped_shaft_construction`, `cylindrical_rod_planning`, and `plan_revolve_shaft_profile`.
- Frequent operations in the latest run: Extrusion, Revolution, Cut, RevCut, HoleWzd, Chamfer, Fillet.
- Source model examples: 000166, 001957, 040325, 069352, 080683, 100698, 112997, 144199, 148300, 160726, 172872, 210853, 218803, 225038, 232675, 239358.
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
## Retrieval metadata
Triggers:
- `cylindrical rod`
- `simple shaft`
- `shaft`
- `rod`
- `pin`
- `standoff`
Secondary triggers:
- `stepped shaft`
- `end hole`
- `threaded end`
- `shaft groove`
- `keyway`
- `chamfered ends`
Operation triggers:
- `extrude circle`
- `revolve shaft profile`
- `cut end hole`
- `cut slot`
- `chamfer ends`
Exclusions:
- `spindle assembly`
- `bearing assembly`
- `flange`
- `washer`
- `bolt head`
- `gear`
@@ -0,0 +1,538 @@
# CNC 常加工零件 references-only 优化调研与实施计划
本文档用于规划如何只通过 `references` 强化 `text-to-cad` 对常见 CNC 加工零件的生成质量。这里不新增 factory、validator、router 或专用生成器,只给 CAD agent 在写 build123d 代码前可阅读的建模知识:如何识别零件类型、该类零件的稳定结构特征、哪些参数必须显式命名、推荐建模顺序、常见失败模式,以及适合做 references-only 优化的实施方案。
结论先说清楚:references 是软约束,只能提高平均生成质量,不能保证工程正确性。凡是强依赖标准、曲线、产品负形、流体/气动/热管理、法规或精密公差的代表件,references 可以帮助生成“看起来像”的简化模型,但不适合作为可靠优化对象。
## 资料依据
本计划参考了公开 CNC 设计指南、典型零件结构介绍和行业加工资料,包括 Protolabs 与 Xometry 的 CNC 设计建议、法兰结构资料、轴承座/带座轴承资料、丝杠/花键/同步带轮资料、箱体/泵体/叶轮/螺旋桨/模具/夹具等制造资料。文末列出链接。资料只用于总结结构特征和建模指导,不等同于标准件规范、强度校核、流体性能、密封性能、医疗/航空合规或可加工性承诺。
## references-only 优化原则
每个 reference 文件建议按下面结构写:
1. 识别词:中文、英文、别名、相邻概念。
2. 结构不变量:生成结果要像这个零件时必须具备的结构。
3. 常见变体:避免把所有提示词压成一个模板。
4. 默认策略:用户没有明确变体时生成什么。
5. 参数角色:必须变成命名参数的尺寸。
6. feature roles:建模计划里必须显式出现的功能结构。
7. 建模顺序:更稳的 build123d 构造顺序。
8. 常见失败:reference 要主动提醒模型避免什么。
9. 检查目标:人工或通用 inspect 能看的几何事实。
写法上要多用“默认”“通常”“除非用户指定”,少用“必须永远生成某一种”。这样可以提升质量,同时保留多样性。
## 通用 CNC 建模规则
- 功能尺寸必须命名参数化,不要散落魔法数字。
- 原点和坐标轴应跟主功能基准一致:轴线、中心孔、安装面、分型面、密封面、定位面等。
- 先做基础实体,再做主孔/主腔/主轴线特征,再做次级孔槽口袋,最后倒角/圆角。
- 用户没有指定时,孔、槽、台阶、壁厚要取保守可加工的比例,避免深窄槽、薄壁、尖锐内角和无意义微小特征。
- 盘套类、轴类优先保证同轴;板类优先保证孔位和边距;箱体类优先保证安装面、壁厚、孔轴线和腔体关系。
- 不要从 reference 推断真实公差、疲劳强度、密封性能、转子动平衡、医疗合规、航空适航等。
## 建议落地方式
第一阶段不要为每个代表件都建独立文件。建议先建 7 个 family reference
- `references/part-families/shaft.md`
- `references/part-families/disc-sleeve.md`
- `references/part-families/housing.md`
- `references/part-families/plate.md`
- `references/part-families/freeform-surface.md`
- `references/part-families/mold-tooling.md`
- `references/part-families/precision-fixture.md`
如果后续 A/B 对比发现 broad family 不够,再拆出高价值专用文件,例如 `flange.md``bearing-housing.md``stepped-shaft.md``mounting-plate.md``timing-pulley.md`
## 1. 轴类零件
代表件:传动轴、主轴、阶梯轴、花键轴、丝杠。
共同结构特征:
- 以一条旋转轴线为核心。
- 由一个或多个同轴圆柱段构成。
- 常有轴颈、轴肩、退刀槽、键槽、花键、螺纹、中心孔、端部倒角。
- 建模重点是轴向分段、直径变化、同轴关系、扭矩传递特征和安装/支承位置。
### 传动轴
适合 references-only 优化。
实施计划:
- 识别:`传动轴``drive shaft``transmission shaft``motor shaft`,以及和齿轮、带轮、联轴器安装相关的轴。
- 不变量:长轴体;至少一个安装轴颈或扭矩传递界面;端部倒角;明确主轴线。
- 变体:光轴、阶梯传动轴、带键槽轴、横孔轴、端部螺纹轴、带卡簧槽轴。
- 默认:用户未说明时,生成沿 X 轴的两段或三段阶梯轴,中间较大轴颈,一处键槽,端部倒角。
- 参数角色:`total_length``section_lengths``section_diameters``journal_diameter``shoulder_positions``keyseat_width``keyseat_depth``keyseat_length``end_chamfer`
- feature roles`coaxial_stack``journals``shoulders``keyseat``end_chamfers`、可选 `retaining_grooves`
- 建模顺序:按轴向站位生成圆柱段并 fuse;切键槽;切槽/孔;最后倒角。
- 常见失败:不同段不同轴;键槽切穿整根轴;没有安装轴颈;没有参数名;竖着建导致观察不直观。
### 主轴
适合 reference 指导,但不适合宣称精度。
实施计划:
- 识别:`主轴``spindle``machine spindle`、工具/工件夹持轴、带轴承轴颈的高速旋转轴。
- 不变量:主旋转轴线;轴承轴颈;定位轴肩;一端工具/工件接口;重要基准面。
- 变体:主轴毛坯、阶梯主轴、锥鼻主轴、端部螺纹主轴、中空主轴。
- 默认:生成带两个轴承轴颈、定位轴肩、鼻端接口的阶梯主轴;只有用户要求时加通孔。
- 参数角色:`bearing_journal_diameter``bearing_spacing``nose_diameter``nose_length``taper_angle``through_bore_diameter``relief_groove_width`
- feature roles`bearing_journals``datum_shoulders``tool_nose`、可选 `through_bore``relief_grooves`
- 建模顺序:同轴分段;旋转生成锥面;切同轴通孔;切退刀槽;最后倒角/小圆角。
- 常见失败:做成普通装饰轴;缺轴承支承段;缺定位轴肩;声称跳动或精度。
### 阶梯轴
高度适合 references-only 优化。
实施计划:
- 识别:`阶梯轴``stepped shaft``multi-diameter shaft`
- 不变量:多段同轴圆柱;各段长度、直径和顺序明确;轴肩存在。
- 变体:普通阶梯轴、带键槽、带端部螺纹、带中心孔。
- 默认:三段同轴轴,中间直径较大,两端倒角,不主动加键槽。
- 参数角色:`segment_lengths``segment_diameters``total_length``shoulder_radii``chamfer_size`
- feature roles`coaxial_cylindrical_stack``shoulders``end_chamfers`、可选 `keyseat``center_holes`
- 建模顺序:分段圆柱;合并为单体;切可选特征;最后处理肩部和端部边。
- 常见失败:段顺序错;圆柱断开;缺轴肩;段长不守恒。
### 花键轴
适合做简化 reference;不适合承诺真实渐开线花键标准。
实施计划:
- 识别:`花键轴``spline shaft``involute spline``straight spline`
- 不变量:基础轴体;某一轴向区域有环向重复的齿或槽;齿槽围绕主轴均布。
- 变体:直齿外花键、简化渐开线外花键、锯齿花键、局部花键。
- 默认:生成简化直齿外花键段,均布齿数;用户给标准时只作为命名参数记录,不声称标准完全正确。
- 参数角色:`shaft_diameter``spline_outer_diameter``spline_root_diameter``spline_length``tooth_count``tooth_width_angle``root_fillet_radius`
- feature roles`base_shaft``spline_region``repeated_teeth_or_grooves``transition_shoulders`
- 建模顺序:基础轴;围绕轴线 polar pattern 齿/槽;加过渡退刀或肩部;倒角。
- 常见失败:把花键当单键槽;齿数不均;齿没有连到轴;齿数过高导致拓扑失败。
### 丝杠
适合做简化 reference;不适合做真实滚珠丝杠内部循环结构或认证螺纹。
实施计划:
- 识别:`丝杠``lead screw``ACME screw``trapezoidal screw``梯形丝杠`
- 不变量:长轴;螺旋传动螺纹区域;螺距/导程和大径命名;通常有端部轴颈。
- 变体:梯形丝杠、ACME 丝杠、简化螺旋脊、带两端支承轴颈的丝杠。
- 默认:中间简化梯形螺纹区域,两端光轴轴颈和退刀槽。
- 参数角色:`major_diameter``minor_diameter``pitch``lead``thread_length``starts``journal_diameter``end_length`
- feature roles`screw_core``helical_thread``end_journals``relief_grooves``chamfers`
- 建模顺序:芯轴;添加或扫掠简化螺旋牙;生成端部轴颈;加退刀槽和倒角。
- 常见失败:做成普通紧固螺纹;缺螺距/导程;全长螺纹导致没有支承端。
## 2. 盘套类零件
代表件:法兰盘、端盖、轴承座、同步带轮、齿轮毛坯。
共同结构特征:
- 多数以中心轴线、中心孔或回转体为核心。
- 常有中心孔、轮毂、凸台、止口、螺栓圆、密封面、沟槽、法兰边、带齿/齿坯外圆。
### 法兰盘
高度适合 references-only 优化。
实施计划:
- 识别:`法兰``法兰盘``flange``flange plate``pipe flange`
- 不变量:圆盘或带轮毂圆盘;中心孔;螺栓圆孔阵列;密封/贴合面。
- 变体:平面法兰、凸面法兰、环槽法兰、带颈法兰、盲板法兰、转接法兰。
- 默认:圆形平面或凸面法兰,中心通孔,均布螺栓孔。
- 参数角色:`outside_diameter``thickness``bore_diameter``bolt_count``bolt_circle_diameter``bolt_hole_diameter``raised_face_diameter``raised_face_height``hub_diameter``hub_height`
- feature roles`flange_disk``central_bore``bolt_circle_holes``sealing_face`、可选 `raised_face``hub``gasket_groove`
- 建模顺序:圆盘;可选凸面/轮毂;切中心孔;切螺栓圆;可选密封槽;最后倒角/圆角。
- 常见失败:缺中心孔;螺栓孔不在圆上;孔数错;孔不均布;中心孔与外圆不同轴。
### 端盖
适合 references-only 优化。
实施计划:
- 识别:`端盖``轴承端盖``end cover``bearing cover``cap`
- 不变量:盖板或浅盖体;定位止口或凸缘;螺栓孔;密封/贴合面;可选中心孔或密封孔。
- 变体:盲端盖、轴承压盖、法兰式端盖、带油封孔端盖。
- 默认:浅圆盖,带定位止口、螺栓圆;用户提到轴/轴承时加中心孔或油封孔。
- 参数角色:`cover_outer_diameter``cover_thickness``pilot_diameter``pilot_depth``bolt_count``bolt_circle_diameter``seal_bore_diameter`
- feature roles`cover_plate``pilot_lip``bolt_circle_holes``seal_bore``gasket_land`
- 建模顺序:盖体圆盘;加止口/凸台;切中心孔;切螺栓孔;切密封槽;倒角。
- 常见失败:没有定位止口;盖子方向不清;螺栓孔不对称;边缘过薄。
### 轴承座
适合 references-only 优化,但必须保留变体,不要写死成一种外形。
实施计划:
- 识别:`轴承座``bearing housing``pillow block``bearing seat``bearing support``带座轴承`
- 不变量:轴承安装孔/轴承座孔;轴通过的中心轴线;支撑轴承的实体材料;安装底座或法兰;安装孔。
- 变体:立式轴承座、法兰轴承座、板式轴承座、剖分式轴承座、筒夹式轴承座。
- 默认:用户未说明时生成立式 pillow-block 风格:平底座、上部轴承凸台、同轴轴承孔/轴孔、两个底座安装孔、加强肋。
- 参数角色:`bearing_outer_diameter``bearing_width``shaft_diameter``bearing_center_height``base_length``base_width``base_thickness``mount_hole_spacing``mount_hole_diameter``boss_outer_diameter``wall_thickness``rib_thickness`
- feature roles`base_block``bearing_boss``bearing_seat_bore``shaft_clearance_bore``mounting_holes``reinforcing_ribs`、可选 `split_cap`
- 建模顺序:底座;加轴承支撑体/凸台;同轴切轴承孔和轴孔;切安装孔;加与底座和凸台相交的加强肋;最后圆角。
- 常见失败:像普通支架而没有轴承孔;加强肋悬空;安装孔不对称;轴承中心高未定义。
### 同步带轮
适合视觉和参数化 reference;真实齿形标准需要齿形数据。
实施计划:
- 识别:`同步带轮``timing pulley``synchronous pulley``belt pulley`
- 不变量:外圆上有按齿数/节距重复的齿或槽;中心孔;轮毂或紧定/键连接;可选挡边。
- 变体:无挡边、单挡边、双挡边、带轮毂、键槽孔、紧定螺钉孔。
- 默认:简化双挡边同步带轮,中心孔、轮毂、按 `tooth_count` 均布的齿槽。
- 参数角色:`tooth_count``belt_pitch``pulley_width``bore_diameter``hub_diameter``flange_diameter``flange_thickness``tooth_depth``keyway_width``set_screw_hole_diameter`
- feature roles`toothed_rim``pitch_circle``flanges``bore``hub`、可选 `keyway``set_screw_holes`
- 建模顺序:轮坯和轮毂;环向重复切齿槽或加齿;加挡边;切中心孔/键槽/紧定孔;倒角。
- 常见失败:生成光滑带轮没有齿;齿数不对;齿不均布;把挡边当齿。
### 齿轮毛坯
适合 references-only 优化。注意它不是最终齿轮齿形生成。
实施计划:
- 识别:`齿轮毛坯``gear blank``gear body`
- 不变量:圆形坯体;中心孔;端面;可选轮毂、键槽、螺栓/销孔、减重孔。
- 变体:普通圆盘毛坯、带轮毂毛坯、腹板毛坯、带减重孔毛坯、带键槽毛坯。
- 默认:回转圆盘加中心孔和轮毂,端面倒角;用户提到传扭时加键槽。
- 参数角色:`outside_diameter``face_width``bore_diameter``hub_diameter``hub_width``keyway_width``keyway_depth``lightening_hole_count`
- feature roles`gear_blank_body``central_bore``hub`、可选 `keyway``lightening_holes`
- 建模顺序:旋转或圆柱生成毛坯/轮毂;切中心孔;切键槽/减重孔;倒角。
- 常见失败:用户只要毛坯却生成不准确齿形;中心孔不同轴;指定键槽却遗漏。
## 3. 箱体/壳体类零件
代表件:变速箱体、减速器壳、发动机缸体/缸盖、泵体。
共同结构特征:
- 多个加工基准面和装配面。
- 轴承孔、轴孔、盖板接口、法兰接口、螺栓孔、油孔/水道/流道、凸台、加强肋、安装脚。
- references-only 适合指导“结构像箱体”,但不能替代铸造设计、流体设计、强度校核或真实产品图纸。
### 变速箱体
适合简化 references-only 优化。
实施计划:
- 识别:`变速箱体``gearbox housing``transmission housing`
- 不变量:齿轮腔;轴承孔或轴孔;安装面;盖板/分型面;螺栓孔;油口或密封相关结构。
- 变体:整体箱体、剖分箱体、侧盖箱体、多轴箱体、电机法兰箱体。
- 默认:圆角矩形壳体,内部腔体,两组同轴轴承孔,盖板法兰和螺栓孔,安装脚,油口。
- 参数角色:`length``width``height``wall_thickness``shaft_center_distance``bearing_bore_diameter``bore_axis_positions``cover_flange_thickness``bolt_count``mounting_foot_dimensions`
- feature roles`main_cavity``bearing_bores``cover_face``mounting_feet``bolt_patterns``oil_ports``ribs`
- 建模顺序:外壳包络;抽壳/切主腔;加轴承凸台;切轴承孔;加盖板法兰和安装脚;切螺栓孔/油口;最后圆角和肋。
- 常见失败:只是一个盒子;无轴承孔;轴孔不对齐;无盖板接口;随机打孔。
### 减速器壳
适合 references-only 优化,尤其是简化平行轴、蜗轮蜗杆或行星减速器壳。
实施计划:
- 识别:`减速器壳``减速箱壳``reducer housing``gear reducer case`
- 不变量:输入/输出轴承支撑;轴距或同轴关系;齿轮腔;安装底座/法兰;盖板接口。
- 变体:平行轴减速器、直角/蜗杆减速器、行星减速器外壳、剖分壳体。
- 默认:紧凑两轴减速器壳,齿轮腔、输入/输出轴承孔、盖板法兰、安装脚。
- 参数角色:`input_bore_diameter``output_bore_diameter``shaft_spacing``housing_length``housing_width``housing_height``wall_thickness``cover_bolt_count``mounting_hole_spacing`
- feature roles`gear_cavity``input_bearing_bore``output_bearing_bore``cover_flange``mounting_base``oil_fill_drain_ports`
- 建模顺序:外壳;主腔;轴承凸台和孔;盖板面;安装脚;螺栓孔/油口;肋和圆角。
- 常见失败:没有定义输入/输出轴;轴线关系混乱;缺盖板;没有安装方式。
### 发动机缸体/缸盖
不适合作为 references-only 优化对象。
原因:真实缸体/缸盖强依赖缸数和缸径、燃烧室、气门机构、水套、油道、缸垫孔型、铸造与加工工艺、强度和热管理。reference 可以生成教学级简化块体,但不能可靠优化“发动机缸体/缸盖”这类工业零件。因此不写实施计划。
### 泵体
只适合简化 references-only 优化;不适合流体性能或真实水力设计。
实施计划:
- 识别:`泵体``泵壳``pump body``pump housing``volute`
- 不变量:叶轮腔或流体腔;入口/出口通道或法兰;密封/轴承基准;安装脚或安装面;螺栓孔。
- 变体:离心泵蜗壳、直通泵体、带盖泵壳、带轴承架泵体。
- 默认:简化离心泵体,圆形叶轮腔、切向出口、轴向入口法兰、盖板螺栓孔和安装脚。
- 参数角色:`chamber_diameter``chamber_depth``inlet_diameter``outlet_diameter``outlet_angle``flange_diameters``bolt_count``wall_thickness``seal_bore_diameter`
- feature roles`impeller_chamber``inlet_port``outlet_port``flanges``seal_bore``cover_bolt_pattern``mounting_feet`
- 建模顺序:泵体包络;切叶轮腔;加入口/出口凸台和法兰;切简化流道;切盖板孔;加安装脚和圆角。
- 常见失败:普通盒子打孔;没有叶轮腔;入口出口不连通;缺密封面。
## 4. 板类零件
代表件:安装底板、连接板、模板、面板。
共同结构特征:
- 主体是平板或近似平板。
- 重点是厚度、外形、孔阵列、槽、沉孔/沉头、螺纹孔、基准边、倒角。
### 安装底板
高度适合 references-only 优化。
实施计划:
- 识别:`安装底板``mounting plate``base plate`
- 不变量:平板实体;安装孔;厚度;基准边或中心线。
- 变体:矩形板、圆角板、开槽板、网格孔板、沉孔板、螺纹孔板。
- 默认:居中矩形板,四角安装孔,小倒角。
- 参数角色:`length``width``thickness``corner_radius``hole_diameter``hole_offset_x``hole_offset_y``slot_length``counterbore_diameter`
- feature roles`plate_body``mounting_holes``slots``counterbores``datum_edges``chamfers`
- 建模顺序:板体;切孔/槽;切沉孔;倒角。
- 常见失败:孔离边太近;通孔/盲孔语义错;缺厚度;原点任意。
### 连接板
高度适合 references-only 优化。
实施计划:
- 识别:`连接板``转接板``connecting plate``adapter plate`
- 不变量:连接两个或多个孔型/接口;孔型之间有明确相对位置。
- 变体:平面转接板、偏置转接板、长槽调节板、三角连接板、狗骨连杆板。
- 默认:矩形转接板,两个命名孔型 A/B 和中心线基准。
- 参数角色:`plate_length``plate_width``thickness``pattern_a_hole_count``pattern_a_spacing``pattern_b_hole_count``pattern_b_spacing``pattern_offset`
- feature roles`plate_body``pattern_a``pattern_b``adjustment_slots``datum_centerlines`
- 建模顺序:外形;切孔型 A 和 B;切槽或口袋;倒角。
- 常见失败:把两个孔型合成一个;丢失偏置关系;孔位不对称。
### 模板
在几何明确时适合 references-only 优化;自由曲线模板需要尺寸或参考图。
实施计划:
- 识别:`模板``drill template``routing template``inspection template`
- 不变量:薄板;参考外形;导向孔/槽;定位缺口或基准边。
- 变体:钻孔模板、铣削/雕刻模板、检测模板、轮廓模板。
- 默认:薄矩形模板,导向孔和定位缺口。
- 参数角色:`outline_dimensions``thickness``guide_hole_diameter``guide_hole_positions``alignment_notch_size`
- feature roles`template_outline``guide_holes``alignment_notches``slots``reference_edges`
- 建模顺序:外形;切导向特征;切缺口;倒角。
- 常见失败:做得过厚;缺导向特征;没有基准边。
### 面板
适合 references-only 优化。
实施计划:
- 识别:`面板``front panel``control panel``connector panel`
- 不变量:平板面;安装孔;功能开口;边缘处理。
- 变体:设备前面板、仪表面板、连接器面板、通风面板、盖板。
- 默认:矩形面板,四角安装孔;只有用户说明功能时加显示窗/按钮孔/连接器开口。
- 参数角色:`panel_length``panel_width``panel_thickness``cutout_size``cutout_position``mounting_hole_diameter``hole_offsets``corner_radius`
- feature roles`panel_body``mounting_holes``connector_cutouts``display_window``ventilation_slots``edge_chamfers`
- 建模顺序:面板主体;切主开口;切小孔/槽;倒角去毛刺。
- 常见失败:遗漏用户指定开口;做成 UI 卡片状而不是可加工面板;孔未参数化。
## 5. 异形件/曲面零件
代表件:叶轮、涡轮叶片、螺旋桨、凸轮。
共同判断:这类零件往往需要曲线、扭转、截面分布、运动规律或气动/流体设计。references-only 可以指导简化外形,但很难保证真实工程正确。
### 叶轮
适合简化 references-only 优化;不适合水力/气动性能。
实施计划:
- 识别:`叶轮``impeller``centrifugal impeller``compressor wheel`
- 不变量:中心轮毂;中心孔;背板或盖板;重复叶片;绕轴对称。
- 变体:开式叶轮、半开式叶轮、闭式叶轮、带分流叶片、后弯叶片。
- 默认:开式离心叶轮,背板、轮毂、中心孔、均布后弯叶片。
- 参数角色:`outer_diameter``hub_diameter``bore_diameter``blade_count``blade_height``blade_thickness``blade_sweep_angle``blade_start_radius``blade_end_radius``backplate_thickness`
- feature roles`backplate``hub``bore``main_blades`、可选 `splitter_blades``blade_root_fillets`
- 建模顺序:背板/轮毂;生成一片扫掠或 loft 叶片;环向阵列;切中心孔;叶根圆角。
- 常见失败:像平面风扇;叶片不连到轮毂;叶片数错;没有中心孔。
### 涡轮叶片
不适合作为 references-only 优化对象。
原因:真实涡轮叶片需要翼型截面、扭转、平台、榫根、冷却孔、材料/工艺约束和严格曲面质量。reference 只能生成玩具级外形,不能可靠优化工业涡轮叶片。因此不写实施计划。
### 螺旋桨
不适合作为通用 references-only 优化对象。
原因:螺旋桨需要直径、桨距或桨距分布、翼型截面、扭转、弦长分布、桨毂、应用场景。缺少这些时,reference 只会得到“视觉像螺旋桨”的模型,而不是正确叶片。因此不写实施计划。
### 凸轮
当作为“带厚度的 2D 轮廓凸轮”时适合;不适合没有运动规律的动态设计验证。
实施计划:
- 识别:`凸轮``cam``disk cam``cam profile``lift curve`
- 不变量:非圆轮廓;绕中心旋转;驱动从动件升程;中心孔或安装孔;厚度。
- 变体:偏心凸轮、盘形凸轮、径向凸轮、简化沟槽凸轮。
- 默认:盘形凸轮,基圆、一段升程-停歇-回程轮廓、中心孔;传扭场景加键槽。
- 参数角色:`base_circle_radius``lift``rise_angle``dwell_angle``return_angle``follower_radius``thickness``bore_diameter``keyway_width`
- feature roles`cam_profile``base_circle``lobe``bore``keyway`、可选 `face_groove`
- 建模顺序:根据简化运动规律生成闭合 2D 极坐标轮廓;拉伸;切中心孔/键槽;倒角。
- 常见失败:只是圆盘加凸点;轮廓不光顺;无中心孔;轮廓自交。
## 6. 模具零件
代表件:注塑模仁、压铸模芯、冲压模具镶件。
共同判断:模仁/模芯主形状通常来自产品几何的正负形,不适合靠通用 reference 猜出来。references-only 只适合辅助特征,如螺钉孔、销孔、冷却孔、基准面、避空和安装方式。
### 注塑模仁/型腔镶件
不适合作为 references-only 优化对象。
原因:核心型腔面取决于塑件几何、缩水率、拔模、顶出、浇口、冷却、分型线和模架结构。没有产品几何时,reference 无法决定主形体。因此不写实施计划。
### 压铸模芯
不适合作为 references-only 优化对象。
原因:压铸模芯依赖铸件产品、分型策略、滑块/抽芯、拔模、浇排系统、热管理和合金工艺。reference-only 无法可靠优化。因此不写实施计划。
### 冲压模具镶件
对简单镶件、冲头、凹模套、轮廓块适合;复杂成形镶件仍需工艺设计。
实施计划:
- 识别:`冲压模具镶件``stamping die insert``punch insert``die button`
- 不变量:工具钢块或圆形镶件;工作刃口/轮廓;定位特征;紧固孔;避空或落料空间。
- 变体:矩形轮廓镶件、圆凹模套、冲头镶件、压料相关镶件。
- 默认:矩形镶件块,带指定工作轮廓、螺钉孔、销孔和非工作边倒角。
- 参数角色:`block_length``block_width``block_height``working_profile``clearance_offset``screw_hole_positions``dowel_hole_positions``relief_depth`
- feature roles`insert_body``working_edge_or_profile``screw_holes``dowel_holes``relief_pocket``datum_faces`
- 建模顺序:块体;切/凸出工作轮廓;加避空;切定位孔和螺钉孔;倒非工作边。
- 常见失败:没有定位孔;工作刃口和避空不区分;只做装饰轮廓;声称冲裁间隙正确。
## 7. 精密零件
代表件:量规、夹具定位件、光学基座、医疗器械零件。
共同边界:references 可以改善结构和基准表达,但不能声明公差、校准、计量认证、医疗合规或光学性能。
### 量规
适合简单量规几何;不适合认证计量。
实施计划:
- 识别:`量规``gauge``go/no-go gauge``inspection gauge`
- 不变量:受控测量几何;手柄或主体;测量开口/塞规/环规特征;基准面或标识。
- 变体:塞规、环规、间隙规、轮廓规、台阶规。
- 默认:简单台阶/间隙规,带测量开口和手柄。
- 参数角色:`nominal_size``go_size``no_go_size``gauge_thickness``handle_length``measuring_slot_width``relief_radius`
- feature roles`gauge_body``measuring_feature``handle``datum_face``relief_notches`
- 建模顺序:平面量规主体;切测量开口;加手柄/避空;倒安全边。
- 常见失败:像装饰尺子;没有实际测量特征;声称已校准。
### 夹具定位件
高度适合 references-only 优化。
实施计划:
- 识别:`夹具定位件``定位块``fixture locator``locating block``datum block`
- 不变量:定位基准面;定位销/销孔;安装孔;夹持或接触面;重复定位逻辑。
- 变体:定位块、V 形块、止挡块、销定位件、支承垫块。
- 默认:矩形定位块,基准面、两个销孔、安装孔;如果定位圆料则加 V 槽或台阶。
- 参数角色:`block_length``block_width``block_height``datum_face_offset``dowel_pin_diameter``dowel_spacing``mounting_hole_diameter``v_groove_angle``step_height`
- feature roles`locator_body``datum_faces``dowel_holes``mounting_holes``contact_surface``v_groove_or_step`
- 建模顺序:块体;加工基准/接触特征;切销孔和安装孔;倒角。
- 常见失败:没有基准;孔位不相对基准定义;只是普通块;定位孔布局过约束。
### 光学基座
适合结构 reference;不适合声明光学平面度或对准精度。
实施计划:
- 识别:`光学基座``optical base``optical mounting base``precision base`
- 不变量:平基座;规则孔阵列或安装孔;凸台/导轨/安装垫;基准边;稳定支撑。
- 变体:光学面包板式基座、镜架底座、导轨转接座、棱镜安装基座。
- 默认:矩形平基座,规则孔阵列,两条基准边,可选凸台。
- 参数角色:`base_length``base_width``base_thickness``hole_grid_pitch``hole_diameter``pad_height``pad_positions``slot_dimensions`
- feature roles`precision_base``hole_grid``datum_edges``raised_mounting_pads``slots``chamfers`
- 建模顺序:基座;切孔阵列;加凸台/槽;倒角。
- 常见失败:随机装饰孔;没有基准边;声称光学平面度。
### 医疗器械零件
不适合作为一个通用 references-only 代表件。
原因:`医疗器械零件` 范围过宽,而且常涉及安全、法规、材料、生物相容性、灭菌和临床用途。可以针对明确几何子类写 reference,例如简单手术夹具、定位块、试模件,但不能对这个大类做通用优化。因此不写实施计划。
## 优先级建议
最适合第一批做 reference 的代表件:
- 阶梯轴
- 传动轴
- 法兰盘
- 端盖
- 轴承座
- 安装底板
- 连接板
- 夹具定位件
- 简化叶轮
- 凸轮
暂不建议作为 reference-only 首批目标:
- 发动机缸体/缸盖
- 涡轮叶片
- 螺旋桨
- 注塑模仁/型腔镶件
- 压铸模芯
- 泛化的医疗器械零件
这些对象需要标准、产品几何、截面曲线、运动规律、工艺决策、性能计算或法规边界。只靠 reference 很容易生成“像”,但不容易生成“对”。
## 在 text-to-cad 中的使用建议
只做 references 时,推荐先把本文拆成 7 个短 reference 文件,并在 `skills/cad/SKILL.md``Progressive references` 中加触发说明:
- 用户请求出现轴、主轴、丝杠、花键、键槽时,加载 `part-families/shaft.md`
- 出现法兰、端盖、轴承座、同步带轮、齿轮毛坯时,加载 `part-families/disc-sleeve.md`
- 出现箱体、壳体、减速器壳、泵体时,加载 `part-families/housing.md`
- 出现底板、连接板、模板、面板时,加载 `part-families/plate.md`
- 出现叶轮、凸轮、叶片、螺旋桨时,加载 `part-families/freeform-surface.md`,并对不适合项保持强 caveat。
- 出现模仁、模芯、冲压镶件时,加载 `part-families/mold-tooling.md`,并提醒主形体需要产品几何。
- 出现量规、夹具定位、光学基座、医疗件时,加载 `part-families/precision-fixture.md`,并避免精度/合规承诺。
这一步会影响生成,因为 CAD agent 在写代码前会获得“这类零件应该有哪些结构”的上下文。但它不会像 factory 那样强制固定几何,也不会像 validator 那样保证正确性。
## 资料链接
- Protolabs CNC milling design guidelines: https://www.protolabs.com/services/cnc-machining/cnc-milling/design-guidelines/
- Xometry CNC CAD design tips: https://www.xometry.com/resources/machining/10-tips-improve-cad-cnc-design/
- Texas Flange flange guide: https://texasflange.com/blog/the-complete-guide-to-flanges/
- ASKUBAL pillow block / bearing housing overview: https://www.askubal.de/en/knowledge-base/bearing-units/pillow-block/
- Roton trapezoidal lead screws: https://www.roton.com/products/trapezoidal-lead-screws-nuts/general-information/
- NASA OpenVSP propeller geometry notes: https://www.nasa.gov/reference/openvsp-propellers/
- Marine propeller geometric parameters: https://link.springer.com/article/10.1007/s00773-022-00878-6
- Injection mold component overview: https://fecision.com/parts-and-components-of-an-injection-mold/
- CNC fixture design / workholding reference: https://resources.utec.co/workholding/fixture-design-cnc-machining/
@@ -0,0 +1,894 @@
# CNC machined part family reference optimization plan
This document is a first-pass implementation plan for improving `text-to-cad`
generation quality using reference guidance only. It is not a validator or a
factory design. The goal is to teach the CAD agent the recurring geometry,
datum choices, parameter roles, feature roles, and common failure modes of
common CNC machined part families before it writes build123d source.
Research sources used for this plan include public CNC design-for-machining
guides from Protolabs and Xometry, flange references from Texas Flange and
ASME-flange guides, bearing-housing descriptions from ASKUBAL, lead-screw and
spline references from Roton, igus, Machinery's Handbook excerpts, and online
manufacturing articles on gearbox housings, impellers, propellers, mold
components, and fixture parts. These sources should be treated as guidance for
recognition and modeling structure, not as a substitute for standards, drawings,
tolerances, or certification.
## Reference optimization principle
References should improve the first generated model without locking all output
to one template. Each part-family reference should be written as:
1. Recognition terms: English and Chinese names, aliases, and adjacent part names.
2. Invariants: features that must exist for the part to be semantically correct.
3. Variants: common forms the user may ask for.
4. Default strategy: what to do when the user gives no variant.
5. Parameter roles: dimensions that must become named parameters.
6. Feature roles: modeling units the agent should plan explicitly.
7. Construction sequence: safe build123d order.
8. Common failures: mistakes the reference should actively prevent.
9. Validation targets: geometry facts to inspect manually or with generic CAD tools.
Important: references are soft constraints. They should say "default to" and
"unless the user specifies otherwise", not "always generate this exact shape".
## Cross-family CNC modeling rules
Use these rules in every part-family reference:
- Keep all functional dimensions as named parameters, not buried numbers.
- Choose the origin from the dominant functional datum: shaft axis, bearing axis,
plate center, mounting face, split face, or sealing face.
- Build base geometry first, cut primary holes/bores second, add secondary
holes/pockets/slots third, and apply fillets/chamfers last.
- Prefer standard drilled/tapped/clearance hole sizes when the user does not
give a custom size.
- Avoid thin walls, deep narrow pockets, blind holes without depth, tiny features,
and square internal corners unless explicitly requested.
- For CNC-like models, use internal radii or relieved corners for milled pockets;
use external chamfers for handling and deburring.
- For multi-face parts, preserve datum relationships over visual symmetry.
- Do not claim real machinability, tolerance compliance, sealing performance,
fatigue strength, medical suitability, or aerospace suitability from reference
guidance alone.
## Suggested reference file layout
Do not create dozens of files immediately. Start with seven family references:
- `references/part-families/shaft.md`
- `references/part-families/disc-sleeve.md`
- `references/part-families/housing.md`
- `references/part-families/plate.md`
- `references/part-families/freeform-surface.md`
- `references/part-families/mold-tooling.md`
- `references/part-families/precision-fixture.md`
Later, split high-value representatives such as flange, bearing housing, stepped
shaft, mounting plate, and timing pulley into dedicated files only if the broad
family references are not enough.
## 1. Shaft-like parts
Representative parts: drive shaft, spindle, stepped shaft, spline shaft, lead screw.
Family recognition terms:
- English: shaft, drive shaft, spindle, stepped shaft, spline shaft, lead screw,
threaded shaft, keyseat, journal, shoulder.
- Chinese: 轴, 传动轴, 主轴, 阶梯轴, 花键轴, 丝杠, 轴肩, 轴颈, 键槽.
Shared structure:
- Dominant rotational axis.
- One or more cylindrical sections.
- Functional journals, shoulders, grooves, threads, splines, keyseats, end holes,
chamfers, and reliefs.
- Datum relationships are mostly axial station, diameter, concentricity, and
orientation of torque-transfer features.
### Drive shaft
Suitability: suitable for references optimization.
Implementation plan:
- Recognition: use when the request mentions `drive shaft`, `transmission shaft`,
`motor shaft`, `传动轴`, or shaft mounted gears/pulleys/couplings.
- Invariants: long rotational body; at least one torque-transfer interface or
mounted-component journal; end chamfers; clear axis convention.
- Variants: plain shaft, shouldered shaft, keyed shaft, cross-drilled shaft,
threaded-end shaft, shaft with retaining-ring grooves.
- Default: if under-specified, generate a two- or three-step shaft along X with
chamfered ends, one central journal, one keyseat on the largest diameter, and
optional retaining grooves only if requested.
- Parameter roles: total_length, axis, section_lengths, section_diameters,
journal_diameter, shoulder_positions, keyseat_width, keyseat_depth,
keyseat_length, end_chamfer, groove_width, groove_diameter.
- Feature roles: rotational_stack, shoulders, journals, keyseat, end_chamfers,
retaining_grooves, threaded_ends.
- Construction sequence: revolve or union cylinders by axial stations; cut
keyseat with an overshooting rectangular tool; cut grooves; cut end features;
apply chamfers/fillets last.
- Common failures to prevent: vertical shaft when user expects horizontal,
non-coaxial sections, keyseat cutting fully through the shaft, arbitrary
section lengths, unlabeled parameters.
- Validation targets: total length, section diameters, coaxiality, keyseat
location and non-through depth, chamfer existence, single connected solid.
### Spindle
Suitability: suitable for reference guidance, but not for precision claims.
Implementation plan:
- Recognition: use for `spindle`, `machine spindle`, `主轴`, tool-holding shafts,
bearing journals, taper noses, and precision rotating shafts.
- Invariants: dominant axis, bearing journals, locating shoulders, tool/work
interface at one end, high-importance datum surfaces.
- Variants: simple spindle blank, stepped spindle, taper-nose spindle, threaded
nose, through-bore spindle.
- Default: create a stepped shaft with two bearing journals, shoulders, a nose
interface, and optional through-bore if requested.
- Parameter roles: bearing_journal_diameter, bearing_spacing, nose_diameter,
nose_length, taper_angle, through_bore_diameter, shoulder_width,
relief_groove_width, end_thread_length.
- Feature roles: bearing_journals, datum_shoulders, tool_nose, through_bore,
relief_grooves, end_threads.
- Construction sequence: build coaxial stack; add nose taper by revolve; cut
through-bore coaxially; add relief grooves; chamfer shoulders last.
- Common failures to prevent: treating spindle as a decorative shaft, missing
bearing journals, missing locating shoulders, claiming runout/tolerance.
- Validation targets: coaxial sections, bearing journal count, nose location,
through-bore coaxiality, axial spacing.
### Stepped shaft
Suitability: highly suitable for references optimization.
Implementation plan:
- Recognition: `stepped shaft`, `multi-diameter shaft`, `阶梯轴`, `多段直径`.
- Invariants: multiple coaxial cylindrical sections with ordered axial lengths
and diameters.
- Variants: simple stepped shaft, stepped shaft with keyway, stepped shaft with
end thread, stepped shaft with center holes.
- Default: generate three coaxial sections along X, with larger middle section,
shoulders, end chamfers, and no keyway unless requested.
- Parameter roles: total_length, segment_lengths, segment_diameters,
axis_direction, shoulder_radii, chamfer_size, optional_keyway.
- Feature roles: coaxial_cylindrical_stack, shoulders, end_chamfers, keyseat,
center_drill_holes.
- Construction sequence: build cylinders by segment; fuse as one solid; cut
optional features; chamfer/fillet shoulders and ends last.
- Common failures to prevent: stacking along Z by default when prompt says shaft,
wrong segment order, detached cylinders, missing shoulders.
- Validation targets: axis direction, total length, each segment diameter and
length, single solid.
### Spline shaft
Suitability: suitable as a simplified reference model; exact spline standards
require drawings or standards and should not be promised.
Implementation plan:
- Recognition: `spline shaft`, `involute spline`, `straight spline`, `花键轴`.
- Invariants: shaft plus repeated axial teeth or grooves on a spline region,
centered around the main axis.
- Variants: straight-sided external spline, simplified involute-like spline,
serrated spline, partial-length spline.
- Default: use a simplified straight-sided external spline region with evenly
spaced teeth unless the user specifies an involute standard.
- Parameter roles: shaft_diameter, spline_outer_diameter, spline_root_diameter,
spline_length, tooth_count, tooth_width_angle, pressure_angle_if_known,
root_fillet_radius.
- Feature roles: base_shaft, spline_region, repeated_teeth_or_grooves,
transition_shoulders, chamfers.
- Construction sequence: build base shaft; add/cut repeated teeth/grooves around
axis using polar pattern; add transition reliefs; chamfer ends.
- Common failures to prevent: treating spline as a single keyway, uneven tooth
spacing, teeth not attached to shaft, excessive tooth count causing fragile
topology.
- Validation targets: tooth count, angular spacing, spline length, coaxiality,
tooth attachment to shaft.
### Lead screw
Suitability: suitable for simplified references; unsuitable for accurate ball
screw internals or certified thread geometry without standards.
Implementation plan:
- Recognition: `lead screw`, `ACME screw`, `trapezoidal screw`, `丝杠`,
`梯形丝杠`.
- Invariants: long shaft with helical or visually helical thread region; thread
pitch/lead and major diameter are named.
- Variants: trapezoidal thread, ACME thread, simple helical ridge, threaded
region plus plain bearing journals.
- Default: generate a shaft with plain end journals and a central simplified
trapezoidal helical thread region.
- Parameter roles: major_diameter, minor_diameter, pitch, lead, thread_length,
starts, thread_angle, journal_diameter, end_length.
- Feature roles: screw_core, helical_thread, end_journals, relief_grooves,
chamfers.
- Construction sequence: build core shaft; add swept helical ridge or simplified
visual thread; add end journals and reliefs; chamfer ends.
- Common failures to prevent: modeling fastening thread instead of power screw,
omitting pitch/lead, thread over entire part when journals are requested.
- Validation targets: thread region length, major/minor diameter envelope,
pitch count if modeled discretely, coaxial end journals.
## 2. Disc and sleeve parts
Representative parts: flange, end cover, bearing housing, timing pulley, gear blank.
Family recognition terms:
- English: flange, cover, end cap, bearing seat, bearing housing, pulley, timing
pulley, gear blank, hub, bushing, sleeve.
- Chinese: 法兰盘, 端盖, 轴承座, 带座轴承, 同步带轮, 齿轮毛坯, 轮毂, 轴套.
Shared structure:
- Often axisymmetric or partly axisymmetric.
- Central bore, hub, boss, bolt circle, counterbores, sealing face, shoulder,
groove, flange rim, or belt/gear interface.
### Flange
Suitability: highly suitable for references optimization.
Implementation plan:
- Recognition: `flange`, `flange plate`, `pipe flange`, `法兰`, `法兰盘`.
- Invariants: circular disk or hubbed disk; central bore; bolt-hole pattern;
sealing or mating face.
- Variants: flat face, raised face, ring groove, weld-neck-like hub, blind
flange, adapter flange.
- Default: circular flat or raised-face flange with central through-bore and
evenly spaced bolt holes on a bolt circle.
- Parameter roles: outside_diameter, thickness, bore_diameter, bolt_count,
bolt_circle_diameter, bolt_hole_diameter, raised_face_diameter,
raised_face_height, hub_diameter, hub_height, fillet_radius.
- Feature roles: flange_disk, central_bore, bolt_circle_holes, sealing_face,
raised_face_or_groove, hub, edge_fillets.
- Construction sequence: cylinder disk; optional hub/raised face; cut central
bore; cut patterned bolt holes; add gasket groove if requested; fillet last.
- Common failures to prevent: missing central bore, bolt holes not on a circle,
non-coaxial bore, extra random holes, fillets before cuts.
- Validation targets: OD, thickness, bore diameter, bolt count, bolt circle,
angular spacing, sealing face coaxiality.
### End cover
Suitability: suitable for references optimization.
Implementation plan:
- Recognition: `end cover`, `bearing cover`, `cap`, `端盖`, `轴承端盖`.
- Invariants: cover plate/cap, locating lip or pilot diameter, bolt holes,
sealing face, optional central opening or boss.
- Variants: blind end cover, bearing retainer cover, flanged cap, cover with
oil seal bore.
- Default: shallow round cover with pilot lip, bolt circle, and optional central
boss if the request mentions shaft/bearing.
- Parameter roles: cover_outer_diameter, cover_thickness, pilot_diameter,
pilot_depth, bolt_count, bolt_circle_diameter, bolt_hole_diameter,
seal_bore_diameter, gasket_groove_width.
- Feature roles: cover_plate, pilot_lip, bolt_circle_holes, seal_bore,
gasket_land, outer_chamfer.
- Construction sequence: disk/cap body; add lip or boss; cut central/seal bore;
cut bolt holes; add gasket groove; chamfer/fillet last.
- Common failures to prevent: no locating lip, bolt holes not symmetric,
wrong cover side, thin unsupported rim.
- Validation targets: pilot diameter/depth, bolt circle, center opening,
thickness, sealing face.
### Bearing housing
Suitability: suitable, but must preserve variants and avoid one fixed template.
Implementation plan:
- Recognition: `bearing housing`, `pillow block`, `bearing seat`, `bearing
support`, `轴承座`, `带座轴承`, `轴承支座`.
- Invariants: bearing seat or bearing bore; shaft clearance axis; mounting base
or flange; material supporting the bearing; mounting holes.
- Variants: pillow block, flanged bearing housing, plate-mounted bearing seat,
split bearing housing, cartridge holder.
- Default: if under-specified, generate a pillow-block style single solid:
flat base, raised bearing boss, coaxial bearing/shaft bore, two mounting holes,
and reinforcing ribs.
- Parameter roles: bearing_outer_diameter, bearing_width, shaft_diameter,
bearing_center_height, base_length, base_width, base_thickness,
mount_hole_diameter, mount_hole_spacing, boss_outer_diameter,
wall_thickness, rib_thickness.
- Feature roles: base_block, bearing_boss, bearing_seat_bore,
shaft_clearance_bore, mounting_holes, side_walls, reinforcing_ribs,
split_cap_if_requested.
- Construction sequence: create base; add bearing boss/support body; cut bearing
bore and shaft clearance coaxially; add mounting holes; add ribs that intersect
base and boss; fillet last.
- Common failures to prevent: looking like a generic bracket, missing bearing
bore, ribs floating or detached, mounting holes asymmetric, bearing center
height undefined.
- Validation targets: bore coaxiality, center height, mounting-hole symmetry,
bore diameter/depth, connected solid, planar base.
### Timing pulley
Suitability: suitable for visual/parametric references; exact tooth standard
requires profile data.
Implementation plan:
- Recognition: `timing pulley`, `synchronous pulley`, `belt pulley`,
`同步带轮`.
- Invariants: toothed circumference matched to pitch/tooth count, central bore,
hub or set-screw/keyway interface, optional flanges.
- Variants: flangeless, single-flange, double-flange, hubbed, keyed bore,
set-screw bore.
- Default: simplified double-flanged timing pulley with central bore, hub, and
repeated grooves/teeth based on tooth_count and pitch.
- Parameter roles: tooth_count, belt_pitch, pulley_width, bore_diameter,
hub_diameter, hub_length, flange_diameter, flange_thickness,
tooth_depth, keyway_width, set_screw_hole_diameter.
- Feature roles: toothed_rim, pitch_circle, flanges, bore, hub, keyway,
set_screw_holes.
- Construction sequence: build pulley blank and hub; add/cut repeated tooth
grooves around rim; add flanges; cut bore/keyway/set screws; chamfer last.
- Common failures to prevent: smooth pulley with no teeth, wrong tooth count,
teeth not evenly spaced, missing bore, flanges confused with gear teeth.
- Validation targets: tooth count, width, bore diameter, flange presence, hub
coaxiality.
### Gear blank
Suitability: suitable for references optimization. This is not final gear-tooth
generation unless requested.
Implementation plan:
- Recognition: `gear blank`, `gear body`, `齿轮毛坯`.
- Invariants: round blank with bore, faces, optional hub, keyway, bolt/dowel
holes, weight-relief pockets.
- Variants: plain gear blank, hubbed blank, webbed blank, blank with lightening
holes, blank with keyway.
- Default: axisymmetric disk with central bore, hub, chamfers, and optional
keyway if torque transfer is mentioned.
- Parameter roles: outside_diameter, face_width, bore_diameter, hub_diameter,
hub_width, web_thickness, keyway_width, keyway_depth, lightening_hole_count.
- Feature roles: gear_blank_body, central_bore, hub, keyway, lightening_holes,
reference_faces.
- Construction sequence: turn-like disk/hub by revolve; cut bore; cut keyway or
pockets; chamfer reference faces.
- Common failures to prevent: adding inaccurate gear teeth when user asked only
for blank, non-coaxial bore, missing hub/keyway when specified.
- Validation targets: OD, face width, bore, hub, keyway location, symmetry.
## 3. Housing and casing parts
Representative parts: gearbox housing, reducer housing, engine block/head, pump body.
Family recognition terms:
- English: housing, casing, gearbox housing, reducer housing, pump body, engine
block, cylinder head, valve body.
- Chinese: 箱体, 壳体, 变速箱体, 减速器壳, 发动机缸体, 缸盖, 泵体, 阀体.
Shared structure:
- Multiple machined datums and mating faces.
- Bearing bores or shaft holes.
- Cover/flange interfaces, bolt patterns, oil/coolant/flow passages, bosses,
ribs, feet, pads, and wall thickness.
### Gearbox housing
Suitability: suitable for simplified references optimization. Full industrial
gearbox housings need drawings and are not guaranteed by references alone.
Implementation plan:
- Recognition: `gearbox housing`, `transmission housing`, `变速箱体`.
- Invariants: cavity for gears, bearing bores or shaft openings, mounting faces,
cover/split face, bolt patterns, oil/seal features.
- Variants: one-piece housing, split housing, side cover housing, multi-shaft
housing, motor flange housing.
- Default: rectangular/rounded housing with internal cavity, two coaxial bearing
bores, cover flange with bolt holes, mounting feet, and oil port.
- Parameter roles: length, width, height, wall_thickness, shaft_center_distance,
bearing_bore_diameter, bore_axis_positions, cover_flange_thickness,
bolt_count, mounting_foot_dimensions, seal_groove_diameter.
- Feature roles: main_cavity, bearing_bores, split_or_cover_face, mounting_feet,
bolt_patterns, oil_ports, ribs, seal_grooves.
- Construction sequence: create outer housing shell; hollow cavity; add bosses
around bores; cut bearing bores; add cover flange and mounting feet; cut bolt
holes and ports; add ribs/fillets last.
- Common failures to prevent: simple box with no bores, misaligned bearing bores,
no cover face, random holes, walls too thin.
- Validation targets: bore count and axes, wall thickness envelope, flange face,
bolt pattern, cavity existence, mounting feet.
### Reducer housing
Suitability: suitable for references optimization, especially simplified worm,
spur, or planetary reducer housings.
Implementation plan:
- Recognition: `reducer housing`, `gear reducer case`, `减速器壳`, `减速箱壳`.
- Invariants: input/output bearing support, shaft center distance, gear cavity,
mounting base/flange, cover interface.
- Variants: parallel-shaft reducer, right-angle/worm reducer, planetary reducer
shell, split-case reducer.
- Default: compact two-shaft reducer housing with gear cavity, two bearing-bore
axes, cover flange, and mounting feet.
- Parameter roles: input_bore_diameter, output_bore_diameter, shaft_spacing,
housing_length, housing_width, housing_height, wall_thickness,
cover_bolt_count, mounting_hole_spacing.
- Feature roles: gear_cavity, input_bearing_bore, output_bearing_bore,
cover_flange, mounting_base, oil_fill_drain_ports.
- Construction sequence: outer shell; cavity; bearing bosses and bores; cover
face; feet; bolt/port holes; ribs/fillets.
- Common failures to prevent: no defined shaft axes, bores not parallel/coaxial
according to variant, missing cover, no mounting method.
- Validation targets: shaft spacing, bore diameter, cover bolt pattern, base
plane, cavity and wall thickness.
### Engine block / cylinder head
Suitability: not suitable for references-only optimization as a representative
industrial part. The real geometry depends on combustion chamber design,
cylinder layout, valve train, water jackets, oil galleries, head gasket pattern,
casting constraints, and application-specific standards. A reference can help
make an educational simplified block/head, but it should not be used as a
general optimized CNC part family. No implementation plan.
### Pump body
Suitability: suitable only for simplified pump-body references; not suitable for
accurate hydraulic/flow performance without drawings or CFD geometry.
Implementation plan:
- Recognition: `pump body`, `pump housing`, `volute`, `泵体`, `泵壳`.
- Invariants: impeller chamber or flow cavity, inlet/outlet ports or flanges,
seal/bearing datum, mounting feet/pads, bolt patterns.
- Variants: centrifugal volute body, simple inline pump body, cover-mounted pump
casing, bearing-frame pump body.
- Default: simplified centrifugal pump body with circular impeller chamber,
tangential discharge, axial inlet flange, cover bolt pattern, and mounting feet.
- Parameter roles: chamber_diameter, chamber_depth, inlet_diameter,
outlet_diameter, outlet_angle, flange_diameters, bolt_count, wall_thickness,
seal_bore_diameter, mounting_hole_spacing.
- Feature roles: impeller_chamber, inlet_port, outlet_port, flanges,
seal_bore, cover_bolt_pattern, mounting_feet.
- Construction sequence: create body envelope; cut chamber; add inlet/outlet
bosses/flanges; cut passages as simplified cylinders/sweeps; add cover bolt
pattern and feet; fillet last.
- Common failures to prevent: ordinary box with holes, missing volute/chamber,
inlet/outlet not connected, no mounting/sealing face.
- Validation targets: chamber existence, inlet/outlet axes, flange bolt patterns,
seal bore coaxiality, wall continuity.
## 4. Plate parts
Representative parts: mounting base plate, connecting plate, template, panel.
Family recognition terms:
- English: plate, mounting plate, base plate, connecting plate, template, panel,
tooling plate, fixture plate.
- Chinese: 板, 安装底板, 连接板, 模板, 面板, 工装板, 夹具板.
Shared structure:
- Mostly prismatic flat body.
- Hole patterns, slots, pockets, counterbores, tapped holes, datum edges, chamfers.
### Mounting base plate
Suitability: highly suitable for references optimization.
Implementation plan:
- Recognition: `mounting plate`, `base plate`, `安装底板`.
- Invariants: flat rectangular or shaped plate, mounting holes, datum edges or
centerline, thickness.
- Variants: rectangular, rounded-corner, slotted, grid-hole, counterbored,
tapped-hole plate.
- Default: centered rectangular plate with four corner mounting holes and small
edge chamfers if no hole count is specified.
- Parameter roles: length, width, thickness, corner_radius, hole_diameter,
hole_offset_x, hole_offset_y, slot_length, counterbore_diameter.
- Feature roles: plate_body, mounting_holes, slots, counterbores, datum_edges,
chamfers.
- Construction sequence: base plate; cut holes/slots; counterbores; chamfer.
- Common failures to prevent: holes too close to edges, non-through holes when
through holes requested, arbitrary origin, missing thickness.
- Validation targets: bounding box, hole count, hole positions, thickness,
slot dimensions.
### Connecting plate
Suitability: highly suitable for references optimization.
Implementation plan:
- Recognition: `connecting plate`, `adapter plate`, `连接板`, `转接板`.
- Invariants: plate connecting two patterns, usually two or more bolt patterns
and datum relationship between them.
- Variants: flat adapter, offset adapter, slotted adjustment plate, triangular or
dogbone link plate.
- Default: rectangular adapter plate with two named bolt patterns and datum
centerlines.
- Parameter roles: plate_length, plate_width, thickness, pattern_a_hole_count,
pattern_a_spacing, pattern_b_hole_count, pattern_b_spacing, pattern_offset.
- Feature roles: plate_body, pattern_a, pattern_b, adjustment_slots,
datum_centerlines.
- Construction sequence: plate outline; cut pattern A and B; add slots or pockets;
chamfer last.
- Common failures to prevent: merging two patterns into one, losing offset, holes
not symmetric when expected.
- Validation targets: both bolt patterns, relative offset, thickness, hole sizes.
### Template
Suitability: suitable for references optimization when template geometry is
explicit. Freeform template outlines need dimensions or a reference image.
Implementation plan:
- Recognition: `template`, `drill template`, `routing template`, `模板`.
- Invariants: thin plate, reference outline, guide holes/slots, marking or
alignment features.
- Variants: drill jig template, router template, inspection template, gauge-like
profile template.
- Default: thin rectangular template with guide holes and alignment notches.
- Parameter roles: outline_dimensions, thickness, guide_hole_diameter,
guide_hole_positions, alignment_notch_size, label_text_if_requested.
- Feature roles: template_outline, guide_holes, alignment_notches, slots,
reference_edges.
- Construction sequence: outline; cut guide features; add notches; chamfer.
- Common failures to prevent: too thick, missing guide features, no datum edge.
- Validation targets: outline size, guide-hole count and position, flatness.
### Panel
Suitability: suitable for references optimization.
Implementation plan:
- Recognition: `panel`, `front panel`, `control panel`, `面板`.
- Invariants: flat plate face, cutouts, mounting holes, optional display/button
holes, edge treatment.
- Variants: rectangular front panel, instrument panel, connector panel, vented
panel, cover panel.
- Default: rectangular panel with corner mounting holes and one central cutout
only if function is specified.
- Parameter roles: panel_length, panel_width, panel_thickness, cutout_size,
cutout_position, mounting_hole_diameter, hole_offsets, corner_radius.
- Feature roles: panel_body, mounting_holes, connector_cutouts, display_window,
ventilation_slots, edge_chamfers.
- Construction sequence: panel body; cut main openings; cut small holes/slots;
chamfer/deburr edges.
- Common failures to prevent: missing user-specified cutouts, rounded card-like
visual instead of machinable panel, holes not parameterized.
- Validation targets: panel size, cutout size/position, hole count, thickness.
## 5. Freeform and curved-surface parts
Representative parts: impeller, turbine blade, propeller, cam.
Family recognition terms:
- English: impeller, turbine blade, propeller, cam, airfoil, blade, helical,
swept, twisted.
- Chinese: 叶轮, 涡轮叶片, 螺旋桨, 凸轮, 叶片, 扭转, 曲面.
### Impeller
Suitability: suitable for simplified references optimization. Exact hydraulic or
aero performance requires blade curves or a drawing.
Implementation plan:
- Recognition: `impeller`, `centrifugal impeller`, `compressor wheel`, `叶轮`.
- Invariants: central hub, bore, backplate or shroud depending on variant,
repeated blades, rotational symmetry.
- Variants: open impeller, semi-open impeller, closed impeller, splitter blades,
backward-curved blades.
- Default: open centrifugal impeller with backplate, hub, bore, and evenly spaced
backward-curved blades.
- Parameter roles: outer_diameter, hub_diameter, bore_diameter, blade_count,
blade_height, blade_thickness, blade_sweep_angle, blade_start_radius,
blade_end_radius, backplate_thickness.
- Feature roles: backplate, hub, bore, main_blades, splitter_blades,
blade_to_hub_transitions.
- Construction sequence: build backplate/hub; create one blade from a swept or
lofted profile; polar pattern blades; fuse carefully; add bore; fillet roots.
- Common failures to prevent: flat fan disk with rectangles, blades not attached,
wrong blade count, no hub/bore, random blade directions.
- Validation targets: blade count, even angular spacing, outer diameter, hub and
bore coaxiality, blade attachment.
### Turbine blade
Suitability: not suitable for references-only optimization as a general
representative part. Real turbine blades require airfoil sections, twist,
platform/root geometry, material/process constraints, cooling holes, and strict
surface quality. A reference can produce a toy blade, but it will not improve
industrial turbine-blade generation reliably. No implementation plan.
### Propeller
Suitability: not suitable for references-only optimization as a general
representative part. A propeller needs diameter, pitch or pitch distribution,
airfoil sections, blade twist, chord distribution, hub geometry, and application
context. Without those, references only produce a plausible visual propeller.
No implementation plan.
### Cam
Suitability: suitable when the cam is treated as a 2D profile with thickness;
not suitable for dynamic design validation without motion law data.
Implementation plan:
- Recognition: `cam`, `disk cam`, `凸轮`, `cam profile`, `lift curve`.
- Invariants: rotating body with non-circular profile that drives follower lift;
bore/keyway or mounting hole; thickness.
- Variants: eccentric cam, plate cam, radial disk cam, groove cam simplified as
face groove.
- Default: plate cam with base circle, one rise-dwell-return lobe, central bore,
and keyway if torque transfer is requested.
- Parameter roles: base_circle_radius, lift, rise_angle, dwell_angle,
return_angle, follower_radius, thickness, bore_diameter, keyway_width.
- Feature roles: cam_profile, base_circle, lobe, bore, keyway, optional_groove.
- Construction sequence: generate 2D polar profile from simple motion law or
simplified lobe curve; extrude; cut bore/keyway; chamfer edges.
- Common failures to prevent: simple circle with bump lacking lift parameters,
lobe not smooth, no bore, profile self-intersection.
- Validation targets: base radius, max radius/base + lift, thickness, bore,
smooth closed outline.
## 6. Mold parts
Representative parts: injection mold core/cavity insert, die-casting core, stamping die insert.
Family recognition terms:
- English: mold insert, core insert, cavity insert, die insert, punch insert,
stamping insert, ejector pin holes, cooling channels, parting face.
- Chinese: 模仁, 模芯, 型腔镶件, 压铸模芯, 冲压模具镶件, 顶针孔, 冷却水路, 分型面.
### Injection mold core/cavity insert
Suitability: not suitable for references-only optimization as a representative
part unless the molded-product geometry is supplied. The core/cavity surface is
the negative/positive of a product and depends on shrinkage, draft, ejection,
cooling, gates, and parting-line decisions. References can guide auxiliary
features, but not the main form. No implementation plan.
### Die-casting core
Suitability: not suitable for references-only optimization as a representative
part without the cast product, parting strategy, slides/cores, draft, gating,
thermal management, and alloy process constraints. No implementation plan.
### Stamping die insert
Suitability: limited but suitable for simple inserts, punches, die buttons, and
profile blocks when the cutting/forming profile is specified.
Implementation plan:
- Recognition: `stamping die insert`, `punch insert`, `die button`,
`冲压模具镶件`.
- Invariants: tool-steel block or round insert, working profile/cut edge,
locating features, fastener holes, relief/clearance.
- Variants: rectangular profile insert, round die button, punch insert,
stripper-related insert.
- Default: rectangular insert block with specified working profile pocket or
protrusion, dowel holes, screw holes, and relief chamfers.
- Parameter roles: block_length, block_width, block_height, working_profile,
clearance_offset, screw_hole_positions, dowel_hole_positions, relief_depth.
- Feature roles: insert_body, working_edge_or_profile, screw_holes, dowel_holes,
relief_pocket, datum_faces.
- Construction sequence: block; cut/raise working profile; add relief; cut
locating and screw holes; chamfer non-working edges.
- Common failures to prevent: ignoring locating holes, decorative profile with
no datum, working edge not separated from relief, claiming cutting clearance.
- Validation targets: body size, profile presence, screw/dowel hole positions,
relief side, datum faces.
## 7. Precision parts
Representative parts: gauge, fixture locating part, optical base, medical device part.
Family recognition terms:
- English: gauge, fixture, locating block, optical base, precision base, datum,
dowel pin, V-block, inspection fixture.
- Chinese: 量规, 夹具定位件, 光学基座, 精密基座, 基准面, 定位销孔, V形块.
Shared boundary:
- References can improve geometric structure and datum clarity.
- References must not claim tolerance, calibration, metrology certification,
medical compliance, or optical performance.
### Gauge
Suitability: suitable for simple gauge geometry; not for certified metrology.
Implementation plan:
- Recognition: `gauge`, `go/no-go gauge`, `inspection gauge`, `量规`.
- Invariants: controlled reference geometry, handle/body, measuring feature,
datum or label.
- Variants: plug gauge, ring gauge, gap gauge, profile gauge, step gauge.
- Default: if unspecified, create a simple step/gap gauge with labeled measuring
openings and a handle tab.
- Parameter roles: nominal_size, go_size, no_go_size, gauge_thickness,
handle_length, measuring_slot_width, relief_radius.
- Feature roles: gauge_body, measuring_feature, handle, datum_face,
relief_notches.
- Construction sequence: flat gauge body; cut measuring openings; add handle and
reliefs; chamfer safe edges.
- Common failures to prevent: decorative ruler-like part, no actual measuring
feature, claiming calibration.
- Validation targets: measuring slot/ring/plug size, thickness, handle, datum.
### Fixture locating part
Suitability: highly suitable for references optimization.
Implementation plan:
- Recognition: `fixture locator`, `locating block`, `datum block`,
`夹具定位件`, `定位块`.
- Invariants: datum faces, locating pins/holes, mounting holes, clamping/contact
surfaces, repeatable positioning logic.
- Variants: locating block, V-block, stop block, pin locator, rest pad.
- Default: rectangular locating block with datum face, two dowel holes, mounting
holes, and a step or V-groove if locating round stock is mentioned.
- Parameter roles: block_length, block_width, block_height, datum_face_offset,
dowel_pin_diameter, dowel_spacing, mounting_hole_diameter,
v_groove_angle, step_height.
- Feature roles: locator_body, datum_faces, dowel_holes, mounting_holes,
contact_surface, v_groove_or_step.
- Construction sequence: body; machine datum/contact features; cut dowel and
mounting holes; add chamfers.
- Common failures to prevent: no datum, holes not tied to datum, cosmetic block,
over-constrained pin layout.
- Validation targets: datum face existence, dowel-hole count/spacing, mounting
holes, contact surface geometry.
### Optical base
Suitability: suitable for structural reference guidance, not optical alignment
claims.
Implementation plan:
- Recognition: `optical base`, `optical mounting base`, `光学基座`.
- Invariants: flat base, grid or pattern of threaded/clearance holes, raised
pads or rails, datum edges, stable support.
- Variants: optical breadboard-like base, lens mount base, rail adapter, prism
mount base.
- Default: flat rectangular base with regular hole grid, two datum edges, and
optional raised pads.
- Parameter roles: base_length, base_width, base_thickness, hole_grid_pitch,
hole_diameter, pad_height, pad_positions, slot_dimensions.
- Feature roles: precision_base, hole_grid, datum_edges, raised_mounting_pads,
slots, chamfers.
- Construction sequence: base; add/cut hole grid; add pads/slots; chamfer.
- Common failures to prevent: random decorative holes, no datum edges, claiming
optical flatness or alignment.
- Validation targets: grid pitch, hole count, base thickness, pad positions.
### Medical device part
Suitability: not suitable as a single references-only representative. "Medical
device part" is too broad and often safety/regulatory-critical. References can
be written for a specific geometry such as a simple surgical fixture, clamp, or
implant trial, but not for the category as a whole. No implementation plan.
## Rollout plan for reference-only optimization
1. Add a short trigger section to `skills/cad/SKILL.md` that tells the agent to
load a part-family reference after the CAD brief and before planning geometry.
2. Create the seven broad family reference files listed above.
3. Keep each family reference under roughly 150-220 lines so the agent can load
it without drowning out the user request.
4. Start with the highest-return families:
- `plate.md`
- `disc-sleeve.md`
- `shaft.md`
- `housing.md`
5. Add `freeform-surface.md`, `mold-tooling.md`, and `precision-fixture.md`
with stronger caveats because these families contain unsuitable subtypes.
6. Run A/B manual checks on representative prompts before adding validators or
factories.
## Recommended first references
The first version should prioritize the representatives with the best
reference-only payoff:
- Stepped shaft
- Drive shaft
- Flange
- End cover
- Bearing housing
- Mounting base plate
- Connecting plate
- Fixture locating part
- Simple impeller
- Cam
Do not start reference-only rollout with:
- Engine block/cylinder head
- Turbine blade
- Propeller
- Injection mold core/cavity insert
- Die-casting core
- Generic medical device part
These require product geometry, curves, standards, regulatory constraints, or
domain calculations that references alone cannot supply reliably.
## Source bibliography
- Protolabs CNC milling design guidelines:
https://www.protolabs.com/services/cnc-machining/cnc-milling/design-guidelines/
- Protolabs CNC machining design guide:
https://di1vc2liilbw2.cloudfront.net/guides/cnc-machining/
- Xometry CNC CAD design tips:
https://www.xometry.com/resources/machining/10-tips-improve-cad-cnc-design/
- Xometry manufacturing standards:
https://www.xometry.com/manufacturing-standards/
- Texas Flange flange anatomy:
https://texasflange.com/blog/the-complete-guide-to-flanges/
- Flange face types:
https://www.coastalflange.com/blog/flange-facing-for-b16-5-and-b16-47-flanges/
- ASKUBAL pillow block / bearing housing overview:
https://www.askubal.de/en/knowledge-base/bearing-units/pillow-block/
- Roton trapezoidal lead screws:
https://www.roton.com/products/trapezoidal-lead-screws-nuts/general-information/
- igus ACME and trapezoidal lead screws:
https://www.igus.com/lead-screws/acme-thread-sizes
- Machinery's Handbook excerpt on splines:
https://online.flippingbook.com/view/954046886/992/
- Timing pulley design guide:
https://www.sourcebyspec.com/encyclopedia/timing-pulley.html
- Gear and gear-blank feature discussion:
https://www.cncmachining-services.com/custom-cnc-machining-gears/
- Gearbox housing feature references:
https://dzmaking.com/gear-housing/
- Pump housing inspection/feature discussion:
https://www.insvision3d.com/en/news/industry-articles/closing-the-loop-on-pump-housing-quality-with-3d-inspection/
- Cylinder head machining complexity reference:
https://www.cncmachining-services.com/cylinder-head-machining/
- Engine block feature reference:
https://www.motor.com/magazine-summary/performance-perspectives-cnc-machining-an-engine-block/
- Impeller five-axis / geometry references:
https://jeekrapid.com/blog/impeller-machining/
https://journals.sagepub.com/doi/10.1177/1687814017726484
- Propeller geometry references:
https://link.springer.com/article/10.1007/s00773-022-00878-6
https://howthingsfly.si.edu/propulsion/propellers
- Injection mold component references:
https://fecision.com/parts-and-components-of-an-injection-mold/
https://www.cavitymold.com/core-and-cavity-design-fundamentals-a-comprehensive-guide-for-injection-mold-engineers/
- Die-casting tooling reference:
https://cwmdiecast.com/die-cast-tooling-101/
- Fixture and locating feature references:
https://resources.utec.co/workholding/fixture-design-cnc-machining/
https://www.cnctal.com/automation-fixture-parts/
https://cnfx.com/industry/machinery-and-equipment-manufacturing/component/locating-pins
@@ -0,0 +1,76 @@
# Plate Like Reference
## When to read this file
Read this reference when the request matches these triggers:
- `bracket`
- `plate`
- `支架`
- `板`
## Object boundary
Use this reference to improve first-pass CAD modeling structure. It is not a validator, factory, or engineering certification. Do not model adjacent assemblies unless the user explicitly requests them.
## Recognition
This node represents `mechanical_part.plate_like` in the generated taxonomy. It was mined from 2 source models.
## Core invariants
- The main body is a flat prismatic plate.
- Holes, slots, and pockets should be positioned from datum edges or centerlines.
- Thickness and edge treatment should be named.
## Common variants
- Keep variants driven by the user prompt and source evidence.
- Prefer adding a child variant skill when one branch becomes too specific.
- Do not collapse all examples into one fixed template.
## Required parameter roles
- `datum_edges`
- `flat_body`
- `holes_or_slots`
## Feature roles
- `datum_edges`
- `flat_body`
- `holes_or_slots`
## Operation / construction sequence
1. Build the primary base solid first.
2. Add major bosses or protrusions second.
3. Apply subtractive cuts, holes, pockets, and slots after the base is valid.
4. Create repeated features through patterns rather than independent ad hoc geometry.
5. Apply fillets and chamfers last.
## Common failures to prevent
- Unnamed magic dimensions hidden inside geometry calls.
- Decorative features with no functional role.
- Fillets or chamfers applied before major cuts.
- Claiming engineering performance or standard compliance from reference guidance alone.
## Validation targets
- The generated model contains the core invariants above.
- Major feature roles are visible or represented by named parameters.
- Repeated features have consistent count, spacing, and datum relationship.
- The output is a valid solid or explicitly requested assembly.
## Exclusions
- `shaft`
- `vessel`
## Source evidence summary
- Source sample count: 2
- Sample ids: 000124, 001082
- Operation evidence: HoleWzd: 4, Chamfer: 4, Cut: 4, Extrusion: 2, LPattern: 2
- Taxonomy confidence: 0.74
@@ -0,0 +1,195 @@
# Bearing housing reference
Read this file when the request is for a bearing housing, bearing seat, bearing
support, pillow block, flanged bearing housing, cartridge bearing holder,
`轴承座`, `轴承支座`, `轴承座壳体`, or `带座轴承` housing.
This reference improves first-pass geometry structure only. It does not certify
bearing fit, tolerance class, coaxiality, lubrication, load capacity, sealing,
casting quality, or manufacturability.
## Object boundary
By default, model the machined housing/seat part that supports a bearing. Do not
model the rolling elements, cage, shaft, bolts, grease nipple, seals, or a full
mounted-bearing assembly unless the user explicitly asks for those as separate
parts.
If the prompt says `bearing`, `bearing seat`, or `轴承座` without more detail,
the semantic core is the support body with a bearing bore and mounting features.
## Recognition
Use this reference when the request includes:
- English: bearing housing, bearing seat, bearing support, pillow block,
plummer block, flanged bearing housing, cartridge bearing holder.
- Chinese: 轴承座, 轴承支座, 轴承座壳体, 立式轴承座, 法兰轴承座, 带座轴承.
- Adjacent intent: shaft clearance, bearing bore, bearing pocket, mounting feet,
flange mount, bolt holes, cap, split housing, support ribs, grease port.
## Core invariants
A generated bearing housing should include:
- A bearing bore or bearing pocket sized by a named bearing outer diameter.
- A shaft clearance axis coaxial with the bearing bore.
- Material around the bore thick enough to read as support, not just a thin ring.
- A mounting method: base feet, flange, side lugs, or plate mount.
- Mounting holes with symmetric placement relative to the functional datum.
- A stable datum convention: bearing axis and mounting face are primary.
- Edge relief: practical chamfers/fillets after major cuts.
For a pillow-block style housing, the minimum recognizable structure is:
- Flat base.
- Raised bearing boss or arched support.
- Horizontal bearing bore through the support.
- Two base mounting holes.
- Optional ribs connecting boss to base.
## Default strategy
When under-specified, generate a simple pillow-block bearing housing:
- Bearing axis along X.
- Flat rectangular base on the XY plane.
- Bearing center above the base by `bearing_center_height`.
- Cylindrical or rounded boss around the bearing bore.
- Coaxial through bore for shaft clearance plus a larger bearing seat/pocket.
- Two elongated or round mounting holes in the base, symmetric about the Y axis.
- Two ribs from base to bearing boss if there is enough room.
- Fillets/chamfers last.
Do not default to a full assembly. Do not default to visible bearing balls. Do
not default to a random bracket with holes but no bearing bore.
## Common variants
### Pillow block / plummer block
- Base-mounted housing with horizontal bearing axis.
- Two or four mounting holes in the base.
- Raised central boss, arch, or saddle around the bore.
- Optional split cap only if the user asks for split housing or cap bolts.
### Flanged bearing housing
- Circular, square, oval, or diamond flange around the bearing bore.
- Bearing axis usually normal to the flange face.
- Bolt holes arranged on a bolt circle or symmetric flange pattern.
- Optional pilot lip or locating register on the mounting face.
### Plate-mounted bearing seat
- Flat or block-like plate with bearing pocket.
- Bearing axis may be normal to the plate.
- Mounting holes belong to the plate datum, not random locations.
### Split bearing housing
- Lower base and upper cap are separate parts or clearly separated bodies.
- Cap bolts and split plane must be represented if split form is requested.
- If source-level assembly is not needed, a simplified monolithic split-line
groove is acceptable, but label the assumption.
### Cartridge bearing holder
- Cylindrical sleeve-like body with flange or clamp features.
- Bearing pocket and shaft clearance are coaxial.
- Include retaining shoulder or snap-ring groove when requested.
## Required parameter roles
Use semantic parameters before modeling:
- `bearing_outer_diameter`
- `bearing_width`
- `shaft_diameter`
- `shaft_clearance_diameter`
- `bearing_bore_diameter`
- `bearing_bore_depth`
- `bearing_center_height`
- `wall_thickness`
- `base_length`
- `base_width`
- `base_thickness`
- `mount_hole_diameter`
- `mount_hole_spacing_x`
- `mount_hole_spacing_y`
- `boss_outer_diameter`
- `boss_width`
- `flange_outer_diameter`
- `flange_thickness`
- `bolt_count`
- `bolt_circle_diameter`
- `rib_thickness`
- `fillet_radius`
Only define optional parameters used by the selected variant.
## Feature roles to plan explicitly
Use these roles in the CAD brief and source structure:
- `mounting_base`
- `bearing_boss`
- `bearing_seat_bore`
- `shaft_clearance_bore`
- `mounting_holes`
- `flange_mount`
- `pilot_lip`
- `support_ribs`
- `split_cap`
- `cap_bolts`
- `retaining_groove`
- `grease_port`
- `datum_mounting_face`
- `datum_bearing_axis`
## Construction sequence
1. Select the variant from the prompt. If no variant is specified, use pillow
block.
2. Define the datum system:
- pillow block: base bottom face is Z=0; bearing axis is X.
- flanged housing: flange mounting face is the primary plane; bore axis is
normal to it.
3. Build the base, flange, or plate body first.
4. Add the bearing boss/support body and ensure it intersects the base/flange.
5. Cut the shaft clearance bore and bearing seat coaxially.
6. Cut mounting holes using symmetric positions or bolt circle pattern.
7. Add ribs only when they intersect both base/flange and boss.
8. Add optional split line, cap features, grease port, retaining groove, or pilot
lip if requested.
9. Apply chamfers and fillets last.
For through bores and mounting holes, overshoot cutting tools through the target
faces. Avoid coplanar cutting tools.
## Common failures to prevent
- A generic bracket with no bearing bore.
- A ring with no mounting method.
- Bearing bore and shaft clearance not coaxial.
- Missing bearing center height in pillow-block form.
- Mounting holes asymmetric when the variant implies symmetry.
- Ribs that float or only touch at an edge.
- Base thinner than mounting-hole diameter without reason.
- Side openings or decorative slots with no bearing-seat purpose.
- Generating balls, cages, shafts, fasteners, or seals when the user asked only
for the housing.
- Confusing bearing housing with shaft, flange, or motor mount.
## Validation targets
After generation, inspect or reason about:
- Bearing bore diameter/depth and shaft clearance diameter.
- Coaxiality of bearing bore and shaft clearance.
- Bearing center height above mounting face for pillow-block form.
- Base or flange dimensions and thickness.
- Mounting-hole count, size, and symmetry or bolt-circle diameter.
- Boss wall thickness around bearing seat.
- Rib attachment to both base/flange and boss when ribs exist.
- Single connected solid unless the user requested an assembly or split housing.
@@ -0,0 +1,160 @@
# Cup / mug reference
Read this file when the request is for a drinking vessel: cup, mug, water cup,
tumbler, drinking cup, tea cup, coffee cup, handled cup, travel mug,
`水杯`, `杯子`, `马克杯`, or `茶杯`.
This reference improves first-pass geometry structure only. It does not certify
food safety, insulation, thermal performance, ergonomics, leak resistance, or
manufacturability.
## Recognition
Use this reference when the prompt describes a vessel whose primary geometry is
an open container for liquid. Distinguish the common cases:
- `cup` / `水杯` / `drinking cup`: default open cup, usually without handle.
- `mug` / `马克杯` / `handled cup`: open cup with one handle on the side.
- `tumbler`: taller cup, optionally with a lid, usually without handle.
- `travel mug`: handled cup with a lid; may be an assembly if the lid is modeled.
- `bottle`: do not use this reference unless the user explicitly wants a cup-like
bottle or bottle mug hybrid.
If the prompt does not mention a handle, do not add one by default.
## Core invariants
A recognizable cup should have:
- An open top.
- A hollow interior cavity.
- A continuous side wall.
- A closed bottom.
- A continuous rim at the opening.
- A stable standing base or foot ring.
A handled mug should add:
- Exactly one main handle unless the prompt requests multiple handles.
- The handle mounted on one side of the cup body.
- Two attachment regions that intersect the cup body solidly.
- A clear gap between handle and cup body so the hand can pass through.
- A loop, C-shape, or arched form that reads as a graspable handle, not a fin,
tab, rib, or broken bracket.
## Default strategy
When the request is just `cup` or `水杯`, generate a simple open cup with no
handle:
- Revolved outer body.
- Hollow interior.
- Closed bottom.
- Slight outward taper or cylindrical wall.
- Small rim radius or lip.
- Optional foot ring.
When the request is `mug`, `马克杯`, or `handled cup`, generate the same cup
body plus one handle.
## Handle strategy
The handle is the failure-prone part, so make it explicit:
- The handle must be a single continuous shape.
- It should attach to the cup body at two distinct points on the same side.
- The two attachment points should be vertically separated and usually near the
upper-middle and lower-middle height of the cup.
- The handle should project outward far enough to create finger clearance, but
not so far that it looks like a secondary looped part.
- The handle cross-section should be rounded or softly rectangular, not a thin
blade.
- The handle opening should remain open; do not fill the inside of the loop with
a solid web unless the prompt asks for a closed grip.
- Avoid multiple disconnected handle fragments, fins, or stray tabs.
- The handle should not attach at the rim edge only; it should engage the side
wall with real thickness.
## Required parameter roles
Use semantic parameters before modeling:
- `height`
- `top_outer_diameter`
- `bottom_outer_diameter`
- `wall_thickness`
- `bottom_thickness`
- `inner_depth`
- `rim_radius`
- `rim_thickness`
- `foot_ring_diameter`
- `foot_ring_height`
- `handle_thickness`
- `handle_width`
- `handle_clearance`
- `handle_projection`
- `handle_attach_height_top`
- `handle_attach_height_bottom`
- `handle_attach_offset`
- `handle_loop_radius`
- `handle_loop_height`
Only define the handle parameters when a handle is requested.
## Feature roles to plan explicitly
- `cup_body`
- `inner_cavity`
- `rim`
- `base`
- `foot_ring`
- `handle`
- `handle_attachment_upper`
- `handle_attachment_lower`
- `handle_clearance`
- `lid_seat` or `lid_interface` if a lid is requested
## Construction sequence
1. Build the cup body first using revolve or a smooth profile sweep.
2. Hollow the interior while preserving the bottom thickness.
3. Add the rim and optional foot ring.
4. If a handle is requested, place it on one side only.
5. Model the handle as one continuous loop or arched solid with two clear
attachment zones.
6. Fuse the handle to the cup body so it reads as one coherent mug, not as a set
of touching tabs.
7. Add a lid only if the prompt explicitly requests a covered container.
8. Apply final fillets/chamfers last.
For the handle, the tool or sweep profile should overshoot into the cup wall at
both attachment regions. Avoid tangent-only contact.
## Common failures to prevent
- Solid cup with no hollow interior.
- Cup with no bottom.
- Cup body turned into a bottle or vase shape when the prompt says cup.
- Handle omitted when the prompt says mug or handled cup.
- Handle added when the prompt only says cup or water cup.
- Handle split into two disconnected pieces.
- Handle as a tiny fin, tab, or shard instead of a loop.
- Handle attached only at the rim edge or at one point.
- Handle crossing through the cup cavity or cutting into the interior wall.
- Multiple handles unless explicitly requested.
- Lid generated when the prompt did not ask for one.
## Validation targets
After generation, inspect or reason about:
- Opening is on top and remains open.
- Interior cavity exists and is connected to the opening.
- Bottom is closed and has nonzero thickness.
- Rim is continuous.
- If a handle exists, exactly one main handle is present.
- Handle has two attachment regions on the same side.
- Handle has sufficient clearance from the cup wall.
- Handle does not appear as a broken fragment or a separate brace.
- The model is a single connected solid unless a lid or travel-mug assembly is
explicitly requested.
@@ -0,0 +1,73 @@
# Generic Rotational Part Reference
## When to read this file
Read this reference when the request matches these triggers:
- `rotational part`
- `turned part`
## Object boundary
Use this reference to improve first-pass CAD modeling structure. It is not a validator, factory, or engineering certification. Do not model adjacent assemblies unless the user explicitly requests them.
## Recognition
This node represents `mechanical_part.rotating_part.generic_rotational_part` in the generated taxonomy. It was mined from 2 source models.
## Core invariants
- Identify the dominant base body before adding secondary features.
- Keep repeated holes, slots, bosses, and cuts parameterized.
- Preserve datum relationships over decorative symmetry.
## Common variants
- Keep variants driven by the user prompt and source evidence.
- Prefer adding a child variant skill when one branch becomes too specific.
- Do not collapse all examples into one fixed template.
## Required parameter roles
- `circular_edges`
- `cylindrical_faces`
- `main_axis`
## Feature roles
- `circular_edges`
- `cylindrical_faces`
- `main_axis`
## Operation / construction sequence
1. Build the primary base solid first.
2. Add major bosses or protrusions second.
3. Apply subtractive cuts, holes, pockets, and slots after the base is valid.
4. Create repeated features through patterns rather than independent ad hoc geometry.
5. Apply fillets and chamfers last.
## Common failures to prevent
- Unnamed magic dimensions hidden inside geometry calls.
- Decorative features with no functional role.
- Fillets or chamfers applied before major cuts.
- Claiming engineering performance or standard compliance from reference guidance alone.
## Validation targets
- The generated model contains the core invariants above.
- Major feature roles are visible or represented by named parameters.
- Repeated features have consistent count, spacing, and datum relationship.
- The output is a valid solid or explicitly requested assembly.
## Exclusions
- `box-like housing`
## Source evidence summary
- Source sample count: 2
- Sample ids: 000924, 001075
- Operation evidence: Extrusion: 2, HoleWzd: 2, LPattern: 2, Cut: 1, Chamfer: 1
- Taxonomy confidence: 0.74
@@ -0,0 +1,80 @@
# Shaft Like Reference
## When to read this file
Read this reference when the request matches these triggers:
- `journal`
- `shaft`
- `spindle`
- `主轴`
- `轴`
- `键槽`
## Object boundary
Use this reference to improve first-pass CAD modeling structure. It is not a validator, factory, or engineering certification. Do not model adjacent assemblies unless the user explicitly requests them.
## Recognition
This node represents `mechanical_part.rotating_part.shaft_like` in the generated taxonomy. It was mined from 6 source models.
## Core invariants
- A single dominant rotation axis organizes the part.
- Cylindrical sections should be coaxial and station-based.
- Journals, shoulders, grooves, threads, or keyways should attach to plausible axial regions.
## Common variants
- Keep variants driven by the user prompt and source evidence.
- Prefer adding a child variant skill when one branch becomes too specific.
- Do not collapse all examples into one fixed template.
## Required parameter roles
- `coaxial_sections`
- `journals`
- `main_axis`
- `shoulders`
## Feature roles
- `coaxial_sections`
- `journals`
- `main_axis`
- `shoulders`
## Operation / construction sequence
1. Define the shaft axis and axial stations first.
2. Build the coaxial stack by revolve or fused cylinders.
3. Cut bores, keyways, grooves, threads, or splines after the base shaft is valid.
4. Overshoot subtractive tools through the shaft.
5. Apply shoulder reliefs, chamfers, and fillets last.
## Common failures to prevent
- Unnamed magic dimensions hidden inside geometry calls.
- Decorative features with no functional role.
- Fillets or chamfers applied before major cuts.
- Claiming engineering performance or standard compliance from reference guidance alone.
## Validation targets
- The generated model contains the core invariants above.
- Major feature roles are visible or represented by named parameters.
- Repeated features have consistent count, spacing, and datum relationship.
- The output is a valid solid or explicitly requested assembly.
## Exclusions
- `assembly`
- `housing`
## Source evidence summary
- Source sample count: 6
- Sample ids: 000166, 000780, 001563, 001836, 001942, 001957
- Operation evidence: Cut: 6, Extrusion: 2, Chamfer: 1, HoleWzd: 1
- Taxonomy confidence: 0.95
@@ -20,13 +20,18 @@ class CadRouterTests(unittest.TestCase):
def parse(self, *args: str):
return route_module.build_parser().parse_args(list(args))
def test_routes_gear_to_simplecadapi(self) -> None:
def test_new_standard_part_routes_to_simplecadapi(self) -> None:
result = route_module.route(self.parse("生成一个模数 2 的人字齿轮"))
self.assertEqual("simplecadapi", result["selected_backend"])
self.assertTrue(result["routing_policy"]["keyword_backend_scoring"])
self.assertEqual(
"herringbone_gear",
result["routing_policy"]["standard_part_match"]["type"],
)
def test_routes_new_standard_factories_to_simplecadapi(self) -> None:
def test_catalogued_standard_factories_route_to_simplecadapi(self) -> None:
for request in (
"生成一个 M8 六角螺栓",
"生成一个六角螺栓本体",
"生成一个 08B 滚子链轮",
"生成一个模数 2 的直齿锥齿轮",
):
@@ -34,26 +39,26 @@ class CadRouterTests(unittest.TestCase):
result = route_module.route(self.parse(request))
self.assertEqual("simplecadapi", result["selected_backend"])
def test_routes_semantic_units_and_tolerances_to_simplecadapi(self) -> None:
result = route_module.route(
self.parse("生成参数化法兰,保留语义标签、毫米单位和公差链")
)
self.assertEqual("simplecadapi", result["selected_backend"])
self.assertIn(
"semantic_graph_and_replay",
result["routing_policy"]["requirements"],
)
def test_routes_custom_freeform_surface_to_build123d(self) -> None:
def test_non_catalogued_part_routes_to_build123d(self) -> None:
result = route_module.route(
self.parse("生成带 NURBS 自由曲面的定制外壳")
)
self.assertEqual("build123d", result["selected_backend"])
def test_routes_connecting_rod_to_build123d(self) -> None:
def test_custom_mechanical_part_routes_to_build123d(self) -> None:
result = route_module.route(self.parse("生成中心距 120mm 的发动机连杆"))
self.assertEqual("build123d", result["selected_backend"])
def test_standard_part_words_used_as_features_do_not_route_to_simplecadapi(self) -> None:
for request in (
"生成带 8 个螺栓孔的轮毂法兰",
"生成带轴承座的机加工壳体",
"生成齿轮安装法兰盘",
):
with self.subTest(request=request):
result = route_module.route(self.parse(request))
self.assertEqual("build123d", result["selected_backend"])
def test_existing_step_stays_step_first(self) -> None:
result = route_module.route(self.parse("修改现有零件的孔径", "--existing-model"))
self.assertEqual("build123d", result["selected_backend"])
@@ -106,7 +111,9 @@ class CadRouterTests(unittest.TestCase):
self.assertEqual(
{
"primary_format": "step",
"editable_contract": "DesignIR 3.0",
"editable_contract": "DesignIR 3.0 normalized contract",
"editable_contract_role": "contract_after_backend_generation",
"authoritative_geometry_source": "build123d native Python source",
},
result["artifact_contract"],
)
@@ -125,10 +132,7 @@ class CadRouterTests(unittest.TestCase):
self.assertIn("explicit backend override", selected["reasons"])
def test_probe_reports_simplecadapi_202_capability_contract(self) -> None:
result = route_module.route(self.parse("生成一个齿轮", "--probe"))
simplecad = result["availability"]["simplecadapi"]
self.assertTrue(simplecad["available"])
self.assertTrue(simplecad["compatible"])
simplecad = route_module.probe_backends()["simplecadapi"]
self.assertEqual("2.0.2", simplecad["minimum_version"])
def test_generalized_experience_is_loaded_without_case_geometry(self) -> None:
@@ -422,7 +426,7 @@ class CadRouterTests(unittest.TestCase):
plan["experience_query_plan"]["requires_source_inspection"]
)
self.assertEqual(
"DesignIR 3.0", result["artifact_contract"]["editable_contract"]
"DesignIR 3.0 normalized contract", result["artifact_contract"]["editable_contract"]
)
self.assertEqual(
"DesignIR 3.0 / SurfaceIR",