Skip to content

Add CadQuery Viewer: STL/STEP viewer that sends annotated positions to the AI - #18

Merged
HanZijie merged 1 commit into
mainfrom
feat/cq-viewer
Oct 15, 2026
Merged

HanZijie merged 1 commit into
mainfrom
feat/cq-viewer

Conversation

@HanZijie

@HanZijie HanZijie commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

What

New Mini App plugins/hanzijie/cq-viewer — CadQuery Viewer, a local 3D viewer for MiniMax Code.

preview

  • Project folders: lists .stl / .step / .stp in a registered folder (or the folder passed via /viewer?root=); tick to open; open files reload when they change on disk.
  • STEP, natively: converted locally with Python + CadQuery/OCP into a compact mesh that keeps the assembly — part names, placements, colours (part and face level), and real B-rep edges with seams hidden. Cached in dataDir/step-cache/; falls back to a single merged mesh if needed.
  • CadQuery push: python/cqview.py → show(part_or_assembly, "name") writes to ~/.cqview/scene/ and the viewer shows it immediately (assemblies as STEP).
  • Annotate → AI: pick mode with an explicit confirm step, multiple points, a note per point plus an instruction, preview, then one-click send to the conversation. Sending uses the MCP Apps ui/message request when the Host grants message; otherwise it falls back to window.mcode.miniapp.message.append (fills the conversation input). The same data is mirrored to ~/.cqview/selection.json for agents/scripts.
  • Full-window viewer: orthographic Z-up camera, 7 views, fit, edges, grid, section plane, light/dark.

Checks

npm run check: plugins/hanzijie/cq-viewer: OK — 0 errors, 0 warnings; 55/55 tests pass. (The two existing warnings belong to other packages.)

Data & access (summary — full detail in the README)

  • Reads only .stl/.step/.stp under folders the user registers (inside $HOME, not $HOME itself, ~/Library, or hidden top-level dirs; no symlinks; depth/entry/file caps) and ~/.cqview/scene/.
  • Writes outside dataDir only ~/.cqview/roots.json and ~/.cqview/selection.json, atomically.
  • Spawns Python only when a STEP file is opened (-c "import cadquery" probe, step_mesh.py <in> <out>, inline fallback); argument arrays, no shell, killed on dispose.
  • No network; three.js r170 vendored (MIT).

Tested environment

  • MiniMax Code 3.1.1-preview.4597 (Electron 42), macOS 26 (Apple Silicon); Python 3.9, CadQuery 2.5.2 / OCP 7.7.2.
  • In MiniMax Code: install, open, ?root= folder registration, Host bridge detection.
  • Headless Chrome E2E against the same Node entry: STL/STEP opening (multi-level assembly with names, colours, instances), live reload, pick → confirm → notes, ui/message against a simulated MCP Apps host, the page-bridge fallback against a simulated bridge, copy mode without a Host.

Not verified

  • Windows.
  • In this preview build the ui/initialize handshake is not answered, so Send uses mcode.miniapp.message.append; that the text actually lands in the conversation input was not confirmed.
  • STEP files with face-level colours exported by other CAD tools (code path present, no real-world fixture).

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…o the AI

A local 3D viewer for MiniMax Code:
- Browse a project folder and open .stl / .step / .stp; open files reload on change.
- STEP is converted locally (Python + CadQuery/OCP) into a mesh that keeps the
  assembly: part names, placements, colours, and real B-rep edges.
- python/cqview.py pushes CadQuery parts and assemblies into the viewer live.
- Pick mode with confirmation: mark several surface points, add notes and an
  instruction, then send the composed message to the conversation (MCP Apps
  ui/message when granted, otherwise the MiniMax Code page bridge).

npm run check: plugins/hanzijie/cq-viewer OK (0 errors, 0 warnings).
@HanZijie
HanZijie merged commit 198fe02 into main Oct 15, 2026
1 check passed

@MyPrototypeWhat MyPrototypeWhat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢贡献,这个 Viewer 做得很完整!看到 PR 已经合并了,我这边补充一些合并后的复查结果,想请你开一个后续 PR 修一下下面两处需要修改的问题(可选建议也欢迎一起处理)。

我在本地用临时 home 跑了 Node 端,也在 headless Chrome 里打开了页面:路径约束很严,../、编码路径、绝对路径、软链、扩展名、home 外和隐藏目录的登记都被正确拒绝;STEP 转换(CadQuery 2.8)能保留装配的零件名和颜色,同一文件的并发请求只转换一次,损坏文件和缺少 CadQuery 时的提示也很清楚;页面没有任何外部请求,vendor 的 three.js r170 与上游一致。npm run check 通过。

