Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e333991
docs: add SolidWorks API reference for SW2GZ implementation
Aryan01b Jul 1, 2026
185bd84
feat(robot): minimal Create Robot wizard + exporter (v3 rebuild)
Aryan01b Jul 1, 2026
af33ca2
docs(progress): robot v3 wizard live + column-major transform fix
Aryan01b Jul 1, 2026
5913648
docs(progress): revert failed link-hierarchy wizard attempt, DLL rede…
Aryan01b Jul 2, 2026
43bde51
docs(spec): robot joint/link relative pose + multi-mesh links design
Aryan01b Jul 2, 2026
1595678
feat(robot): Matrix3-parameterized InertialAggregator.Combine overloads
Aryan01b Jul 2, 2026
bbfc3eb
fix(robot): joint origin relative to declared parent, root by tree st…
Aryan01b Jul 2, 2026
9a83c1d
fix(robot): warn on unresolvable joint parent instead of silent zero …
Aryan01b Jul 2, 2026
9f3f739
feat(robot): union every assigned mesh component per link, not just t…
Aryan01b Jul 2, 2026
8b61a89
fix(robot): resolve Color namespace ambiguity from System.Drawing import
Aryan01b Jul 2, 2026
fc9bf81
feat(robot): combine mass/inertia of every assigned mesh component pe…
Aryan01b Jul 2, 2026
73a2551
test(robot): distinguish per-component pose lookup from shared-frame …
Aryan01b Jul 2, 2026
9f3dc18
feat(robot): surface which mesh component is primary in the Links UI
Aryan01b Jul 2, 2026
a67c4c8
docs(robot): update Sw2gzRobotExporter header to describe multi-mesh/…
Aryan01b Jul 2, 2026
05a6672
docs(progress): robot joint/link relative pose + multi-mesh links, li…
Aryan01b Jul 2, 2026
7e55b0f
fix(robot): highlight selected link's mesh when clicked in the hierarchy
Aryan01b Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ is just an at-a-glance cache.
See memory `sw2gz-build-deploy` (MSBuild path, `SolutionDir` param, regasm +
SolidWorks-lock gotchas).

## SolidWorks API reference

