This commit is contained in:
orion kindel
2026-02-20 14:47:29 -06:00
parent 8b7ad814fd
commit 442351f47d
7 changed files with 240 additions and 13 deletions

View File

@@ -8,10 +8,13 @@
"arrays", "arrays",
"elmish", "elmish",
"elmish-html", "elmish-html",
"foldable-traversable",
"integers", "integers",
"maybe", "maybe",
"numbers", "numbers",
"ordered-collections",
"prelude", "prelude",
"strings",
"tuples", "tuples",
"typelevel-prelude", "typelevel-prelude",
"unsafe-coerce" "unsafe-coerce"

View File

@@ -23,10 +23,13 @@ package:
- arrays - arrays
- elmish - elmish
- elmish-html - elmish-html
- foldable-traversable
- integers - integers
- maybe - maybe
- numbers - numbers
- ordered-collections
- prelude - prelude
- strings
- tuples - tuples
- typelevel-prelude - typelevel-prelude
- unsafe-coerce - unsafe-coerce

View File

@@ -56,8 +56,8 @@ instance Serialize Solver where
data Enable = Disable | Enable data Enable = Disable | Enable
instance Serialize Enable where instance Serialize Enable where
serialize Enable = "Enable" serialize Enable = "enable"
serialize Disable = "Disable" serialize Disable = "disable"
data Coordinate = Local | Global data Coordinate = Local | Global
instance Serialize Coordinate where instance Serialize Coordinate where
@@ -100,6 +100,7 @@ type Props_option =
, sdf_iterations :: Int , sdf_iterations :: Int
, sdf_initpoints :: Int , sdf_initpoints :: Int
, actuatorgroupdisable :: Array Int , actuatorgroupdisable :: Array Int
, solver :: Solver
) )
option = tag @Props_option "option" :: Tag Props_option option = tag @Props_option "option" :: Tag Props_option
@@ -130,7 +131,7 @@ type Props_flag =
, multiccd :: Enable , multiccd :: Enable
, sleep :: Enable , sleep :: Enable
) )
flag = tag @Props_flag "flag" :: Tag Props_flag flag = tagNoContent @Props_flag "flag" :: TagNoContent Props_flag
type Props_compiler = type Props_compiler =
( autolimits :: Boolean ( autolimits :: Boolean

View File

@@ -1,12 +1,16 @@
module Mujoco.XML.Node.Prop (class Serialize, serialize, serializeProps, class SerializeProps', serializeProps') where module Mujoco.XML.Node.Prop (class Serialize, serialize, serializeProps, class SerializeProps', serializeProps', renames, unrenames) where
import Prelude import Prelude
import Data.Array as Array import Data.Array as Array
import Data.Int as Int import Data.Int as Int
import Data.Map (Map)
import Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.Number.Format (toString) as Number import Data.Number.Format (toString) as Number
import Data.Symbol (class IsSymbol, reflectSymbol) import Data.Symbol (class IsSymbol, reflectSymbol)
import Data.Tuple (Tuple) import Data.Tuple (Tuple)
import Data.Tuple as Tuple
import Data.Tuple.Nested ((/\)) import Data.Tuple.Nested ((/\))
import Prim.Row (class Cons, class Union) import Prim.Row (class Cons, class Union)
import Prim.RowList (class RowToList, RowList) import Prim.RowList (class RowToList, RowList)
@@ -15,6 +19,12 @@ import Record.Unsafe (unsafeSet, unsafeHas, unsafeGet) as Record
import Type.Prelude (Proxy(..)) import Type.Prelude (Proxy(..))
import Unsafe.Coerce (unsafeCoerce) import Unsafe.Coerce (unsafeCoerce)
renames :: Map String String
renames = Map.fromFoldable ["size" /\ "mjcf:size"]
unrenames :: Map String String
unrenames = Map.fromFoldable $ map Tuple.swap $ (Map.toUnfoldable renames :: Array _)
class Serialize a where class Serialize a where
serialize :: a -> String serialize :: a -> String
@@ -60,11 +70,14 @@ instance SerializeProps' () RL.Nil where
patchUnsafe :: forall (@k :: Symbol) a b @r @lacksK. IsSymbol k => Cons k a lacksK r => (a -> b) -> Record r -> Record r patchUnsafe :: forall (@k :: Symbol) a b @r @lacksK. IsSymbol k => Cons k a lacksK r => (a -> b) -> Record r -> Record r
patchUnsafe f r = patchUnsafe f r =
let let
k = reflectSymbol $ Proxy @k k' = reflectSymbol $ Proxy @k
k = fromMaybe k' $ Map.lookup k' renames
btoa = unsafeCoerce :: b -> a btoa = unsafeCoerce :: b -> a
in in
if Record.unsafeHas k r then if Record.unsafeHas k' r then
Record.unsafeSet k (btoa $ f $ Record.unsafeGet k r) r Record.unsafeSet k (btoa $ f $ Record.unsafeGet k' r) r
else else
r r

View File

@@ -14,11 +14,13 @@ module Mujoco.XML.Node
import Prelude import Prelude
import Data.FoldableWithIndex (foldlWithIndex)
import Data.String as String
import Elmish.HTML (empty, fragment, text) as HTML import Elmish.HTML (empty, fragment, text) as HTML
import Elmish.HTML.Internal (tag, tagNoContent) as HTML import Elmish.HTML.Internal (tag, tagNoContent) as HTML
import Elmish.React (ReactElement) import Elmish.React (ReactElement)
import Elmish.React as React
import Mujoco.XML.Node.Prop (class SerializeProps', serializeProps) import Mujoco.XML.Node.Prop (class SerializeProps', serializeProps)
import Mujoco.XML.Node.Prop as Prop
import Prim.Row (class Union) import Prim.Row (class Union)
import Prim.RowList (class RowToList) import Prim.RowList (class RowToList)
import Unsafe.Coerce (unsafeCoerce) import Unsafe.Coerce (unsafeCoerce)
@@ -29,7 +31,6 @@ type Tag props
= forall r missing a propsrl = forall r missing a propsrl
. Children a . Children a
=> Union r missing props => Union r missing props
=> React.ValidReactProps (Record r)
=> RowToList props propsrl => RowToList props propsrl
=> SerializeProps' props propsrl => SerializeProps' props propsrl
=> Record r => Record r
@@ -39,7 +40,6 @@ type Tag props
type TagNoContent props type TagNoContent props
= forall r missing propsrl = forall r missing propsrl
. Union r missing props . Union r missing props
=> React.ValidReactProps (Record r)
=> RowToList props propsrl => RowToList props propsrl
=> SerializeProps' props propsrl => SerializeProps' props propsrl
=> Record r => Record r
@@ -48,7 +48,15 @@ type TagNoContent props
foreign import data Node :: Type foreign import data Node :: Type
render :: Node -> String render :: Node -> String
render = renderToString <<< toReact render =
let
unrenameProps str =
foldlWithIndex unrenameProp str Prop.unrenames
unrenameProp from str to =
String.replaceAll (String.Pattern from) (String.Replacement to) str
in
unrenameProps <<< renderToString <<< toReact
fromReact :: ReactElement -> Node fromReact :: ReactElement -> Node
fromReact = unsafeCoerce fromReact = unsafeCoerce
@@ -64,7 +72,6 @@ tag
-> ( forall r missing a. -> ( forall r missing a.
Union r missing props Union r missing props
=> Children a => Children a
=> React.ValidReactProps (Record r)
=> Record r => Record r
-> a -> a
-> Node -> Node
@@ -78,7 +85,6 @@ tagNoContent
=> String => String
-> ( forall r missing. -> ( forall r missing.
Union r missing props Union r missing props
=> React.ValidReactProps (Record r)
=> Record r => Record r
-> Node -> Node
) )

View File

@@ -4,6 +4,7 @@ import Prelude
import Control.Monad.Error.Class (try) import Control.Monad.Error.Class (try)
import Data.Either (isLeft) import Data.Either (isLeft)
import Data.Tuple.Nested ((/\))
import Effect.Aff (Aff) import Effect.Aff (Aff)
import Effect.Class (liftEffect) import Effect.Class (liftEffect)
import Mujoco.MJCF as X import Mujoco.MJCF as X
@@ -18,9 +19,208 @@ ok = void <<< renderSpec
fail :: Node -> Aff Unit fail :: Node -> Aff Unit
fail = (liftEffect <<< assertTrue <<< isLeft) <=< (try <<< renderSpec) fail = (liftEffect <<< assertTrue <<< isLeft) <=< (try <<< renderSpec)
w :: Node -> Node
w = X.mujoco {} <<< X.worldbody {}
spec :: Spec Unit spec :: Spec Unit
spec = spec =
describe "MJCF" do describe "MJCF" do
it "</>" $ fail $ X.empty it "</>" $ fail $ X.empty
it "<mujoco>" $ ok $ X.mujoco {} unit it "<mujoco>" $ ok $ X.mujoco {} unit
it "<worldbody>" $ ok $ X.mujoco {} $ X.worldbody {} unit it "<worldbody>" $ ok $ X.mujoco {} $ X.worldbody {} unit
describe "compiler" do
it "empty" $ ok $ X.mujoco {}
[ X.compiler {}
, X.worldbody {} unit
]
it "angle=radian" $ ok $ X.mujoco {}
[ X.compiler { angle: X.Radian }
, X.worldbody {} unit
]
it "autolimits + coordinate" $ ok $ X.mujoco {}
[ X.compiler { autolimits: true, coordinate: X.Local }
, X.worldbody {} unit
]
it "inertiafromgeom=auto" $ ok $ X.mujoco {}
[ X.compiler { inertiafromgeom: X.InertiaFromGeomAuto }
, X.worldbody {} unit
]
it "boundmass + boundinertia" $ ok $ X.mujoco {}
[ X.compiler { boundmass: 0.01, boundinertia: 0.001 }
, X.worldbody {} unit
]
describe "size" do
it "empty" $ ok $ X.mujoco {}
[ X.size {}
, X.worldbody {} unit
]
it "memory" $ ok $ X.mujoco {}
[ X.size { memory: "16M" }
, X.worldbody {} unit
]
it "nuser fields" $ ok $ X.mujoco {}
[ X.size { nuser_body: 2, nuser_jnt: 1, nuser_geom: 3 }
, X.worldbody {} unit
]
describe "option" do
it "timestep + integrator" $ ok $ X.mujoco {}
[ X.option { timestep: 0.001, integrator: X.RK4 } unit
, X.worldbody {} unit
]
it "gravity + solver" $ ok $ X.mujoco {}
[ X.option { gravity: 0.0 /\ 0.0 /\ (-9.81), solver: X.Newton } unit
, X.worldbody {} unit
]
describe "statistic" do
it "extent + center" $ ok $ X.mujoco {}
[ X.statistic { extent: 2.0, center: 0.0 /\ 0.0 /\ 1.0 }
, X.worldbody {} unit
]
describe "asset" do
describe "mesh" do
it "inline vertex" $ ok $ X.mujoco {}
[ X.asset {}
[ X.mesh { name: "tetra", vertex: [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0] } unit ]
, X.worldbody {} unit
]
describe "hfield" do
it "nrow + ncol + size" $ ok $ X.mujoco {}
[ X.asset {}
[ X.hfield { name: "terrain", nrow: 10, ncol: 10, size: 1.0 /\ 1.0 /\ 0.5 /\ 0.1 } ]
, X.worldbody {} unit
]
describe "texture" do
it "procedural checker" $ ok $ X.mujoco {}
[ X.asset {}
[ X.texture
{ name: "grid"
, type: X.Texture2d
, builtin: X.BuiltinChecker
, width: 512
, height: 512
, rgb1: 0.9 /\ 0.9 /\ 0.9
, rgb2: 0.1 /\ 0.1 /\ 0.1
}
]
, X.worldbody {} unit
]
describe "material" do
it "rgba + specular" $ ok $ X.mujoco {}
[ X.asset {}
[ X.material { name: "mat1", rgba: 0.8 /\ 0.2 /\ 0.2 /\ 1.0, specular: 0.8 } unit ]
, X.worldbody {} unit
]
describe "body" do
describe "joint" do
it "hinge" $ ok $ w $
X.body { name: "b1", pos: 0.0 /\ 0.0 /\ 0.5 }
[ X.joint { type: X.Hinge, axis: 1.0 /\ 0.0 /\ 0.0 }
, X.geom { type: X.GSphere, size: 0.1 /\ 0.0 /\ 0.0 } unit
]
it "slide with range" $ ok $ w $
X.body { name: "slider", pos: 0.0 /\ 0.0 /\ 1.0 }
[ X.joint { type: X.Slide, axis: 0.0 /\ 0.0 /\ 1.0, range: (-1.0) /\ 1.0, limited: X.AutoBoolTrue }
, X.geom { type: X.GBox, size: 0.1 /\ 0.1 /\ 0.1 } unit
]
it "stiffness + damping" $ ok $ w $
X.body { pos: 0.0 /\ 0.0 /\ 0.0 }
[ X.joint { type: X.Hinge, stiffness: 100.0, damping: 10.0, armature: 0.1 }
, X.geom { type: X.GSphere, size: 0.05 /\ 0.0 /\ 0.0 } unit
]
describe "freejoint" do
it "basic" $ ok $ w $
X.body { name: "free_body", pos: 0.0 /\ 0.0 /\ 1.0 }
[ X.freejoint { name: "fj" }
, X.geom { type: X.GSphere, size: 0.1 /\ 0.0 /\ 0.0 } unit
]
describe "geom" do
it "sphere" $ ok $ w $
X.geom { type: X.GSphere, size: 1.0 /\ 0.0 /\ 0.0 } unit
it "capsule fromto" $ ok $ w $
X.geom { type: X.GCapsule, fromto: [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], size: 0.05 /\ 0.0 /\ 0.0 } unit
it "box with material" $ ok $ X.mujoco {}
[ X.asset {}
[ X.material { name: "red", rgba: 1.0 /\ 0.0 /\ 0.0 /\ 1.0 } unit ]
, X.worldbody {} $
X.geom { type: X.GBox, size: 0.5 /\ 0.5 /\ 0.5, material: "red" } unit
]
it "plane" $ ok $ w $
X.geom { type: X.GPlane, size: 5.0 /\ 5.0 /\ 0.1 } unit
it "friction + density" $ ok $ w $
X.geom { type: X.GSphere, size: 0.1 /\ 0.0 /\ 0.0, friction: 0.5 /\ 0.005 /\ 0.0001, density: 500.0 } unit
describe "site" do
it "basic" $ ok $ w $
X.body { pos: 0.0 /\ 0.0 /\ 0.0 }
[ X.geom { type: X.GSphere, size: 0.1 /\ 0.0 /\ 0.0 } unit
, X.site { name: "s1", pos: 0.0 /\ 0.0 /\ 0.1, size: 0.01 /\ 0.01 /\ 0.01 }
]
describe "camera" do
it "fixed" $ ok $ w $
X.camera { name: "cam1", pos: 0.0 /\ (-2.0) /\ 1.0, fovy: 60.0 }
it "tracking" $ ok $ w $
X.body { name: "target_body", pos: 0.0 /\ 0.0 /\ 0.5 }
[ X.geom { type: X.GSphere, size: 0.1 /\ 0.0 /\ 0.0 } unit
, X.camera { name: "tracker", mode: X.CamTargetbody, target: "target_body", pos: 1.0 /\ 0.0 /\ 0.5 }
]
describe "light" do
it "spotlight" $ ok $ w $
X.light { name: "spot1", pos: 0.0 /\ 0.0 /\ 3.0, dir: 0.0 /\ 0.0 /\ (-1.0), diffuse: 1.0 /\ 1.0 /\ 1.0 }
it "directional" $ ok $ w $
X.light { name: "sun", type: X.LightDirectional, dir: 0.0 /\ (-1.0) /\ (-1.0), castshadow: true }
describe "inertial" do
it "explicit mass + diaginertia" $ ok $ w $
X.body { pos: 0.0 /\ 0.0 /\ 0.0 }
[ X.inertial { pos: 0.0 /\ 0.0 /\ 0.0, mass: 1.0, diaginertia: 0.01 /\ 0.01 /\ 0.01 }
, X.geom { type: X.GSphere, size: 0.1 /\ 0.0 /\ 0.0 } unit
]
describe "composite" do
it "full model"
$ ok
$ X.mujoco { model: "test" }
[ X.compiler { angle: X.Radian, inertiafromgeom: X.InertiaFromGeomTrue }
, X.option { timestep: 0.002, gravity: 0.0 /\ 0.0 /\ (-9.81) }
[ X.flag { contact: X.Enable } ]
, X.asset {}
[ X.texture { name: "grid", type: X.Texture2d, builtin: X.BuiltinChecker, width: 256, height: 256, rgb1: 0.9 /\ 0.9 /\ 0.9, rgb2: 0.1 /\ 0.1 /\ 0.1 }
, X.material { name: "floor_mat", texture: "grid", texrepeat: 5.0 /\ 5.0 } unit
]
, X.worldbody {}
[ X.geom { type: X.GPlane, size: 5.0 /\ 5.0 /\ 0.1, material: "floor_mat" } unit
, X.light { name: "top", pos: 0.0 /\ 0.0 /\ 3.0, dir: 0.0 /\ 0.0 /\ (-1.0) }
, X.body { name: "ball", pos: 0.0 /\ 0.0 /\ 1.0 }
[ X.freejoint {}
, X.geom { type: X.GSphere, size: 0.1 /\ 0.0 /\ 0.0, rgba: 1.0 /\ 0.0 /\ 0.0 /\ 1.0 } unit
]
]
]

View File

@@ -33,6 +33,7 @@ spec =
it "serializes number array" $ serialize [1.0, 2.0] `shouldEqual` "1 2" it "serializes number array" $ serialize [1.0, 2.0] `shouldEqual` "1 2"
it "serializes tuple" $ serialize (1 /\ 2) `shouldEqual` "1 2" it "serializes tuple" $ serialize (1 /\ 2) `shouldEqual` "1 2"
it "serializes nested tuple" $ serialize (1 /\ 2 /\ 3) `shouldEqual` "1 2 3" it "serializes nested tuple" $ serialize (1 /\ 2 /\ 3) `shouldEqual` "1 2 3"
it "serializes real(4)" $ serialize (1.0 /\ 1.0 /\ 0.5 /\ 0.1) `shouldEqual` "1 1 0.5 0.1"
describe "SerializeProps" do describe "SerializeProps" do
let let