Repository navigation
Conversation
Replace the PyQt interface with an egui and wgpu application, and run CadQuery scripts in an isolated Python worker. Rebuild the editor, 3D viewer, debugger and panels; add process recovery, export support, updated documentation and CI. Remove the legacy GUI and packaging.
Move Rust and Python tests into purpose-based directories under tests/. Add coverage for editor state, scene geometry, worker protocol, script isolation, exports, and process recovery. Update CI and documentation for the new layout. The suite now contains 79 Rust and 127 Python tests.
Preserve OCCT face, edge, and vertex identifiers for every displayed part and carry their mesh associations through protocol v2. Pick vertices and edges in screen space with face occlusion, retain the exact hit position, and highlight the selected subshape in the viewer and Objects panel. Add regression coverage for transformed geometry, merged assemblies, protocol decoding, picking priority, occlusion, and topology selection lifecycle.
Cache tessellated meshes by BREP content and tolerance with a bounded LRU, and let the GUI advertise retained geometry so unchanged meshes travel as lightweight references. Keep assembly geometry in local coordinates with per-instance transforms, allowing repeated profiles to share CPU and GPU buffers while preserving topology picking and transformed bounds. Add a lazily built triangle BVH for face picking and regression coverage for cache reuse, protocol references, transformed instances, large dimensions, and BVH pruning.
Require explicit conflict resolution when the watched script changes while the editor buffer has local modifications. Provide compare, keep-local, reload, and save-copy actions in the conflict dialog. Create a durable recovery backup before confirmed replacement, keep reload separate from model execution, and report exact watcher paths so imported modules can rerun without reloading the main document. Add regression coverage for protected reloads, external-update classification, recovery copies, save-copy behavior, snapshot validation, and changed-path reporting.
Write document, copy, recovery, and history files through synchronized temporary files before atomically replacing their destinations. Preserve Unix permissions and Windows ACLs and file metadata during replacement, while retaining line endings and UTF-8 BOM state. Reject invalid UTF-8 without opening or rewriting the source file. Add five-second crash checkpoints, startup session recovery, and a bounded local history with previews and explicit version restoration from the File menu. Cover interrupted writes, non-UTF-8 input, BOM and metadata preservation, simulated restart recovery for named and untitled documents, checkpoint deduplication, saved baselines, cleanup, and history retention.
Move all documentation into docs/: installation and a guided first-model tour, a user guide covering the interface, scripting API, 3D view, editor, debugging, console, exporting, preferences, shortcuts and troubleshooting, and a development guide covering architecture, the worker protocol, building, testing and contributing. Add a roadmap with the bugs and problems found in the current code, gaps in the everyday experience and planned features for beginners and advanced users, organized into milestones. Replace the PyQt-era screenshots with captures of the current interface, move the changelog and the test guide into docs/, and reduce the worker README to pointers into the documentation.
Reformat the three files reported by the CI format check with black 26.10.1, the version CI installs. No behavior changes.
After Stop, Restart Python or a worker crash the GUI keeps the previous objects on screen and releases their keys after the next run. A fresh worker numbered its objects from n1 again, so that release freed the new objects and the CQ Inspector, export and upload failed. The GUI now passes its worker generation in CQ_EDITOR_SESSION and the worker prefixes its registry keys with it (s1-n1, s2-n1, ...), so stale releases are harmless. The inspector state is reset when the worker restarts, and the unknown-object error suggests rendering again. Add regression tests for prefixed registries, stale releases after a restart and the session environment of a real worker process. Mark the fix and the CI format fix as resolved in the roadmap, add status columns to the roadmap tables, and update the protocol, architecture, testing and user guide pages.
The property sheet of the Objects panel was a bottom panel without a scroll area. Panels grow to fit their contents, so in a short pane the sheet took the whole height, and the tree's scroll area, which keeps a 64-point minimum, was drawn over it. The sheet's contents now scroll, its height is capped so the tree keeps at least 80 points (or 60% of a very short pane), and the tree's scroll area may shrink below its default minimum. Add headless layout tests that render a sheet much taller than the pane and check that the tree keeps its room above it. Mark BUG-02 as resolved in the roadmap.
Breakpoints, the error line and the debug line were plain line numbers. Adding or removing lines above a breakpoint moved the code but not the marker, so the debugger stopped on a different statement than the one marked. The editor now remembers the text its markers refer to and, whenever the text changes, moves them with a line map: lines before the edited block keep their number, lines after it shift, lines inside it map by position, and a breakpoint on a deleted line is removed. Comparing texts covers typing, editor commands, reloads and restored versions alike. Runs and debugger steps sync the markers first, so Python receives current line numbers. Add tests for inserted, split, merged, deleted and indented lines and for the editor's marker syncing. Mark BUG-04 as resolved in the roadmap.
The editor already requests completions when a "." is typed; the editor guide and the roadmap said the popup only opened on Ctrl+Space.
Apply pointer movement incrementally so orbit, pan, and drag zoom no longer compound the full drag distance every frame. Add an upper-left zoom overlay with magnifier buttons, a logarithmic percentage slider, and a Fit-relative reference. Keep overlay interactions out of scene picking and wheel handling. Cover incremental motion, percentage conversion, responsive layout, and real overlay button interaction with regression tests.
Switch to a closed-hand cursor as soon as orbit or pan is pressed and keep it active throughout the drag, including outside the viewport. Use a vertical resize cursor for drag zoom. Keep navigation gestures and cursors away from the navigation cube and zoom overlay, preserve click selection, and verify that orbit changes only camera direction.
Audit the last PyQt revision into a versioned compatibility matrix with explicit Verified, Enhanced, Changed, Gap, and Unverified states. Require test evidence or a documented decision for every inventoried capability. Protect the complete legacy shortcut set and mouse gesture mapping, and execute all eight unchanged pre-rewrite examples as worker compatibility fixtures. Record stereo rendering, rich Jupyter output, bundled distribution, preference-default differences, and GUI-only checks without claiming false equivalence. Fix Ctrl+left-drag panning when an input event only carries the Ctrl modifier. Derive search navigation tooltips from the real command bindings so Previous correctly reports Alt+F3 instead of conflicting with the Top view shortcut.
Align the legacy example regression test with Black 26.10.1, the formatter used by continuous integration. This is a formatting-only follow-up with no behavior change.
Connect picked face, edge, and vertex identifiers to the live OCCT subshapes retained by the Python worker. Add protocol v4 measurement requests for exact entity properties, minimum distance, closest points, supported angles, curve/surface type, area, length, radius, and diameter. Add Auto, Object, Face, Edge, and Vertex picking modes, retain up to two highlighted topology selections, and present results in a dockable Measurements panel. Explicitly scope topology identifiers to the current scene entry so they are never treated as persistent names after remodeling or a worker restart. Cover numeric OCCT results, stale identifiers, protocol serialization, selection filtering, dual highlights, and display precision. Update the user and development documentation for the new workflow and identity guarantees.
Picking tested every visible part and, inside each part, every edge segment and vertex on each pointer move, so hover latency grew linearly with the assembly: about 9 us per part in release builds, 38 ms at 4,096 parts and 152 ms at 16,384 parts. Add a generic bounds BVH to spatial.rs and pick in two phases. The viewer retains a SceneIndex over the world bounds of the visible parts and rebuilds it only when the scene's pick revision changes (parts added, removed, shown or hidden); camera moves and selection changes keep it. Each mesh lazily builds edge-segment and vertex BVHs, shared by all of its instances, which are pruned in screen space against the pick tolerance. The camera matrices are now computed once per pick instead of once per projected point. BVH queries return indices in ascending order, so coincident parts and entities resolve exactly as the linear scan did; without this, a duplicate shown after an object could win the pick. Pick revisions come from a process-wide counter, so an index is never reused for a different scene, and an index entry whose node key no longer matches is ignored instead of reporting another node's part. Tests compare the indexed pick with a linear scan across orthographic, perspective and close-up views in every selection mode, check that the edge and vertex BVHs never drop a primitive within tolerance, and cover index rebuilds, result ordering and pruning. The ignored pick_latency_on_growing_assemblies benchmark sweeps the pointer over 64 to 16,384 instanced parts: the median pick takes 4 us and 0.1 ms respectively (0.6 ms and 152 ms before), and rebuilding the index for 16,384 parts takes about 4 ms. Document the two-phase picking, how to run the benchmark and why GPU id picking was not adopted, and mark PERF-03 as resolved.
The worker sends a mesh as a bare reference when the request advertised it in known_geometry or when it already sent it earlier for the same request, including previous pauses of a debugging session. The GUI, however, only kept meshes alive while the current scene showed them. A debugging session that paused inside a function (where the script's objects are not locals) dropped those meshes, and when they reappeared the next pause and the final result failed with "worker referenced unavailable mesh" and "Cannot update the 3D scene". Debug runs also advertised no geometry at all, so every debugging session retransmitted and re-uploaded every mesh. Replace the scene's weak mesh map with a MeshLibrary keyed by content id: - Meshes advertised in a request and meshes received for it are pinned until the request ends (run_finished, console_result or a worker restart), which is exactly as long as the worker may reference them. scene_add now carries its request id for this. - Recently used meshes are retained within a 128 MB budget (least recently used first out), and known_geometry advertises them too. A render after Clear, a debugging session and repeated runs therefore receive references for unchanged geometry instead of the arrays. - A full mesh for geometry that is still held resolves to the existing Arc<Mesh>. The renderer keys GPU buffers by mesh and keeps them while the mesh lives, so retained and resent geometry is never uploaded again. The unused Mesh::geometry_id field is removed; the library owns that mapping. Debug runs now advertise known geometry like normal runs. Tests replay the failing debugging sequence without retention (it fails when pinning is disabled), check that advertised geometry survives clearing the view until its request ends, cover the retention budget and eviction order, mesh reuse for resent geometry, mesh byte sizes and the scene_add request id. Document the reference contract and the GUI mesh library.
Every mesh carried two copies of its triangles: the display mesh read from OCCT's glTF writer, and separate pick triangles tagged with their OCCT face. The pick copy was built by walking every triangulation node and triangle from Python, which cost more than the meshing itself on detailed parts (725 ms of 2.2 s for a plate with 324 filleted holes, 123 ms against 38 ms of meshing for a knurled cylinder) and enlarged every mesh message by about a third. The glTF writer emits each face's triangles in TopExp_Explorer order, so per-face triangle counts (one call per face) tag every display triangle with its 1-based face id. The display triangles are then also the pick triangles, and face_positions / face_indices stay empty. If the counts ever fail to add up to the display triangle count, the worker falls back to the previous separate extraction instead of guessing. The GUI already picked on the display triangles when no separate set was present, but only reported a face id and built face highlights from the separate set. Both now use triangle_faces with whichever triangles are picked. Measured on fresh shapes (tessellate, before -> after): - plate with 324 filleted holes: 2189 ms -> 1476 ms, 7.87 MB -> 5.22 MB - knurled cylinder: 182 ms -> 53 ms, 0.18 MB -> 0.13 MB - fine sphere: 34 ms -> 21 ms, 0.18 MB -> 0.11 MB Tests compare the new face ids face by face with the former extraction on ten shape kinds (fillets, reversed faces of a cut, torus seams, compounds, a solid listed twice, sketch faces, moved shapes, open shells), cover the fallback and the count check, and check picking and face highlights on meshes without separate pick triangles. Mark PERF-01 resolved and record the measured cost of the temporary glTF file under PERF-02.
A run only reported the script's duration, so a slow render could not be attributed to CadQuery, meshing, serialization, transfer or the GPU, and whether unchanged parts were really reused was invisible. The worker now accumulates, for the whole request (a debugging session included), the time spent on content ids (BREP hashing), on meshing geometry not sent as a reference and on base64 encoding, and counts the meshes it meshed, served from its cache or sent as bare references, plus the raw bytes sent. run_finished carries them as `timings`, together with the worker's wall clock just before sending. The GUI's reader thread stamps when it read the reply and how long parsing took, so the transfer time (serialization and pipe) follows from the two clocks; it is reported as unknown if they disagree. After decoding the scene, one "Run timings" line goes to the Log panel. The renderer logs GPU uploads (count, size and time) at the debug level. RunFinished is boxed inside Message to keep the enum small. Tests cover the reported timings and counts, and the completion criterion end to end through the server: after editing one part of a three-part assembly, a GUI that advertises its meshes gets one meshed part and two references, and a worker without GUI geometry re-meshes only the edited part and serves the others from its cache. Rust tests cover decoding with and without timings, the transfer computation and the report. Checked against the real worker through the Rust reader: re-running an assembly after changing one part went from 3 meshed parts and 0.53 MB to 1 meshed part, 2 references and 0.08 MB. Document the timings, add a troubleshooting entry, and record why base64 in JSON is kept: about 35 ms to encode and 6 ms to parse 5 MB of arrays, against more than a second of meshing for the same model.
Every bug still open in the roadmap (BUG-05 to BUG-20, PERF-02) is fixed, together with eight problems a review of the whole code found (BUG-21 to BUG-28). Each item in docs/roadmap.md now records its resolution and evidence. Worker and runs: - Worker events, automatic reload and recovery checkpoints run in eframe's App::logic, which also runs while the window is minimized, instead of waiting for a repaint (BUG-05). - Opening another file clears the previous model, selection, inspector and variables; a run or debugging session still going for that file is discarded and its objects released (BUG-12). - Local imports are asked for again after every save and successful run, and the watched set is replaced by the script and its current imports without duplicates (BUG-09). - The console wraps debug() like show_object, so objects are named after their variable (BUG-13). DebugStop derives from BaseException, so a script's `except Exception` no longer swallows Stop (BUG-22). - The glTF round trip reuses one scratch folder per worker, and the next worker removes files left by workers stopped while meshing. OCCT 7.8 can only write glTF to a file; the file itself costs about 1 ms of the writer's 95 ms (PERF-02). - A scene that cannot be decoded no longer drops the run's variables and a queued render; a failed run request clears the debug line (BUG-27). Interface: - Headings get the Phosphor icons, so dialog titles show them (BUG-06). - Help > Documentation opens docs/README.md instead of the PyQt wiki (BUG-08). - Variables: name and type columns take a share of the width, the value column keeps at least 80 points, and clicking a row opens a detail view with the whole value (up to 2000 characters from the worker). The table and the detail view scroll instead of overlapping in a short pane (BUG-10). - After a failed run the empty view and the Objects panel point to the Traceback, and empty-panel texts are centered rows instead of justified ones (BUG-11). - Cell labels of the Variables and Traceback tables are not selectable, so a click on their text selects the row or jumps to the line (BUG-28). - The status bar lays out its fixed indicators first, so a long message no longer hides them (BUG-25). The Log filter ignores case in sources too (BUG-26). - The completion popup no longer panics when an edit leaves its anchor inside a multi-byte character (BUG-23). Files and data: - Quitting with "Don't save" no longer leaves a recovery session that the next start offers to restore (BUG-21). - Local history lives in one folder per document (stable FNV-1a hash of the path) with timestamps in file names, so a checkpoint reads no other version; versions older than 90 days and the oldest beyond 64 MB are pruned once per session, and the flat history of earlier releases is migrated (BUG-16). - Files that are not UTF-8 can be opened as Windows-1252 after a prompt; saving converts them to UTF-8 and the status bar says so meanwhile (BUG-17). - Extracting a new worker removes the folders of other versions (BUG-15). - The log is also written to logs/cq-editor.log in the data folder, rotated at 2 MB, with GUI panics and a thread that flushes buffered lines every second (BUG-20). Platform and build: - In Windows release builds, --help and argument errors go to the console of the parent shell, or to a message box without one (BUG-14). - build.rs embeds the icon and version information with winresource; a missing resource compiler only warns, except in release CI where CQ_EDITOR_REQUIRE_RESOURCES=1 makes it an error (BUG-18). - eframe is used without its "links" feature, which removes webbrowser and the hand-made libshlwapi.a of GNU builds; egui links are opened with the open crate, and only http(s) URLs are opened, so a link in an upload service's reply cannot start a local program (BUG-19, BUG-24). Tests cover the fixes where the code allows it (theme fonts, documentation and URL checks, link capture, Windows-1252 documents, the recovery layout, migration and pruning, worker extraction, log rotation, empty-state layout, the Variables table, the completion anchor, the Log filter, console and debugger behavior, scratch files). The interface fixes were also checked in the running application, and BUG-14 and BUG-18 with the release executable. Docs, the changelog and the roadmap are updated.
Bound the worker to the Python, CadQuery, and OCP families it actually supports, and add blocking CI lanes for the released combinations plus an early-warning lane for CadQuery master. Adapt generated collection types and TopoDS downcasts across OCP 7 and 8, expose the OCP version in worker readiness and diagnostics, and record reproducible environment data in test and benchmark output. Add topology-aware compatibility coverage and document the support policy, exact matrix, update procedure, and installation guidance.
Reject count-preserving face reordering by comparing each proposed glTF face group with OCCT triangulation bounds and representative triangle centroids before assigning topology identifiers. Keep the independent per-face pick mesh as a safe fallback, tolerate triangle reordering within one face, and cover the behavior across the OCP 7.8, 7.9, and 8.0 compatibility matrix. Document the validation path and its measured cost on a 324-hole plate.
Flush captured stdout and stderr every 64 Ki characters, cap each stream at 1 MiB per request, and add explicit truncation markers. Limit Python log records to 256 Ki characters and repeated records to 1 MiB per request. Replace unbounded Rust JSON and stderr line readers with a draining bounded reader. Apply separate 2 MiB control, 512 MiB mesh-response, and 256 KiB native-stderr limits, reporting oversized protocol messages as worker failures. Cover fragmented output, request resets, repeated logs, bounded line recovery, message classification, and a real worker process that writes 2 MiB without a newline.
Mesh ids hashed only the shape's BREP and the tessellation tolerances, but the GUI keeps meshes across Python restarts. After switching to another environment or upgrading OCP, the new worker could answer with a reference to a mesh made by the previous mesher. Ids now also hash the mesh generator: MESH_FORMAT, the OCP version and the installed OCP binary. The worker no longer reports OCP as unknown for conda installs, which carry no pip metadata. src/app.rs had grown to about 2,000 lines. It is now src/app/, one module per concern around the central Action dispatch: documents, runs, worker events, watching and services. Runs, queued renders, stale results and pending requests are a pure state machine (Execution) whose transitions are tested without a window or a process, including the queued render after a failed run that previously had no automated test. An inspector geometry reply that arrived after another workplane or step was selected no longer draws its overlay: the pending request remembers its workplane and entry.
Undo and redo are in the Edit menu (Ctrl+Z, Ctrl+Y, Ctrl+Shift+Z) and use an editor history of its own. TextEdit's history grouped edits by a second of stillness, counted caret moves as steps and lived in egui memory beyond the document, so undo after opening another file brought back the previous file's text. Typing is now grouped while it continues at the caret, and an indent, a comment, an accepted completion, a Replace all or a reload is one step each. The search bar gains whole-word matching and a replace row (Ctrl+H), and Ctrl+G opens a Go to line bar that accepts line:column. Searching now starts from the caret: the first Enter or F3 after typing a query used to skip the first match. The bars take their Enter and Escape keys, so the editor never receives them as well, and Replace remembers the match it selected because an unfocused TextEdit collapses its selection. Completions are offered after the first characters of a name, three by default and configurable, outside comments and strings and without status messages. Resting the pointer on a name shows its signatures and docstring, or the type of a variable, through a new describe request served by jedi. Breakpoints are remembered per file in the settings, with the text of their lines, and return to the line with that text when the file changed in the meantime. The property sheet of the Objects panel shows the geometry type, area or length, radius and center of the selected face, edge or vertex. When Python cannot start, including the Windows Store placeholder, or CadQuery cannot be imported, the dialog says which, lists the interpreters found on the computer and offers Choose interpreter and the installation guide, which is also in the Help menu. The logging test no longer fails when other tests log at the same time.
The ninth example builds a welded workbench of 20 x 20 mm square tube as an assembly, with its cutting plan and a summary of the tube used printed to the Log panel. The example smoke tests now run every bundled example; the documentation states that only examples 01 to 08 are unchanged pre-rewrite compatibility fixtures, and counts nine examples.
Bundle a pinned Python 3.12, CadQuery 2.8, OCP 7.9 and Jedi runtime in native Windows, Linux and macOS installers. Add application shortcuts, non-destructive Python file associations, bundled-runtime discovery and clean Windows uninstall integration. Build Apple Silicon and Intel macOS packages separately, smoke-test every installed runtime before publication, preserve platform-specific lockfiles and publish SHA-256 checksums from version tags. Synchronize the Rust and worker versions, compatibility contract, roadmap and installation documentation.
Use a space-free per-user Library prefix required by constructor's relocatable PKG bootstrap. Align the macOS smoke test with the installed runtime and add a packaging regression check for invalid PKG paths.
Release builds use fat LTO with one codegen unit (opt-level stays 3): the Windows executable goes from 21.5 to 17.9 MB. The icon stores its 64 to 192 px images as PNG with identical pixels (308 KB to 50 KB) and is embedded once, through a shared static, instead of twice. The installers exclude the Boost, Eigen and TBB headers, CMake files and import libraries that the vtk metapackage brings for compiling against VTK. Nothing installed links Boost's DLLs; the runtime libraries stay. Each installer is about 19 MB smaller, an installation about 230 MB and 17,000 files smaller.
- Warn once per repeated object name in show_object() and debug() - Add CadQuery snippet library in Edit menu and Command Palette - Add file search filter in the Project panel - Add object and subassembly name filter in the Objects panel - Add line/block duplication and up/down movement in the editor - Add configurable 3D selection mode shortcuts - Allow restoring individual or all modified script parameters - Allow copying measurements as structured Text, JSON, or CSV - Add reverse history search (Ctrl+R) in the Python console - Allow pinning favorite commands in the Command Palette - Add resolution presets and aspect-ratio display in Screenshot dialog - Allow copying rendered screenshots directly to the system clipboard
The grid lies on the XY plane at the lowest point of the shown model, so the model stands on it, instead of at z = 0 following the camera. Its spacing follows the size of the model in 1, 2 or 5 steps instead of the zoom, with a major line every five, and its lines keep round coordinates while the view turns and zooms. The floor grows in doublings to reach the edge of the view and fades into the distance; lines too close together on screen fade out instead of blurring. The X, Y and Z axes are short positive guides from the origin, solid where visible and dashed where the model hides them, with a dot marking the origin, so a part built around the origin no longer hides them.
An orbit begun on the model turns around the point picked under the pointer, which keeps its place and depth on screen, also in a panned view; begun on empty space it turns around the middle of the view as before. Both turntable and trackball orbits do this. The point is picked after any camera transition has ended, so a drag started during one turns around what is under the pointer in the view it moves. Right drag now orbits like left drag instead of zooming; the wheel, pinch and Shift + left drag still zoom. The compatibility matrix marks the mouse bindings as Changed and records the decision.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This pull request proposes a major modernization of CQ-editor, rebuilding its graphical interface in Rust while retaining Python and CadQuery for CAD modeling and script execution.
The work consists of 51 commits focused on improving usability, performance, reliability, and the overall development experience.
Main improvements
Motivation
The goal is to make CQ-editor more responsive, approachable, and powerful while preserving CadQuery as its modeling foundation.
This implementation also provides a foundation for future improvements to the editing experience, rendering performance, and CAD inspection tools.
Scope and compatibility
This is a substantial architectural rewrite rather than a small incremental change. It replaces significant portions of the existing PyQt application, so compatibility, packaging, migration, and long-term maintenance require careful review.
I understand that integrating a change of this magnitude may require discussion, revisions, or a different integration strategy.
I welcome feedback from the maintainers on whether this approach aligns with the future direction of CQ-editor.
Testing
The repository includes automated tests covering the Rust interface, Python worker, application behavior, rendering, and communication between components.
Additional information
The full implementation and commit history are available at:
https://github.com/igors93/CQ-editor
Thank you for taking the time to review this proposal.