[`docs/reference/solidworks-api.md`](docs/reference/solidworks-api.md) —
every SolidWorks COM API member this codebase calls, grouped by category
(app/doc lifecycle, ribbon, PropertyManagerPage UI, assembly/component,
mates, geometry/tessellation, mass properties, coordinate systems,
selection, events, color, persistence), each with file provenance,
usage context, and an **[active]**/**[legacy]** flag (legacy = inherited-
upstream export pipeline, may not run on the current gutted robot-mode
path). Read this before adding any new SW COM call — check whether the
member is already used elsewhere first. Local offline API browser:
`C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\api\apihelp.chm`.

## Conventions

- **PowerShell + git.** git writes normal status (e.g. `Switched to branch`)
Expand Down
83 changes: 59 additions & 24 deletions SW2GZ/Build/InertialAggregator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static class InertialAggregator
// Steps:
// 1) total mass
// 2) mass-weighted COM, with each part's local COM rotated into the assembly frame
// via R_f = Matrix3.FromQuaternion(frame.Rotation) before translation
// via R_f before translation
// 3) for each part, transform its inertia tensor from part frame to assembly frame
// as I_a = R_f · I_part · R_fᵀ, then translate to the combined COM via the
// parallel-axis theorem, and sum.
Expand All @@ -23,33 +23,55 @@ public static class InertialAggregator
// link-local frame — use the (parts, linkAnchor) overload below for that.
public static MassProps Combine(IReadOnlyList<(MassProps Props, Pose Frame)> parts)
{
if (parts == null) return new MassProps(0, Vector3.Zero, Matrix3.Identity);
if (parts.Count == 0)
if (parts == null)
return new MassProps(0, Vector3.Zero, Matrix3.Identity);
var matrixParts = parts
.Select(p => (p.Props, Matrix3.FromQuaternion(p.Frame.Rotation), p.Frame.Position))
.ToList();
return CombineCore(matrixParts);
}

// Matrix3-parameterized twin of the overload above. Same algorithm,
// same result for an equivalent rotation — exists so callers that
// already work entirely in Matrix3/Vector3 (e.g. Sw2gzRobotExporter,
// which reads SolidWorks component poses as Matrix3 directly) never
// need to construct a Quaternion just to call in here. Deliberately
// NOT implemented by converting to Quaternion and delegating to the
// overload above — a Matrix3-to-Quaternion conversion is new
// coordinate-conversion code, exactly the category that has already
// produced two real bugs in this codebase (the Transform2.ArrayData
// column-major bug, the mate-classification bug). Both overloads
// instead share CombineCore; only the Quaternion overload ever
// converts (Quaternion -> Matrix3, an already-proven, already-used
// direction), never the reverse.
public static MassProps Combine(IReadOnlyList<(MassProps Props, Matrix3 Rotation, Vector3 Position)> parts)
{
return CombineCore(parts);
}

private static MassProps CombineCore(IReadOnlyList<(MassProps Props, Matrix3 R, Vector3 Position)> parts)
{
if (parts == null || parts.Count == 0)
return new MassProps(0, Vector3.Zero, Matrix3.Identity);

double totalMass = parts.Sum(p => p.Props.Mass);
if (totalMass <= 0)
return new MassProps(0, Vector3.Zero, Matrix3.Identity);

// Cache R_f per part: FromQuaternion + the rotated COM offset are
// needed in both the COM pass and the parallel-axis pass.
var rotations = new Matrix3[parts.Count];
// Per-part rotated COM offset in assembly frame (double precision),
// i.e. (f.Position + R_f * p.ComLocal). Reused for the parallel-axis d.
// i.e. (pos + R * p.ComLocal). Reused for the parallel-axis d.
var partComsX = new double[parts.Count];
var partComsY = new double[parts.Count];
var partComsZ = new double[parts.Count];

double comX = 0.0, comY = 0.0, comZ = 0.0;
for (int i = 0; i < parts.Count; i++)
{
var (p, f) = parts[i];
var R_f = Matrix3.FromQuaternion(f.Rotation);
rotations[i] = R_f;
var (p, R_f, pos) = parts[i];
var (rx, ry, rz) = R_f.Mul((double)p.ComLocal.X, p.ComLocal.Y, p.ComLocal.Z);
double pcx = f.Position.X + rx;
double pcy = f.Position.Y + ry;
double pcz = f.Position.Z + rz;
double pcx = pos.X + rx;
double pcy = pos.Y + ry;
double pcz = pos.Z + rz;
partComsX[i] = pcx; partComsY[i] = pcy; partComsZ[i] = pcz;
double w = p.Mass / totalMass;
comX += w * pcx; comY += w * pcy; comZ += w * pcz;
Expand All @@ -60,11 +82,9 @@ public static MassProps Combine(IReadOnlyList<(MassProps Props, Pose Frame)> par
var I = new double[3, 3];
for (int i = 0; i < parts.Count; i++)
{
var (p, _) = parts[i];
var R_f = rotations[i];
var (p, R_f, _) = parts[i];
var I_rot = R_f * p.InertiaAtComLocal * R_f.Transpose();

// Offset from assembly COM to this part's COM, in double precision.
double dx = partComsX[i] - comX;
double dy = partComsY[i] - comY;
double dz = partComsZ[i] - comZ;
Expand Down Expand Up @@ -108,20 +128,35 @@ public static MassProps Combine(
if (linkAnchor == null || linkAnchor == Pose.Identity) return assemblyFrame;
if (assemblyFrame.Mass <= 0) return assemblyFrame;

// R = link anchor rotation (assembly → link is Rᵀ; world rotates by R
// to land in link orientation only if we are going link → world).
// We need a vector expressed in the LINK frame, so we apply Rᵀ.
Matrix3 R = Matrix3.FromQuaternion(linkAnchor.Rotation);
return RebaseCore(assemblyFrame, R, linkAnchor.Position);
}

// Matrix3-parameterized twin of the rebase overload above — see the
// Combine(parts) overload for why this exists instead of routing
// through Quaternion.
public static MassProps Combine(
IReadOnlyList<(MassProps Props, Matrix3 Rotation, Vector3 Position)> parts,
Matrix3 anchorR, Vector3 anchorT)
{
MassProps assemblyFrame = Combine(parts);
if (assemblyFrame.Mass <= 0) return assemblyFrame;
return RebaseCore(assemblyFrame, anchorR, anchorT);
}

private static MassProps RebaseCore(MassProps assemblyFrame, Matrix3 R, Vector3 anchorPosition)
{
// R = link anchor rotation. We need a vector expressed in the
// LINK frame, so we apply Rᵀ (R^-1 for an orthonormal rotation).
Matrix3 Rinv = R.Transpose();

// COM offset in assembly coords, then rotate into link frame.
double dx = assemblyFrame.ComLocal.X - linkAnchor.Position.X;
double dy = assemblyFrame.ComLocal.Y - linkAnchor.Position.Y;
double dz = assemblyFrame.ComLocal.Z - linkAnchor.Position.Z;
double dx = assemblyFrame.ComLocal.X - anchorPosition.X;
double dy = assemblyFrame.ComLocal.Y - anchorPosition.Y;
double dz = assemblyFrame.ComLocal.Z - anchorPosition.Z;
var (lx, ly, lz) = Rinv.Mul(dx, dy, dz);
var comLink = new Vector3((float)lx, (float)ly, (float)lz);

// I_link = R^-1 · I_assembly · R (same tensor at the same point,
// I_link = R^-1 · I_assembly · R (same tensor at the same point,
// re-expressed in the rotated basis).
Matrix3 Ilink = Rinv * assemblyFrame.InertiaAtComLocal * R;

Expand Down
21 changes: 15 additions & 6 deletions SW2GZ/SW/SwAddin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ private bool TryGetActiveAssembly(out ModelDoc2 modeldoc)
// Sw2gzCreateWorldPmp / Sw2gzCreateAssetPmp). Held as fields — same
// COM-handler-rooting reason as _openPanel (the SW PMP handler
// interface is freed on AfterClose).
// Robot mode v2 — robot Create wizard removed; OpenCreateRobot is a stub.
private SW2GZ.UI.Pmp.Sw2gzCreateRobotPmp _createRobotPmp;
private SW2GZ.UI.Pmp.Sw2gzCreateWorldPmp _createWorldPmp;
private SW2GZ.UI.Pmp.Sw2gzCreateAssetPmp _createAssetPmp;
// True while a Create wizard PMP is open. The mode pills read this and
Expand Down Expand Up @@ -523,11 +523,20 @@ public void OpenCreatePmp()

private void OpenCreateRobot(ModelDoc2 modelDoc, SW2GZ.URDFExport.Sw2gzDoc doc)
{
// Robot mode v2 — the inherited robot implementation was removed for a
// clean rebuild. The mode pill + Create/Edit Robot button stay; this
// is a deliberate no-op stub. World and Asset modes are unaffected.
MessageBox.Show("Robot mode is not implemented yet — coming in a future build.",
"SW2GZ");
try
{
_wizardOpen = true;
_createRobotPmp = new SW2GZ.UI.Pmp.Sw2gzCreateRobotPmp(
(SldWorks)SwApp, modelDoc, doc,
d => PersistDoc(modelDoc, d), OnWizardClosed);
_createRobotPmp.Show();
}
catch (Exception e)
{
_wizardOpen = false;
logger.Error("OpenCreateRobot failed", e);
MessageBox.Show("Could not open Create Robot: " + e.Message);
}
}

private void OpenCreateWorld(ModelDoc2 modelDoc, SW2GZ.URDFExport.Sw2gzDoc doc)
Expand Down
4 changes: 4 additions & 0 deletions SW2GZ/SW2GZ.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -503,10 +503,13 @@
<Compile Include="Validate\OutputValidator.cs" />
<Compile Include="SwSurface\Abstractions\IMassProperties.cs" />
<Compile Include="SwSurface\SolidWorksMassProperties.cs" />
<Compile Include="SwSurface\Abstractions\IComponentPoses.cs" />
<Compile Include="SwSurface\SolidWorksComponentPoses.cs" />
<Compile Include="SwSurface\Abstractions\LinkSpec.cs" />
<Compile Include="SwSurface\Abstractions\IAssemblyWalker.cs" />
<Compile Include="SwSurface\SolidWorksAssemblyWalker.cs" />
<Compile Include="URDFExport\Sw2gzModelExporter.cs" />
<Compile Include="URDFExport\Sw2gzRobotExporter.cs" />
<Compile Include="URDFExport\Sw2gzWorldExporter.cs" />
<Compile Include="URDFExport\Sw2gzAssetExporter.cs" />
<Compile Include="URDFExport\Sw2gzModelPreviewer.cs" />
Expand Down Expand Up @@ -544,6 +547,7 @@
<Compile Include="UI\Ribbon\Sw2gzRibbonRegistrar.cs" />
<Compile Include="UI\Ribbon\Sw2gzModeChangeOverlay.cs" />
<Compile Include="UI\Pmp\Sw2gzStubPmp.cs" />
<Compile Include="UI\Pmp\Sw2gzCreateRobotPmp.cs" />
<Compile Include="UI\Pmp\Sw2gzCreateWorldPmp.cs" />
<Compile Include="UI\Pmp\Sw2gzCreateAssetPmp.cs" />
<Compile Include="UI\Pmp\Sw2gzWorldSettingsPmp.cs" />
Expand Down
20 changes: 20 additions & 0 deletions SW2GZ/SwSurface/Abstractions/IComponentPoses.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
Copyright (c) 2026 Aryan Arlikar. MIT License — see CONTRIBUTING.md.

Raw SolidWorks Component2.Transform2 pose (assembly-frame rotation +
translation) for a given component, read via the verified COLUMN-major
convention (see memory sw-mathtransform-column-major — confirmed against
Component2.GetBox ground truth). Lets exporters compute the exact relative
parent/child joint transform instead of approximating one from mesh bounding
boxes.
*/
using System.Numerics;
using SW2GZ.Math;

namespace SW2GZ.SwSurface.Abstractions
{
public interface IComponentPoses
{
(Matrix3 Rotation, Vector3 Translation) GetPose(string componentPathName);
}
}
16 changes: 10 additions & 6 deletions SW2GZ/SwSurface/SolidWorksAssemblyWalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -583,14 +583,18 @@ private static bool BothEntitiesPlanarFaces(Mate2 mate)
// PART-LOCAL point, returning the corresponding ASSEMBLY-frame point.
// Different from RotateByComponent above (which is for direction
// vectors and ignores translation).
// ArrayData's 3x3 rotation block is COLUMN-major (verified against
// Component2.GetBox ground truth — see memory
// sw-mathtransform-column-major); the naive row-major read silently
// inverts rotation for any non-identity component orientation.
private static Vector3 TransformByComponent(Component2 comp, Vector3 v)
{
MathTransform xform = comp.Transform2;
if (xform == null) return v;
if (!(xform.ArrayData is double[] d) || d.Length < 12) return v;
float x = (float)(d[0] * v.X + d[1] * v.Y + d[2] * v.Z + d[9]);
float y = (float)(d[3] * v.X + d[4] * v.Y + d[5] * v.Z + d[10]);
float z = (float)(d[6] * v.X + d[7] * v.Y + d[8] * v.Z + d[11]);
float x = (float)(d[0] * v.X + d[3] * v.Y + d[6] * v.Z + d[9]);
float y = (float)(d[1] * v.X + d[4] * v.Y + d[7] * v.Z + d[10]);
float z = (float)(d[2] * v.X + d[5] * v.Y + d[8] * v.Z + d[11]);
return new Vector3(x, y, z);
}

