Meshfree Module
GearMaster · GM_modules.Meshfree · v0.3 · Author: Simone Lucertini
Overview
The Meshfree module provides tools to generate the node (point cloud)
distribution required by the Meshless Local Petrov-Galerkin (MLPG) method
directly from a GM_CadModel geometry.
Unlike classical FEA, MLPG does not require a mesh. Only a scattered set of nodes with associated normals and boundary flags is needed. This module handles:
- Extracting planar faces from a BRep (OCC) CAD model.
- Placing boundary nodes along face edges at a uniform spacing
h. - Filling the interior with quasi-random nodes via the Bridson Poisson Disk algorithm.
- Saving and loading point clouds in the
.gmpcloudformat (with embedded display settings). - Visualising the cloud interactively using the
PointCloudEditor(PyVista + PyQt5). - Generating point clouds for individual CAD entities directly from the editor GUI.
- Defining boundary condition joints on selected CAD sub-entities via the interactive editor.
- Packaging geometry, point cloud, and joints into a unified
GM_MfreeModelcontainer. - Saving and loading the complete analysis model in the
.gmmfmformat (single portable file).
Quick Start
1 — Load a CAD model
from GM_modules.Cad import GM_CadModel
model = GM_CadModel()
model.load("my_part.step")
2 — Generate the point cloud
from GM_modules.Meshfree.src.generator import PointCloudGenerator gen = PointCloudGenerator(h=2.0, seed=42) cloud = gen.generate(model) print(cloud) # GM_PointCloud(name='my_part', dim=2, n=312, boundary=48, interior=264)
3 — Save and reload
cloud.save("my_part.gmpcloud")
from GM_modules.Meshfree.src.point_cloud import GM_PointCloud
cloud2 = GM_PointCloud.load("my_part.gmpcloud")
4 — Visualise with the interactive editor
from GM_modules.Meshfree.src.point_cloud_editor import PointCloudEditor editor = PointCloudEditor(model, cloud) editor.show(title="My Part — Point Cloud")
5 — Visualise with plain pyvista
import pyvista as pv plotter = pv.Plotter() cloud.add_to_pyvista_plotter(plotter, color="red", boundary_color="blue") plotter.show()
6 — Build a complete meshfree model and apply joints
from GM_modules.Meshfree.src.mfree_model import GM_MfreeModel
# Wrap geometry + cloud into a unified container
mfree = GM_MfreeModel(cad_model=model, point_cloud=cloud, name="my_part")
# Open the editor with the model attached.
# Apply joints via: RMB on viewport → Apply Joint → External…
editor = PointCloudEditor(model, cloud)
editor.mfree_model = mfree
editor.show(title="My Part — Meshfree Editor")
# Save the complete model (geometry + cloud + joints) to a single file.
mfree.save("my_part.gmmfm")
# Reload later (e.g. in a solver node).
mfree2 = GM_MfreeModel.load("my_part.gmmfm")
Dependencies
| Package | Purpose | Install |
|---|---|---|
pythonocc-core ≥ 7.7 |
BRep face / edge extraction from STEP geometry | conda install -c conda-forge pythonocc-core |
numpy |
Array operations, Bridson algorithm | bundled with conda base |
matplotlib |
2-D containment test (matplotlib.path.Path) |
conda install matplotlib |
pyvista |
3-D visualisation of the point cloud | conda install -c conda-forge pyvista |
joblib |
Parallel entity processing (optional) | conda install joblib |
MLPG Method — Brief Overview
The Meshless Local Petrov-Galerkin method (Atluri & Zhu, 1998) solves PDEs by defining a weak form on a local support domain around each node, without requiring a global mesh. The inputs required by any MLPG solver are:
- Interior nodes: scattered inside the problem domain.
- Boundary nodes: placed on the domain boundary, with associated outward normals for applying Neumann conditions.
- A characteristic length h that controls the support radius of the basis / weight functions (e.g. MLS, Wendland RBF).
This module handles step 1 (node generation). The assembly of the local weak forms and the linear system will be implemented in a subsequent module.
h as roughly 1/10 of the smallest characteristic dimension of
the domain. The module does not automatically scale h to the
geometry — you must provide it in the same units as the CAD model (usually mm).
The GM_PointCloud Object
GM_PointCloud is the central data container. Every node is represented
by aligned arrays:
| Attribute | Shape | dtype | Description |
|---|---|---|---|
points | (N, 3) | float64 | 3-D coordinates. For 2-D problems z = 0. |
normals | (N, 3) | float64 | Unit outward normals. Interior nodes carry the face normal. |
is_boundary | (N,) | bool | True for nodes on geometry edges. |
entity_ids | (N,) | str (U64) | ID of the CAD entity each node belongs to. |
dimension | scalar | int | 2 or 3. |
name | scalar | str | Human-readable label (default = model name). |
display_settings | scalar | PointDisplaySettings | Viewport rendering options (representation, normal mode, point size, gaussian opacity/emissive).
Persisted inside .gmpcloud files. |
Useful methods
| Method | Returns | Description |
|---|---|---|
filter_by_entity(id) | GM_PointCloud | Subset for a single CAD entity. |
filter_boundary() | GM_PointCloud | Only boundary nodes. |
filter_interior() | GM_PointCloud | Only interior nodes. |
merge(clouds, name) | GM_PointCloud | Static method; concatenate multiple clouds. |
save(path) | None | Write .gmpcloud archive. |
load(path) | GM_PointCloud | Static method; read .gmpcloud archive. |
to_pyvista() | pyvista.PolyData | Convert to pyvista point-cloud mesh. |
add_to_pyvista_plotter(p, ...) | None | Add boundary + interior actors to an open plotter. |
Samplers
All samplers derive from BaseSampler and share the same interface:
sampler = SurfaceSampler2D(h=2.0) cloud = sampler.sample(entity) # entity is a CadEntity leaf
SurfaceSampler2D Implemented
Places boundary nodes uniformly along every edge of a planar face at arc-length
spacing h. Uses OCC GCPnts_UniformAbscissa for accurate
arc-length discretisation.
Outward in-plane normal at each node is computed as:
n = T × face_normal, where T is the edge tangent in the
CCW wire-traversal direction. This guarantees the correct outward direction for
a standard FACE_OUTER_BOUND.
InteriorSampler2D Implemented
Fills the interior using the Fast Poisson Disk algorithm (Bridson 2007). The steps are:
- Generate candidate nodes in the 2-D axis-aligned bounding box of the face
with minimum separation
h. - Reject candidates outside the face boundary using
matplotlib.path.Path.contains_points(). - Lift surviving 2-D points to 3-D world coordinates.
- Assign all normals equal to the face normal (suitable for MLPG).
seed=<int> to InteriorSampler2D (or to
PointCloudGenerator) to get deterministic results across runs.
SurfaceSampler3D Planned
Will place nodes on the surface of 3-D solid / shell entities. Not yet implemented;
calling sample() raises NotImplementedError.
InteriorSampler3D Planned
Will fill the interior of 3-D solids using a 3-D extension of the Bridson algorithm. Not yet implemented.
The .gmpcloud File Format
A .gmpcloud file is a ZIP archive containing five
entries:
| Entry | Content |
|---|---|
manifest.json |
Metadata: format, version, name, dimension, n_points, n_boundary, n_interior.
Optional "extra" key for user metadata (e.g. h value). |
points.npy |
Node coordinates, float64, shape (N, 3). |
normals.npy |
Unit outward normals, float64, shape (N, 3). |
flags.npy |
uint8, shape (N, 2). Column 0 = is_boundary. Column 1 reserved. |
entity_ids.npy |
Unicode str array (U64), shape (N,). |
.gmcad format used by the CAD module — ZIP +
JSON manifest + binary numpy payloads — making the pattern consistent across the
GearMaster ecosystem.
GM_MfreeModel
GM_MfreeModel is the unified container that wraps all data needed for
a meshfree analysis: CAD geometry, the point cloud, and boundary condition joints.
It is the central object exchanged between nodes in the GearMaster Visual workflow:
CAD_Editor → Meshfree_Editor → [GM_MfreeModel] → Mfree_Solver
| Property | Type | Description |
|---|---|---|
.cad_model | GM_CadModel | BRep geometry; always present. |
.point_cloud | GM_PointCloud | None | Meshfree node distribution; None until generated. |
.joints | list[GM_Joint] | Boundary condition joints; empty list until defined. |
.has_point_cloud | bool | True when a non-empty cloud is attached. |
.has_joints | bool | True when at least one joint is defined. |
Persistence via the .gmmfm format (see below):
model.save("analysis.gmmfm")
model2 = GM_MfreeModel.load("analysis.gmmfm")
Boundary Conditions — Joints
Boundary conditions in GearMaster Meshfree are defined as Joints. A joint ties a scoping region (one or more CAD faces, edges, or vertices) to a reference point and prescribes which degrees of freedom are constrained on all cloud nodes found within that region.
Joint types
| Type | Description |
|---|---|
Fixed |
All 6 DOFs (Tx, Ty, Tz, Rx, Ry, Rz) are locked. Classic fixed support. |
Custom |
User selects which DOFs are locked via toggle buttons in the dialog: dark red = locked, grey = free. |
Defining a joint in the PointCloudEditor
- Select one or more faces / edges / vertices on the CAD geometry in the 3-D viewport.
- Right-click in the viewport → Apply Joint → External…
- Fill in the dialog: name, type, behaviour, DOFs, and optionally adjust the reference point coordinates.
- Click OK. The joint appears in the Boundary Conditions panel and its actors (a violet sphere + connector lines) remain visible in the viewport.
Data stored per joint
| Field | Description |
|---|---|
name | Human-readable label. |
scoping | List of CAD sub-entities (entity id + type + index). |
joint_type | "Fixed" or "Custom". |
behaviour | "Rigid" (only option currently). |
reference_point | 3-D coordinates [mm] of the constraint anchor (geometric centroid of scoping by default). |
constrained_dofs | List of locked DOF names, e.g. ["Tx","Ty","Tz","Rx","Ry","Rz"]. |
connected_point_indices | Indices into the cloud of all nodes belonging to the scoping region. |
The .gmmfm File Format
A .gmmfm (GearMaster Meshfree Model) file is a ZIP archive
that bundles the complete analysis model — geometry, point cloud, and joints —
into a single portable file.
| Entry | Presence | Content |
|---|---|---|
manifest.json | Always | Format version (1.1), model name, has_point_cloud and has_joints flags. |
geometry.gmcad | Always | Embedded CAD model in the standard .gmcad format. |
pointcloud.gmpc | When cloud exists | Embedded point cloud in the standard .gmpcloud format. |
joints.json | When joints exist | JSON array of serialised GM_Joint objects, including connected_point_indices. |
connected_point_indices are serialised inside joints.json,
so a solver can load a .gmmfm file and use the joints immediately
— without reopening the editor or recomputing geometry.
API — GM_PointCloud
from GM_modules.Meshfree.src.point_cloud import GM_PointCloud
GM_PointCloud is a Python dataclass. All constructor arguments are
validated in __post_init__.
Constructor
GM_PointCloud(
points : np.ndarray, # (N, 3) float64
normals : np.ndarray, # (N, 3) float64
is_boundary : np.ndarray, # (N,) bool
entity_ids : np.ndarray, # (N,) str
dimension : int = 2, # 2 or 3
name : str = "point_cloud",
display_settings : PointDisplaySettings = PointDisplaySettings(),
)
Read-only properties
cloud.n_points # int – total node count cloud.n_boundary # int – boundary node count cloud.n_interior # int – interior node count cloud.unique_entity_ids # list[str] – sorted unique entity identifiers
API — PointDisplaySettings
from GM_modules.Meshfree.src.point_display_settings import PointDisplaySettings
Lightweight dataclass that controls how a GM_PointCloud is rendered
inside the PointCloudEditor. Persisted in manifest.json
of every .gmpcloud file (format version "1.1").
Fields
| Field | Type | Default | Description |
|---|---|---|---|
representation | str | "flat" |
"flat" — circular flat points rendered on the GPU (fast, zero performance cost)."gaussian" — gaussian splat shader (smooth, halo-like blobs).
|
normal_mode | str | "none" |
"none" — uniform solid colour per entity."color" — Z-component of the surface normal mapped to a diverging coolwarm colormap."disc" — each point replaced by a disc glyph oriented along its normal
(performance-sensitive: a warning is shown above 5 000 points).
|
point_size | float | 6.0 |
Base point size in screen pixels. Boundary nodes are rendered at point_size × 1.4. |
gaussian_opacity | float | 1.0 |
Opacity for gaussian splats, in the range [0, 1]. Has no effect in flat mode. |
gaussian_emissive | bool | False |
When True, gaussian splats are rendered with a glowing/emissive look. Has no effect in flat mode. |
Serialisation
d = settings.to_dict() # dict – JSON-serialisable settings = PointDisplaySettings.from_dict(d) # restore from dict (tolerant of missing keys)
from_dict fills any missing keys with the field defaults, so older
.gmpcloud files (format "1.0") that contain no
display_settings entry are loaded without errors.
API — SurfaceSampler2D
from GM_modules.Meshfree.src.samplers.surface_sampler_2d import SurfaceSampler2D
sampler = SurfaceSampler2D(
h : float, # arc-length spacing along edges
n_max : int | None = None, # hard node limit per entity
)
cloud = sampler.sample(entity) # entity: CadEntity leaf with planar face(s)
| Parameter | Default | Description |
|---|---|---|
h | required | Spacing between consecutive boundary nodes (model units). |
n_max | None |
If set, randomly subsample to at most n_max nodes. |
API — InteriorSampler2D
from GM_modules.Meshfree.src.samplers.interior_sampler_2d import InteriorSampler2D
sampler = InteriorSampler2D(
h : float, # minimum inter-node distance
n_max : int | None = None, # hard node limit per entity
k : int = 30, # Bridson candidate count per active sample
seed : int | None = None, # random seed (None = non-deterministic)
)
cloud = sampler.sample(entity)
| Parameter | Default | Description |
|---|---|---|
h | required | Minimum distance between any two interior nodes. |
n_max | None |
Hard upper limit; subsample randomly when exceeded. |
k | 30 | Bridson algorithm candidate trials per active sample. Higher values → denser packing, slower runtime. |
seed | None |
Integer seed for np.random.default_rng.
Pass the same value to reproduce the exact same cloud. |
API — PointCloudGenerator
from GM_modules.Meshfree.src.generator import PointCloudGenerator
gen = PointCloudGenerator(
h : float,
n_max_per_entity : int | None = None,
k : int = 30,
seed : int | None = None,
n_jobs : int = 1,
verbose : bool = False,
)
cloud = gen.generate(model) # model: GM_CadModel (loaded)
cloud = gen.generate_entity(entity) # single entity convenience
| Parameter | Default | Description |
|---|---|---|
h | required | Characteristic spacing, forwarded to all samplers. |
n_max_per_entity | None |
Applied independently to surface and interior clouds. |
k | 30 | Bridson k parameter (interior sampler). |
seed | None |
Random seed for interior sampling. |
n_jobs | 1 | Parallel workers via joblib.
-1 = all CPU cores; 1 = sequential. |
verbose | False |
Print progress summary after generation. |
UserWarning.
API — I/O Functions
from GM_modules.Meshfree.src.io.gmpcloud_io import save_gmpcloud, load_gmpcloud
# Save
path = save_gmpcloud(cloud, "output.gmpcloud", extra_metadata={"h": 2.0})
# Load
cloud = load_gmpcloud("output.gmpcloud")
# Convenience wrappers on GM_PointCloud:
cloud.save("output.gmpcloud")
cloud2 = GM_PointCloud.load("output.gmpcloud")
The .gmpcloud extension is appended automatically when absent.
extra_metadata is an optional dictionary stored in
manifest.json under the key "extra"; it is not
read back by load_gmpcloud.
"1.1".
The manifest now includes a "display_settings" object that stores all
PointDisplaySettings fields.
Older files at format "1.0" (without "display_settings")
are read transparently — missing fields fall back to the dataclass defaults.
API — PointCloudEditor
from GM_modules.Meshfree.src.point_cloud_editor import PointCloudEditor
editor = PointCloudEditor(
model : GM_CadModel,
cloud : GM_PointCloud | None = None,
linear_deflection : float = 0.1,
angular_deflection : float = 0.5,
background_color : str = "white",
mode : str = "view", # "view" or "edit"
)
editor.show(
title : str = "",
window_size : tuple[int, int] = (1280, 800),
blocking : bool = True,
)
PointCloudEditor extends CadEditor with a
Point Cloud panel in the left sidebar. CAD geometry and point cloud
share the same PyVista 3-D viewport.
Left panel — Point Cloud tree
Each loaded entity appears as a collapsible tree node with Interior and Boundary child items. Checkboxes toggle actor visibility in the viewport directly.
Meshfree menu
| Menu item | Description |
|---|---|
| Generate Point Cloud… | Placeholder for future global generation over the entire model. |
| Point Display… | Opens the Point Display dialog (enabled only when a cloud is loaded). See below for details. |
Per-entity generation — CAD tree right-click
Right-click any leaf entity in the CAD feature tree and choose Generate Point Cloud.
- A dialog asks for the node spacing
h(model units). - If a cloud already exists for that entity, a confirmation dialog offers OK (overwrite) or Cancel.
- The new entity sub-cloud is merged into the global
GM_PointCloudand the viewport updates immediately. - Colour assignment cycles through a fixed palette and remains stable across overwrite operations.
Point Display dialog
Accessed via Meshfree → Point Display….
| Group | Controls | Effect |
|---|---|---|
| Point Representation | Radio buttons: Flat / Gaussian | Switches between GPU circular points and gaussian splat shader. |
| Gaussian Splats Settings | Opacity slider (0–100 %) + Emissive checkbox (shown only when Gaussian is selected) |
Controls transparency and glowing appearance of gaussian splats. |
| Normal Display | Radio buttons: None / Color / Disc |
None: uniform colour. Color: maps normal Z to a diverging colormap. Disc: glyph discs oriented along normals. A performance warning is shown when the cloud exceeds 5 000 points. |
The dialog has three buttons:
- Apply — applies settings immediately; dialog stays open for further adjustments.
- OK — applies settings and closes the dialog.
- Cancel — reverts to the settings that were active when the dialog was opened, then closes.
cloud.display_settings and saved
automatically when the cloud is written to a .gmpcloud file.
They are restored when the file is reloaded.
Left panel — Boundary Conditions tree
Below the Point Cloud panel a second tree widget lists all joints under a collapsible “Joints” root node. Each joint entry shows:
- Scoping — summary of selected sub-entities (e.g. 2 faces).
- Type — Fixed or Custom.
- Behaviour — Rigid.
- Ref. point — coordinates of the reference point.
- Ref. point ID / Constrained DOFs / Connected points — read-only computed values (rendered in grey).
Joint context menu (RMB in BC tree)
| Target | Action | Effect |
|---|---|---|
| “Joints” root item | Remove All Joints | Deletes all joints from the model and removes their viewport actors. |
| Specific joint item | Edit Joint | Reopens the configuration dialog pre-filled with the joint’s current values. Live preview is active during editing. |
| Specific joint item | Remove Joint | Deletes the joint from the model and removes its viewport actors. |
Apply Joint dialog
Opened via RMB on the viewport → Apply Joint → External… (requires a selection).
| Field | Description |
|---|---|
| Name | Joint label; auto-incremented default provided. |
| Joint Type | Fixed or Custom. Selecting Custom activates DOF toggle buttons. |
| Behaviour | Currently only Rigid. |
| Reference Point | X / Y / Z [mm]; pre-filled with the geometric centroid of the scoping. Fully editable; live preview updates on every valid keystroke. |
| Constrained DOFs |
Fixed: all 6 pills shown in dark red (read-only). Custom: each button toggles — dark red = locked, grey = free. |
API — GM_MfreeModel
from GM_modules.Meshfree.src.mfree_model import GM_MfreeModel
Constructor
GM_MfreeModel(
cad_model : GM_CadModel,
point_cloud : GM_PointCloud | None = None,
name : str = "",
)
Joint management
model.add_joint(joint) # add a GM_Joint (raises ValueError on duplicate id) model.remove_joint(joint_id) # remove by id (raises KeyError if not found) joint = model.get_joint(joint_id) # retrieve by id (raises KeyError if not found) model.joints # list[GM_Joint] – copy of the internal list model.has_joints # bool
Persistence
path = model.save("analysis.gmmfm") # returns resolved Path
model = GM_MfreeModel.load("analysis.gmmfm")
API — GM_Joint
from GM_modules.Meshfree.src.joints import GM_Joint, ScopingItem
Constructor
GM_Joint(
name : str,
scoping : list[ScopingItem],
joint_type : str = "Fixed", # "Fixed" | "Custom"
behaviour : str = "Rigid",
joint_id : str | None = None, # auto-generated 8-char hex if None
)
Key attributes
| Attribute | Type | Description |
|---|---|---|
id | str | Unique 8-char uppercase hex identifier. |
reference_point | np.ndarray (3,) | None | Constraint anchor [mm]. Populated by the editor after geometry computation. |
constrained_dofs | list[str] | Locked DOFs. Default all 6: ["Tx","Ty","Tz","Rx","Ry","Rz"]. |
connected_point_indices | np.ndarray (K,) int | None | Indices of cloud nodes in the scoping region. Set by the editor; serialised to .gmmfm. |
Serialisation
d = joint.to_dict() # JSON-serialisable dict joint = GM_Joint.from_dict(d) # reconstruct (tolerant of missing keys)
ScopingItem
ScopingItem(
entity_id : str, # CAD entity identifier
sub_entity_type : str, # "face" | "edge" | "vertex"
sub_entity_index : int,
)
Module File Structure
GM_modules/Meshfree/
├── __init__.py
├── src/
│ ├── __init__.py # exports GM_PointCloud, PointCloudGenerator, GM_MfreeModel
│ ├── mfree_model.py # GM_MfreeModel container
│ ├── point_cloud.py # GM_PointCloud dataclass
│ ├── point_display_settings.py # PointDisplaySettings dataclass
│ ├── generator.py # PointCloudGenerator orchestrator
│ ├── joints.py # GM_Joint, ScopingItem, JOINT_TYPES, BEHAVIOURS
│ ├── joint_dialog.py # Qt dialog for boundary condition joints
│ ├── point_cloud_editor.py # PointCloudEditor + _PointCloudEditorWindow
│ ├── samplers/
│ │ ├── __init__.py
│ │ ├── base_sampler.py # BaseSampler ABC + OCC helpers
│ │ ├── surface_sampler_2d.py # boundary nodes on planar edges
│ │ ├── interior_sampler_2d.py # interior nodes (Bridson + rejection)
│ │ ├── surface_sampler_3d.py # PLACEHOLDER
│ │ └── interior_sampler_3d.py # PLACEHOLDER
│ └── io/
│ ├── __init__.py
│ ├── gmpcloud_io.py # save / load .gmpcloud
│ └── gmmfm_io.py # save / load .gmmfm
├── tests/
│ ├── test_point_cloud_io.py # 37 tests
│ ├── test_samplers_2d.py # 49 tests
│ └── test_generator.py # 30 tests
├── examples/
│ └── example_generate_point_cloud.py
└── docs/
└── manual.html ← this file
Roadmap
| Feature | Status |
|---|---|
| GM_PointCloud dataclass + I/O (.gmpcloud) | Done |
| SurfaceSampler2D (boundary nodes, planar faces) | Done |
| InteriorSampler2D (Bridson + rejection, planar faces) | Done |
| PointCloudGenerator (orchestrator + joblib parallelism) | Done |
| PointCloudEditor interactive GUI (per-entity generation, display settings) | Done |
| GM_MfreeModel container + .gmmfm I/O | Done |
| Boundary conditions — Joints (Fixed / Custom DOF) in PointCloudEditor | Done |
| BC panel: RMB edit/remove, Custom DOF toggle, live 3-D preview | Done |
| SurfaceSampler3D (nodes on 3-D solid surfaces) | Planned |
| InteriorSampler3D (3-D Poisson disk) | Planned |
| MLPG solver (local weak form assembly) | Planned |