From ea6d6a783f4821d7b9a856e0b0a13a4a065bbc0f Mon Sep 17 00:00:00 2001 From: tlnguyen2009 Date: Wed, 22 Oct 2025 16:57:21 -0400 Subject: [PATCH 01/23] Fixed logic for calculating slope of slider Fixed the logic error in calculating the slope of the linear function of the slider. Before the fix, the code was incorrectly using the slider's initial Y coordinate stored in the Joint object, instead of its Y coordinate from the previous frame. The fix was to consistently use both prevJointPosition.x and prevJointPosition.y when calculating n. --- .../Edit/joint-edit-panel/joint-edit-panel.component.ts | 1 + src/app/services/kinematic-solver.service.ts | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/components/SideNav/Edit/joint-edit-panel/joint-edit-panel.component.ts b/src/app/components/SideNav/Edit/joint-edit-panel/joint-edit-panel.component.ts index e9e915b9..443b598a 100644 --- a/src/app/components/SideNav/Edit/joint-edit-panel/joint-edit-panel.component.ts +++ b/src/app/components/SideNav/Edit/joint-edit-panel/joint-edit-panel.component.ts @@ -178,6 +178,7 @@ export class jointEditPanelComponent implements OnInit, OnDestroy{ if (this.pendingAngle == null) return; this.setJointAngle(this.pendingAngle); + this.getMechanism().notifyChange(); this.pendingAngle = undefined; } diff --git a/src/app/services/kinematic-solver.service.ts b/src/app/services/kinematic-solver.service.ts index 548d0982..8c158a53 100644 --- a/src/app/services/kinematic-solver.service.ts +++ b/src/app/services/kinematic-solver.service.ts @@ -657,10 +657,9 @@ export class PositionSolverService { } const prevJointPosition: Coord = prevPositions[solveOrder.indexOf(solvePrerequisite.jointToSolve.id)]; - const n = - solvePrerequisite.jointToSolve!.coords.y - - m * - prevPositions[solveOrder.indexOf(solvePrerequisite.jointToSolve.id)].x; + + const n = prevJointPosition.y - m * prevJointPosition.x; + // get a, b, c values const a = 1 + Math.pow(m, 2); const b = -h * 2 + m * (n - k) * 2; From 8a5bcd25ed202f1ae11b172bde7a43501b51d05f Mon Sep 17 00:00:00 2001 From: tlnguyen2009 Date: Wed, 22 Oct 2025 17:15:31 -0400 Subject: [PATCH 02/23] Fixed the degree/radians option for slider angle input Adding more logic to allow users to enter value in degrees/radians. The function will also check and display correct values when the users switch between degree and radians in Settings. --- .../joint-edit-panel.component.html | 4 +- .../joint-edit-panel.component.ts | 37 +++++++++++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/app/components/SideNav/Edit/joint-edit-panel/joint-edit-panel.component.html b/src/app/components/SideNav/Edit/joint-edit-panel/joint-edit-panel.component.html index 8cef8856..83a4d175 100644 --- a/src/app/components/SideNav/Edit/joint-edit-panel/joint-edit-panel.component.html +++ b/src/app/components/SideNav/Edit/joint-edit-panel/joint-edit-panel.component.html @@ -88,12 +88,12 @@ - deg + {{ angleSuffix }} diff --git a/src/app/components/SideNav/Edit/joint-edit-panel/joint-edit-panel.component.ts b/src/app/components/SideNav/Edit/joint-edit-panel/joint-edit-panel.component.ts index 443b598a..7f5d1393 100644 --- a/src/app/components/SideNav/Edit/joint-edit-panel/joint-edit-panel.component.ts +++ b/src/app/components/SideNav/Edit/joint-edit-panel/joint-edit-panel.component.ts @@ -175,9 +175,23 @@ export class jointEditPanelComponent implements OnInit, OnDestroy{ // This is for angle of a Joint attached to Slider confirmJointAngle(): void { - if (this.pendingAngle == null) return; + let newAngle = this.pendingAngle; + + if (newAngle == null) return; - this.setJointAngle(this.pendingAngle); + if (this.angleSuffix === 'rad') { //need to convert the 'raw' into degrees if the current unit is 'rad', we have to convert it because the logic in backend only works with unit in degrees. + newAngle = newAngle * 180 / Math.PI; + } + + const currJoint = this.getCurrentJoint(); + const oldAngle = currJoint.angle; //get angle of current joint + + if (Math.abs(oldAngle - newAngle) < 1e-6) { //return right away if the new and old angle are the same + this.pendingAngle = undefined; + return; + } + + this.setJointAngle(newAngle); //setJointAngle already recorded undoRedoService this.getMechanism().notifyChange(); this.pendingAngle = undefined; } @@ -507,8 +521,23 @@ export class jointEditPanelComponent implements OnInit, OnDestroy{ return 0; } - getJointAngle2(): number { - return this.getCurrentJoint().angle; + getSliderJointAngle(): number { + let angleInDegrees = this.getCurrentJoint().angle; //angle in backend system always return in degrees + let angleInRadians = angleInDegrees * Math.PI / 180; + + if (this.angleSuffix === 'º') { + if (angleInDegrees < 0) { // Normalize the angle to be within [0, 360] degrees + angleInDegrees += 360; + } + return parseFloat(angleInDegrees.toFixed(3)); + } else if(this.angleSuffix === 'rad') { + if (angleInRadians < 0) { + angleInRadians += 2 * (Math.PI); // Normalize to be within [0, 2pi] + } + return parseFloat(angleInRadians.toFixed(3)); + } + + return 0; } getJointGround() { From 3fe5be885382610fe22064d126e82e8deb878d19 Mon Sep 17 00:00:00 2001 From: tlnguyen2009 Date: Fri, 24 Oct 2025 22:32:15 -0400 Subject: [PATCH 03/23] Fix wrong animation with mechanism when slider angle equals 90 degrees (vertical) Before the fix, when the slider became vertical (angle = 90 degrees), the slider didn't move, and the coupler link was strangely animated by stretching out and in. After the fix, everything became normal as it should be. The issue was in the old code, which mistakenly used the old point coordinates instead of the current ones. In other words, when the crank moves an angle theta to a new position, we should use this new position to draw a circle analysis instead of the last position. --- src/app/services/kinematic-solver.service.ts | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/app/services/kinematic-solver.service.ts b/src/app/services/kinematic-solver.service.ts index 8c158a53..b7e8e176 100644 --- a/src/app/services/kinematic-solver.service.ts +++ b/src/app/services/kinematic-solver.service.ts @@ -652,7 +652,8 @@ export class PositionSolverService { nextPositions[solveOrder.indexOf(solvePrerequisite.knownJointOne!.id)!].y; let m = Math.tan((solvePrerequisite.jointToSolve!.angle * Math.PI) / 180); - if (m > 1000 || m < -1000) { + + if (m > 1000 || m < -1000) { // when angle is 90 degrees, tan will be 1/0 = undefined m = Number.MAX_VALUE; } const prevJointPosition: Coord = @@ -666,18 +667,17 @@ export class PositionSolverService { const c = Math.pow(h, 2) + Math.pow(n - k, 2) - Math.pow(r, 2); // get discriminant const d = Math.pow(b, 2) - 4 * a * c; + + console.log('value of disciminant: ', d); - //if discriminant is too big or not a number, use alternative method + //if discriminant is too big or not a number (NaN), use alternative method. We will see this case when angle of slider = 90 degrees if (isNaN(d) || !isFinite(d)) { - let temp_a: number = 1; - let temp_b: number = -2 * solvePrerequisite.knownJointOne!._coords.y; - let temp_c: number = - Math.pow(solvePrerequisite.knownJointOne!._coords.y, 2) + - Math.pow( - solvePrerequisite.knownJointOne!._coords.x - prevJointPosition.x, - 2 - ) - - Math.pow(r, 2); + // Line (vertical slider): x = t + // Circle: (x - h)^2 + (y - k)^2 = r^2 + const t = prevJointPosition.x; + const temp_a: number = 1; + const temp_b: number = -2 * k; + const temp_c: number = Math.pow(k, 2) + Math.pow(t-h,2) - Math.pow(r, 2); let temp_d: number = Math.pow(temp_b, 2) - 4 * temp_a * temp_c; if (temp_d < 0) { return undefined; @@ -697,8 +697,7 @@ export class PositionSolverService { y = y_2; } x = prevJointPosition.x; - //if discriminant is normal, calculate intersection points and return closest. - } else { + } else { //if discriminant is normal, calculate intersection points and return closest. if (d >= 0) { const x_1 = (-b + Math.sqrt(Math.pow(b, 2) - 4 * a * c)) / (2 * a); const y_1 = m * x_1 + n; @@ -722,6 +721,7 @@ export class PositionSolverService { y = intersectionPoints[0].y; } } else { + console.log('something weird happens to calculation logic with solveCircleLine() in kinematic-solver.service.ts') return undefined; } } From eb8f6f23c54d085eb2ca0ac118b8b017cce879e2 Mon Sep 17 00:00:00 2001 From: tlnguyen2009 Date: Sun, 2 Nov 2025 12:27:01 -0500 Subject: [PATCH 04/23] Fixed: Mechanism with slider doesn't move or animate when hit play button at some position on the grid When solving for the slider's position, a quadratic equation provides two possible solutions (x1, x2). The correct solution is the one physically closest to the slider's previous position. The old code always picked x1, which caused errors when x2 was the correct solution for some position on the grid. This commit updates the logic to choose the solution (x1 or x2) nearest to the previous position. --- src/app/services/kinematic-solver.service.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/app/services/kinematic-solver.service.ts b/src/app/services/kinematic-solver.service.ts index b7e8e176..6014120f 100644 --- a/src/app/services/kinematic-solver.service.ts +++ b/src/app/services/kinematic-solver.service.ts @@ -656,9 +656,10 @@ export class PositionSolverService { if (m > 1000 || m < -1000) { // when angle is 90 degrees, tan will be 1/0 = undefined m = Number.MAX_VALUE; } + const prevJointPosition: Coord = prevPositions[solveOrder.indexOf(solvePrerequisite.jointToSolve.id)]; - + const n = prevJointPosition.y - m * prevJointPosition.x; // get a, b, c values @@ -668,7 +669,6 @@ export class PositionSolverService { // get discriminant const d = Math.pow(b, 2) - 4 * a * c; - console.log('value of disciminant: ', d); //if discriminant is too big or not a number (NaN), use alternative method. We will see this case when angle of slider = 90 degrees if (isNaN(d) || !isFinite(d)) { @@ -698,7 +698,7 @@ export class PositionSolverService { } x = prevJointPosition.x; } else { //if discriminant is normal, calculate intersection points and return closest. - if (d >= 0) { + if (d >= 0) { // discriminant d >= 0, there is at least 1 solution and at most 2 solutions const x_1 = (-b + Math.sqrt(Math.pow(b, 2) - 4 * a * c)) / (2 * a); const y_1 = m * x_1 + n; const x_2 = (-b - Math.sqrt(Math.pow(b, 2) - 4 * a * c)) / (2 * a); @@ -716,12 +716,15 @@ export class PositionSolverService { Math.pow(x_2 - prevJointPosition.x, 2) + Math.pow(y_2 - prevJointPosition.y, 2) ); - if (intersection1Diff < intersection2Diff) { + if (intersection1Diff < intersection2Diff) { // (x1,y1) closer to the last slider's position x = intersectionPoints[0].x; y = intersectionPoints[0].y; + } else { // (x2,y2) closer to the last slider's position + x = intersectionPoints[1].x; + y = intersectionPoints[1].y; } - } else { - console.log('something weird happens to calculation logic with solveCircleLine() in kinematic-solver.service.ts') + } else { // discriminant < 0, there is no solution and it's also the signal about limit of the movement of the crank and we need to swap direction. + console.log('crank hits its limit and ready to change direction'); return undefined; } } From a51b7d0036b9c2c1cd11f3ffced730a372a9065a Mon Sep 17 00:00:00 2001 From: tlnguyen2009 Date: Sun, 2 Nov 2025 12:39:17 -0500 Subject: [PATCH 05/23] Fixed: Unexpected double blue path line for slider The slider's blue path is generated from a series of connected points. A previous optimization attempted to draw this path by only using two endpoints. However, the logic to find these points was incorrectly reused from a function designed for linkages. This bug caused the slider path to render incorrectly, appearing as a "double closed loop" like the outline of a linkage instead of a single, continuous line. This commit removes the faulty optimization. The path is now drawn by connecting all of its points in sequence, which resolves the rendering bug. --- src/app/services/svg-path.service.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/app/services/svg-path.service.ts b/src/app/services/svg-path.service.ts index c1eff505..19a0fec9 100644 --- a/src/app/services/svg-path.service.ts +++ b/src/app/services/svg-path.service.ts @@ -29,13 +29,6 @@ export class SVGPathService { return ''; } - //check if coordinates are collinear. If they are, use the two returned coords(the end points) to draw a line - const collinearCoords: Coord[] | undefined = this.findCollinearCoords(allCoords); - if (collinearCoords !== undefined) { - - return this.calculateTwoPointPath(collinearCoords[0], collinearCoords[1], radius); - } - let pathData = `M ${allCoords[0].x},${allCoords[0].y} `; for (let i = 1; i < allCoords.length; i++) { const currentCoord = allCoords[i]; @@ -84,7 +77,6 @@ export class SVGPathService { return undefined; } } - // If all coords have the same slope with the 'start' point, they are collinear return [start, end]; } From be163ac94967b370803e82f6b080093601f0a5d1 Mon Sep 17 00:00:00 2001 From: mwalsh001 Date: Sun, 30 Nov 2025 21:16:58 -0500 Subject: [PATCH 06/23] Correct velocity calculations for angular velocity of a 4-bar link. Displays in analysis graph. Only displays one link velocity at a time that is hard-coded, needs to include the velocities of the selected link. --- package-lock.json | 88 ++- package.json | 2 + .../link-analysis-panel.component.html | 4 +- .../link-analysis-panel.component.ts | 22 +- .../services/analysis-solver.service.spec.ts | 55 ++ src/app/services/analysis-solver.service.ts | 703 ++++++++++-------- 6 files changed, 552 insertions(+), 322 deletions(-) create mode 100644 src/app/services/analysis-solver.service.spec.ts diff --git a/package-lock.json b/package-lock.json index 7585e033..91a7c47c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,8 @@ "lodash": "^4.17.21", "lz-string": "^1.5.0", "material": "^0.7.5", + "math.js": "^1.1.46", + "mathjs": "^15.1.0", "ng2-charts": "^5.0.4", "papaparse": "^5.4.1", "rxjs": "~7.8.0", @@ -3295,7 +3297,6 @@ "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", - "dev": true, "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -8374,6 +8375,18 @@ "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", "dev": true }, + "node_modules/complex.js": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.3.tgz", + "integrity": "sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ==", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -9037,6 +9050,11 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==" + }, "node_modules/default-browser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", @@ -9545,6 +9563,11 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==" + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -11283,6 +11306,11 @@ "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", "dev": true }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==" + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -12259,6 +12287,45 @@ "resolved": "https://registry.npmjs.org/material/-/material-0.7.9.tgz", "integrity": "sha512-6NJ2bKKXKR024377IhFAyP4OEejmktlHsHwRCas/cyyoPUykbAOYtctP8sDwTtCFE2ot9YitgnWzbMj0vVoRbA==" }, + "node_modules/math.js": { + "version": "1.1.46", + "resolved": "https://registry.npmjs.org/math.js/-/math.js-1.1.46.tgz", + "integrity": "sha512-D4DS+oENshM6xI94mhzJFkH1D0jvSHyKjNuLbki2IdGM3RZ74WQDSw8KLIMy/76JFhl7BvY1/KkJc38kOvv/6Q==" + }, + "node_modules/mathjs": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-15.1.0.tgz", + "integrity": "sha512-HfnAcScQm9drGryodlDqeS3WAl4gUTYGDcOtcqL/8s23MZ28Ib1i8XnYK3ZdjNuaW/L4BAp9lIp8vxAMrcuu1w==", + "dependencies": { + "@babel/runtime": "^7.26.10", + "complex.js": "^2.2.5", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "^5.2.1", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.2.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mathjs/node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -14061,7 +14128,6 @@ "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, "license": "MIT" }, "node_modules/regex-parser": { @@ -14483,6 +14549,11 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -15488,6 +15559,11 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -15613,6 +15689,14 @@ "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", "dev": true }, + "node_modules/typed-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", + "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==", + "engines": { + "node": ">= 18" + } + }, "node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", diff --git a/package.json b/package.json index 17dd9920..bb1aa915 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "lodash": "^4.17.21", "lz-string": "^1.5.0", "material": "^0.7.5", + "math.js": "^1.1.46", + "mathjs": "^15.1.0", "ng2-charts": "^5.0.4", "papaparse": "^5.4.1", "rxjs": "~7.8.0", diff --git a/src/app/components/SideNav/Analysis/link-analysis-panel/link-analysis-panel.component.html b/src/app/components/SideNav/Analysis/link-analysis-panel/link-analysis-panel.component.html index 3d32376f..149cbc86 100644 --- a/src/app/components/SideNav/Analysis/link-analysis-panel/link-analysis-panel.component.html +++ b/src/app/components/SideNav/Analysis/link-analysis-panel/link-analysis-panel.component.html @@ -39,8 +39,8 @@ [btn1Action]="toggleGraph.bind(this, GraphType.referenceJointAngularVelocity)" [graphText]= "currentGraphType === GraphType.referenceJointAngularVelocity ? 'Close Graph' : 'Show Graph'">> -

ω: N/A rad/s

- +

ω: rad/s

+