需要修改(请在后续 PR 中修复)

  1. ~/.cqview 的并发写入(server.mjs 的 writeJsonAtomic / addRoot,详见行内评论):tmp 文件名只用了 pid 和毫秒时间戳,同一毫秒内的两次写入会撞名。实测 40 个并发保存标注时有 20 个返回 500(ENOENT);8 个并发登记文件夹时有 5 个返回 500,roots.json 也丢了条目。docs/security.md 要求写 dataDir 之外的文件时串行化并发写入。建议 tmp 名加随机后缀,并让读改写排队执行。
  2. dispose 之后仍会启动 Python(详见行内评论):转换过程中 Host 停止 Mini App 时,dispose 会杀掉 step_mesh.py,但这个失败(signal_SIGKILL)会让代码走进 fallback,再启动一个新的 python -c 转换进程。这个进程不在清理范围内,而 Host 不会回收入口进程派生的子进程(docs/runtime.md)。我实测它在 dispose 之后被启动,并且把结果写进了缓存。建议 dispose 后 runChild 直接拒绝,被信号结束(包括超时)时不再进入 fallback。

建议(可选)

  1. 服务端没有校验 Host / Origin。带 Host: evil.example 和外部 Origin 的 POST /api/roots 能成功登记目录,通过 DNS rebinding 的网页可以借此读取 home 下的模型文件。建议只接受本机的 Host,非同源的写请求返回 403。
  2. step-cache/ 只增不减,可以考虑清理旧条目。
  3. host-bridge.js 里 APP_INFO.version 是 1.3.0,plugin.json 是 1.0.0,建议统一。

后续 PR 提上来后我会尽快复查,有问题随时交流,谢谢!

/** @param {string} file @param {unknown} data */
async function writeJsonAtomic(file, data) {
await mkdir(EXCHANGE_DIR, { recursive: true });
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同一毫秒内的两次写入会使用同一个 tmp,后一次 rename 会 ENOENT。实测 40 个并发 POST /api/selection 有 20 个 500;roots.json 的读改写也会丢条目。建议 tmp 名加 randomUUID(),并把 roots/selection 的读改写放进一个 promise 队列。

if (code === 'exit_3' || code === 'exit_4') {
throw new HttpError(422, 'step_convert_failed', code === 'exit_3' ? '这个 STEP 文件读不出来,可能已损坏' : '这个 STEP 文件里没有实体');
}
log.warn('cqviewer.step.mesh_failed', { code: code ?? 'unknown' });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dispose 用 SIGKILL 结束 step_mesh.py 后,这里只记一条日志就继续走 fallback,在 dispose 之后又启动了一个 Python 进程(实测会运行完并写入缓存)。超时也一样,最坏会跑 2×300 s。建议 signal_* 时直接抛错,并在 runChild 里检查 disposed。

@HanZijie

HanZijie commented Oct 15, 2026 via email

Copy link
Copy Markdown
Collaborator Author

HanZijie added a commit that referenced this pull request Oct 15, 2026
… loopback-only routes (#20)

Follow-up to the post-merge review of #18.

Required:
- Concurrent writes: temp files are named <file>.<pid>.<uuid>.tmp (two writes in the
  same millisecond collided before: 40 concurrent selection saves gave 27x 500), and the
  selection save joins the existing write queue with roots/scene/request writes. The temp
  file is removed on failure and takes the existing file's mode (docs/security.md).
- dispose during STEP conversion: runChild refuses once disposed, so the fallback can no
  longer start a Python nobody cleans up. A conversion stopped by the time limit, by
  dispose, or killed with SIGKILL/SIGTERM fails without the fallback (worst case is now
  1x300 s, not 2x). A native crash (SIGSEGV, SIGABRT ...) still tries the simpler
  fallback, which is what it is for. Watchers are not recreated after dispose, and
  dispose waits for a write already in progress.

Optional suggestions, also done:
- Every route (page included) refuses a non-loopback Host and an Origin other than this
  listener (DNS rebinding could register folders and read models before).
- step-cache pruning on the first conversion after start: unused for 30 days, beyond
  1 GB (LRU, a hit refreshes mtime), temp files older than a day; only its own files.
- host-bridge APP_INFO and MCP serverInfo versions follow plugin.json (test checks).

tests/server.test.mjs (9 tests, fake Python for the conversion paths); 19/19 plugin tests.
npm run check: plugins/hanzijie/cq-viewer OK (0 errors, 0 warnings).

Co-authored-by: HanZijie <zijiehan1998@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants