4 Commits

Author SHA1 Message Date
forbes
ef16ecbaa2 fix: viewport background gradient — Overlay0/Mantle 3-color gradient (#188)
Change viewport background from 2-color Base→Crust gradient to
3-color Overlay0→Mantle→Overlay0 gradient for better visual depth.
2026-02-14 13:03:37 -06:00
forbes
2bf969c62a fix: use TangentPlane MapMode for tangent-to-cylinder datums (#58)
plane_tangent_to_cylinder() now derives a vertex from the cylinder
face's edges and uses TangentPlane MapMode with AttachmentOffset to
encode the angular position. This makes tangent datums parametric —
they auto-update when cylinder geometry changes.

Add _find_cylinder_vertex() and _vertex_angle_on_cylinder() helpers.
Store vertex_angle in ZTools_Params for the edit panel to compute
AttachmentOffset updates. Falls back to manual placement when no
vertex can be resolved (non-Body datums).
2026-02-08 18:53:13 -06:00
forbes
04f9df75cb fix: update AttachmentOffset during angled datum editing (#66)
on_param_changed() now recomputes AttachmentOffset.Rotation for angled
datums and recalculates Placement for tangent_cylinder datums when the
angle spinner changes. Previously only ZTools_Params was updated,
leaving the visual representation unchanged until a manual recompute.

Add _resolve_source_refs() helper to parse ZTools_SourceRefs and
resolve stored object/subname pairs to actual shapes for the rotation
and placement math.
2026-02-08 18:42:28 -06:00
forbes
2132c4e64c fix: use append instead of fragile insert chain in PartDesign menu (#57)
Replace the chained insert operations in modifyMenuBar() with independent
append operations. The old approach anchored on PartDesign_Boolean and
chained each subsequent command off the previous insertion — a single
missing anchor caused a cascade failure.

The new approach uses append with PartDesign_Body as the parent locator.
Each operation is independent, so a failure in one does not affect the
others.
2026-02-08 18:07:18 -06:00
4 changed files with 147 additions and 36 deletions

View File

@@ -27,13 +27,13 @@
<FCUInt Name="colorError" Value="4086016255"/>
</FCParamGroup>
<FCParamGroup Name="View">
<FCUInt Name="BackgroundColor" Value="505294591"/>
<FCUInt Name="BackgroundColor2" Value="286333951"/>
<FCUInt Name="BackgroundColor3" Value="404235775"/>
<FCUInt Name="BackgroundColor4" Value="825378047"/>
<FCUInt Name="BackgroundColor" Value="404235775"/>
<FCUInt Name="BackgroundColor2" Value="1819313919"/>
<FCUInt Name="BackgroundColor3" Value="1819313919"/>
<FCUInt Name="BackgroundColor4" Value="404235775"/>
<FCBool Name="Simple" Value="0"/>
<FCBool Name="Gradient" Value="1"/>
<FCBool Name="UseBackgroundColorMid" Value="0"/>
<FCBool Name="UseBackgroundColorMid" Value="1"/>
<FCUInt Name="HighlightColor" Value="3416717311"/>
<FCUInt Name="SelectionColor" Value="3032415999"/>
<FCUInt Name="PreselectColor" Value="2497893887"/>

View File

@@ -365,26 +365,10 @@ class _ZToolsPartDesignManipulator:
def modifyMenuBar(self):
return [
{
"insert": "ZTools_DatumCreator",
"menuItem": "PartDesign_Boolean",
"after": "",
},
{
"insert": "ZTools_DatumManager",
"menuItem": "ZTools_DatumCreator",
"after": "",
},
{
"insert": "ZTools_EnhancedPocket",
"menuItem": "ZTools_DatumManager",
"after": "",
},
{
"insert": "ZTools_RotatedLinearPattern",
"menuItem": "ZTools_EnhancedPocket",
"after": "",
},
{"append": "ZTools_DatumCreator", "menuItem": "PartDesign_Body"},
{"append": "ZTools_DatumManager", "menuItem": "PartDesign_Body"},
{"append": "ZTools_EnhancedPocket", "menuItem": "PartDesign_Body"},
{"append": "ZTools_RotatedLinearPattern", "menuItem": "PartDesign_Body"},
]

View File

@@ -2,6 +2,7 @@
# Custom ViewProvider for ZTools datum objects
import json
import math
import FreeCAD as App
import FreeCADGui as Gui
@@ -141,6 +142,23 @@ class ZToolsDatumViewProvider:
return None
def _resolve_source_refs(datum_obj):
"""Parse ZTools_SourceRefs and resolve to (object, subname, shape) tuples."""
refs_json = getattr(datum_obj, "ZTools_SourceRefs", "[]")
try:
refs = json.loads(refs_json)
except json.JSONDecodeError:
return []
doc = datum_obj.Document
resolved = []
for ref in refs:
obj = doc.getObject(ref.get("object", ""))
sub = ref.get("subname", "")
shape = obj.getSubObject(sub) if obj and sub else None
resolved.append((obj, sub, shape))
return resolved
class DatumEditTaskPanel:
"""
Task panel for editing existing ZTools datum objects.
@@ -364,8 +382,57 @@ class DatumEditTaskPanel:
self.datum_obj.AttachmentOffset = new_offset
self._update_params({"distance": distance})
elif ztools_type in ("angled", "tangent_cylinder"):
self._update_params({"angle": self.angle_spin.value()})
elif ztools_type == "angled":
angle = self.angle_spin.value()
if self._has_attachment():
refs = _resolve_source_refs(self.datum_obj)
if len(refs) >= 2 and refs[0][2] and refs[1][2]:
face_normal = refs[0][2].normalAt(0, 0)
edge_shape = refs[1][2]
edge_dir = (
edge_shape.Vertexes[-1].Point - edge_shape.Vertexes[0].Point
).normalize()
face_rot = App.Rotation(App.Vector(0, 0, 1), face_normal)
local_edge_dir = face_rot.inverted().multVec(edge_dir)
angle_rot = App.Rotation(local_edge_dir, angle)
self.datum_obj.AttachmentOffset = App.Placement(
App.Vector(0, 0, 0), angle_rot
)
self._update_params({"angle": angle})
elif ztools_type == "tangent_cylinder":
angle = self.angle_spin.value()
if self._has_attachment():
params_json = getattr(self.datum_obj, "ZTools_Params", "{}")
try:
params = json.loads(params_json)
except json.JSONDecodeError:
params = {}
vertex_angle = params.get("vertex_angle", 0.0)
offset_rot = App.Rotation(App.Vector(0, 0, 1), angle - vertex_angle)
self.datum_obj.AttachmentOffset = App.Placement(
App.Vector(0, 0, 0), offset_rot
)
else:
refs = _resolve_source_refs(self.datum_obj)
if refs and refs[0][2]:
face = refs[0][2]
if isinstance(face.Surface, Part.Cylinder):
cyl = face.Surface
axis = cyl.Axis
center = cyl.Center
radius = cyl.Radius
rad = math.radians(angle)
if abs(axis.dot(App.Vector(1, 0, 0))) < 0.99:
local_x = axis.cross(App.Vector(1, 0, 0)).normalize()
else:
local_x = axis.cross(App.Vector(0, 1, 0)).normalize()
local_y = axis.cross(local_x)
radial = local_x * math.cos(rad) + local_y * math.sin(rad)
tangent_point = center + radial * radius
rot = App.Rotation(App.Vector(0, 0, 1), radial)
self.datum_obj.Placement = App.Placement(tangent_point, rot)
self._update_params({"angle": angle})
elif ztools_type in ("normal_to_edge", "on_edge"):
parameter = self.param_spin.value()

View File

@@ -903,6 +903,41 @@ def plane_angled(
return plane
def _find_cylinder_vertex(obj, face_subname):
"""Find a vertex subname from a cylindrical face's edges."""
face = obj.getSubObject(face_subname)
if not face or not face.Edges:
return None
edge = face.Edges[0]
if not edge.Vertexes:
return None
vertex_point = edge.Vertexes[0].Point
for i, v in enumerate(obj.Shape.Vertexes, 1):
if v.Point.isEqual(vertex_point, 1e-6):
return f"Vertex{i}"
return None
def _vertex_angle_on_cylinder(obj, vertex_sub, cylinder):
"""Compute the angular position of a vertex on a cylinder surface."""
vertex = obj.getSubObject(vertex_sub)
if not vertex:
return 0.0
point = vertex.Point
relative = point - cylinder.Center
axis = cylinder.Axis
radial = relative - axis * relative.dot(axis)
if radial.Length < 1e-10:
return 0.0
radial.normalize()
if abs(axis.dot(App.Vector(1, 0, 0))) < 0.99:
local_x = axis.cross(App.Vector(1, 0, 0)).normalize()
else:
local_x = axis.cross(App.Vector(0, 1, 0)).normalize()
local_y = axis.cross(local_x)
return math.degrees(math.atan2(radial.dot(local_y), radial.dot(local_x)))
def plane_tangent_to_cylinder(
face: Part.Face,
angle: float = 0,
@@ -966,16 +1001,41 @@ def plane_tangent_to_cylinder(
if body:
plane = body.newObject("PartDesign::Plane", name)
# TangentPlane mode needs (face, vertex). Without a vertex reference,
# fall back to manual placement.
_setup_ztools_datum(
plane,
placement,
"tangent_cylinder",
{"angle": angle, "radius": radius},
source_refs,
is_plane=True,
# TangentPlane MapMode needs (face, vertex). Derive a vertex from
# the cylinder face's edges and encode the angular offset.
vertex_sub = (
_find_cylinder_vertex(source_object, source_subname)
if source_object and source_subname
else None
)
if vertex_sub:
vertex_angle = _vertex_angle_on_cylinder(source_object, vertex_sub, cyl)
offset_angle = angle - vertex_angle
offset_rot = App.Rotation(App.Vector(0, 0, 1), offset_angle)
att_offset = App.Placement(App.Vector(0, 0, 0), offset_rot)
_setup_ztools_datum(
plane,
placement,
"tangent_cylinder",
{"angle": angle, "radius": radius, "vertex_angle": vertex_angle},
source_refs,
is_plane=True,
map_mode="TangentPlane",
support=[
(source_object, source_subname),
(source_object, vertex_sub),
],
offset=att_offset,
)
else:
_setup_ztools_datum(
plane,
placement,
"tangent_cylinder",
{"angle": angle, "radius": radius},
source_refs,
is_plane=True,
)
else:
plane = doc.addObject("Part::Plane", name)
plane.Length = 50