Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ never kept a changelog before, so this covers everything since the last
PyPI release, v1.1.0 (2023-04-30) — effectively three years of accumulated
work on the `future` branch, now merged into `main`, plus everything since.

## [Unreleased]

### Changed

- **`Axes` honors spatialgeometry's `color` override**: when a color is
given, all three axes are drawn in it, in both the plain-line and
`arrows=True` styles. With no color (`color` is `null` on the wire) they
are drawn red/green/blue as before. Needs the matching spatialgeometry
change; with an older spatialgeometry, which always sends a color, every
`Axes` would draw in that one grey.
The minimum spatialgeometry version is raised to 1.4.2, the first release
that sends `color: null`.

## [2.0.1] - 2026-09-20

### New
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ authors = [
{ name = "Peter Corke", email = "rvc@petercorke.com" },
]

dependencies = ["numpy>=2.0", "spatialgeometry>=1.4.0", "websockets"]
dependencies = ["numpy>=2.0", "spatialgeometry>=1.4.2", "websockets"]

license = { file = "LICENSE" }

Expand Down
17 changes: 15 additions & 2 deletions src/swift/public/js/shapes.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,18 +204,31 @@ const _AXIS_ROTATIONS = [
new THREE.Euler(0, 0, 0), // +Z -> +Z, no rotation needed
];

function loadAxes(part, scene, cb) {
/**
* spatialgeometry.Axes. `part.color` is null when the user never gave a
* color, which means the conventional X red / Y green / Z blue; otherwise
* it is a single color override applied to all three axes, in either style
* (plain lines or arrows).
*/
export function loadAxes(part, scene, cb) {
const colors = part.color == null ? _AXIS_COLORS : [part.color, part.color, part.color];

let axes;
if (part.arrows) {
axes = new THREE.Group();
for (let i = 0; i < 3; i++) {
const arrow = makeArrow(part.length, part.radius, part.linewidth, 0.2, 0.2, _AXIS_COLORS[i]);
const arrow = makeArrow(part.length, part.radius, part.linewidth, 0.2, 0.2, colors[i]);
arrow.setRotationFromEuler(_AXIS_ROTATIONS[i]);
axes.add(arrow);
}
axes.userData.disposables = axes.children.flatMap((a) => a.userData.disposables);
} else {
axes = new THREE.AxesHelper(part.length);
// AxesHelper's own default is the same X red / Y green / Z blue, so
// this only needs to run for an override -- and goes through
// setColors() rather than a bare material color, since AxesHelper
// draws with per-vertex colors that a material color would multiply.
if (part.color != null) axes.setColors(colors[0], colors[1], colors[2]);
}
setPose(axes, part.t, part.q);
finish(part, axes, scene, cb);
Expand Down
53 changes: 52 additions & 1 deletion src/swift/public/js/shapes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { test } from "node:test";
// static `import` is hoisted before any stubbing here would run. A dynamic
// import(), which isn't hoisted, lets `window` be stubbed first.
globalThis.window ??= { innerWidth: 0, innerHeight: 0 };
const { retrieveUrl } = await import("./shapes.js");
const { retrieveUrl, loadAxes } = await import("./shapes.js");

// Regression test for jhavl/swift#152: a Windows path's backslashes must be
// converted to forward slashes *before* the drive-letter strip, not after --
Expand Down Expand Up @@ -50,3 +50,54 @@ test("retrieveUrl URI-encodes characters that need it", (t) => {
const url = retrieveUrl("C:\\My Meshes\\arm link.stl");
assert.equal(url, "/retrieve/My%20Meshes/arm%20link.stl");
});

// spatialgeometry.Axes: color is null when none was given (X red / Y green /
// Z blue), else one color overriding all three axes in either style.
const axesPart = (extra) => ({
stype: "axes",
t: [0, 0, 0],
q: [0, 0, 0, 1],
length: 1,
radius: 0,
linewidth: 1,
arrows: false,
color: null,
opacity: 1,
...extra,
});

function loaded(part) {
loadAxes(part, { add() {} }, () => {});
return part.mesh;
}

// AxesHelper's per-vertex colors: two vertices per axis, 3 floats each.
const lineColors = (mesh) => {
const a = mesh.geometry.attributes.color.array;
return [0, 6, 12].map((i) => Array.from(a.slice(i, i + 3)));
};

// One color per arrow; the shaft is a Line2 or a Mesh, and either way the
// material's color is the one that matters.
const arrowColors = (group) => group.children.map((arrow) => arrow.children[0].material.color.getHex());

test("Axes with no color is red/green/blue in line style", (t) => {
const c = lineColors(loaded(axesPart({})));
assert.deepEqual(c, [[1, 0, 0], [0, 1, 0], [0, 0, 1]]);
});

test("Axes with no color is red/green/blue in arrow style", (t) => {
assert.deepEqual(arrowColors(loaded(axesPart({ arrows: true }))), [0xff0000, 0x00ff00, 0x0000ff]);
});

test("Axes color overrides all three axes in line style", (t) => {
const c = lineColors(loaded(axesPart({ color: 0xff00ff })));
assert.deepEqual(c, [[1, 0, 1], [1, 0, 1], [1, 0, 1]]);
});

test("Axes color overrides all three axes in arrow style, line and tube shafts", (t) => {
for (const radius of [0, 0.02]) {
const mesh = loaded(axesPart({ arrows: true, radius, color: 0xff00ff }));
assert.deepEqual(arrowColors(mesh), [0xff00ff, 0xff00ff, 0xff00ff]);
}
});
Loading