Architecture¶
Package layout¶
SimpleCAD is a FreeCAD workbench written in Python. FreeCAD supplies Part
(OpenCascade), Mesh, Coin/Pivy and Qt; NumPy and SciPy are used for mesh and
surface fitting math. Results are ordinary Part and Mesh objects in .FCStd
files: no custom document objects or saved Python proxies.
InitGui.py registers gui.workbench.SimpleCADWorkbench
simplecad/
__init__.py ROOT and __version__ only
core/ geometry helpers, coincidence, preferences
modeling/ Part algorithms behind the Draw and Modify tools
surfaces/ drawing on and editing curved CAD faces
mesh/ raw STL editing and mesh-to-CAD conversion
repair/ Heal to precision, hole rebuilding, slivers
inspection/ Measure and smart selection
document/ undoable document writes; materials, locks, visibility
jobs/ background worker processes
mcp/ local MCP server, bridge and model API
gui/ workbench, commands, controller, snapping, panels
previews/ temporary unpickable Coin previews
tools/ one module per tool panel or session
scripts/ worker entry points, MCP stdio server, installers, test runners
tests/unit/ unittest suites (FreeCAD's Python, no GUI)
tests/gui/ suites that run inside a FreeCAD GUI process
tests/benchmarks/ timing scripts run like GUI suites
tests/fixtures/ BREP and mesh fixtures
Dependencies mostly point one way. gui uses every other package and starts
the gui/tools sessions. mcp drives document and the algorithm packages
directly; it reaches the GUI only for Qt (gui.qt) and to check whether a
manual tool is active (gui.controller). The algorithm packages (modeling,
surfaces, mesh, repair, inspection) never import gui, so their tests
run without a GUI, and the worker scripts in scripts/ import them with
FreeCAD's own Python. A few links inherited from the flat layout still point the
other way: core.geometry calls modeling.subdivision, repair.healing,
repair.slivers and surfaces.line from inside functions; modeling.analytic,
mesh.stretch, surfaces.operations and jobs.mesh use document.locking or
document.operations; and document.appearance reaches gui.controller.
Untangling these is follow-up work.
Inside the package, imports are relative and keep short local names
(from ..core import geometry as geo); tests and scripts import absolutely
(from simplecad.mesh import region).
The sections below were written while every module sat directly in
simplecad/; their module references have been updated to the new paths. Older
notes and commit messages may use the old names. This table maps them:
| Package | Same name | Renamed |
|---|---|---|
core/ |
coincident, geometry, preferences |
— |
modeling/ |
analytic, arcs, deformation, drawing, elongating, forming, intersection, joints, offsetting, patterns, rotation, rounding, slicing, subdivision, union, vertex_edit |
— |
surfaces/ |
— | surface_drawing → drawing, surface_edit → edit, surface_fit → fit, surface_line → line, surface_object → skin, surface_offset → offset, surface_ops → operations, surface_wrap → wrap |
mesh/ |
— | mesh_collisions → collisions, mesh_conversion → conversion, mesh_curved → curved, mesh_glue → glue, mesh_handles → handles, mesh_planar → planar, mesh_region → region, mesh_region_edit → region_edit, mesh_retriangulate → retriangulate, mesh_slicing → slicing, mesh_stretch → stretch, mesh_surfaces → surfaces, mesh_transform → transform |
repair/ |
healing, holes, slivers |
— |
inspection/ |
measuring, smart_select |
— |
document/ |
appearance, locking, operations, visibility |
material_mapping → materials |
jobs/ |
— | mesh_jobs → mesh, mesh_worker_diagnostics → diagnostics, shape_jobs → shape, workers → runtime |
mcp/ |
— | mcp_api → api, mcp_bridge → bridge, mcp_catalog → catalog, mcp_connection → connection, mcp_protocol → protocol |
gui/ |
command_state, commands, controller, display, face_browser, pull_snapping, settings_dialog, snapping, surface_display, viewport_style, workbench |
selection → box_selection, toolbar_ui → toolbar, ui → qt, status_bar, panels |
gui/previews/ |
— | preview → overlay, rotate_preview → rotate, round_preview → shaded, slice_preview → slice |
gui/tools/ |
— | analytic_ui → analytic, appearance_ui → appearance, arc_ui → arc, curvature_ui → curvature, deform_ui → deform, elongate_ui → elongate, forming_ui → forming, joint_ui → joint, mcp_ui → mcp, measure_ui → measure, mesh_handles_ui → mesh_handles, mesh_region_ui → mesh_region, mesh_split_ui → mesh_split, mesh_stretch_ui → mesh_stretch, mesh_ui → mesh_convert, mirror_ui → mirror, offset_ui → offset, push_pull_ui → push_pull, rotate_ui → rotate, round_ui → round, selection_ui → selection, slice_ui → slice, smart_select_ui → smart_select, subdivide_ui → subdivide, surface_ui → surface |
Workbench startup and the controller¶
InitGui.py imports and registers the class defined in simplecad/gui/workbench.py. Defining the class in a regular module gives it its own namespace: FreeCAD executes addon startup scripts inside a loader function, where imports made by the script are not automatically globals for nested class bodies. This distinction caused the 0.1.0 ROOT startup error and is covered by regression tests in 0.1.1. See the FreeCAD loader source.
gui/workbench.py → gui/commands.py → gui/controller.py
├── gui/status_bar.py status bar controls
├── gui/snapping.py camera rays, picking, inference
├── gui/previews/overlay.py temporary unpickable Coin nodes
├── gui/tools/* tool panels and sessions
└── document/operations.py → core/geometry.py, modeling/* → Part / OpenCascade
↓
FreeCAD document transaction
The workbench registers lazily through the standard Initialize, Activated, and Deactivated hooks. Every toolbar and menu command shares one controller; the AI connection menu opens an independent connection panel. Only that controller owns the current tool, captured document/view, mouse callback, preview, and context timer. Tool completion, cancellation, switching tools, switching documents/views, and workbench deactivation release those resources.
Local MCP access¶
scripts/simplecad_mcp.py is a standalone Python 3.9+ stdio process with no third-party dependencies. mcp/protocol.py implements newline JSON-RPC initialization, version negotiation, ping, tools/list and tools/call with structured results and image content. Only tools capability is advertised. mcp/catalog.py supplies the shared 46-tool schemas and validates their bounded JSON vocabulary on both sides. A single worker serializes model calls while the reader can still answer ping and cancel work that has not started. EOF cancels queued work; native operations already in progress finish through the existing transaction boundary.
mcp/connection.py reads owner-private connection files and talks only to authenticated IPv4 loopback endpoints. Discovery refuses multiple live instances, and a client pins the first selected instance instead of switching targets after failure. mcp/bridge.py starts explicitly from the modeless connection panel, never automatically on import or workbench activation. It uses a random token/port, validates Host and rejects Origin headers, bounds request size and queue depth, and queues calls to a Qt timer. HTTP workers never read FreeCAD objects. Duplicate transport IDs share an existing task/result while retained in the bounded cache. A 45-second wait cancels an unstarted task; an already-running operation reports uncertainty and must be inspected before retrying. Stop cancels queued tasks, shuts down the listener and removes its private file. The bridge remains available across workbench switches and stops on application exit.
mcp.api.ModelAPI asserts GUI-thread execution, resolves explicit internal document/object/element names, and invokes document/operations.py rather than executing arbitrary code or simulating input. It rejects model changes in inspection-only mode, during active manual interactions, with Undo disabled, or against an inactive document. Revisions snapshot ordered object identity/labels, native shape identity (including Placement), eye metadata and Undo/Redo counts. A changed snapshot issues a new session-scoped revision; every document edit must supply the current token. Geometry/eye edits retain native Undo transactions. Settings changes are global preferences, and new document creation has normal FreeCAD lifecycle semantics. The API never opens, saves, closes or exports user files; viewport capture uses a temporary PNG that is deleted after encoding.
Inspection is paginated and includes hidden objects/elements, editable status, topology counts, areas, normals on planar faces, edge midpoints, adjacency, placements, visibility and appearance. Native material fingerprints participate in revision checks, including manual view-property changes without a transaction. Revision fingerprints normalize channels to six decimals to ignore native redraw rounding while retaining meaningful appearance changes. Model text is untrusted data. The setup panel exposes Start/Stop, Allow model changes, a copyable Codex registration command and optional generic configuration. No token is displayed or copied. See MCP setup and tool reference for operation semantics and the supported protocol boundaries.
Native modeling and face browser¶
modeling.union.combine() performs a native batch Boolean fuse and removeSplitter() refinement on copies of every selected solid. Closed shells are promoted; nested compounds are traversed without discarding sheets, lines or points. Open, invalid or mixed inputs fail. A batch fuse allows a third piece to bridge two otherwise disconnected pieces, independent of selection order. The result must be one closed, valid solid and pass native Boolean/self-intersection checks. Each input's intersection with the result must retain its volume, and subtracting all inputs from the result must leave no material. These checks use the final refined shape: an unrefined intermediate with redundant curved faces can report incorrect volume even when refinement produces the correct valid boundary.
document.operations.unite() resolves whole owning objects, rejects linked/dependent or expression-driven inputs, computes all geometry before mutation and rechecks source snapshots. One document transaction assigns the final solid to the first object, labels it Union and removes the other selected objects. Failure aborts the transaction; Undo restores the original objects, labels and placements. No parametric feature, hidden source objects or preview is added. The controller selects the resulting object and refreshes the face browser. Union removes deliberate coplanar divisions as part of refinement; the existing coincident-geometry cleanup remains available when those divisions should be retained.
gui.face_browser.FaceBrowser adds a Faces/Objects tab bar and a separate Qt tree as siblings of the native tree in Gui::TreePanel. The native tree keeps its C++ items and original parent. FreeCAD's tree implementation assumes native document-object item types in child traversal, so Python FaceN rows are never injected into it. No document objects, proxies or geometry are created for the browser. A separate dock is the fallback when the native Model tree is absent. Deactivation removes the additions and restores the native tree; a deleted/recreated panel is detected on refresh.
Non-null shapes in the active document are listed, by their current labels, including lines and points. Expanding a face-bearing object lazily creates its face rows, with surface type and area in tooltips. Native selection observers synchronize picks from the 3D view and Objects tree. Face-row clicks write native subelement selection; hover writes native preselection and only clears a highlight still owned by the browser. Parent selection takes precedence over selected child faces. SimpleCAD shortcuts also accept focus inside this custom tree, while normal object-tree/property-editor keyboard behavior remains unchanged.
Document observers queue refreshes after creation, edits, deletion, recompute, restore and Undo/Redo. Shape snapshots guard against stale rows between an edit and the queued refresh. Face selections are cleared on Shape changes because native face indices may be renumbered. A 500 ms fallback handles document switches, labels and visibility. Browsing doesn't edit geometry or Undo history. A single face explicitly picked in the browser supplies Push/Pull's initial face and anchor, including back-facing faces.
The fixed eye column intercepts press/release without selecting its row. Space and the context menu can toggle multiple rows in one transaction, with parents taking precedence over children. Object eyes use native ViewObject.Visibility; face eyes use document/visibility.py and retain child choices when the parent is off. Showing a child also shows its parent. Face mutations use the ordinary editable-object guard. Selection caches on the changed owner are cleared and only still-visible subelement selections restored; other selected objects are retained.
document/visibility.py stores hidden one-based face indices plus a geometry signature in the hidden SC_FaceVisibility ViewObject string property. No Shape, document objects or proxies are added. Its signature hashes ordered local face/edge geometry with rounded coordinates, ignoring object Placement and tessellation; raw BREP bytes are unsuitable because serialization changes after harmless location updates. A changed signature reveals all faces without erasing the old metadata, so Undo can restore the original mask. Native view-property transactions and FCStd persistence handle history and save/reopen. Shared edges and vertices remain visible while any adjoining face is visible; loose geometry is retained.
modeling.subdivision.Profile creates a local plane centered on the selected face, aligned to its longest straight outer edge plus the requested grid angle. Collinear boundary vertices are ignored when recognizing outlines. Triangles default to barycentric subdivisions: N equal lengths on each side produce N² similar triangles. Convex polygons default to a centroid-to-corner fan, optionally subdividing each wedge by that same triangle construction. This preserves the symmetry of regular polygons without enforcing equal areas on irregular ones. Circular disks use equal-angle radial sectors; concave/holed outlines default to grids. Shape-following patterns operate on the actual corners, so their orientation does not depend on world axes.
Evenly spaced grids divide the actual local bounding extents into equal intervals and extend each divider across the entire face. The extra clipping margin is excluded from spacing calculations. Rows-only/columns-only set the unused dimension to one. Optional equal-area cuts retain monotone bisection: horizontal cumulative areas choose rows, then each row's cumulative area chooses columns. Straight polygon contours use half-plane clipping and shoelace areas, subtracting holes; curved outlines use native face/rectangle common area. Neither path approximates the saved boundary geometry.
Subdivision clips cutter edges to the selected face and transforms them back into world coordinates. A solid uses its generalFuse() source mapping to retain one solid. Open surface collections instead imprint the selected face alone: processing all source faces together would also split existing, unrelated intersections. replace_surface_face() propagates boundary vertices to neighboring edges without splitting those neighboring surfaces, then reconnects the regions through the existing sewing helpers. A compound fallback retains intentional duplicate surfaces if the sewing/duplicate helpers would remove them. Returned regions are matched to native result faces, excluding unrelated coplanar overlaps from the region count.
Validation requires unchanged surface area and solid volume, valid native topology, and exactly the expected change in face count. Triangles and equal-area grids additionally validate equal native areas. Shape-following patterns validate the expected pattern count. Equal-area grids reject disconnected cells; spaced grids accept empty cells and disconnected pieces and report the actual region count. That count is passed explicitly to the guarded commit, so rows × columns is not assumed for clipped grids. A region-count failure reports a pattern-specific message; failure to retain neighbors is reported separately. Unrelated loose edges and isolated points survive; no refinement removes intentional dividers. Counts are limited to 100, and total grid counts use the nearest pair of integer factors.
document.operations.prepare_subdivision() captures exactly one planar FaceN on a plain Part shape; a standalone single face can be selected as an object. Its document/shape/dependency snapshot is checked before previews and Apply, including native objects retained in Undo history after deletion. gui.tools.subdivide.SubdivideSession uses the existing unpickable shaded preview and reversible native masks. Only relevant pattern controls are shown; triangle and fan counts are remembered independently within a session. Grid controls support total, row/column and single-direction input modes. Preview calculations debounce for 200 ms and cache unchanged inputs. Mode switching updates counts synchronously; Apply interprets pending spin-box text before computing the current plan. One native Shape write commits the subdivisions; a single-part layout makes no edit. D is scoped to the 3D viewport.
modeling/rotation.py captures rotation frames from unchanged native shapes. World axes are global; local axes transform the basis by the original Placement; main axes reuse the slicing inertia calculation, with a longest-local-bounds fallback for compounds/points that lack PrincipalProperties. Single-shape centers use native CenterOfMass when available; selections and compounds use optimal bounds. Shared pivots use the first selected object's axis, preserving the relative geometry of the selection; individual pivots use each object's axis. Unit-aware angles are finite and reduced modulo 360 before native rotation. A pivoted delta placement is left-multiplied onto each original placement, following the FreeCAD Rotation API.
document.operations.prepare_rotation() captures whole plain Part objects, deduplicating subelement selections. Linked, expression-driven and transformed-container targets are rejected. The context check includes document membership: an object removed into native Undo history may still report isValid(). Preview produces transient shape copies; commit_rotation() recalculates every placement before one transaction and writes Placement only. Topology, analytic geometry, labels and identities survive; whole turns create no transaction. The original placement snapshot stays fixed throughout a session.
gui.tools.rotate.RotateSession provides axis and center choices, signed angle/unit entry, a −180°…180° slider, common-angle presets and Reset. Object-axis choices initially select individual centers. Angle changes debounce for 25 ms; Apply/Enter flushes any pending input. gui.previews.rotate.RotatePreview builds source meshes once using the shared preview renderer, then updates Coin transforms for smooth angle changes instead of rebuilding tessellation. The unpickable colored guide shows the axis, ring and angle spoke. NativeMask hides source drawing without changing saved properties. Invalid inputs restore the source and disable Apply. Cancel, tool/workbench/document/view changes, Undo and source changes all release masks and overlays. T is viewport-only; it does not intercept text fields or global shortcuts.
The controller uses SoEvent callbacks for pointer input and a Qt filter for shortcuts scoped to the view. Rectangle/Line progress from first point to destination to commit. Push/Pull captures a face and normal, previews a signed extrusion distance, then commits on a meaningful drag release or second click. Move/Copy capture source objects and a base point. Numerical input uses the same commit path as pointer input.
gui.tools.selection.DragSelection handles idle Select gestures through the controller's existing Qt event filter, restricted to the active 3D OpenGL widget. It delays an unmodified/selection-modified left press until the pointer moves six logical pixels. A short click replays the original native press/release pair with its positions and modifiers, preserving FreeCAD picking; editing tools bypass this handler. Left-then-middle navigation replays the initial press before yielding to the navigation style. The selection box is an unpickable, camera-facing Coin overlay with a local depth override, solid blue or dashed green lines and a mode label. It uses the same scene-graph lifecycle as drawing guides, avoiding Qt/OpenGL transparency issues on macOS.
On release, logical Qt coordinates are scaled to the view's physical pixel dimensions and their Y direction reversed. gui.box_selection.box_selection() projects native Part faces/edges with global placements and filters hidden/unselectable objects and hidden seams. It evaluates complete containment for a rightward drag and triangle/segment intersection for a leftward drag. Triangulated interiors preserve holes and concavities; curved edges use a quarter-pixel deflection estimate at the nearest bounding corner's depth. Bounding boxes are used only for broad rejection, never as sufficient evidence of a hit. Geometry behind a perspective camera is excluded; obscured geometry in visible objects remains eligible, intentionally treating the box as a selection volume.
Hits become native FaceN/EdgeN selections on release, never whole-object substitutions. Shift adds and Ctrl/Command toggles; toggling a tree-selected whole object first expands it to its untouched subelements. Bare vertex objects are also supported, but vertices of faces/edges are not separate box hits. Selection does not open a transaction. Esc, wheel/navigation changes, tool/workbench changes, a changed document/view or Undo count discard the overlay without committing the box. No persistent projection or topology-index cache survives a gesture. Part links and meshes are out of scope, and geometry spanning the camera plane is conservatively omitted rather than clipped. Move/Copy retain whole-object behavior.
modeling/rounding.py implements constant-radius native solid fillets. It resolves an original EdgeN/FaceN selection once, deduplicates face boundaries, and expands whole-object selections. Edge adjacency and surface normals at three curve-length positions reject coplanar divisions, tangent boundaries and periodic seams. Only a single closed solid or closed shell with no extra surface/loose-edge payload is accepted. Every radius is calculated against a fresh copy of the captured original with makeFillet(), then normalized and validated as one positive-volume solid. There is no global refinement, control mesh, or persistent Python feature. Zero returns the captured original and commits no edit; unsupported or failed radii report a recoverable message.
document.operations.prepare_rounding(), preview_rounding() and commit_rounding() separate the captured topology, kernel calculation and atomic write. Every target must remain valid and match its original native shape, including placement. All selected objects are calculated and validated before one apply_plan() transaction; failure never applies a subset. The original objects retain identity and display properties. Saved radius history is deliberately not introduced; Undo provides reversal after Apply.
gui.tools.round.RoundSession owns a small nonmodal radius panel, unit-bearing text input, a slider, a zero/original comparison button and explicit Apply/Cancel. Input changes debounce for 180 ms; Enter/Apply computes any pending value synchronously. Invalid input clears the previous preview and disables Apply so an old valid radius cannot be mistaken for the entered value. Default/range hints use box extents and selected edge lengths, while typed values may exceed the slider range and are still checked by the kernel. The controller owns the session and cancels it on context or source changes.
gui.previews.shaded.RoundPreview reads native writeInventor() output into an unpickable Coin graph. It retains shaded surfaces, supplies object color/transparency and separately draws boundaries with hidden seams omitted. Polygon offset prevents the mesh from fighting its boundary lines. Temporary NativeMask switches hide original view-provider children in the scene graph without modifying Shape, Visibility, any saved view property or the document's modified flag. Native children remain alive and are restored before Apply and on every cleanup path. Cancel restores the original selection only if the source topology still matches; applied results keep their new native topology.
modeling/slicing.py builds world-normal X/Y/Z planes or uses the native principal inertia axis with the lowest absolute moment as the long direction. Near-equal moments fall back deterministically to a local bounding axis. For a multiple selection the combined geometry defines one common plane. A rotated local frame and optimalBoundingBox(False) supply geometric extents independent of tessellation: ordinary bounds of a trimmed fillet can extend beyond the actual cut. Offsets are measured from the center of those extents. An oversized bounded face covers the cross section.
BOPTools.SplitAPI.slice(..., "Split"), shipped with FreeCAD, partitions solids, shells and wires while keeping the cutting tool out of the result. Pieces are classified by geometric bounds on either side of the plane. Each solid remains separate; open faces and loose edges are assembled per side without caps, and isolated vertex payloads are retained. Closed single shells are converted to enclosed solids. Validity, solid volume and open-surface area checks reject incomplete cuts. Gap movement copies the calculated pieces and translates them symmetrically by half the gap; it does not run another Boolean or fuse across the cut.
document.operations.prepare_slicing() captures whole selected objects, even from subelement selection, with source topology/placement snapshots. Links/dependents/group membership and support-linked profiles are rejected because replacing one source with several objects cannot safely maintain those references. preview_slicing() requires geometry on both sides. commit_slicing() validates every result before a single transaction, reuses each original for its first piece, and creates ordinary Part features for the remaining pieces with matching overall display properties. No hidden source object, custom proxy, or persistent cut parameter is stored. Undo/Redo includes the split, new objects, labels and separation.
gui.tools.slice.SliceSession owns a nonmodal plane/offset/separation/gap panel with unit-aware text, two sliders and explicit Apply/Cancel. Input is debounced; Enter/Apply resolves pending edits synchronously. It caches the cut by plane mode and offset, so gap-only changes just reposition existing results. Invalid input restores the originals and disables Apply. gui.previews.slice.SlicePreview shares the shaded native-mask implementation used for rounding and adds an unpickable translucent plane and normal guide. The controller cancels on source, document, view, workbench or Undo changes and restores native drawing before committing. U is scoped to the viewport; panel typing keeps normal Qt behavior.
modeling.joints.StudJoint holds native length-based stud, clearance, pattern, position, wall and side parameters. add_studs() operates on the unseparated result of a single two-solid cut. It resolves the shared planar cut region by intersecting the two sets of cap faces, uses its centroid for a stable layout frame, and assigns male/female roles without changing the slice side/order. Pattern directions stay fixed when roles flip. Root and socket envelope solids must fit inside their respective original pieces; the socket envelope includes entry flare, radial wall, bottom relief and rear wall. Pattern spacing preserves a web between sockets. No partial joint is returned when a member fails.
Analytic cylinders/cones form fused tapered studs and subtractive blind sockets. Clearance is radial/per side; negative values intentionally produce nominal interference. Boolean results must each be one valid positive-volume closed solid. No global refinement removes user-drawn regions. preview_stud_joints() adds the optional joint between the slice and separation stages. The UI caches by cut key plus immutable joint parameters; gap-only changes translate this result without recalculating either Boolean stage. Existing commit_slicing() stores the pieces in one transaction, so Undo includes both connector geometry and separation.
gui.tools.joint.JointControls embeds a hidden-until-enabled set of pattern, size, clearance and flip controls; spacing, face offsets and wall are behind Layout and wall. Invalid input keeps the original visible and Apply disabled. create_fit_test() calculates three one-stud pairs before creating a separate native document, using the current clearance ±0.10 mm. Sockets are turned mouth-up and every part is placed on Z = 0 in a nonoverlapping layout. Native labels and a read-only information string record each trial; there is no Python proxy. scripts/make_fit_test.py generates the default .FCStd, individual STLs and combined plate using FreeCAD's mesher, checking closed positive-volume meshes before export.
core/geometry.py is independent of the GUI and document. It calculates plane frames, ray intersections, rectangle/circle/polygon faces, and Boolean results. A supplied face must be planar. Ordinary extrusion results are validated and normalized to one solid. A past-face extension deliberately keeps intersecting native faces, as described below. Supported profiles are resolved against the current solid by coplanarity and overlap area; stale topology indices are not stored.
core.geometry.divide_face_with_edges() combines the new line with connected loose edges whose full lengths lie on coplanar surface faces. Connectivity includes endpoint contact and intersections; disconnected line networks are excluded. Only faces whose bounds meet the line or a loose line are examined, found through a cached FaceIndex of face bounds and flatness. Faces within a hundredth of the modeling precision of the line count as coplanar: converted meshes leave neighbouring triangles leaning by fractions of a micron. drape() then cuts a line crossing them into one segment per face, meeting on the shared edges, and modeling.drawing.covers_surface() keeps triangle and loop completion from drawing a duplicate face over them. On a plain solid or shell (or a compound holding one solid), split_faces_locally() fuses just the faces under the network with their boundary edges and vertices. Faces elsewhere that use a replaced edge or vertex are rebuilt together with the new images, so shared edges stay shared, and replaceShape() swaps only faces in; a divider ending inside an edge thereby reconnects to the neighbouring face. check_local_split() validates only the removed and added faces: the face count must follow from them, each added face must be valid, together they must cover the replaced area, every piece keeps its face's outward normal as placed and the shape must still close. A successful local split derives the result's FaceIndex and records a Change, from which snaps and seam checks update without listing the part again. Any failure falls back to split_whole(), where Shape.generalFuse() partitions every face; its source-solid mapping retains native shared topology and volume, and it is validated as a whole. When the local faces gain no region, the whole shape cannot either, so no fallback runs. Open surfaces are supplied as individual faces; the output's face collection retains unchanged faces even when their history entries are empty. Only edges shared by at least two resulting faces become dividing boundaries. A second split uses these boundary portions alone, keeping dangling internal wires out of the face topology. Surface area, solid volume, validity and increased face count are checked; no overlay face or saved face-index dependency is created.
document.operations.draw_line() collects loose edges from visible editable objects without dependents. It computes all shape replacements and remaining line portions before writing. In one Undo transaction it replaces the target shape, consumes incorporated lines, and preserves unused branches in their original objects where possible. The target's own loose edges are retained too. Retracing an old segment runs the same region detection, so existing drawings can be repaired without rebuilding them. A line which still encloses no region remains an ordinary independent edge.
removeSplitter() would erase intentional coplanar divisions. Dividing a face never refines it. Push/Pull detects coplanar faces sharing an edge and skips global refinement when such boundaries exist, preserving other drawn regions through later operations. This can retain additional coplanar seam edges from an extrusion.
For Auto Line, the snapper captures planar faces, their area centroids, endpoints, and edge midpoints. A first corner snap resolves its plane before ray intersection, even when native picking reports only a vertex. Endpoint/midpoint/face-center snaps return exact 3D points regardless of whether they share a face with the anchor. A common face still supplies the working plane when available; free pointer motion stays on that plane. Loose-line vertices can start a 3D line without any face. The controller captures Auto mode when drawing starts, so this path also runs when the anchor has no adjacent faces. Explicit XY/XZ/YZ modes stay fixed. Push/Pull outlines the actual picked face before the first click, and clears whole-object selection when starting so the individual region is visible.
Line commits retain the existing callback and preview node. The controller moves the anchor to the committed endpoint, clears measurements and the per-segment axis lock, and rebuilds the snap snapshot/adjacent-face list after every segment. This is necessary when an operation consumes loose objects or changes native face numbering. Each object's snap geometry is cached as gui.snapping.ObjectSnaps until its shape, hidden faces or snap settings change; faces and edges that keep their native identity reuse their data, so a rebuild samples only new pieces. After a recorded local split, ObjectSnaps.derived() drops the removed faces and the edges they lose and adds the new ones, without listing the part. While no tool, drag or modal dialog is active, Controller.warm_snaps() advances gui.snapping.warm() about 12 ms at a time, so the first drawing tool on a dense converted part finds its snaps ready. A tool opened mid-way finishes only what is left. Auto's fallback plane passes through the new anchor, while an explicit plane stays fixed for the entire chain. Committed segments are ordinary native geometry with independent Undo transactions. Esc or a tool change discards only the pending preview; a zero-length repeat click does nothing. Keyboard Undo ends the session, and a changed document Undo count also ends it for menu/toolbar Undo so cached topology cannot outlive the edit it describes.
After exact geometric targets, Snapper.snap_line() tries the nearest projected world X/Y/Z axis through the anchor. The capture band defaults to 12 pixels (configurable) and eight degrees, with an 18-pixel origin dead zone; near ties favor an axis in the working plane. Explicit planes filter out other axes. axis_point() solves the closest point between the axis and cursor ray, including perspective projection. Orthographic cameras use the camera direction directly to avoid float cancellation when an almost-empty model has a very thin clipping range; their axes may extend outside that range. Near view-parallel axes are omitted from automatic inference, and perspective targets behind the eye are rejected. X/Y/Z key locks bypass automatic target priorities and can be used with typed lengths when the axis points into the view. A small lock dead zone avoids choosing a negative direction from pixel roundoff at the anchor. Signed numeric lengths follow the current preview direction, and locks reset after each segment.
Snap.axis carries the inference name to the preview: X is red, Y green, Z blue; other line previews retain the usual teal. Center/midpoint guides keep their own colors and only claim exact point captures. Snap off disables automatic point and axis inference, while an explicit axis lock still constrains the segment. Rectangle and Move/Copy retain their existing inference paths.
Snapper.nearby_points() is shared by Auto Line and the fixed-plane/spatial point paths. Face centers within 40 screen pixels and endpoints/midpoints within 24 screen pixels are ranked by distance; within two pixels of the nearest target, geometry on the face under the pointer takes priority. This resolves overlapping front/back projections without making distant face-owned targets override closer points. Endpoint, midpoint, and face-center labels break exact remaining ties. Fixed-plane filtering happens before ranking. Face centers come from the planar face's native CenterOfMass; curved edge midpoints use half arc length. A centroid outside a concave/holed face's material is still associated with that face for drawing-plane inference. Target collection retains the existing visibility, editable-object, container, and Move/Copy exclusions.
Capture radii and individual snap switches are loaded from SimpleCAD preferences when the snap snapshot is built. Defaults remain 24 px for endpoints/midpoints and 40 px for centers; FreeCAD selection preferences are unaffected. Disabled point types do not enter the snapshot, and their guides are suppressed. Seam midpoints are always excluded. Planar faces are still retained for drawing-plane inference when center snapping is disabled. Line also asks Snapper.center_hint() for the hovered editable face's centroid, respecting explicit-plane constraints. An active center snap can supply the hint directly, including centers in holes. Preview draws an unpickable amber ring/cross with a dark outline and a screen-aligned label. The empty target says “Center”; a filled target says “Center snapped” only when an exact endpoint/midpoint/center inference coincides with that center. Ordinary plane/face/axis motion does not claim a capture. Pointer updates rebuild the target in a camera-facing plane at a fixed pixel size. A local depth override keeps only this UI target readable; wire previews retain normal occlusion. Snap off, tool completion and cancellation hide/remove the guide with the existing preview lifecycle.
Snapper also keeps each snapshot edge with its exact half-length point. midpoint_hint() returns the hovered editable edge's midpoint, or the nearest boundary midpoint within 60 pixels on the hovered planar face. Hidden/foreign geometry and off-plane targets are filtered. This guide distance does not alter snap capture distances or ranking. An active Midpoint inference directly supplies its exact target. Preview uses a shared SnapGuide for both the amber center ring and the blue 24-pixel midpoint diamond. Each has a dark outline, point-capture fill and label; midpoint text sits above its target to separate it from nearby center text. Both guides are unpickable, camera-facing, fixed in screen size and confined to the existing preview node. Only exact geometric captures produce the “snapped” label.
After attempting division of an existing face, modeling/drawing.py searches visible editable geometry for two straight sides that close a triangle with the new edge. Collinear segments are followed across extra vertices. Part builds the planar triangular faces; overlap with existing faces or solid interiors rejects duplicates and internal surfaces. This missing-face path deliberately handles triangular loops, not arbitrary polygon cycles. Closed polygon networks lying on existing faces are handled by the subdivision path above.
Drawing-loop scans filter degenerate edges, missing endpoint pairs and negligible lengths before reading Edge.Curve. Valid spherical poles have no 3D curve, so an unguarded type check would stop Line even on an unrelated object. Arc-loop scans use the same eligibility checks. These filters affect connection candidates only; native pole topology is retained.
For a face with holes, divide_face_with_edges() also traverses its existing boundary wires when collecting connected loose cutting edges. Two distant inner-to-outer bridges can enclose a region through those boundaries without touching one another. Boundary wires participate only in connectivity, never as additional cutters. The existing second fuse pass retains only actual shared face dividers, leaving a lone bridge and disconnected interior loops as loose lines. Faces without holes keep the direct line-connectivity path.
New triangles can be sewn to one open surface object when they share free boundaries without creating edges incident to more than two faces. Native shape fixing reconciles orientation, while face count and area are checked. A closed result becomes a validated solid. A bridge on a closed solid, or a patch that cannot join one open object, becomes a separate plain face object. Fully consumed loose-edge sides without dependents are removed/updated in the same document transaction; Undo restores those edges and the original surfaces. No Python proxy or persistent graph is stored.
document/operations.py is the only document writer. Every successful edit uses one named transaction. Any exception aborts it. Kernel calculations happen before opening a transaction. Preview movement has no document side effects. Existing object placements are included in the shapes passed to the kernel; translated and rotated solids are covered by regression tests.
Delete consumes Gui.Selection.getSelectionEx() subelement names, keeping whole-object selections distinct from EdgeN and FaceN. document.operations.delete_selection() combines selections per object and resolves every result before opening one transaction. A whole-object selection takes precedence over that object's subelements. Invalid/vertex selections fail before mutation; deleting the last remaining element uses the same dependency check as whole-object deletion.
core.geometry.erase_elements() resolves adjacency against the original shape. A graph joins only the regions connected by selected coplanar edges; each resulting group is refined in isolation, with its outward normal restored. The complete face set is then sewn to reconcile split boundary edges. A pure merge on a solid reconstructs its shells as a valid solid and checks its volume; unselected divisions, inner wires, and cavity shells survive. Angled/boundary edges and selected faces remove their incident regions, leaving valid native surfaces instead of an invalid open Solid. When a merged group also contains a face marked for removal, the group is removed so no explicitly selected edge remains. Pre-existing loose edges survive unless selected; newly orphaned face boundaries are not kept.
core/coincident.py defines geometric identity for edges/faces: topology identity is a fast path, followed by world bounds, length/area, and full common length/area. Opposite orientations are accepted. Full coincidence is required; endpoint contact and partial overlap do not qualify. The linear bounds tolerance is 1e-7 mm. Face deduplication precedes adjacency counting, with selections mapped from their original indices. This avoids counting repeated copies of one face as separate neighbors.
Deletion expands EdgeN/FaceN selections to matching geometry in all visible editable objects. Whole-object selections stay scoped to that object. Open objects linked by a selected matching edge are evaluated together, so cross-object flat faces merge instead of disappearing. A keeper retains the resulting geometry and the other consumed objects are removed in the same transaction. Solid boundaries are evaluated per solid, while matching edges on added sheets also receive the erase. Dependency checks run on the complete plan before mutation.
core.geometry.is_seam_edge() delegates to native Edge.isSeam(Face) for each incident surface. erase_elements() filters seam edges after validating the original selection; a seam-only request returns a copy of the original geometry before any sewing/reconstruction. Explicit face deletion remains allowed.
document.operations.delete_selection() filters seam seeds before expanding geometric matches across objects. It remembers whole-object selections separately so removing all seam subelements cannot turn a selection into an accidental whole-object deletion. Coincidence expansion also skips destination seams, allowing a real loose line drawn over a seam to be erased independently. An empty plan creates no transaction. The controller receives changed-object and ignored-seam counts and reports a seam-only no-op or a mixed edit accurately. Native topology and display properties are preserved; models require no migration.
The menu-only cleanup builds a plan that retains unique faces/edges, gives solids priority over overlay surfaces, and joins open objects sharing matching edges. It never refines flat-face divisions. Sewing must preserve unique face count, unique edge count, and area; otherwise a valid surface compound preserves the geometry. This prevents sewing's looser tolerance from collapsing merely nearby edges. Solids are kept separate from attached sheets; mixed solid/surface compounds require separation. Repeated cleanup with no differences creates no Undo step. New standalone faces and free lines reuse exact matches, and multiple new triangular patches are sewn before storage.
Open shapes can be saved and reopened as native Part shells/faces/compounds, moved, copied, erased, or used as a source of individual face extrusions. Move/Copy still operate on whole objects.
core.geometry.pull_source() resolves the picked face against current world geometry. A native solid remains the Boolean base. A closed single shell covering the whole surface is converted into an oriented, validated solid; this also supplies the normal used by the preview. An open surface uses the selected face as its extrusion base. Unique unpicked faces and pre-existing loose edges form the remainder, so no surrounding geometry is silently dropped. Curved/foreign faces and mixed solid/surface compounds are rejected before editing.
Preview normal/contact queries pass include_remainder=False: they resolve the native face without assembling or validating unrelated open surfaces. Commit extracts the face and removes it from a native shell without resewing untouched topology, validating the extracted face and remainder instead of also repeating a whole-source shell check. Independent or duplicated surfaces retain the coincidence/assembly fallback. Closed-shell promotion and solid result validation remain in place.
plan_push_pull() computes the extrusion and remainder before any document writes. The low-level push_pull() remains responsible for face extrusion, solid edits, past-face extension, and validation. In one transaction, document.operations.extrude() keeps the picked object's identity for the edited shape and places any remainder in a separate plain Part feature with the source appearance. This avoids persisting a mixed solid/surface object that later tools cannot edit. Undo restores the original joined surfaces and removes the extra object. Both the remaining surfaces and the new solid can be pulled again. Existing support-profile fuse/cut behavior is retained.
Circle captures its drawing plane on the first click. Unlike Rectangle's separate supported profile, a circle on existing geometry immediately partitions its host face. core.geometry.circle() projects the center onto that plane and constructs one analytic Part.Circle edge, a closed wire and a planar face, oriented to the drawing normal. Nonfinite/zero/negative radii are rejected before document changes. Only the wire preview is sampled (96 segments); the saved face and cylinder remain analytic. Pointer radius uses the distance from the center, with geometric snaps filtered to the captured plane and no plane-axis radius inference. Exact radius entry works before pointer motion. After capturing the center, Circle removes coincident points from its private snap snapshot so the center magnet cannot hold the radius at zero; other targets and both guides remain available. Circle reuses Line's visible center/midpoint guides, fixes its plane after the center click, and releases its overlay/callback after commit or cancellation. O starts Circle in the viewport; C remains Copy.
Polygon uses the same captured plane, center/midpoint guides and radius interaction as Circle. core.geometry.polygon_points() projects the center and first vertex into that plane, then rotates their radius vector in equal angular steps; polygon() saves a planar face bounded by native straight edges. The Sides status-bar control accepts 3–100, defaults to 6, and remembers its value for the current FreeCAD process. Changing it only redraws the preview. Exact radius entry preserves the last pointer direction, or uses the plane's U direction before pointer motion. G starts Polygon in the viewport; Esc also cancels from the side-count editor. Completion, Undo and context changes release the interaction without storing custom model properties.
Orthographic cursor rays use the camera's view direction directly. This keeps plane intersections valid when an empty or hidden scene collapses the near/far clipping range and their floating-point difference becomes zero. Perspective rays keep their per-pixel direction. Circle's GUI checks exercise empty-plane drawing as well as supported face drawing; Line and guide regressions cover the shared ray change.
core.geometry.divide_face_with_profile() requires a circle or polygon profile to fit completely on one current planar face. It partitions solids using the source-solid mapping from generalFuse(). For open surfaces it divides the support face alone, then uses modeling.subdivision.replace_surface_face() to reconnect shared boundaries without splitting unrelated intersecting faces. The inner region must match the profile; native face count must increase while total area and solid volume stay unchanged. Intentional divisions, unrelated loose edges and isolated vertices survive. No refinement erases the new boundary. Native region selection works without overlapping objects or special pick priority. Deleting its FaceN leaves an opening; deleting a dividing EdgeN merges the coplanar regions through the existing erase path.
document.operations.draw_circle() and draw_polygon() share draw_profile(). It prefers the clicked host, but skips exact duplicate profiles/edges while looking for a containing face. This repairs legacy circles even when the old overlay was picked first. It computes the split and duplicate removals before apply_plan() checks dependencies and commits one Undo transaction. Hidden objects are excluded, repeated native regions create no extra edit, and profiles in empty space become independent filled faces. An oversized profile started on a face fails without creating an overlay.
Coincident geometry uses optimalBoundingBox(False, False) so display triangulation cannot perturb circle bounds and reject an exact match. The existing 1e-7 mm comparison tolerance includes a 1e-12 mm roundoff allowance at OCC's padded spline bounds; full common area/length still proves equality. A tessellated, tilted circle and transformed line/spline geometry are both covered by regressions.
The stored model consists of plain Part features. Rectangle profiles and legacy circle profiles add an App::PropertyLink to the support object, which FreeCAD persists itself. No custom Python Proxy or view-provider object is needed to reopen a model. When a profile is extruded, one transaction updates the solid and removes the consumed profile; Undo restores both.
The first MVP deliberately limits direct edits to plain Part features outside transformed containers. Editing a generated Pad or a Part Box's Shape would be overwritten by recompute, so those objects need a simple copy first. Before recomputing, document.operations.settle() clears the touched state of plain Part features that have no dependents or expressions and whose shape already sits at their Placement. Recomputing such a feature would only stamp that Placement onto the shape again, which gives every face and edge a new native identity and redraws the whole object a second time. Snapshot copying and native shape persistence keep later parametric conversion possible without imposing it now.
Possible next steps: improve snap ranking and occlusion, cache projected points, preserve sensible drawing directions between operations, complete arbitrary polygon loops in empty space, and add drag-distance inference to other faces. Optimize Boolean previews only after interaction and cancellation behavior have been tested with the user's mouse/trackpad.
Primary API references¶
- FreeCAD workbench creation
- FreeCAD view API
- FreeCAD Draft snapper source
- FreeCAD Part split API and generalFuse mapping
FreeCAD's installed 1.1.3 modules and runtime were also inspected. The implementation is independent of Draft, Sketcher, and Part Design.
Surface color and opacity¶
document/appearance.py reads and writes native ViewObject.ShapeAppearance materials, with one entry for a uniform object or one entry per face. Opacity is 100 * (1 - Transparency). Edits validate all targets first, copy complete materials and change only the requested fields, then write in one native transaction. A parent target takes precedence over its children. No geometry or extra document objects are created. Copy retains the full material list; topology-changing operations retain FreeCAD's native face-material assignment behavior.
gui/tools/appearance.py opens from the tree's third-column color swatch, its context menu or the SimpleCAD menu. The dialog supports multiple targets, mixed values, a native color picker and synchronized opacity slider/spin box. Preview changes only temporary Coin material/binding fields, avoiding document edits that would erase existing Redo history even when canceled. Apply restores the native representation before committing; Cancel restores the previous appearance and selection. Live preview validates the shape snapshot on every change.
SeamDisplay tracks material fingerprints and refreshes the native view representation only when appearance or the owning view changes. FreeCAD 1.1.3 can otherwise keep displaying colors cached before save/reopen. The saved material list is the source of truth; a separate display repair restores per-face Coin transparency if FreeCAD's global Transparency callback flattened it. This repair adds no transaction and is paused during temporary previews. Preview entry/exit also refreshes native render caches. Eye masks are reapplied afterward; edges, picking identities and printable geometry stay unchanged. Closed/removed objects release cached state.
For mixed face opacity, gui/surface_display.py adds an unpickable standard Coin face renderer while keeping the native SoBrepFaceSet invisible and pickable in its original parent. This avoids FreeCAD 1.1's highlight path borrowing another face's transparency. The visible renderer shares native coordinate/normal indices, including eye masks, and uses per-triangle material indices mapped from native face triangle counts. Selection observers tint only RGB; connected alpha retains each face's opacity. Whole-object highlighting can still add FreeCAD's brighter emission. No native material or geometry is edited by highlighting. Material changes invalidate the packed-color render cache, and triangle-material indices rebuild only when face triangle counts change. Rebuilt native display branches, closed documents and uniform-opacity transitions release the temporary nodes and sensors.
The face browser uses document.appearance.describe_all() to read native materials once for all rows. SeamDisplay caches seam membership against object/shape identity and reads the face list once during a seam scan. A changed shape, Placement or replacement object invalidates that cache. Idle updates and pointer highlighting do not redo geometry adjacency scans.
Face visibility, seam display and preferences¶
core/preferences.py loads a bounded immutable settings snapshot from User parameter:BaseApp/Preferences/Mod/SimpleCAD. The small Qt dialog in gui/settings_dialog.py saves only on OK; Cancel leaves preferences intact and Restore Defaults changes the draft until accepted. Opening settings ends the current interaction using the normal cancel path. Settings never create document transactions. Seam visibility applies immediately; the next drawing tool loads the saved snap choices. Automatic axis disable affects both world and plane inference; explicit X/Y/Z locks remain available.
gui.display.SeamDisplay owns reversible native display masks scoped to ordinary Part::Feature objects. It identifies periodic seams with Edge.isSeam(Face) and finds native SoBrepEdgeSet nodes by type, visiting shared nodes once. Seams and boundaries owned only by hidden faces have their polyline coordinate indices collapsed to their first coordinate. Keeping every polyline slot, separator, and vertex count preserves native EdgeN pick indices and per-edge materials. Removing a polyline or leaving an empty slot renumbers later picks in Coin and can cause invalid renderer indices. The native renderer's index-to-selection contract is in SoBrepEdgeSet.cpp for the tested FreeCAD build.
FaceMask collapses each hidden face's triangles in SoBrepFaceSet.coordIndex, retaining partIndex and every original triangle slot. PointMask temporarily replaces native point sets with visible ranges using the original coordinate indices, and restores the native nodes on release. These changes affect drawing and preserve surviving FaceN/EdgeN/VertexN references without altering BREP geometry. Coin can still report a collapsed edge at its original endpoint: SimpleCAD's click handler filters hidden subelements from the native hit list, preferring a visible boundary at the same front depth. Snapping and box selection apply the same visibility rules.
The manager caches original and masked arrays. Coin field sensors plus queued document observers refresh regenerated tessellation; a 500 ms fallback detects replaced scene graphs without a document edit. Unexpected edge layouts remain native and face layouts are validated instead of risking wrong pick indices. Before masking, stale hidden-edge preselection/selection is cleared without dropping ordinary selections. Original arrays are restored only if they still match this manager's mask, then sensors are detached. Workbench deactivation releases the seam preference but keeps saved face masks active. Closed documents and removed objects release their records and visibility caches. No Python view provider or proxy is saved into an FCStd document; opening without SimpleCAD displays native complete geometry, while loading SimpleCAD reapplies the saved face choices.
Exact opposite-face contact and past-face extensions¶
core.geometry.opposite_face() groups parallel, opposite-facing faces at a common signed distance. A translated copy of the whole profile must be covered by that group, and the swept prism must lie completely in the source material up to contact. This handles divided back faces while rejecting center-ray/bounding-box false positives, partial coverage, and paths crossing an existing void. Contact is cached when Push/Pull captures its source; menu/keyboard Undo discards the interaction and its cache. Exact numeric coincidence uses the kernel EPS tolerance, independent of the mouse capture radius.
Before contact, the negative extrusion remains a Boolean pocket. At contact it remains a Boolean through-hole. Beyond a recognized contact, extend_past_face() cuts to that plane, restores the original opposite faces, and continues the cavity walls to the translated cap. The wall and preserved far face intersect. The result therefore uses a native compound of faces, with no solid assertion and no saved source-history properties or Python proxy. Its BREP faces remain valid and editable. The original opposing face divisions are retained. If the entire original prism was consumed, the outside prism is a normal solid; zero thickness itself is still rejected.
recover_extended_cap() recognizes the cap's complete prismatic wall back to a planar opening using geometric common areas. It reconstructs a closed, valid source solid by replacing only that wall and cap with the opening profile, then verifies that the current cap is beyond a qualifying opposite face. pull_setup() expresses the next edit in this reconstructed source's coordinates. This allows an extended cap to grow, return to a hole/pocket, or restore the original plane without hidden state; Move, Copy, and native save/reopen retain the behavior. Loose edges are handled by the existing remainder path. An ordinary open surface that does not satisfy this reconstruction keeps its independent-face extrusion behavior.
Before trying cap heights, recovery requires candidate geometry on every prism-wall supporting surface. Planar walls use parallel normals and plane distance; curved walls conservatively filter by surface type. Missing wall support rejects ordinary imported faces immediately. Remaining area intersections only test faces contained in the prism-wall bounds. These filters do not replace the complete-wall, empty-opening, closed-solid and opposite-contact proofs.
gui.snapping.pull_drag() measures motion along the normal using the camera ray, including perspective. Face-on views retain the vertical-drag fallback. snap_pull() compares the current and target cap positions in screen space (or the fallback scale), so a finite capture band releases when the pointer moves farther. pull_faces and pull_radius are SimpleCAD preferences; the default radius is 24 px, with saved values retained. The general Snap checkbox disables capture and its guide. Typed dimensions bypass mouse capture. Preview adds an unpickable amber Opposite face guide, while the wire preview continues to show the requested cap position without document writes.
The exact-contact behavior is intentionally limited to full, parallel exits through continuous material. Without such a contact, the existing Boolean-cut behavior remains. This does not implement arbitrary intersections of sloping or curved opposite faces.
Saved geometry locks¶
document/locking.py stores object locks and local BREP face references on plain Part features. Output property flags keep metadata changes from recomputing topology. Face matches are cached by metadata and native shape; unmatched references fail closed. Whole-object transforms and direct face edits have early checks. The common document.operations.transaction captures protected geometry and validates it after recomputation, aborting the entire native transaction if any locked face/object changes or disappears. This also catches shared-edge, supported-profile and multi-object edits without excluding locked objects from snapping or picking. Protected documents require native Undo for rollback. The Faces tree exposes a fourth, cached-icon padlock column; MCP exposes explicit lock state and the same setter.
Material preservation and line latency¶
document/materials.py captures native materials before each GUI document transaction and remaps changed objects after geometry recomputation and lock validation. Native topology identity is preferred; bounded surface overlap identifies descendants and surviving regions after renumbering. Only faces with a style different from the predominant style need geometric matching in the common case. Merged regions choose their largest contributor, new surfaces use the predominant style, and uniform/rigidly moved shapes have fast paths. Changes are saved native ShapeAppearance values within the geometry Undo transaction; no second persistent material database or background remapper is added.
core.coincident.loose_edges resolves native boundary ownership using hash buckets verified by isSame before checking geometric coincidence for genuinely loose candidates. This avoids quadratic OCC comparisons among solid boundaries. Snapper snapshots each object's faces once for seam checks instead of repeatedly reading native Shape.Faces for every edge.
Spheres¶
Sphere uses the shared draw controller for center/radius input, viewport-only B shortcut and snap guides. Auto placement accepts planar supports or a camera-facing plane through a curved-face hit, but never retains a support link. The preview is three sampled great circles plus the radius segment; it never builds a Part solid during motion. Commit calls core.geometry.sphere for a complete analytic solid and document.operations.new_shape for one native transaction. create_sphere uses the same path with explicit world-space center and radius. Existing display, materials, locking, transforms and Union operate on its native Part geometry.
Copy, cut and paste¶
gui.clipboard claims ⌘C, ⌘X and ⌘V (Ctrl on Windows and Linux) in the controller's event filter by accepting the ShortcutOverride, so FreeCAD's own Std_Copy/Std_Paste do not also run. It acts only when the 3D viewer, the face browser or FreeCAD's tree has focus; text fields and other views such as spreadsheets keep their own meaning. document.clipboard.snapshot copies whole plain Part shapes and Mesh::Feature objects, with placement and view properties, at Copy or Cut time; cut removes them through apply_plan so locks and dependents are refused in one transaction. The system clipboard carries only a random token in application/x-simplecad-objects; Paste uses the stored copies only while that token is still current. paste adds plain objects, without support links, in one transaction, then the GUI selects them and starts Move for Part shapes. Selections with parametric or contained objects use FreeCAD's Std_Copy, and Paste falls back to Std_Paste for FreeCAD's application/x-documentobject data.
Surface intersections¶
modeling.intersection.split performs one native general fuse on selected faces. Unselected owner faces and loose edges are retained, and source/output surface area is checked. The result is a compound of surface regions: three- and four-way crossing edges deliberately remain nonmanifold until the user deletes unwanted regions. It avoids premature sewing and its quadratic boundary comparisons. document.operations.intersect combines owners in one transaction with lock/link guards. A grouped pre-edit material snapshot preserves all contributing owners' styles. Delete sews remaining faces as before and promotes a closed valid manifold shell to a native solid. No persistent Python proxies or new metadata are required. The GUI I command and MCP intersect_faces share this implementation.
Solid edge merge repair¶
For coplanar edge deletion, erase_elements first builds the retained faces directly. If their boundaries do not form a valid closed solid, it reconciles the face set using native general fuse. It checks face count, validity, closure and volume before commit. This avoids a whole-shell sewing step that can disconnect an unrelated spherical seam and tiny valid boundary edges. Open-surface deletions retain the existing sewing path. No global refinement removes unselected face divisions.
Arcs¶
modeling/arcs.py represents a curve as center, radial vector, unit normal and signed sweep. Center/start/sweep and endpoint/bulge constructors validate finite, coplanar, nondegenerate inputs. Preview samples are pure vector/trigonometric calculations; Part.Arc runs only on commit. Two-point construction uses sagitta to compute the circle center and signed sweep, including major arcs.
gui.tools.arc.ArcInput captures the second point and final sweep/bulge without document edits. Mouse sweep unwraps around the center for clockwise/counterclockwise major arcs. Controller integrates A/H, Auto/explicit planes, native snap guides, staged Tab input, Escape and document/Undo cancellation. Radius-constrained or bulge-constrained preview points do not claim a snap when they differ from the actual snap target.
document.operations.draw_edge shares the existing face general-fuse partition and material/lock transaction path between lines and arcs. modeling.arcs.completions searches the loose-edge endpoint graph for a simple closed planar loop containing an arc; it leaves branches intact and rejects overlap with existing faces. Straight-only scenes exit before graph construction, reusing the loose-edge snapshot already collected by Line. The older triangle completion remains for straight edges. create_arc and create_2_point_arc call the same operation after schema and revision checks.
Offset¶
modeling.offsetting.Profile captures one flat face or straight/circular edge and its drawing plane. Mouse distance is signed nearest-boundary distance for faces, signed perpendicular distance for lines, and radius difference for circles/arcs. Face profiles use native 2D intersection joins for sharp corners. Periodic circles are offset analytically to avoid kernel seam failures; holes offset in the opposite sense and are cut from the filled outer offset. Containment, area and shape validity reject collapsed or crossed layouts. Lines translate within their plane; circular edges change radius while preserving the curve basis, parameter range and orientation.
gui.tools.offset.OffsetInput reuses the controller's unpickable wire preview and click/Tab/Esc lifecycle. It throttles updates to 30 ms and computes only the selected profile in the preview, never whole-model booleans. Commit synchronously uses the latest exact distance. Cached source identity and document/Undo changes invalidate the session. Unsigned UI dimensions follow cursor side; explicit signs override it. Edge Auto planes follow a supporting flat face or a view-facing plane containing a loose line.
document.operations.offset runs one native transaction. Face boundaries are clipped to the owner's coplanar footprint before imprints; solid volume is checked, and open surfaces use local region replacement to preserve unrelated intersections. Added area excludes existing coplanar surfaces. Flat open owners acquire the added border in-place; solid owners retain a separate border object for any extension beyond their footprint. The border inherits the selected material; ordinary geometry mapping preserves existing face styles. Edge offsets deliberately bypass drawing completion and face-splitting operations. MCP offset uses the same operation with explicit names, distance and a normal for lines.
Push/Pull height references¶
gui.pull_snapping.PullReferences caches visible parallel faces and constant-height edge polylines when a source is captured, including tree preselection. Levels are signed projections on the actual extrusion normal. Source-level geometry, hidden elements, seams and degenerate poles are excluded. Mouse motion projects cached edges for a full-edge capture band, preferring frontmost nearly coincident candidates and checking native pick depth to reject occluded edges. Without an edge target, native face picking resolves a cached parallel-face height. The original opposite-contact magnet wins conflicts near its stop. Preview labels the chosen reference as Face level or Edge level using the shared amber marker. Cancel releases the cache. No geometry Boolean or document write runs during reference updates.
The reference index uses one native seam lookup per object. Push/Pull initializes only the general snap preferences, omitting the drawing-point index it does not use. Ordinary gui.snapping.pick preserves support-profile priority and native hit order while resolving only the first visible, valid subshape, rather than materializing every occluded hit. Boundary-priority drawing picks retain their depth/type ranking. Height-reference occlusion queries share a pick for coincident integer pixels within one mouse event; the cache is discarded before the next motion so camera and scene changes are respected.
Push/Pull cuts that separate solids¶
Negative Boolean cuts may return multiple solids when the removed region disconnects a bridge or wedge. core.geometry.push_pull() validates the result and every closed, nonempty solid, then returns all pieces. The document writer places the largest solid in the original owner and each other solid in a separate plain Part feature, mapping source face materials into the new owners. All writes use one existing transaction with lock validation and Undo/Redo. Existing capped past-face extensions keep their surface behavior. document.operations.extrude() retains its default primary-object return; return_pieces=True exposes all resulting owners to the controller and MCP.
Vertex deformation¶
modeling.vertex_edit.Neighborhood captures the picked world-space vertex, adjoining planar polygon loops and native constrained triangulations once. Pointer previews move cached coordinates only. Commit reconstructs flat faces with their wires/holes, or uses the captured triangles when a face bends out of plane, then sews and validates the solid (including BOP checks). Other faces keep their surfaces. Closed single-shell solids and open polygon faces/linear edges are supported; curved incident geometry and mixed solid payloads are rejected.
document.operations.move_vertex checks stale captures and adjoining locks, assigns the rebuilt shape in one transaction and supplies explicit face material lineage. The transaction's face_materials override avoids guessing inheritance from geometric overlap when a face has moved. Lock validation and all appearance writes remain in the same Undo step. The controller uses existing native picking and line-axis inference, with V, drag/release or click/move/click, XYZ triplets, signed axis distances and cancellation. MCP uses the same writer through move_vertex.
Scaling and profile formation¶
modeling/forming.py implements affine scaling, ordered native Part.makeLoft, and Wire.makePipeShell sweeps. Scaling works in local coordinates and reapplies the original Placement, keeping local axes meaningful. Uniform scaling preserves analytic geometry. General affine transforms produce splines; planar faces are reconstructed as native Planes, and fully straight-edged models retain Line edges. Shared curved topology is transformed once before planar face replacement, with closed curved shells retained for poles/cavities. Scaling tracks an explicit original face index for every output face, so document.operations.scale_objects supplies transaction material overrides instead of geometric-overlap guesses.
gui.tools.forming.FormingSession handles the three panels. Scale uses cached Coin meshes with a center, scale orientation and factors; no native geometry builds occur while adjusting those controls. Loft/Sweep debounce native previews by 150 ms, cache unchanged options/order, and never write document objects during previews. Apply checks captured source topology, commits a new plain Part feature (Loft/Sweep) or replaces existing shapes (Scale), and includes source visibility in the same transaction. Selecting subelements never hides their parent objects. The three MCP mutations call the same operation functions. Output solids pass native validity/closure checks; loft/sweep additionally run BOP checks. Affine transforms preserve topology without treating OCC's spline C0 continuity diagnostic as invalid volume geometry.
Bend and soft deformation¶
modeling.deformation.Region captures a segmented MeshPart tessellation once and maps every triangle to its native source face. Shared edge midpoints refine the triangulation conformingly, avoiding T-junctions between adjacent faces. Detail is bounded (3–16) and captures above 4000 faces are rejected. Soft Move applies a radial quintic smoothstep falloff to cached points. Bend applies a constant-curvature arc in an axis/direction frame, with a fixed region before the origin and tangent continuation beyond the bend length. The kernel rejects excessive inner compression before constructing geometry.
The preview updates one cached Coin coordinate array and per-face influence colors; it does not create Part faces during control changes. On Apply, the region checks collapsed triangles and Mesh self-intersections/closure, reconstructs and sews native triangle faces, validates the solid, and maps each new face to its source material/visibility. Local geometry is rebuilt under the original Placement. document.operations.commit_deformation applies shape, material, visibility and Shaded display changes atomically. No-op strength retains the exact source geometry. Controller lifecycle checks invalidate stale source captures; simplecad_deform calls the same writer.
Fine surfaces made the old all-edge/all-face display seam scan expensive. core.geometry.seam_indices now visits only incident edge/face pairs, with native hash buckets and identity verification. Hidden edge/vertex masks likewise index incident topology by hash instead of repeatedly scanning all visible/hidden boundaries. This preserves native selection numbering while avoiding quadratic display work after deformation.
Mesh conversion¶
mesh/planar.py reconstructs planar regions before creating CAD triangle faces. Native mesh segmentation supplies candidates; an independent SVD plane fit bounds every source vertex against the user tolerance. Original 3D boundary wires retain shared endpoints and holes. Small repair precision avoids collapsing short edges; the maximum repair tolerance remains bounded. Wire count, projected triangle area, native face/shell/solid validity, closure, face count and enclosed volume are checked. Rejected regions retain their original triangles. A failed assembled reconstruction falls back to the original component with an explicit warning.
mesh/conversion.py sews and validates components, optionally decimates a tessellation of the valid result, and refines coplanar boundaries only if validity, closure and volume survive. It preserves nested cavities and reports open surfaces. Invalid simplification falls back to the validated component. Source meshes are copied.
The face browser caches face counts with shape identity rather than enumerating faces every 500 ms. Push/Pull resolves the picked native face before copying geometry; solid faces skip quadratic geometric deduplication, and coplanar-divider detection uses edge adjacency buckets.
jobs/mesh.py snapshots an imported mesh in local coordinates and runs scripts/mesh_worker.py through QProcess using FreeCAD’s Python. Worker files are private temporary snapshots; only the GUI thread applies completed native geometry. Conversion jobs enforce facet/component caps and share one worker slot with mesh splitting. Both operations enforce a five-minute timeout, cancellation, source geometry/placement checks and a single commit transaction. Original visibility and the new objects are undone together. gui/tools/mesh_convert.py and four conversion MCP methods share this service. The Faces tree includes mesh rows with visibility and conversion access, and caches unchanged row styles to avoid repainting thousands of face icons every poll. Snapping finds seam edges via incident-face topology rather than all edge/face pairs.
Mesh-only splitting¶
mesh/slicing.py clips indexed triangles against a plane, sharing intersections across adjacent triangles and canonicalizing intersections to native float32 mesh coordinates. Uncut triangles keep their coordinates and winding. Closed cut contours become temporary planar OCC faces with nested holes for cap triangulation only; the source mesh body never becomes Part geometry. Any tiny contour vertices omitted by cap tessellation are reinserted into cap triangles, so clipped triangles and caps share exactly the same indexed boundary. Non-manifold edges, inconsistent normals, unexpected open boundaries and enclosed-volume changes are rejected. Volume validation uses translated double-precision tetrahedra because native Mesh.Volume accumulation is less precise.
The main axis uses area-weighted surface covariance, avoiding bias from densely tessellated regions; a deterministic local bounding-box axis resolves ties. World and object-local axes are also supported. Offsets are measured from the midpoint of the mesh's projection on the axis. Jobs transform the world plane to source-local coordinates and write native BMS snapshots and results. The same worker slot, timeout, cancellation and source checks serve conversion and splitting. Only Apply creates two plain Mesh::Feature objects, preserves their placement and appearance, optionally adds a gap and hides the source, all in one transaction.
gui/tools/mesh_split.py displays an unpickable plane guide while controls change and starts the worker on Preview split. Ready meshes are drawn directly through Coin; NativeMask temporarily hides the source without changing document properties. Apply, Cancel, Undo, context changes and stale sources release masks and previews. The Faces tree and toolbar expose Split mesh; Slice routes a selected mesh to this panel. Four dedicated MCP methods provide start/status/finish/cancel.
mesh/glue.py optionally replaces the shared planar caps with a height field determined by distance to the cut boundary, capped at the requested depth. This gives 45° slopes and a medial ridge across thin walls, with flat depth-limited interiors on wide sections. One shared conforming triangulation defines both sides; male heights are reduced by the axial glue clearance and the female uses the full profile. Cached midpoint/centroid sampling refines the cap with a sampled tolerance of at most 0.025 mm. Failed triangles refine all their edges to avoid repeatedly creating long slivers. Boundary subdivisions propagate only into adjacent clipped skin triangles. A cap limit of 120,000 triangles bounds refinement.
Output vertices are canonicalized at float32 storage precision. Collapsed facets and opposite duplicate seam flaps are removed, and exposed sub-resolution cracks are stitched by matching boundary points and subdividing existing edge chains. Tiny non-manifold flaps are discarded before native construction can drop their legitimate neighbors. Any winding correction is restricted to facets with altitude below the storage-precision tolerance. This is local numerical cleanup, not source mesh simplification or repair. The worker also exports and reloads each glue result as STL, requiring unchanged facet count, closedness and consistent manifold topology before reporting it ready.
Glue output must remain closed, consistently oriented and manifold. Native mesh collision candidates involving the new cap are confirmed with double-precision plane tests, coplanar overlap area and tolerances matched to stored float32 coordinates; numerical boundary contacts and existing source intersections do not trigger false breakthrough errors. Volume checks subtract the analytically integrated clearance space instead of demanding unchanged combined volume. The optional glue settings travel through the existing worker job and MCP split tool; no additional document objects or CAD conversion are introduced during preview.
Dense selection and deletion¶
Region selection computes periodic seam IDs once per object from incident edges. It reuses the current display tessellation when the object already has its global placement; only nested placements need a geometry copy. Sampling still occurs on each release, preserving camera, zoom, hidden-element and topology behavior. Native selection additions are batched per object. The Faces tree synchronizes contiguous selected rows through QItemSelection ranges and skips unchanged selection state, avoiding repeated per-row selection rebuilding on its 500 ms refresh.
core.coincident.Index is scoped to one operation. It caches optimal geometric bounds and uses topology hash buckets plus neighboring spatial buckets of bounding-box centers. Native matches still require isSame; independent candidates still require the existing full bounds, measure and boolean-overlap checks. Reversed faces, duplicate geometry and differing holes retain their previous semantics. No geometric index persists across document edits.
Deletion uses unique native solid faces and edge adjacency directly; open/compound geometry retains exact coincident-face deduplication through the index. Cross-object selection expansion uses the same indexed equality, and seam IDs are computed once per object. The closed-solid path skips redundant repeat validation of an already validated, unchanged result, while preserving closure, volume and transaction/Undo checks. General boolean rebuilding remains the fallback when merged boundary subdivisions cannot form a valid solid directly.
Small edge selections use native ancestor lookup instead of materializing every face boundary. Bulk seam lookup skips nonperiodic planes and builds the edge map only if a seam is actually found. Small coincidence indexes compare cached bounds directly; larger indexes retain spatial buckets and both still verify exact equality.
For a native solid, merged faces replace their original regions through nonmutating replaceShape/removeShape; unchanged shells and faces retain native identity. Native open shells also use local replacement when valid; independent surfaces retain their assembly path. Full result validation, merged area, closedness, positive solid volume, and volume preservation remain mandatory, with the same boolean reconciliation fallback. Deletion validates the completed result rather than repeating full input and output checks. The deletion planner no longer deep-copies the source before this read-only calculation. Source immutability and retained native faces are verified on a rotated 2,352-face solid. The face browser shares one QIcon instead of reparsing the same SVG for every new face row.
Dense open-shell editing (0.37.4)¶
A line wholly contained in one shell face is split locally. Supplying that face’s boundary edges and vertices as boolean arguments gives explicit history for reconnecting neighboring faces, including endpoints in the middle of a shared edge. Native replacement preserves unrelated tiny imported regions. Multi-face edits use the native shell as a boolean argument; independent surface collections retain geometric assembly. A second split is needed only when dangling tool pieces must be excluded. Area/result validation remains unchanged.
Sparse geometric incidence indexes the queried edges instead of all model boundaries. Triangle attachment builds native edge incidence once and copies its repair shell before mutating fix() calls. Profile drawing uses native vertex buckets to retain isolated vertices without an all-pairs scan. Native Face/Shell/Solid containers have no loose edges outside their faces. The sidebar no longer integrates every face area while recreating rows; the hovered face supplies its details.