pinocchio-js 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2024, Pinocchio Authors
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # Pinocchio WASM
2
+
3
+ Pinocchio WASM is a port of the [Pinocchio](https://github.com/stack-of-tasks/pinocchio) rigid body dynamics library to WebAssembly, enabling high-performance robot dynamics in the browser and Node.js.
4
+
5
+ ## Features
6
+
7
+ - **Core Algorithms:** RNEA (Inverse Dynamics), Forward Kinematics, Center of Mass, Jacobian.
8
+ - **URDF Parsing:** Custom JavaScript parser ensuring compatibility with Pinocchio's model structure.
9
+ - **Cross-Platform:** Runs in Browsers (Chrome, Firefox, Safari) and Node.js.
10
+ - **Lightweight:** ~600KB (gzipped) WASM binary.
11
+ - **Zero-Dependency Runtime:** No external logical dependencies for the basic WASM module (excluding the optional URDF parser).
12
+
13
+ ## Installation
14
+
15
+ Currently, you can build from source or use the provided artifacts.
16
+
17
+ ### Prerequisites for Building
18
+
19
+ 1. **Emscripten SDK (emsdk):** Required for compiling C++ to WASM.
20
+ ```bash
21
+ git clone https://github.com/emscripten-core/emsdk.git
22
+ cd emsdk
23
+ ./emsdk install latest
24
+ ./emsdk activate latest
25
+ source ./emsdk_env.sh
26
+ ```
27
+ 2. **CMake:** Version 3.10 or higher.
28
+ 3. **Python 3:** For build scripts.
29
+
30
+ ## Building from Source
31
+
32
+ We provide a one-step build script `build.sh`.
33
+
34
+ ```bash
35
+ ./build.sh
36
+ ```
37
+
38
+ This will:
39
+ 1. Download dependencies (Pinocchio, Eigen3, Boost headers) automatically.
40
+ 2. Configure the project with `emcmake`.
41
+ 3. Compile to `build/pinocchio.js` and `build/pinocchio.wasm`.
42
+
43
+ ## Usage
44
+
45
+ ### 1. Browser
46
+
47
+ Serve the project root:
48
+ ```bash
49
+ python3 -m http.server 8080
50
+ ```
51
+ Open `http://localhost:8080/tests/browser/index.html`.
52
+
53
+ ### 2. Node.js
54
+
55
+ Install the XML DOM polyfill (required for URDF parsing in Node):
56
+ ```bash
57
+ npm install
58
+ ```
59
+
60
+ Run the smoke test:
61
+ ```bash
62
+ npm run test:smoke
63
+ ```
64
+
65
+ ## API Reference
66
+
67
+ ### `pin.Model`
68
+ - `nq`, `nv`, `njoints`: System dimensions.
69
+ - `addJoint(...)`: Adds a joint (Internal use).
70
+
71
+ ### `pin.Data`
72
+ - Constructed from `Model`.
73
+ - Stores scratchpad data (velocities, accelerations, forces).
74
+
75
+ ### Algorithms
76
+ - `pin.rnea(model, data, q, v, a)`: Recursive Newton-Euler Algorithm (Inverse Dynamics). Returns torques.
77
+ - `pin.forwardKinematics(model, data, q)`: Updates joint placements.
78
+ - `pin.centerOfMass(model, data, q)`: Computes CoM position.
79
+ - `pin.computeTotalMass(model)`: Returns total mass.
80
+
81
+ ### URDF Parser (`src/urdf-parser.mjs`)
82
+ - `parseURDF(xmlString)`: Parses raw XML into intermediate JS object.
83
+ - `buildPinocchioModel(pin, urdfData)`: Converts intermediate object to `pin.Model`.
84
+ * **Note:** Implements "Fixed Joint Reduction". Fixed joints in URDF are fused into their parent joints to ensure numerical stability and correct behavior in Pinocchio.
85
+
86
+ ## Testing
87
+
88
+ Run the full test suite (Node.js required):
89
+
90
+ ```bash
91
+ npm test
92
+ ```
93
+
94
+ This runs:
95
+ - Math tests (SE3, Inertia)
96
+ - Model tests (Joint creation)
97
+ - Algo tests (RNEA, COM)
98
+ - URDF tests (Loading real robot descriptions)
99
+
100
+ ## Contributing
101
+
102
+ 1. Fork the repository.
103
+ 2. Run `npm install` and `./build.sh` to ensure clean environment.
104
+ 3. Add tests for new features in `tests/`.
105
+ 4. Submit a Pull Request.
106
+
107
+ ## License
108
+
109
+ BSD-2-Clause
@@ -0,0 +1,54 @@
1
+ var PinocchioModule=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var k=moduleArg,aa=!!globalThis.window,ba=!!globalThis.WorkerGlobalScope,ca=globalThis.process?.versions?.node&&"renderer"!=globalThis.process?.type,da="./this.program",n=(b,a)=>{throw a;};"undefined"!=typeof __filename?_scriptName=__filename:ba&&(_scriptName=self.location.href);var q="",ea,u;
2
+ if(ca){var fs=require("node:fs");q=__dirname+"/";u=b=>{b=fa(b)?new URL(b):b;return fs.readFileSync(b)};ea=async b=>{b=fa(b)?new URL(b):b;return fs.readFileSync(b,void 0)};1<process.argv.length&&(da=process.argv[1].replace(/\\/g,"/"));process.argv.slice(2);n=(b,a)=>{process.exitCode=b;throw a;}}else if(aa||ba){try{q=(new URL(".",_scriptName)).href}catch{}ba&&(u=b=>{var a=new XMLHttpRequest;a.open("GET",b,!1);a.responseType="arraybuffer";a.send(null);return new Uint8Array(a.response)});ea=async b=>
3
+ {b=await fetch(b,{credentials:"same-origin"});if(b.ok)return b.arrayBuffer();throw Error(b.status+" : "+b.url);}}console.log.bind(console);var w=console.error.bind(console),z,A=!1,ha,fa=b=>b.startsWith("file://"),ia,ja,ka,B,C,D,E,G,la,ma,na,oa,pa=!1;function qa(){var b=ra.buffer;ka=new Int8Array(b);C=new Int16Array(b);B=new Uint8Array(b);D=new Uint16Array(b);E=new Int32Array(b);G=new Uint32Array(b);la=new Float32Array(b);ma=new Float64Array(b);na=new BigInt64Array(b);oa=new BigUint64Array(b)}
4
+ function sa(b){k.onAbort?.(b);b="Aborted("+b+")";w(b);A=!0;b=new WebAssembly.RuntimeError(b+". Build with -sASSERTIONS for more info.");ja?.(b);throw b;}var ta;async function ua(b){if(!z)try{var a=await ea(b);return new Uint8Array(a)}catch{}if(b==ta&&z)b=new Uint8Array(z);else if(u)b=u(b);else throw"both async and sync fetching of the wasm failed";return b}
5
+ async function va(b,a){try{var c=await ua(b);return await WebAssembly.instantiate(c,a)}catch(d){w(`failed to asynchronously prepare wasm: ${d}`),sa(d)}}async function wa(b){var a=ta;if(!z&&!ca)try{var c=fetch(a,{credentials:"same-origin"});return await WebAssembly.instantiateStreaming(c,b)}catch(d){w(`wasm streaming compile failed: ${d}`),w("falling back to ArrayBuffer instantiation")}return va(a,b)}
6
+ class xa{name="ExitStatus";constructor(b){this.message=`Program terminated with exit(${b})`;this.status=b}}var ya=b=>{for(;0<b.length;)b.shift()(k)},za=[],Aa=[],Ba=()=>{var b=k.preRun.shift();Aa.push(b)},Ca=!0;class Da{constructor(b){this.T=b-24}}var Ea=0,Fa=0,H=b=>{for(var a="";;){var c=B[b++];if(!c)return a;a+=String.fromCharCode(c)}},L={},M={},Ga={},N=class extends Error{constructor(b){super(b);this.name="BindingError"}},Ia=b=>{throw new N(b);};
7
+ function Ja(b,a,c={}){var d=a.name;if(!b)throw new N(`type "${d}" must have a positive integer typeid pointer`);if(M.hasOwnProperty(b)){if(c.ya)return;throw new N(`Cannot register type '${d}' twice`);}M[b]=a;delete Ga[b];L.hasOwnProperty(b)&&(a=L[b],delete L[b],a.forEach(e=>e()))}function O(b,a,c={}){return Ja(b,a,c)}
8
+ var Ka=(b,a,c)=>{switch(a){case 1:return c?d=>ka[d]:d=>B[d];case 2:return c?d=>C[d>>1]:d=>D[d>>1];case 4:return c?d=>E[d>>2]:d=>G[d>>2];case 8:return c?d=>na[d>>3]:d=>oa[d>>3];default:throw new TypeError(`invalid integer width (${a}): ${b}`);}},La=b=>{throw new N(b.R.V.S.name+" instance already deleted");},Ma=!1,Na=()=>{},P=b=>{if(!globalThis.FinalizationRegistry)return P=a=>a,b;Ma=new FinalizationRegistry(a=>{a=a.R;--a.count.value;0===a.count.value&&(a.X?a.ba.da(a.X):a.V.S.da(a.T))});P=a=>{var c=
9
+ a.R;c.X&&Ma.register(a,{R:c},a);return a};Na=a=>{Ma.unregister(a)};return P(b)},Oa=[];function Pa(){}
10
+ var Qa=(b,a)=>Object.defineProperty(a,"name",{value:b}),Ra={},Sa=(b,a,c)=>{if(void 0===b[a].Z){var d=b[a];b[a]=function(...e){if(!b[a].Z.hasOwnProperty(e.length))throw new N(`Function '${c}' called with an invalid number of arguments (${e.length}) - expects one of (${b[a].Z})!`);return b[a].Z[e.length].apply(this,e)};b[a].Z=[];b[a].Z[d.ga]=d}},Q=(b,a,c)=>{if(k.hasOwnProperty(b)){if(void 0===c||void 0!==k[b].Z&&void 0!==k[b].Z[c])throw new N(`Cannot register public name '${b}' twice`);Sa(k,b,b);if(k[b].Z.hasOwnProperty(c))throw new N(`Cannot register multiple overloads of a function with the same number of arguments (${c})!`);
11
+ k[b].Z[c]=a}else k[b]=a,k[b].ga=c},Ta=b=>{b=b.replace(/[^a-zA-Z0-9_]/g,"$");var a=b.charCodeAt(0);return 48<=a&&57>=a?`_${b}`:b};function Ua(b,a,c,d,e,f,h,g){this.name=b;this.constructor=a;this.fa=c;this.da=d;this.$=e;this.wa=f;this.ja=h;this.va=g}
12
+ var Va=(b,a,c)=>{for(;a!==c;){if(!a.ja)throw new N(`Expected null or instance of ${c.name}, got an instance of ${a.name}`);b=a.ja(b);a=a.$}return b},Wa=b=>{if(null===b)return"null";var a=typeof b;return"object"===a||"array"===a||"function"===a?b.toString():""+b};
13
+ function Xa(b,a){if(null===a){if(this.na)throw new N(`null is not a valid ${this.name}`);return 0}if(!a.R)throw new N(`Cannot pass "${Wa(a)}" as a ${this.name}`);if(!a.R.T)throw new N(`Cannot pass deleted object as a pointer of type ${this.name}`);return Va(a.R.T,a.R.V.S,this.S)}
14
+ function Ya(b,a){if(null===a){if(this.na)throw new N(`null is not a valid ${this.name}`);if(this.ma){var c=this.Aa();null!==b&&b.push(this.da,c);return c}return 0}if(!a||!a.R)throw new N(`Cannot pass "${Wa(a)}" as a ${this.name}`);if(!a.R.T)throw new N(`Cannot pass deleted object as a pointer of type ${this.name}`);if(!this.la&&a.R.V.la)throw new N(`Cannot convert argument of type ${a.R.ba?a.R.ba.name:a.R.V.name} to parameter type ${this.name}`);c=Va(a.R.T,a.R.V.S,this.S);if(this.ma){if(void 0===
15
+ a.R.X)throw new N("Passing raw pointer to smart pointer is illegal");switch(this.Ca){case 0:if(a.R.ba===this)c=a.R.X;else throw new N(`Cannot convert argument of type ${a.R.ba?a.R.ba.name:a.R.V.name} to parameter type ${this.name}`);break;case 1:c=a.R.X;break;case 2:if(a.R.ba===this)c=a.R.X;else{var d=a.clone();c=this.Ba(c,R(()=>d["delete"]()));null!==b&&b.push(this.da,c)}break;default:throw new N("Unsupported sharing policy");}}return c}
16
+ function Za(b,a){if(null===a){if(this.na)throw new N(`null is not a valid ${this.name}`);return 0}if(!a.R)throw new N(`Cannot pass "${Wa(a)}" as a ${this.name}`);if(!a.R.T)throw new N(`Cannot pass deleted object as a pointer of type ${this.name}`);if(a.R.V.la)throw new N(`Cannot convert argument of type ${a.R.V.name} to parameter type ${this.name}`);return Va(a.R.T,a.R.V.S,this.S)}function $a(b){return this.U(G[b>>2])}
17
+ var ab=(b,a,c)=>{if(a===c)return b;if(void 0===c.$)return null;b=ab(b,a,c.$);return null===b?null:c.va(b)},bb={},cb=(b,a)=>{if(void 0===a)throw new N("ptr should not be undefined");for(;b.$;)a=b.ja(a),b=b.$;return bb[a]},db=class extends Error{constructor(b){super(b);this.name="InternalError"}},eb=(b,a)=>{if(!a.V||!a.T)throw new db("makeClassHandle requires ptr and ptrType");if(!!a.ba!==!!a.X)throw new db("Both smartPtrType and smartPtr must be specified");a.count={value:1};return P(Object.create(b,
18
+ {R:{value:a,writable:!0}}))};function fb(b,a,c,d,e,f,h,g,l,p,m){this.name=b;this.S=a;this.na=c;this.la=d;this.ma=e;this.za=f;this.Ca=h;this.ra=g;this.Aa=l;this.Ba=p;this.da=m;e||void 0!==a.$?this.W=Ya:(this.W=d?Xa:Za,this.Y=null)}
19
+ var gb=(b,a,c)=>{if(!k.hasOwnProperty(b))throw new db("Replacing nonexistent public symbol");void 0!==k[b].Z&&void 0!==c?k[b].Z[c]=a:(k[b]=a,k[b].ga=c)},hb=[],S=(b,a)=>{b=H(b);var c;(c=hb[a])||(hb[a]=c=ib.get(a));if("function"!=typeof c)throw new N(`unknown function pointer with signature ${b}: ${a}`);return c};class jb extends Error{}
20
+ var lb=b=>{b=kb(b);var a=H(b);T(b);return a},U=(b,a)=>{function c(f){e[f]||M[f]||(Ga[f]?Ga[f].forEach(c):(d.push(f),e[f]=!0))}var d=[],e={};a.forEach(c);throw new jb(`${b}: `+d.map(lb).join([", "]));},V=(b,a,c)=>{function d(g){g=c(g);if(g.length!==b.length)throw new db("Mismatched type converter count");for(var l=0;l<b.length;++l)O(b[l],g[l])}b.forEach(g=>Ga[g]=a);var e=Array(a.length),f=[],h=0;for(let [g,l]of a.entries())M.hasOwnProperty(l)?e[g]=M[l]:(f.push(l),L.hasOwnProperty(l)||(L[l]=[]),L[l].push(()=>
21
+ {e[g]=M[l];++h;h===f.length&&d(e)}));0===f.length&&d(e)},mb=b=>{for(;b.length;){var a=b.pop();b.pop()(a)}};function nb(b){for(var a=1;a<b.length;++a)if(null!==b[a]&&void 0===b[a].Y)return!0;return!1}
22
+ function ob(b,a,c,d,e){var f=a.length;if(2>f)throw new N("argTypes array size mismatch! Must at least get return value and 'this' types!");var h=null!==a[1]&&!1,g=nb(a),l=!a[0].qa,p=a[0],m=a[1];c=[b,Ia,c,d,mb,p.U.bind(p),m?.W.bind(m)];for(d=2;d<f;++d)p=a[d],c.push(p.W.bind(p));if(!g)for(d=h?1:2;d<a.length;++d)null!==a[d].Y&&c.push(a[d].Y);g=nb(a);d=a.length-2;m=[];p=["fn"];h&&p.push("thisWired");for(f=0;f<d;++f)m.push(`arg${f}`),p.push(`arg${f}Wired`);m=m.join(",");p=p.join(",");m=`return function (${m}) {\n`;
23
+ g&&(m+="var destructors = [];\n");var v=g?"destructors":"null",r="humanName throwBindingError invoker fn runDestructors fromRetWire toClassParamWire".split(" ");h&&(m+=`var thisWired = toClassParamWire(${v}, this);\n`);for(f=0;f<d;++f){var t=`toArg${f}Wire`;m+=`var arg${f}Wired = ${t}(${v}, arg${f});\n`;r.push(t)}m+=(l||e?"var rv = ":"")+`invoker(${p});\n`;if(g)m+="runDestructors(destructors);\n";else for(f=h?1:2;f<a.length;++f)e=1===f?"thisWired":"arg"+(f-2)+"Wired",null!==a[f].Y&&(m+=`${e}_dtor(${e});\n`,
24
+ r.push(`${e}_dtor`));l&&(m+="var ret = fromRetWire(rv);\nreturn ret;\n");a=(new Function(r,m+"}\n"))(...c);return Qa(b,a)}
25
+ var pb=(b,a)=>{for(var c=[],d=0;d<b;d++)c.push(G[a+4*d>>2]);return c},qb=b=>{b=b.trim();const a=b.indexOf("(");return-1===a?b:b.slice(0,a)},rb=(b,a,c)=>{if(!(b instanceof Object))throw new N(`${c} with invalid "this": ${b}`);if(!(b instanceof a.S.constructor))throw new N(`${c} incompatible with "this" of type ${b.constructor.name}`);if(!b.R.T)throw new N(`cannot call emscripten binding method ${c} on deleted object`);return Va(b.R.T,b.R.V.S,a.S)},tb=[],W=[0,1,,1,null,1,!0,1,!1,1],ub=b=>{9<b&&0===
26
+ --W[b+1]&&(W[b]=void 0,tb.push(b))},X=b=>{if(!b)throw new N(`Cannot use deleted val. handle = ${b}`);return W[b]},R=b=>{switch(b){case void 0:return 2;case null:return 4;case !0:return 6;case !1:return 8;default:const a=tb.pop()||W.length;W[a]=b;W[a+1]=1;return a}},vb={name:"emscripten::val",U:b=>{var a=X(b);ub(b);return a},W:(b,a)=>R(a),aa:$a,Y:null},wb=(b,a,c)=>{switch(a){case 1:return c?function(d){return this.U(ka[d])}:function(d){return this.U(B[d])};case 2:return c?function(d){return this.U(C[d>>
27
+ 1])}:function(d){return this.U(D[d>>1])};case 4:return c?function(d){return this.U(E[d>>2])}:function(d){return this.U(G[d>>2])};default:throw new TypeError(`invalid integer width (${a}): ${b}`);}},xb=(b,a)=>{var c=M[b];if(void 0===c)throw b=`${a} has unknown type ${lb(b)}`,new N(b);return c},yb=(b,a)=>{switch(a){case 4:return function(c){return this.U(la[c>>2])};case 8:return function(c){return this.U(ma[c>>3])};default:throw new TypeError(`invalid float width (${a}): ${b}`);}},Y=(b,a,c)=>{var d=
28
+ B;if(!(0<c))return 0;var e=a;c=a+c-1;for(var f=0;f<b.length;++f){var h=b.codePointAt(f);if(127>=h){if(a>=c)break;d[a++]=h}else if(2047>=h){if(a+1>=c)break;d[a++]=192|h>>6;d[a++]=128|h&63}else if(65535>=h){if(a+2>=c)break;d[a++]=224|h>>12;d[a++]=128|h>>6&63;d[a++]=128|h&63}else{if(a+3>=c)break;d[a++]=240|h>>18;d[a++]=128|h>>12&63;d[a++]=128|h>>6&63;d[a++]=128|h&63;f++}}d[a]=0;return a-e},zb=b=>{for(var a=0,c=0;c<b.length;++c){var d=b.charCodeAt(c);127>=d?a++:2047>=d?a+=2:55296<=d&&57343>=d?(a+=4,++c):
29
+ a+=3}return a},Ab=globalThis.TextDecoder&&new TextDecoder,Bb=(b,a,c,d)=>{c=a+c;if(d)return c;for(;b[a]&&!(a>=c);)++a;return a},Cb=(b=0,a)=>{var c=B;a=Bb(c,b,a,!0);if(16<a-b&&c.buffer&&Ab)return Ab.decode(c.subarray(b,a));for(var d="";b<a;){var e=c[b++];if(e&128){var f=c[b++]&63;if(192==(e&224))d+=String.fromCharCode((e&31)<<6|f);else{var h=c[b++]&63;e=224==(e&240)?(e&15)<<12|f<<6|h:(e&7)<<18|f<<12|h<<6|c[b++]&63;65536>e?d+=String.fromCharCode(e):(e-=65536,d+=String.fromCharCode(55296|e>>10,56320|
30
+ e&1023))}}else d+=String.fromCharCode(e)}return d},Db=globalThis.TextDecoder?new TextDecoder("utf-16le"):void 0,Eb=(b,a,c)=>{b>>=1;a=Bb(D,b,a/2,c);if(16<a-b&&Db)return Db.decode(D.subarray(b,a));for(c="";b<a;++b)c+=String.fromCharCode(D[b]);return c},Fb=(b,a,c)=>{c??=2147483647;if(2>c)return 0;c-=2;var d=a;c=c<2*b.length?c/2:b.length;for(var e=0;e<c;++e)C[a>>1]=b.charCodeAt(e),a+=2;C[a>>1]=0;return a-d},Gb=b=>2*b.length,Hb=(b,a,c)=>{var d="";b>>=2;for(var e=0;!(e>=a/4);e++){var f=G[b+e];if(!f&&!c)break;
31
+ d+=String.fromCodePoint(f)}return d},Ib=(b,a,c)=>{c??=2147483647;if(4>c)return 0;var d=a;c=d+c-4;for(var e=0;e<b.length;++e){var f=b.codePointAt(e);65535<f&&e++;E[a>>2]=f;a+=4;if(a+4>c)break}E[a>>2]=0;return a-d},Jb=b=>{for(var a=0,c=0;c<b.length;++c)65535<b.codePointAt(c)&&c++,a+=4;return a},Kb=0,Lb=[],Mb=b=>{var a=Lb.length;Lb.push(b);return a},Nb=(b,a)=>{for(var c=Array(b),d=0;d<b;++d)c[d]=xb(G[a+4*d>>2],`parameter ${d}`);return c},Ob=(b,a,c)=>{var d=[];b=b(d,c);d.length&&(G[a>>2]=R(d));return b},
32
+ Pb={},Qb=b=>{var a=Pb[b];return void 0===a?H(b):a},Z={},Rb=b=>{ha=b;Ca||0<Kb||(k.onExit?.(b),A=!0);n(b,new xa(b))},Sb=b=>{if(!A)try{b()}catch(a){a instanceof xa||"unwind"==a||n(1,a)}finally{if(!(Ca||0<Kb))try{ha=b=ha,Rb(b)}catch(a){a instanceof xa||"unwind"==a||n(1,a)}}},Tb={},Vb=()=>{if(!Ub){var b={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8",_:da||"./this.program"},a;for(a in Tb)void 0===Tb[a]?delete b[a]:
33
+ b[a]=Tb[a];var c=[];for(a in b)c.push(`${a}=${b[a]}`);Ub=c}return Ub},Ub;
34
+ (()=>{let b=Pa.prototype;Object.assign(b,{isAliasOf:function(c){if(!(this instanceof Pa&&c instanceof Pa))return!1;var d=this.R.V.S,e=this.R.T;c.R=c.R;var f=c.R.V.S;for(c=c.R.T;d.$;)e=d.ja(e),d=d.$;for(;f.$;)c=f.ja(c),f=f.$;return d===f&&e===c},clone:function(){this.R.T||La(this);if(this.R.ia)return this.R.count.value+=1,this;var c=P,d=Object,e=d.create,f=Object.getPrototypeOf(this),h=this.R;c=c(e.call(d,f,{R:{value:{count:h.count,ha:h.ha,ia:h.ia,T:h.T,V:h.V,X:h.X,ba:h.ba}}}));c.R.count.value+=1;
35
+ c.R.ha=!1;return c},["delete"](){this.R.T||La(this);if(this.R.ha&&!this.R.ia)throw new N("Object already scheduled for deletion");Na(this);var c=this.R;--c.count.value;0===c.count.value&&(c.X?c.ba.da(c.X):c.V.S.da(c.T));this.R.ia||(this.R.X=void 0,this.R.T=void 0)},isDeleted:function(){return!this.R.T},deleteLater:function(){this.R.T||La(this);if(this.R.ha&&!this.R.ia)throw new N("Object already scheduled for deletion");Oa.push(this);this.R.ha=!0;return this}});const a=Symbol.dispose;a&&(b[a]=b["delete"])})();
36
+ Object.assign(fb.prototype,{xa(b){this.ra&&(b=this.ra(b));return b},pa(b){this.da?.(b)},aa:$a,U:function(b){function a(){return this.ma?eb(this.S.fa,{V:this.za,T:c,ba:this,X:b}):eb(this.S.fa,{V:this,T:b})}var c=this.xa(b);if(!c)return this.pa(b),null;var d=cb(this.S,c);if(void 0!==d){if(0===d.R.count.value)return d.R.T=c,d.R.X=b,d.clone();d=d.clone();this.pa(b);return d}d=this.S.wa(c);d=Ra[d];if(!d)return a.call(this);d=this.la?d.ua:d.pointerType;var e=ab(c,this.S,d.S);return null===e?a.call(this):
37
+ this.ma?eb(d.S.fa,{V:d,T:e,ba:this,X:b}):eb(d.S.fa,{V:d,T:e})}});k.noExitRuntime&&(Ca=k.noExitRuntime);k.printErr&&(w=k.printErr);k.wasmBinary&&(z=k.wasmBinary);k.thisProgram&&(da=k.thisProgram);if(k.preInit)for("function"==typeof k.preInit&&(k.preInit=[k.preInit]);0<k.preInit.length;)k.preInit.shift()();
38
+ var kb,T,Wb,Xb,ra,ib,Yb={a:(b,a,c)=>{var d=new Da(b);G[d.T+16>>2]=0;G[d.T+4>>2]=a;G[d.T+8>>2]=c;Ea=b;Fa++;throw Ea;},B:()=>sa(""),s:(b,a,c,d,e)=>{a=H(a);d=0n===d;let f=h=>h;if(d){const h=8*c;f=g=>BigInt.asUintN(h,g);e=f(e)}O(b,{name:a,U:f,W:(h,g)=>{"number"==typeof g&&(g=BigInt(g));return g},aa:Ka(a,c,!d),Y:null})},F:(b,a,c,d)=>{a=H(a);O(b,{name:a,U:function(e){return!!e},W:function(e,f){return f?c:d},aa:function(e){return this.U(B[e])},Y:null})},j:(b,a,c,d,e,f,h,g,l,p,m,v,r)=>{m=H(m);f=S(e,f);g&&=
39
+ S(h,g);p&&=S(l,p);r=S(v,r);var t=Ta(m);Q(t,function(){U(`Cannot construct ${m} due to unbound types`,[d])});V([b,a,c],d?[d]:[],x=>{x=x[0];if(d){var I=x.S;var J=I.fa}else J=Pa.prototype;x=Qa(m,function(...Ha){if(Object.getPrototypeOf(this)!==K)throw new N(`Use 'new' to construct ${m}`);if(void 0===y.ea)throw new N(`${m} has no accessible constructor`);var sb=y.ea[Ha.length];if(void 0===sb)throw new N(`Tried to invoke ctor of ${m} with invalid number of parameters (${Ha.length}) - expected (${Object.keys(y.ea).toString()}) parameters instead!`);
40
+ return sb.apply(this,Ha)});var K=Object.create(J,{constructor:{value:x}});x.prototype=K;var y=new Ua(m,x,K,r,I,f,g,p);if(y.$){var F;(F=y.$).ka??(F.ka=[]);y.$.ka.push(y)}I=new fb(m,y,!0,!1,!1);F=new fb(m+"*",y,!1,!1,!1);J=new fb(m+" const*",y,!1,!0,!1);Ra[b]={pointerType:F,ua:J};gb(t,x);return[I,F,J]})},l:(b,a,c,d,e,f,h,g)=>{var l=pb(c,d);a=H(a);a=qb(a);f=S(e,f);V([],[b],p=>{function m(){U(`Cannot call ${v} due to unbound types`,l)}p=p[0];var v=`${p.name}.${a}`;a.startsWith("@@")&&(a=Symbol[a.substring(2)]);
41
+ var r=p.S.constructor;void 0===r[a]?(m.ga=c-1,r[a]=m):(Sa(r,a,v),r[a].Z[c-1]=m);V([],l,t=>{t=[t[0],null].concat(t.slice(1));t=ob(v,t,f,h,g);void 0===r[a].Z?(t.ga=c-1,r[a]=t):r[a].Z[c-1]=t;if(p.S.ka)for(const x of p.S.ka)x.constructor.hasOwnProperty(a)||(x.constructor[a]=t);return[]});return[]})},t:(b,a,c,d,e,f)=>{var h=pb(a,c);e=S(d,e);V([],[b],g=>{g=g[0];var l=`constructor ${g.name}`;void 0===g.S.ea&&(g.S.ea=[]);if(void 0!==g.S.ea[a-1])throw new N(`Cannot register multiple constructors with identical number of parameters (${a-
42
+ 1}) for class '${g.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`);g.S.ea[a-1]=()=>{U(`Cannot construct ${g.name} due to unbound types`,h)};V([],h,p=>{p.splice(1,0,null);g.S.ea[a-1]=ob(l,p,e,f);return[]});return[]})},k:(b,a,c,d,e,f,h,g,l,p)=>{a=H(a);e=S(d,e);V([],[b],m=>{m=m[0];var v=`${m.name}.${a}`,r={get(){U(`Cannot access ${v} due to unbound types`,[c,h])},enumerable:!0,configurable:!0};r.set=l?()=>U(`Cannot access ${v} due to unbound types`,
43
+ [c,h]):()=>{throw new N(v+" is a read-only property");};Object.defineProperty(m.S.fa,a,r);V([],l?[c,h]:[c],t=>{var x=t[0],I={get(){var K=rb(this,m,v+" getter");return x.U(e(f,K))},enumerable:!0};if(l){l=S(g,l);var J=t[1];I.set=function(K){var y=rb(this,m,v+" setter"),F=[];l(p,y,J.W(F,K));mb(F)}}Object.defineProperty(m.S.fa,a,I);return[]});return[]})},D:b=>O(b,vb),u:(b,a,c,d,e)=>{a=H(a);e=0===e?"object":1===e?"number":"string";switch(e){case "object":function h(){}h.values={};O(b,{name:a,constructor:h,
44
+ valueType:e,U:function(g){return this.constructor.values[g]},W:(g,l)=>l.value,aa:wb(a,c,d),Y:null});Q(a,h);break;case "number":var f={};O(b,{name:a,oa:f,valueType:e,U:g=>g,W:(g,l)=>l,aa:wb(a,c,d),Y:null});Q(a,f);delete k[a].ga;break;case "string":f={},O(b,{name:a,ta:{},sa:{},oa:f,valueType:e,U:function(g){return this.sa[g]},W:function(g,l){return this.ta[l]},aa:wb(a,c,d),Y:null}),Q(a,f),delete k[a].ga}},p:(b,a,c)=>{var d=xb(b,"enum");a=H(a);switch(d.valueType){case "object":b=d.constructor;d=Object.create(d.constructor.prototype,
45
+ {value:{value:c},constructor:{value:Qa(`${d.name}_${a}`,function(){})}});b.values[c]=d;b[a]=d;break;case "number":d.oa[a]=c;break;case "string":d.ta[a]=c,d.sa[c]=a,d.oa[a]=a}},r:(b,a,c)=>{a=H(a);O(b,{name:a,U:d=>d,W:(d,e)=>e,aa:yb(a,c),Y:null})},f:(b,a,c,d,e,f,h)=>{var g=pb(a,c);b=H(b);b=qb(b);e=S(d,e);Q(b,function(){U(`Cannot call ${b} due to unbound types`,g)},a-1);V([],g,l=>{l=[l[0],null].concat(l.slice(1));gb(b,ob(b,l,e,f,h),a-1);return[]})},i:(b,a,c,d,e)=>{a=H(a);let f=g=>g;if(0===d){var h=32-
46
+ 8*c;f=g=>g<<h>>>h;e=f(e)}O(b,{name:a,U:f,W:(g,l)=>l,aa:Ka(a,c,0!==d),Y:null})},h:(b,a,c)=>{function d(f){return new e(ka.buffer,G[f+4>>2],G[f>>2])}var e=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array][a];c=H(c);O(b,{name:c,U:d,aa:d},{ya:!0})},E:(b,a)=>{a=H(a);O(b,{name:a,U(c){var d=(d=c+4)?Cb(d,G[c>>2]):"";T(c);return d},W(c,d){d instanceof ArrayBuffer&&(d=new Uint8Array(d));var e="string"==typeof d;if(!(e||ArrayBuffer.isView(d)&&
47
+ 1==d.BYTES_PER_ELEMENT))throw new N("Cannot pass non-string to std::string");var f=e?zb(d):d.length;var h=Wb(4+f+1),g=h+4;G[h>>2]=f;e?Y(d,g,f+1):B.set(d,g);null!==c&&c.push(T,h);return h},aa:$a,Y(c){T(c)}})},o:(b,a,c)=>{c=H(c);if(2===a){var d=Eb;var e=Fb;var f=Gb}else d=Hb,e=Ib,f=Jb;O(b,{name:c,U:h=>{var g=d(h+4,G[h>>2]*a,!0);T(h);return g},W:(h,g)=>{if("string"!=typeof g)throw new N(`Cannot pass non-string to C++ string type ${c}`);var l=f(g),p=Wb(4+l+a);G[p>>2]=l/a;e(g,p+4,l+a);null!==h&&h.push(T,
48
+ p);return p},aa:$a,Y(h){T(h)}})},G:(b,a)=>{a=H(a);O(b,{qa:!0,name:a,U:()=>{},W:()=>{}})},w:()=>{Ca=!1;Kb=0},e:(b,a,c)=>{var [d,...e]=Nb(b,a);a=d.W.bind(d);var f=e.map(l=>l.aa.bind(l));b--;var h={toValue:X};b=f.map((l,p)=>{var m=`argFromPtr${p}`;h[m]=l;return`${m}(args${p?"+"+8*p:""})`});switch(c){case 0:var g="toValue(handle)";break;case 2:g="new (toValue(handle))";break;case 3:g="";break;case 1:h.getStringOrSymbol=Qb,g="toValue(handle)[getStringOrSymbol(methodName)]"}g+=`(${b})`;d.qa||(h.toReturnWire=
49
+ a,h.emval_returnValue=Ob,g=`return emval_returnValue(toReturnWire, destructorsRef, ${g})`);g=`return function (handle, methodName, destructorsRef, args) {\n${g}\n}`;c=(new Function(Object.keys(h),g))(...Object.values(h));g=`methodCaller<(${e.map(l=>l.name)}) => ${d.name}>`;return Mb(Qa(g,c))},b:ub,n:b=>{if(!b)return R(globalThis);b=Qb(b);return R(globalThis[b])},q:(b,a)=>{b=X(b);a=X(a);return R(b[a])},g:b=>{9<b&&(W[b+1]+=1)},d:(b,a,c,d,e)=>Lb[b](a,c,d,e),H:b=>R(Qb(b)),c:b=>{var a=X(b);mb(a);ub(b)},
50
+ m:(b,a,c)=>{b=X(b);a=X(a);c=X(c);b[a]=c},x:(b,a)=>{Z[b]&&(clearTimeout(Z[b].id),delete Z[b]);if(!a)return 0;var c=setTimeout(()=>{delete Z[b];Sb(()=>Xb(b,performance.now()))},a);Z[b]={id:c,Da:a};return 0},y:(b,a,c,d)=>{var e=(new Date).getFullYear(),f=(new Date(e,0,1)).getTimezoneOffset();e=(new Date(e,6,1)).getTimezoneOffset();G[b>>2]=60*Math.max(f,e);E[a>>2]=Number(f!=e);a=h=>{var g=Math.abs(h);return`UTC${0<=h?"-":"+"}${String(Math.floor(g/60)).padStart(2,"0")}${String(g%60).padStart(2,"0")}`};
51
+ b=a(f);a=a(e);e<f?(Y(b,c,17),Y(a,d,17)):(Y(b,d,17),Y(a,c,17))},C:b=>{var a=B.length;b>>>=0;if(2147483648<b)return!1;for(var c=1;4>=c;c*=2){var d=a*(1+.2/c);d=Math.min(d,b+100663296);a:{d=(Math.min(2147483648,65536*Math.ceil(Math.max(b,d)/65536))-ra.buffer.byteLength+65535)/65536|0;try{ra.grow(d);qa();var e=1;break a}catch(f){}e=void 0}if(e)return!0}return!1},z:(b,a)=>{var c=0,d=0,e;for(e of Vb()){var f=a+c;G[b+d>>2]=f;c+=Y(e,f,Infinity)+1;d+=4}return 0},A:(b,a)=>{var c=Vb();G[b>>2]=c.length;b=0;for(var d of c)b+=
52
+ zb(d)+1;G[a>>2]=b;return 0},v:Rb},Zb;Zb=await (async function(){function b(c){c=Zb=c.exports;kb=c.K;k.__ZN5boost13serialization16singleton_module8get_lockEv=c.L;T=c.M;Wb=c.O;Xb=c.P;ra=c.I;ib=c.N;qa();return Zb}var a={a:Yb};if(k.instantiateWasm)return new Promise(c=>{k.instantiateWasm(a,(d,e)=>{c(b(d,e))})});ta??=k.locateFile?k.locateFile("pinocchio.wasm",q):q+"pinocchio.wasm";return b((await wa(a)).instance)}());
53
+ (function(){function b(){k.calledRun=!0;if(!A){pa=!0;Zb.J();ia?.(k);k.onRuntimeInitialized?.();if(k.postRun)for("function"==typeof k.postRun&&(k.postRun=[k.postRun]);k.postRun.length;){var a=k.postRun.shift();za.push(a)}ya(za)}}if(k.preRun)for("function"==typeof k.preRun&&(k.preRun=[k.preRun]);k.preRun.length;)Ba();ya(Aa);k.setStatus?(k.setStatus("Running..."),setTimeout(()=>{setTimeout(()=>k.setStatus(""),1);b()},1)):b()})();pa?moduleRtn=k:moduleRtn=new Promise((b,a)=>{ia=b;ja=a});
54
+ ;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=PinocchioModule;module.exports.default=PinocchioModule}else if(typeof define==="function"&&define["amd"])define([],()=>PinocchioModule);
Binary file
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "pinocchio-js",
3
+ "version": "1.0.1",
4
+ "description": "High-performance WebAssembly bindings for the Pinocchio rigid body dynamics library",
5
+ "keywords": [
6
+ "pinocchio",
7
+ "wasm",
8
+ "webassembly",
9
+ "robotics",
10
+ "dynamics",
11
+ "kinematics",
12
+ "rigid-body-dynamics",
13
+ "physics-engine"
14
+ ],
15
+ "main": "build/pinocchio.js",
16
+ "files": [
17
+ "build/pinocchio.js",
18
+ "build/pinocchio.wasm",
19
+ "src/urdf-parser.mjs",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "test": "node tests/runner.js",
25
+ "test:smoke": "node tests/smoke_test.js"
26
+ },
27
+ "dependencies": {
28
+ "xmldom": "^0.6.0"
29
+ },
30
+ "type": "commonjs",
31
+ "directories": {
32
+ "example": "examples",
33
+ "test": "tests"
34
+ },
35
+ "devDependencies": {},
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/Mostafasaad1/pinocchio-js.git"
39
+ },
40
+ "author": "Mostafa Saad",
41
+ "license": "BSD-2-Clause",
42
+ "bugs": {
43
+ "url": "https://github.com/Mostafasaad1/pinocchio-js/issues"
44
+ },
45
+ "homepage": "https://github.com/Mostafasaad1/pinocchio-js#readme"
46
+ }
@@ -0,0 +1,358 @@
1
+ /**
2
+ * ──────────────────────────────────────────────────────────────
3
+ * Pinocchio WASM — URDF Parser
4
+ * Parses URDF XML using browser's DOMParser and builds a
5
+ * Pinocchio model via the Embind API.
6
+ * ──────────────────────────────────────────────────────────────
7
+ */
8
+
9
+ /**
10
+ * Parse a URDF XML string into a structured JavaScript object.
11
+ *
12
+ * @param {string} urdfString - The URDF XML content
13
+ * @returns {{ links: Object[], joints: Object[], rootLink: string }}
14
+ */
15
+ export function parseURDF(urdfString) {
16
+ const parser = new DOMParser();
17
+ const doc = parser.parseFromString(urdfString, 'text/xml');
18
+
19
+ const errors = doc.getElementsByTagName('parsererror');
20
+ if (errors.length > 0) {
21
+ throw new Error(`URDF parse error: ${errors[0].textContent}`);
22
+ }
23
+
24
+ const robots = doc.getElementsByTagName('robot');
25
+ if (robots.length === 0) throw new Error('No <robot> element found in URDF');
26
+ const robot = robots[0];
27
+
28
+ const robotName = robot.getAttribute('name') || 'unnamed';
29
+
30
+ // ── Parse links ──
31
+ const links = {};
32
+ const linkNodes = Array.from(robot.getElementsByTagName('link'));
33
+ for (const linkEl of linkNodes) {
34
+ const name = linkEl.getAttribute('name');
35
+ const link = { name, mass: 0, com: [0, 0, 0], inertia: [0, 0, 0, 0, 0, 0] };
36
+
37
+ const inertialNodes = linkEl.getElementsByTagName('inertial');
38
+ if (inertialNodes.length > 0) {
39
+ const inertialEl = inertialNodes[0];
40
+ // Mass
41
+ const massNodes = inertialEl.getElementsByTagName('mass');
42
+ if (massNodes.length > 0) {
43
+ link.mass = parseFloat(massNodes[0].getAttribute('value')) || 0;
44
+ }
45
+
46
+ // Center of mass origin
47
+ const originNodes = inertialEl.getElementsByTagName('origin');
48
+ if (originNodes.length > 0) {
49
+ link.com = parseXyz(originNodes[0]);
50
+ }
51
+
52
+ // Inertia tensor (6 unique elements)
53
+ const inertiaNodes = inertialEl.getElementsByTagName('inertia');
54
+ if (inertiaNodes.length > 0) {
55
+ const inertiaEl = inertiaNodes[0];
56
+ link.inertia = [
57
+ parseFloat(inertiaEl.getAttribute('ixx')) || 0,
58
+ parseFloat(inertiaEl.getAttribute('ixy')) || 0,
59
+ parseFloat(inertiaEl.getAttribute('ixz')) || 0,
60
+ parseFloat(inertiaEl.getAttribute('iyy')) || 0,
61
+ parseFloat(inertiaEl.getAttribute('iyz')) || 0,
62
+ parseFloat(inertiaEl.getAttribute('izz')) || 0,
63
+ ];
64
+ }
65
+ }
66
+
67
+ links[name] = link;
68
+ }
69
+
70
+ // ── Parse joints ──
71
+ const joints = [];
72
+ const childToJoint = {}; // child_link_name → joint
73
+
74
+ const jointNodes = Array.from(robot.getElementsByTagName('joint'));
75
+ for (const jointEl of jointNodes) {
76
+ const joint = {
77
+ name: jointEl.getAttribute('name'),
78
+ type: jointEl.getAttribute('type'),
79
+ parentLink: '',
80
+ childLink: '',
81
+ origin: { xyz: [0, 0, 0], rpy: [0, 0, 0] },
82
+ axis: [0, 0, 1], // default Z axis
83
+ limits: { lower: 0, upper: 0, effort: 0, velocity: 0 }
84
+ };
85
+
86
+ const parentNodes = jointEl.getElementsByTagName('parent');
87
+ if (parentNodes.length > 0) joint.parentLink = parentNodes[0].getAttribute('link');
88
+
89
+ const childNodes = jointEl.getElementsByTagName('child');
90
+ if (childNodes.length > 0) joint.childLink = childNodes[0].getAttribute('link');
91
+
92
+ const originNodes = jointEl.getElementsByTagName('origin');
93
+ if (originNodes.length > 0) {
94
+ joint.origin.xyz = parseXyz(originNodes[0]);
95
+ joint.origin.rpy = parseRpy(originNodes[0]);
96
+ }
97
+
98
+ const axisNodes = jointEl.getElementsByTagName('axis');
99
+ if (axisNodes.length > 0) {
100
+ joint.axis = parseXyz(axisNodes[0]);
101
+ }
102
+
103
+ const limitNodes = jointEl.getElementsByTagName('limit');
104
+ if (limitNodes.length > 0) {
105
+ const limitEl = limitNodes[0];
106
+ joint.limits.lower = parseFloat(limitEl.getAttribute('lower')) || 0;
107
+ joint.limits.upper = parseFloat(limitEl.getAttribute('upper')) || 0;
108
+ joint.limits.effort = parseFloat(limitEl.getAttribute('effort')) || 0;
109
+ joint.limits.velocity = parseFloat(limitEl.getAttribute('velocity')) || 0;
110
+ }
111
+
112
+ joints.push(joint);
113
+ childToJoint[joint.childLink] = joint;
114
+ }
115
+
116
+ // ── Find root link (a link that is never a child) ──
117
+ const childLinks = new Set(joints.map(j => j.childLink));
118
+ const rootLink = Object.keys(links).find(name => !childLinks.has(name)) || '';
119
+
120
+ return { robotName, links, joints, rootLink };
121
+ }
122
+
123
+
124
+ /**
125
+ * Build a Pinocchio Model from parsed URDF data.
126
+ *
127
+ * Implements "Fixed Joint Reduction" in JavaScript:
128
+ * URDF Fixed joints are NOT added to the Pinocchio model as joints.
129
+ * Instead, their kinematic transformation is composed into the placement
130
+ * of the child link (and its subsequent children).
131
+ *
132
+ * @param {Object} pin - The Pinocchio WASM module
133
+ * @param {Object} urdfData - Output of parseURDF()
134
+ * @returns {Object} The constructed Pinocchio Model
135
+ */
136
+ export function buildPinocchioModel(pin, urdfData) {
137
+ const { links, joints, rootLink } = urdfData;
138
+ const model = new pin.Model();
139
+
140
+ // Map link names → Pinocchio parent joint ID
141
+ // For fixed joints, the child link shares the same parent joint ID as its parent link.
142
+ const linkToParentJointId = {};
143
+ linkToParentJointId[rootLink] = 0; // Universe
144
+
145
+ // Map link names → SE3 transform relative to the parent joint frame
146
+ // For moving joints, this resets to Identity for the child (since the joint defines the frame).
147
+ // For fixed joints, this accumulates the offset.
148
+ const linkToJointTransform = {};
149
+ linkToJointTransform[rootLink] = createIdentityTransform();
150
+
151
+ // BFS Queue
152
+ const queue = [rootLink];
153
+ const visited = new Set([rootLink]);
154
+
155
+ while (queue.length > 0) {
156
+ const parentLinkName = queue.shift();
157
+ const parentJointId = linkToParentJointId[parentLinkName];
158
+
159
+ // Transform of the parent link frame relative to the parent joint frame
160
+ const parentOffset = linkToJointTransform[parentLinkName] || createIdentityTransform();
161
+
162
+ // Find all child joints
163
+ const childJoints = joints.filter(j => j.parentLink === parentLinkName);
164
+
165
+ for (const joint of childJoints) {
166
+ if (visited.has(joint.childLink)) continue;
167
+ // console.log(`Processing joint: ${joint.name} (type: ${joint.type})`);
168
+ visited.add(joint.childLink);
169
+
170
+ // 1. Calculate absolute placement of this joint/link relative to the PARENT JOINT frame
171
+ // T_child = T_parent_offset * T_joint_origin
172
+ const jointOrigin = createTransformFromXyzRpy(joint.origin.xyz, joint.origin.rpy);
173
+ const placement = composeTransforms(parentOffset, jointOrigin);
174
+
175
+ // 2. Handle Joint Type
176
+ if (joint.type === 'fixed') {
177
+ // REDUCTION: Do NOT add a Pinocchio joint.
178
+ // The child link is rigidly attached to the parent joint.
179
+ // We propagate the placement validation.
180
+
181
+ linkToParentJointId[joint.childLink] = parentJointId;
182
+ linkToJointTransform[joint.childLink] = placement;
183
+
184
+ // Add inertia to the parent joint (transformed by placement)
185
+ addBodyInertia(pin, model, links[joint.childLink], parentJointId, placement);
186
+
187
+ } else {
188
+ // MOVING JOINT: Add a real joint to Pinocchio
189
+
190
+ // Convert JS transform to Pinocchio SE3 object
191
+ const placementSE3 = toPinocchioSE3(pin, placement);
192
+ const jointModel = createJointModel(pin, joint.type, joint.axis);
193
+
194
+ let jointId;
195
+ if (joint.limits.effort > 0 || joint.limits.velocity > 0) {
196
+ const nq = (joint.type === 'floating') ? 7 : 1;
197
+ const nv = (joint.type === 'floating') ? 6 : 1;
198
+ jointId = pin.addJointWithLimits(
199
+ model, parentJointId, jointModel, placementSE3, joint.name,
200
+ new Float64Array(nv).fill(joint.limits.effort),
201
+ new Float64Array(nv).fill(joint.limits.velocity),
202
+ new Float64Array(nq).fill(joint.limits.lower),
203
+ new Float64Array(nq).fill(joint.limits.upper)
204
+ );
205
+ } else {
206
+ jointId = pin.addJoint(model, parentJointId, jointModel, placementSE3, joint.name);
207
+ }
208
+
209
+ // Register new joint
210
+ linkToParentJointId[joint.childLink] = jointId;
211
+ // The new link frame is the new joint frame (Identity offset)
212
+ linkToJointTransform[joint.childLink] = createIdentityTransform();
213
+
214
+ // Add inertia to the NEW joint (Identity placement)
215
+ addBodyInertia(pin, model, links[joint.childLink], jointId, createIdentityTransform());
216
+ }
217
+
218
+ queue.push(joint.childLink);
219
+ }
220
+ }
221
+
222
+ return model;
223
+ }
224
+
225
+ // ─── Internal Helpers: Physics ───────────────────────────────
226
+
227
+ function addBodyInertia(pin, model, linkData, jointId, transform) {
228
+ if (!linkData || linkData.mass <= 0) return;
229
+
230
+ // Construct Pinocchio Inertia object
231
+ const inertia = pin.Inertia.fromMassComInertia(
232
+ linkData.mass,
233
+ linkData.com,
234
+ linkData.inertia
235
+ );
236
+
237
+ // Convert JS transform to Pinocchio SE3
238
+ const placement = toPinocchioSE3(pin, transform);
239
+
240
+ // Append (Pinocchio will use the placement to transform inertia to joint frame)
241
+ pin.appendBodyToJoint(model, jointId, inertia, placement);
242
+ }
243
+
244
+
245
+ // ─── Internal Helpers: Math (SE3 Composition) ────────────────
246
+
247
+ function createIdentityTransform() {
248
+ return {
249
+ R: [1, 0, 0, 0, 1, 0, 0, 0, 1], // Row-major
250
+ t: [0, 0, 0]
251
+ };
252
+ }
253
+
254
+ function createTransformFromXyzRpy(xyz, rpy) {
255
+ const [x, y, z] = xyz;
256
+ const [roll, pitch, yaw] = rpy;
257
+
258
+ // ZYX Euler angles
259
+ const cr = Math.cos(roll), sr = Math.sin(roll);
260
+ const cp = Math.cos(pitch), sp = Math.sin(pitch);
261
+ const cy = Math.cos(yaw), sy = Math.sin(yaw);
262
+
263
+ // Row-major rotation matrix
264
+ const R = [
265
+ cy * cp, cy * sp * sr - sy * cr, cy * sp * cr + sy * sr,
266
+ sy * cp, sy * sp * sr + cy * cr, sy * sp * cr - cy * sr,
267
+ -sp, cp * sr, cp * cr
268
+ ];
269
+
270
+ return { R, t: [x, y, z] };
271
+ }
272
+
273
+ // Composition: T_AB = T_A * T_B
274
+ // R_AB = R_A * R_B
275
+ // t_AB = R_A * t_B + t_A
276
+ function composeTransforms(T1, T2) {
277
+ const R1 = T1.R;
278
+ const R2 = T2.R;
279
+ const t1 = T1.t;
280
+ const t2 = T2.t;
281
+
282
+ // R = R1 * R2
283
+ const R = [
284
+ R1[0] * R2[0] + R1[1] * R2[3] + R1[2] * R2[6], R1[0] * R2[1] + R1[1] * R2[4] + R1[2] * R2[7], R1[0] * R2[2] + R1[1] * R2[5] + R1[2] * R2[8],
285
+ R1[3] * R2[0] + R1[4] * R2[3] + R1[5] * R2[6], R1[3] * R2[1] + R1[4] * R2[4] + R1[5] * R2[7], R1[3] * R2[2] + R1[4] * R2[5] + R1[5] * R2[8],
286
+ R1[6] * R2[0] + R1[7] * R2[3] + R1[8] * R2[6], R1[6] * R2[1] + R1[7] * R2[4] + R1[8] * R2[7], R1[6] * R2[2] + R1[7] * R2[5] + R1[8] * R2[8]
287
+ ];
288
+
289
+ // t = R1 * t2 + t1
290
+ const t = [
291
+ R1[0] * t2[0] + R1[1] * t2[1] + R1[2] * t2[2] + t1[0],
292
+ R1[3] * t2[0] + R1[4] * t2[1] + R1[5] * t2[2] + t1[1],
293
+ R1[6] * t2[0] + R1[7] * t2[1] + R1[8] * t2[2] + t1[2]
294
+ ];
295
+
296
+ return { R, t };
297
+ }
298
+
299
+ function toPinocchioSE3(pin, T) {
300
+ return pin.SE3.fromRotationTranslation(
301
+ new Float64Array(T.R),
302
+ new Float64Array(T.t)
303
+ );
304
+ }
305
+
306
+ /**
307
+ * Map URDF joint type + axis to a Pinocchio JointModel.
308
+ */
309
+ function createJointModel(pin, type, axis) {
310
+ const [ax, ay, az] = axis;
311
+
312
+ switch (type) {
313
+ case 'revolute':
314
+ case 'continuous':
315
+ // Use aligned models when axis matches a principal direction
316
+ if (ax === 1 && ay === 0 && az === 0) return pin.JointModelRX();
317
+ if (ax === 0 && ay === 1 && az === 0) return pin.JointModelRY();
318
+ if (ax === 0 && ay === 0 && az === 1) return pin.JointModelRZ();
319
+ return pin.JointModelRevoluteUnaligned(ax, ay, az);
320
+
321
+ case 'prismatic':
322
+ if (ax === 1 && ay === 0 && az === 0) return pin.JointModelPX();
323
+ if (ax === 0 && ay === 1 && az === 0) return pin.JointModelPY();
324
+ if (ax === 0 && ay === 0 && az === 1) return pin.JointModelPZ();
325
+ return pin.JointModelPrismaticUnaligned(ax, ay, az);
326
+
327
+ case 'floating':
328
+ return pin.JointModelFreeFlyer();
329
+
330
+ case 'fixed':
331
+ // Should not happen with reduction logic, but if used directly:
332
+ return pin.JointModelFixed();
333
+
334
+ default:
335
+ console.warn(`Unknown joint type: ${type}, defaulting to revolute Z`);
336
+ return pin.JointModelRZ();
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Parse xyz attribute from a URDF element.
342
+ * @returns {number[]} [x, y, z]
343
+ */
344
+ function parseXyz(el) {
345
+ const xyz = el.getAttribute('xyz');
346
+ if (!xyz) return [0, 0, 0];
347
+ return xyz.trim().split(/\s+/).map(Number);
348
+ }
349
+
350
+ /**
351
+ * Parse rpy attribute from a URDF element.
352
+ * @returns {number[]} [roll, pitch, yaw]
353
+ */
354
+ function parseRpy(el) {
355
+ const rpy = el.getAttribute('rpy');
356
+ if (!rpy) return [0, 0, 0];
357
+ return rpy.trim().split(/\s+/).map(Number);
358
+ }