| name | DM SDF Primitive Implementation Pattern |
| description | Template and rules for creating new SDF primitive field classes in core/sdf/sdf/. Required reading before implementing any new SdfField subclass. |
DM SDF Primitive Implementation Pattern
Every SDF primitive lives in its own file in core/sdf/sdf/ and follows this exact structure.
File Template
import numpy as np
import FreeCAD
import math
from core.sdf.sdf_field import SdfField
class Sdf{Name}Field(SdfField):
"""One-line description. Reference: sdName() in iquilezles.org/articles/distfunctions/"""
def __init__(self, center: FreeCAD.Vector, param1: float, ...):
self.center = center
self.param1 = param1
...
def evaluate(self, point: FreeCAD.Vector) -> float:
"""Scalar SDF at a single point."""
p = point - self.center
return result
def evaluate_grid(self, points: np.ndarray) -> np.ndarray:
"""Vectorized SDF over (N,3) float32 array. Returns (N,) float32."""
c = np.array([self.center.x, self.center.y, self.center.z])
p = points - c
return result.astype(np.float32)
def bounding_box(self):
"""Returns (min_corner, max_corner) as FreeCAD.Vector pair."""
...
return (self.center - v, self.center + v)
Rules
- Always subtract
self.center from the input point before applying the SDF formula — all IQ formulas assume the shape is at the origin.
evaluate_grid must return np.float32 — always .astype(np.float32) at the end.
bounding_box must be conservative — it can be larger than the shape, never smaller. When in doubt, use a sphere bound: center ± max_radius.
- No gradient override needed —
SdfField.gradient() provides central-difference fallback automatically. Only override if you have an analytical formula (see sphere.py).
- Formulas use Y as the "up" axis — consistent with FreeCAD's coordinate system.
Numpy Vectorization Patterns
Clamp
np.clip(x, 0.0, 1.0)
sign
np.sign(x)
dot(vec, vec) per-row
np.sum(a * b, axis=1)
length per-row
np.linalg.norm(v, axis=1)
cross-product per-row
np.cross(a, b)
Conditional (GLSL ternary)
np.where(condition, a, b)
min/max
np.minimum(a, b)
np.maximum(a, b)
np.min(a, axis=1)
np.max(a, axis=1)
Command Pattern (simple creation at origin)
Each primitive gets a thin command in a commands/cmd_{group}.py file:
import FreeCAD
import FreeCADGui
class CommandDM{Name}:
def GetResources(self):
return {
'Pixmap': 'Create{Name}',
'MenuText': 'Create {Display Name}',
'ToolTip': 'Create an SDF {Display Name} primitive.'
}
def IsActive(self):
return FreeCAD.activeDocument() is not None
def Activated(self):
from core.dm_object import create_dm_object
from core.sdf.sdf.{module} import Sdf{Name}Field
field = Sdf{Name}Field(
center=FreeCAD.Vector(0, 0, 0),
{default_params}
)
obj = create_dm_object(name='{Name}', shape_type='sdf')
obj.Proxy.SdfField = field
obj.touch()
FreeCAD.activeDocument().recompute()
FreeCADGui.addCommand('DM_Create{Name}', CommandDM{Name}())
Serialization
When a new SdfField type is paired with a new PrimitiveCreatorBase subclass:
- Override
get_sdf_type() in the creator to return a unique lowercase string (e.g. "capsule").
- Add a matching
elif sdf_type == "capsule": branch in DMObjectProxy._reconstruct_field()
in core/dm_object.py. Reconstruct the field from fp.Points (a FreeCAD.Vector list)
and fp.Placement. The Points list must contain exactly the control points stored
by _get_final_points() in the creator.
The commit methods (__do_commit and _do_commit_and_edit in PrimitiveCreatorBase) store
SdfType and Points automatically — no extra work needed in the creator.
Files to Read Before Editing
core/sdf/sdf/sphere.py — simplest complete example
core/sdf/sdf/box.py — example with local-space transform
core/sdf/sdf/cylinder.py — example with axis-aligned projection
core/sdf/sdf/sdf_field.py — base class (just pass)
core/sdf/sdf_field.py — abstract base with evaluate_grid fallback
core/dm_object.py:_reconstruct_field — add your branch here