Skip to content

Commit 750baf7

Browse files
committed
Merge branch 'main' into 2027
2 parents 45fbe29 + 337304d commit 750baf7

File tree

12 files changed

+152
-117
lines changed

12 files changed

+152
-117
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ jobs:
100100
cache: "npm"
101101
- name: Setup Emscripten
102102
uses: mymindstorm/setup-emsdk@v14
103+
with:
104+
version: 3.1.74
103105
- name: Install Node.js dependencies
104106
run: npm ci
105107
env:

docs/docs/more-features/custom-assets/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ A model must be included in the folder with the name "model.glb". CAD files must
4343
```json
4444
{
4545
"name": string // Unique name, required for all asset types
46-
"isFTC": string // Whether the model is intended for use on FTC fields instead of FRC fields (default "false")
46+
"isFTC": boolean // Whether the model is intended for use on FTC fields instead of FRC fields (default "false")
4747
"disableSimplification": boolean // Whether to disable model simplification, optional
4848
"rotations": { "axis": "x" | "y" | "z", "degrees": number }[] // Sequence of rotations along the x, y, and z axes
4949
"position": [number, number, number] // Position offset in meters, applied after rotation

docs/docs/tab-reference/3d-field/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Mechanism data can be visualized using 2D mechanisms or articulated 3D component
9292

9393
### 2D Mechanisms
9494

95-
To visualize mechanism data logged using a [`Mechanism2d`](https://docs.wpilib.org/en/stable/docs/software/dashboards/glass/mech2d-widget.html), add the mechanism field to an existing robot or ghost object. The mechanism is projected onto the XZ plane of the robot using simple boxes (as shown below). The robot's origin is centered on the bottom edge of the mechanism.
95+
To visualize mechanism data logged using a [`Mechanism2d`](https://docs.wpilib.org/en/stable/docs/software/dashboards/glass/mech2d-widget.html), add the mechanism field to an existing robot or ghost object. The mechanism is projected onto the XZ or YZ plane of the robot using simple boxes, as shown below. Click the gear icon or right-click on the field name to switch between the XZ and YZ planes. The robot's origin is centered on the bottom edge of the mechanism.
9696

9797
![2D mechanism](./img/3d-field-2.png)
9898

src/hub/controllers/Field3dController.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ export default class Field3dController implements TabController {
255255

256256
// Add data from children
257257
let components: AnnotatedPose3d[] = [];
258-
let mechanisms: MechanismState[] = [];
258+
let mechanismsXZ: MechanismState[] = [];
259+
let mechanismsYZ: MechanismState[] = [];
259260
let visionTargets: AnnotatedPose3d[] = [];
260261
let swerveStates: {
261262
values: SwerveState[];
@@ -271,9 +272,9 @@ export default class Field3dController implements TabController {
271272
}
272273

273274
case "mechanism": {
274-
let state = getMechanismState(window.log, child.logKey, time!, child.options.axis);
275+
let state = getMechanismState(window.log, child.logKey, time!);
275276
if (state !== null) {
276-
mechanisms.push(state);
277+
(child.options.plane === "yz" ? mechanismsYZ : mechanismsXZ).push(state);
277278
}
278279
break;
279280
}
@@ -341,7 +342,8 @@ export default class Field3dController implements TabController {
341342
}
342343
});
343344
}
344-
let mechanism = mechanisms.length === 0 ? null : mergeMechanismStates(mechanisms);
345+
let mechanismXZ = mechanismsXZ.length === 0 ? null : mergeMechanismStates(mechanismsXZ);
346+
let mechanismYZ = mechanismsYZ.length === 0 ? null : mergeMechanismStates(mechanismsYZ);
345347
visionTargets.reverse();
346348
swerveStates.reverse();
347349

@@ -353,7 +355,10 @@ export default class Field3dController implements TabController {
353355
model: source.options.model,
354356
poses: poses,
355357
components: components,
356-
mechanism: mechanism,
358+
mechanisms: {
359+
xz: mechanismXZ,
360+
yz: mechanismYZ
361+
},
357362
visionTargets: visionTargets,
358363
swerveStates: swerveStates
359364
});
@@ -365,7 +370,10 @@ export default class Field3dController implements TabController {
365370
model: source.options.model,
366371
poses: poses,
367372
components: components,
368-
mechanism: mechanism,
373+
mechanisms: {
374+
xz: mechanismXZ,
375+
yz: mechanismYZ
376+
},
369377
visionTargets: visionTargets,
370378
swerveStates: swerveStates
371379
});

src/hub/controllers/Field3dController_Config.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@ const Field3dController_Config: SourceListConfig = {
102102
showDocs: true,
103103
options: [
104104
{
105-
key: "axis",
106-
display: "Axis",
107-
showInTypeName: true,
105+
key: "plane",
106+
display: "Plane",
107+
showInTypeName: false,
108108
values: [
109-
{ key: "x", display: "XZ Axis" },
110-
{ key: "y", display: "YZ Axis" }
109+
{ key: "xz", display: "XZ Plane" },
110+
{ key: "yz", display: "YZ Plane" }
111111
]
112112
}
113113
],

src/shared/log/LogUtil.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,6 @@ export type MechanismState = {
570570
backgroundColor: string;
571571
dimensions: [number, number];
572572
lines: MechanismLine[];
573-
axis: string;
574573
};
575574

576575
export type MechanismLine = {
@@ -580,7 +579,7 @@ export type MechanismLine = {
580579
weight: number;
581580
};
582581

583-
export function getMechanismState(log: Log, key: string, time: number, axis: string = "x"): MechanismState | null {
582+
export function getMechanismState(log: Log, key: string, time: number): MechanismState | null {
584583
// Get general config
585584
let backgroundColor = getOrDefault(log, key + "/backgroundColor", LoggableType.String, time, null);
586585
let dimensions = getOrDefault(log, key + "/dims", LoggableType.NumberArray, time, null);
@@ -665,8 +664,7 @@ export function getMechanismState(log: Log, key: string, time: number, axis: str
665664
return {
666665
backgroundColor: backgroundColor,
667666
dimensions: dimensions,
668-
lines: lines,
669-
axis: axis
667+
lines: lines
670668
};
671669
}
672670

@@ -688,8 +686,7 @@ export function mergeMechanismStates(states: MechanismState[]): MechanismState {
688686
return {
689687
backgroundColor: states[0].backgroundColor,
690688
dimensions: [newWidth, newHeight],
691-
lines: lines,
692-
axis: states[0].axis
689+
lines: lines
693690
};
694691
}
695692

src/shared/renderers/Field3dRenderer.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,10 @@ export type Field3dRendererCommand_GenericRobotObj = {
141141
model: string;
142142
poses: AnnotatedPose3d[];
143143
components: AnnotatedPose3d[];
144-
mechanism: MechanismState | null;
144+
mechanisms: {
145+
xz: MechanismState | null;
146+
yz: MechanismState | null;
147+
};
145148
visionTargets: AnnotatedPose3d[];
146149
swerveStates: {
147150
values: SwerveState[];

src/shared/renderers/Field3dRendererImpl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ export default class Field3dRendererImpl implements TabRenderer {
814814
color: "#000000",
815815
poses: [],
816816
components: [],
817-
mechanism: null,
817+
mechanisms: { xz: null, yz: null },
818818
visionTargets: [],
819819
swerveStates: []
820820
});

src/shared/renderers/LineGraphRenderer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,9 @@ export default class LineGraphRenderer implements TabRenderer {
128128
command.showRightAxis,
129129
command.priorityAxis,
130130
this.lastCursorX,
131-
command.leftFields.map((field) => field.values.length),
132-
command.discreteFields.map((field) => field.values.length),
133-
command.rightFields.map((field) => field.values.length)
131+
command.leftFields.map((field) => [field.values.length, field.color, field.type, field.size]),
132+
command.discreteFields.map((field) => [field.values.length, field.color, field.type, field.toggleReference]),
133+
command.rightFields.map((field) => [field.values.length, field.color, field.type, field.size])
134134
];
135135
let renderStateString = JSON.stringify(renderState);
136136
if (renderStateString === this.lastRenderState) {

src/shared/renderers/field3d/objectManagers/RobotManager.ts

Lines changed: 93 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
1313
import WorkerManager from "../../../../hub/WorkerManager";
1414
import { AdvantageScopeAssets } from "../../../AdvantageScopeAssets";
1515
import { SwerveState } from "../../../geometry";
16+
import { MechanismState } from "../../../log/LogUtil";
1617
import { Units } from "../../../units";
1718
import { transformPx } from "../../../util";
1819
import { Field3dRendererCommand_GhostObj, Field3dRendererCommand_RobotObj } from "../../Field3dRenderer";
@@ -21,6 +22,14 @@ import ObjectManager from "../ObjectManager";
2122
import { FTC_MULTIPLIER, XR_MAX_RADIUS } from "../OptimizeGeometries";
2223
import ResizableInstancedMesh from "../ResizableInstancedMesh";
2324

25+
type MechanismLineData = {
26+
mesh: ResizableInstancedMesh;
27+
geometry: THREE.BoxGeometry;
28+
scale: THREE.Vector3;
29+
translation: THREE.Vector3;
30+
material: THREE.MeshPhongMaterial;
31+
};
32+
2433
export default class RobotManager extends ObjectManager<
2534
Field3dRendererCommand_RobotObj | Field3dRendererCommand_GhostObj
2635
> {
@@ -41,13 +50,8 @@ export default class RobotManager extends ObjectManager<
4150
shininess: this.materialShininess
4251
});
4352
private visionLines: Line2[] = [];
44-
private mechanismLines: {
45-
mesh: ResizableInstancedMesh;
46-
geometry: THREE.BoxGeometry;
47-
scale: THREE.Vector3;
48-
translation: THREE.Vector3;
49-
material: THREE.MeshPhongMaterial;
50-
}[] = [];
53+
private mechanismLinesXZ: MechanismLineData[] = [];
54+
private mechanismLinesYZ: MechanismLineData[] = [];
5155

5256
private swerveContainer: HTMLElement | null = null;
5357
private swerveCanvas: HTMLCanvasElement | null = null;
@@ -100,9 +104,8 @@ export default class RobotManager extends ObjectManager<
100104
this.meshes.forEach((mesh) => {
101105
mesh.dispose();
102106
});
103-
this.mechanismLines.forEach((entry) => {
104-
entry.mesh.dispose();
105-
});
107+
this.mechanismLinesXZ.forEach((entry) => entry.mesh.dispose());
108+
this.mechanismLinesYZ.forEach((entry) => entry.mesh.dispose());
106109
while (this.visionLines.length > 0) {
107110
this.visionLines[0].geometry.dispose();
108111
this.visionLines[0].material.dispose();
@@ -431,95 +434,94 @@ export default class RobotManager extends ObjectManager<
431434
}
432435

433436
// Update mechanism
434-
if (object.mechanism === null) {
435-
// No mechanism data, remove all meshes
436-
while (this.mechanismLines.length > 0) {
437-
this.mechanismLines[0].mesh.dispose(true, object.type === "robot"); // Ghost material is shared, don't dispose
438-
this.mechanismLines.shift();
439-
}
440-
} else {
441-
// Filter to visible lines
442-
let mechanismLines = object.mechanism?.lines.filter(
443-
(line) =>
444-
Math.hypot(line.end[1] - line.start[1], line.end[0] - line.start[0]) >= 1e-3 &&
445-
line.weight * this.MECHANISM_WIDTH_PER_WEIGHT >= 1e-3
446-
);
447-
448-
// Remove extra lines
449-
while (this.mechanismLines.length > mechanismLines.length) {
450-
this.mechanismLines[0].mesh.dispose(true, object.type === "robot"); // Ghost material is shared, don't dispose
451-
this.mechanismLines.shift();
452-
}
453-
454-
// Add new lines
455-
while (this.mechanismLines.length < mechanismLines.length) {
456-
const geometry = new THREE.BoxGeometry(1, 1, 1);
457-
const material =
458-
object.type === "ghost"
459-
? this.ghostMaterial
460-
: new THREE.MeshPhongMaterial({ specular: this.materialSpecular, shininess: this.materialShininess });
461-
this.mechanismLines.push({
462-
mesh: new ResizableInstancedMesh(this.root, [{ geometry: geometry, material: material }]),
463-
geometry: geometry,
464-
scale: new THREE.Vector3(1, 1, 1),
465-
translation: new THREE.Vector3(),
466-
material: material
467-
});
468-
}
469-
470-
// Update children
471-
for (let i = 0; i < mechanismLines.length; i++) {
472-
const line = mechanismLines[i];
473-
const meshEntry = this.mechanismLines[i];
474-
475-
const length = Math.hypot(line.end[1] - line.start[1], line.end[0] - line.start[0]);
476-
const angle = Math.atan2(line.end[1] - line.start[1], line.end[0] - line.start[0]);
477-
478-
// Update length
479-
const newScale = new THREE.Vector3(
480-
object.mechanism.axis == "x" ? length : line.weight * this.MECHANISM_WIDTH_PER_WEIGHT,
481-
object.mechanism.axis == "x" ? line.weight * this.MECHANISM_WIDTH_PER_WEIGHT : length,
482-
line.weight * this.MECHANISM_WIDTH_PER_WEIGHT
437+
let updateMechanism = (state: MechanismState | null, lines: MechanismLineData[], plane: "xz" | "yz") => {
438+
if (state === null) {
439+
// No mechanism data, remove all meshes
440+
while (lines.length > 0) {
441+
lines[0].mesh.dispose(true, object.type === "robot"); // Ghost material is shared, don't dispose
442+
lines.shift();
443+
}
444+
} else {
445+
// Filter to visible lines
446+
let mechanismLines = state?.lines.filter(
447+
(line) =>
448+
Math.hypot(line.end[1] - line.start[1], line.end[0] - line.start[0]) >= 1e-3 &&
449+
line.weight * this.MECHANISM_WIDTH_PER_WEIGHT >= 1e-3
483450
);
484-
const newTranslation =
485-
object.mechanism.axis == "x" ? new THREE.Vector3(length / 2, 0, 0) : new THREE.Vector3(0, length / 2, 0);
486-
if (!newScale.equals(meshEntry.scale) || !newTranslation.equals(meshEntry.translation)) {
487-
meshEntry.geometry.translate(-meshEntry.translation.x, -meshEntry.translation.y, -meshEntry.translation.z);
488-
meshEntry.geometry.scale(1 / meshEntry.scale.x, 1 / meshEntry.scale.y, 1 / meshEntry.scale.z);
489-
meshEntry.geometry.scale(newScale.x, newScale.y, newScale.z);
490-
meshEntry.geometry.translate(newTranslation.x, newTranslation.y, newTranslation.z);
491-
meshEntry.scale = newScale;
492-
meshEntry.translation = newTranslation;
451+
452+
// Remove extra lines
453+
while (lines.length > mechanismLines.length) {
454+
lines[0].mesh.dispose(true, object.type === "robot"); // Ghost material is shared, don't dispose
455+
lines.shift();
493456
}
494457

495-
// Update color
496-
if (object.type !== "ghost") {
497-
meshEntry.material.color = new THREE.Color(line.color);
458+
// Add new lines
459+
while (lines.length < mechanismLines.length) {
460+
const geometry = new THREE.BoxGeometry(1, 1, 1);
461+
const material =
462+
object.type === "ghost"
463+
? this.ghostMaterial
464+
: new THREE.MeshPhongMaterial({ specular: this.materialSpecular, shininess: this.materialShininess });
465+
lines.push({
466+
mesh: new ResizableInstancedMesh(this.root, [{ geometry: geometry, material: material }]),
467+
geometry: geometry,
468+
scale: new THREE.Vector3(1, 1, 1),
469+
translation: new THREE.Vector3(),
470+
material: material
471+
});
498472
}
499473

500-
// Update pose
501-
meshEntry.mesh.setPoses(
502-
object.poses
503-
.map((x) => x.pose)
504-
.map((robotPose) => {
505-
this.dummyRobotPose.rotation.setFromQuaternion(rotation3dToQuaternion(robotPose.rotation));
506-
this.dummyRobotPose.position.set(...robotPose.translation);
474+
// Update children
475+
for (let i = 0; i < mechanismLines.length; i++) {
476+
const line = mechanismLines[i];
477+
const meshEntry = lines[i];
478+
479+
const length = Math.hypot(line.end[1] - line.start[1], line.end[0] - line.start[0]);
480+
const angle = Math.atan2(line.end[1] - line.start[1], line.end[0] - line.start[0]);
481+
482+
// Update length
483+
const newScale = new THREE.Vector3(
484+
length,
485+
line.weight * this.MECHANISM_WIDTH_PER_WEIGHT,
486+
line.weight * this.MECHANISM_WIDTH_PER_WEIGHT
487+
);
488+
const newTranslation = new THREE.Vector3(length / 2, 0, 0);
489+
if (!newScale.equals(meshEntry.scale) || !newTranslation.equals(meshEntry.translation)) {
490+
meshEntry.geometry.translate(-meshEntry.translation.x, -meshEntry.translation.y, -meshEntry.translation.z);
491+
meshEntry.geometry.scale(1 / meshEntry.scale.x, 1 / meshEntry.scale.y, 1 / meshEntry.scale.z);
492+
meshEntry.geometry.scale(newScale.x, newScale.y, newScale.z);
493+
meshEntry.geometry.translate(newTranslation.x, newTranslation.y, newTranslation.z);
494+
meshEntry.scale = newScale;
495+
meshEntry.translation = newTranslation;
496+
}
507497

508-
if (object.mechanism?.axis == "x") {
509-
this.dummyUserPose.position.set(line.start[0] - object.mechanism!.dimensions[0] / 2, 0, line.start[1]);
498+
// Update color
499+
if (object.type !== "ghost") {
500+
meshEntry.material.color = new THREE.Color(line.color);
501+
}
502+
503+
// Update pose
504+
meshEntry.mesh.setPoses(
505+
object.poses
506+
.map((x) => x.pose)
507+
.map((robotPose) => {
508+
this.dummyRobotPose.rotation.setFromQuaternion(rotation3dToQuaternion(robotPose.rotation));
509+
if (plane == "yz") this.dummyRobotPose.rotateZ(Math.PI / 2);
510+
this.dummyRobotPose.position.set(...robotPose.translation);
511+
512+
this.dummyUserPose.position.set(line.start[0] - state.dimensions[0] / 2, 0, line.start[1]);
510513
this.dummyUserPose.rotation.set(0, -angle, 0);
511-
} else {
512-
this.dummyUserPose.position.set(0, line.start[0] - object.mechanism!.dimensions[0] / 2, line.start[1]);
513-
this.dummyUserPose.rotation.set(angle, 0, 0);
514-
}
515-
return {
516-
translation: this.dummyUserPose.getWorldPosition(new THREE.Vector3()).toArray(),
517-
rotation: quaternionToRotation3d(this.dummyUserPose.getWorldQuaternion(new THREE.Quaternion()))
518-
};
519-
})
520-
);
514+
return {
515+
translation: this.dummyUserPose.getWorldPosition(new THREE.Vector3()).toArray(),
516+
rotation: quaternionToRotation3d(this.dummyUserPose.getWorldQuaternion(new THREE.Quaternion()))
517+
};
518+
})
519+
);
520+
}
521521
}
522-
}
522+
};
523+
updateMechanism(object.mechanisms.xz, this.mechanismLinesXZ, "xz");
524+
updateMechanism(object.mechanisms.yz, this.mechanismLinesYZ, "yz");
523525

524526
// Update swerve canvas (disabled in XR)
525527
if (!this.isXR) {

0 commit comments

Comments
 (0)