Expand All @@ -601,9 +605,9 @@ private static Vector3 RotateByComponent(Component2 comp, Vector3 v)
MathTransform xform = comp.Transform2;
if (xform == null) return v;
if (!(xform.ArrayData is double[] d) || d.Length < 9) return v;
float x = (float)(d[0] * v.X + d[1] * v.Y + d[2] * v.Z);
float y = (float)(d[3] * v.X + d[4] * v.Y + d[5] * v.Z);
float z = (float)(d[6] * v.X + d[7] * v.Y + d[8] * v.Z);
float x = (float)(d[0] * v.X + d[3] * v.Y + d[6] * v.Z);
float y = (float)(d[1] * v.X + d[4] * v.Y + d[7] * v.Z);
float z = (float)(d[2] * v.X + d[5] * v.Y + d[8] * v.Z);
return new Vector3(x, y, z);
}

Expand Down
64 changes: 64 additions & 0 deletions SW2GZ/SwSurface/SolidWorksComponentPoses.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright (c) 2026 Aryan Arlikar. MIT License — see CONTRIBUTING.md.

Reads a component's raw Component2.Transform2 pose (assembly-frame rotation +
translation), COLUMN-major (see memory sw-mathtransform-column-major).

SW_INTEROP is defined when building SW2GZ.csproj (COM references); NOT
defined when building the xunit test project, so the same source compiles
in both (mirrors SolidWorksMassProperties / SolidWorksMeshTessellator).
*/
using System;
using System.Numerics;
using SW2GZ.Math;
using SW2GZ.SwSurface.Abstractions;

#if SW_INTEROP
using SolidWorks.Interop.sldworks;
#endif

namespace SW2GZ.SwSurface
{
public sealed class SolidWorksComponentPoses : IComponentPoses
{
#if SW_INTEROP
private readonly AssemblyDoc _doc;
#endif

// Skeleton ctor — preserves the NotImplementedException-when-unwired
// convention shared by SolidWorksMeshTessellator / SolidWorksMassProperties.
public SolidWorksComponentPoses() { }

#if SW_INTEROP
public SolidWorksComponentPoses(AssemblyDoc doc) { _doc = doc; }
#endif

public (Matrix3 Rotation, Vector3 Translation) GetPose(string componentPathName)
{
#if SW_INTEROP
if (_doc == null)
#endif
throw new NotImplementedException(
"SolidWorksComponentPoses.GetPose() requires an assembly doc — pass it via constructor.");

#if SW_INTEROP
Component2 comp = SolidWorksMassProperties.FindComponent(
(object[])_doc.GetComponents(false), componentPathName);
if (comp == null)
throw new SW2GZ.Exceptions.Sw2gzExportException(
"Component path not found in active assembly: " + componentPathName);

MathTransform xform = comp.Transform2;
double[] d = xform?.ArrayData as double[];
if (d == null || d.Length < 12) return (Matrix3.Identity, Vector3.Zero);

var r = new Matrix3(
d[0], d[3], d[6],
d[1], d[4], d[7],
d[2], d[5], d[8]);
var t = new Vector3((float)d[9], (float)d[10], (float)d[11]);
return (r, t);
#endif
}
}
}
14 changes: 9 additions & 5 deletions SW2GZ/SwSurface/SolidWorksMeshTessellator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,12 @@ public MeshData Tessellate(string componentPathName, TessellationLod lod)
if (bodyObjs == null || bodyObjs.Length == 0) continue;

// Per-leaf assembly-frame transform. ArrayData layout:
// [0..8] rotation 3x3 row-major, [9..11] translation,
// [12] scale, [13..15] padding.
// [0..8] rotation 3x3 COLUMN-major, [9..11] translation,
// [12] scale, [13..15] padding. Verified against
// Component2.GetBox ground truth (see memory
// sw-mathtransform-column-major) — the naive row-major
// read silently inverts rotation for any non-identity
// component orientation.
MathTransform xform = leaf.Transform2;
double[] d = xform?.ArrayData as double[];
bool hasXf = d != null && d.Length >= 12;
Expand Down Expand Up @@ -147,9 +151,9 @@ public MeshData Tessellate(string componentPathName, TessellationLod lod)
// (GetVertexPoint returns part-local coords).
if (hasXf)
{
double rx = d[0] * x + d[1] * y + d[2] * z;
double ry = d[3] * x + d[4] * y + d[5] * z;
double rz = d[6] * x + d[7] * y + d[8] * z;
double rx = d[0] * x + d[3] * y + d[6] * z;
double ry = d[1] * x + d[4] * y + d[7] * z;
double rz = d[2] * x + d[5] * y + d[8] * z;
x = rx * sc + d[9]; y = ry * sc + d[10]; z = rz * sc + d[11];
}
verts.Add(new System.Numerics.Vector3((float)x, (float)y, (float)z));
Expand Down
13 changes: 13 additions & 0 deletions SW2GZ/UI/LinkTreeView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,23 @@ private TreeNode BuildNode(LinkDef link)
+ " [" + n + (n == 1 ? " part]" : " parts]");
var node = new TreeNode(label) { Tag = link };
if (n == 0) node.ForeColor = System.Drawing.Color.Firebrick; // unassigned = needs attention
node.ToolTipText = DescribePrimary(link);
foreach (LinkDef child in LinkHierarchy.ChildrenOf(links, link.Name))
node.Nodes.Add(BuildNode(child));
return node;
}

// The first ComponentIds entry defines this link's whole frame
// (mesh anchor, joint origin, inertial rebase) — surfaced on hover
// since the compact node label has no room for it.
private static string DescribePrimary(LinkDef link)
{
List<string> ids = link.ComponentIds;
if (ids == null || ids.Count == 0) return "no mesh assigned";
if (ids.Count == 1) return ids[0];
return "primary: " + ids[0] + " | also: " + string.Join(", ", ids.GetRange(1, ids.Count - 1));
}

public void SelectByLinkName(string name)
{
foreach (TreeNode n in AllNodes(Nodes))
Expand All @@ -124,6 +136,7 @@ public void RefreshActiveNodeLabel()
n.Text = (link.Name ?? "")
+ (isRoot ? " (base)" : "")
+ " [" + parts + (parts == 1 ? " part]" : " parts]");
n.ToolTipText = DescribePrimary(link);
}

private static IEnumerable<TreeNode> AllNodes(TreeNodeCollection nodes)
Expand Down
Loading
Loading