This commit is contained in:
orion kindel
2026-02-20 13:52:29 -06:00
parent 6ca132788f
commit 8b7ad814fd
14 changed files with 541 additions and 3 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@
.log .log
.purs-repl .purs-repl
.env .env
.spec-results

BIN
bun.lockb

Binary file not shown.

View File

@@ -9,11 +9,16 @@
}, },
"devDependencies": { "devDependencies": {
"bun-types": "1.0.11", "bun-types": "1.0.11",
"mujoco_wasm": "^3.2.2",
"purs-tidy": "^0.10.0", "purs-tidy": "^0.10.0",
"spago": "^1.0.0" "spago": "^1.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.0.0" "typescript": "^5.0.0"
}, },
"dependencies": {} "dependencies": {
"react": "17",
"react-dom": "17",
"server": "react-dom/server"
}
} }

View File

@@ -9,6 +9,7 @@
"elmish", "elmish",
"elmish-html", "elmish-html",
"integers", "integers",
"maybe",
"numbers", "numbers",
"prelude", "prelude",
"tuples", "tuples",
@@ -18,6 +19,8 @@
}, },
"test": { "test": {
"dependencies": [ "dependencies": [
"aff-promise",
"assert",
"spec", "spec",
"spec-node" "spec-node"
] ]
@@ -640,6 +643,21 @@
"unsafe-coerce" "unsafe-coerce"
] ]
}, },
"aff-promise": {
"type": "registry",
"version": "4.0.0",
"integrity": "sha256-Jgp3y+NWuuAmwz2V3LlWoX+AvxqfVglY77N5zTw8xnI=",
"dependencies": [
"aff",
"control",
"effect",
"either",
"exceptions",
"foreign",
"prelude",
"transformers"
]
},
"ansi": { "ansi": {
"type": "registry", "type": "registry",
"version": "7.0.0", "version": "7.0.0",
@@ -718,6 +736,16 @@
"unsafe-coerce" "unsafe-coerce"
] ]
}, },
"assert": {
"type": "registry",
"version": "6.0.0",
"integrity": "sha256-hCZ1J8/71nQiRwsSV2j7iicppScOegBFZrLI6sPf9F8=",
"dependencies": [
"console",
"effect",
"prelude"
]
},
"avar": { "avar": {
"type": "registry", "type": "registry",
"version": "5.0.1", "version": "5.0.1",

View File

@@ -15,6 +15,8 @@ package:
test: test:
main: Test.Main main: Test.Main
dependencies: dependencies:
- aff-promise
- assert
- spec - spec
- spec-node - spec-node
dependencies: dependencies:
@@ -22,6 +24,7 @@ package:
- elmish - elmish
- elmish-html - elmish-html
- integers - integers
- maybe
- numbers - numbers
- prelude - prelude
- tuples - tuples

152
src/Mujoco.MJCF.Asset.purs Normal file
View File

@@ -0,0 +1,152 @@
module Mujoco.MJCF.Asset where
import Mujoco.Prelude
asset = tag @() "asset" :: Tag ()
data MeshInertia = Convex | Exact | Legacy | Shell
instance Serialize MeshInertia where
serialize Convex = "convex"
serialize Exact = "exact"
serialize Legacy = "legacy"
serialize Shell = "shell"
type Props_mesh =
( name :: String
, class :: String
, content_type :: String
, file :: String
, scale :: Vec Real
, inertia :: MeshInertia
, smoothnormal :: Boolean
, maxhullvert :: Int
, vertex :: Array Real
, normal :: Array Real
, texcoord :: Array Real
, face :: Array Int
, refpos :: Vec Real
, refquat :: Vec4 Real
, builtin :: String
, params :: Array Real
, material :: String
)
mesh = tag @Props_mesh "mesh" :: Tag Props_mesh
type Props_hfield =
( name :: String
, content_type :: String
, file :: String
, nrow :: Int
, ncol :: Int
, elevation :: Array Real
, size :: Vec4 Real
)
hfield = tagNoContent @Props_hfield "hfield" :: TagNoContent Props_hfield
data TextureType = Texture2d | TextureCube | TextureSkybox
instance Serialize TextureType where
serialize Texture2d = "2d"
serialize TextureCube = "cube"
serialize TextureSkybox = "skybox"
data TextureColorspace = ColorspaceAuto | ColorspaceLinear | ColorspaceSRGB
instance Serialize TextureColorspace where
serialize ColorspaceAuto = "auto"
serialize ColorspaceLinear = "linear"
serialize ColorspaceSRGB = "sRGB"
data TextureBuiltin = BuiltinNone | BuiltinGradient | BuiltinChecker | BuiltinFlat
instance Serialize TextureBuiltin where
serialize BuiltinNone = "none"
serialize BuiltinGradient = "gradient"
serialize BuiltinChecker = "checker"
serialize BuiltinFlat = "flat"
data TextureMark = MarkNone | MarkEdge | MarkCross | MarkRandom
instance Serialize TextureMark where
serialize MarkNone = "none"
serialize MarkEdge = "edge"
serialize MarkCross = "cross"
serialize MarkRandom = "random"
type Props_texture =
( name :: String
, type :: TextureType
, colorspace :: TextureColorspace
, content_type :: String
, file :: String
, gridsize :: Int /\ Int
, gridlayout :: String
, fileright :: String
, fileleft :: String
, fileup :: String
, filedown :: String
, filefront :: String
, fileback :: String
, builtin :: TextureBuiltin
, rgb1 :: Vec Real
, rgb2 :: Vec Real
, mark :: TextureMark
, markrgb :: Vec Real
, random :: Real
, width :: Int
, height :: Int
, hflip :: Boolean
, vflip :: Boolean
, nchannel :: Int
)
texture = tagNoContent @Props_texture "texture" :: TagNoContent Props_texture
type Props_material =
( name :: String
, class :: String
, texture :: String
, texrepeat :: Real /\ Real
, texuniform :: Boolean
, emission :: Real
, specular :: Real
, shininess :: Real
, reflectance :: Real
, metallic :: Real
, roughness :: Real
, rgba :: Vec4 Real
)
material = tag @Props_material "material" :: Tag Props_material
data LayerRole
= RoleRgb
| RoleNormal
| RoleOcclusion
| RoleRoughness
| RoleMetallic
| RoleOpacity
| RoleEmissive
| RoleOrm
| RoleRgba
instance Serialize LayerRole where
serialize RoleRgb = "rgb"
serialize RoleNormal = "normal"
serialize RoleOcclusion = "occlusion"
serialize RoleRoughness = "roughness"
serialize RoleMetallic = "metallic"
serialize RoleOpacity = "opacity"
serialize RoleEmissive = "emissive"
serialize RoleOrm = "orm"
serialize RoleRgba = "rgba"
type Props_layer =
( texture :: String
, role :: LayerRole
)
layer = tagNoContent @Props_layer "layer" :: TagNoContent Props_layer
type Props_model =
( name :: String
, file :: String
, content_type :: String
)
model = tagNoContent @Props_model "model" :: TagNoContent Props_model
type Props_plugin = (plugin :: String, instance :: String)
plugin = tag @Props_plugin "plugin" :: Tag Props_plugin

248
src/Mujoco.MJCF.Body.purs Normal file
View File

@@ -0,0 +1,248 @@
module Mujoco.MJCF.Body where
import Mujoco.Prelude
data SleepPolicy = SleepAuto | SleepNever | SleepAllowed | SleepInit
instance Serialize SleepPolicy where
serialize SleepAuto = "auto"
serialize SleepNever = "never"
serialize SleepAllowed = "allowed"
serialize SleepInit = "init"
type Props_body =
( name :: String
, childclass :: String
, mocap :: Boolean
, pos :: Vec Real
, quat :: Vec4 Real
, axisangle :: Vec4 Real
, xyaxes :: Array Real
, zaxis :: Vec Real
, euler :: Vec Real
, gravcomp :: Real
, sleep :: SleepPolicy
, user :: Array Real
)
body = tag @Props_body "body" :: Tag Props_body
worldbody = tag @Props_body "worldbody" :: Tag Props_body
type Props_inertial =
( pos :: Vec Real
, quat :: Vec4 Real
, axisangle :: Vec4 Real
, xyaxes :: Array Real
, zaxis :: Vec Real
, euler :: Vec Real
, mass :: Real
, diaginertia :: Vec Real
, fullinertia :: Array Real
)
inertial = tagNoContent @Props_inertial "inertial" :: TagNoContent Props_inertial
data JointType = Free | Ball | Slide | Hinge
instance Serialize JointType where
serialize Free = "free"
serialize Ball = "ball"
serialize Slide = "slide"
serialize Hinge = "hinge"
data AutoBool = AutoBoolFalse | AutoBoolTrue | AutoBoolAuto
instance Serialize AutoBool where
serialize AutoBoolFalse = "false"
serialize AutoBoolTrue = "true"
serialize AutoBoolAuto = "auto"
type Props_joint =
( name :: String
, class :: String
, type :: JointType
, group :: Int
, pos :: Vec Real
, axis :: Vec Real
, springdamper :: Real /\ Real
, solreflimit :: Real /\ Real
, solimplimit :: Vec5 Real
, solreffriction :: Real /\ Real
, solimpfriction :: Vec5 Real
, stiffness :: Real
, range :: Real /\ Real
, limited :: AutoBool
, actuatorfrcrange :: Real /\ Real
, actuatorfrclimited :: AutoBool
, actuatorgravcomp :: Boolean
, margin :: Real
, ref :: Real
, springref :: Real
, armature :: Real
, damping :: Real
, frictionloss :: Real
, user :: Array Real
)
joint = tagNoContent @Props_joint "joint" :: TagNoContent Props_joint
type Props_freejoint =
( name :: String
, group :: Int
, align :: AutoBool
)
freejoint = tagNoContent @Props_freejoint "freejoint" :: TagNoContent Props_freejoint
data GeomType = GPlane | GHfield | GSphere | GCapsule | GEllipsoid | GCylinder | GBox | GMesh | GSdf
instance Serialize GeomType where
serialize GPlane = "plane"
serialize GHfield = "hfield"
serialize GSphere = "sphere"
serialize GCapsule = "capsule"
serialize GEllipsoid = "ellipsoid"
serialize GCylinder = "cylinder"
serialize GBox = "box"
serialize GMesh = "mesh"
serialize GSdf = "sdf"
data FluidShape = FluidNone | FluidEllipsoid
instance Serialize FluidShape where
serialize FluidNone = "none"
serialize FluidEllipsoid = "ellipsoid"
type Props_geom =
( name :: String
, class :: String
, type :: GeomType
, contype :: Int
, conaffinity :: Int
, condim :: Int
, group :: Int
, priority :: Int
, size :: Vec Real
, material :: String
, rgba :: Vec4 Real
, friction :: Vec Real
, mass :: Real
, density :: Real
, shellinertia :: Boolean
, solmix :: Real
, solref :: Real /\ Real
, solimp :: Vec5 Real
, margin :: Real
, gap :: Real
, fromto :: Array Real
, pos :: Vec Real
, quat :: Vec4 Real
, axisangle :: Vec4 Real
, xyaxes :: Array Real
, zaxis :: Vec Real
, euler :: Vec Real
, hfield :: String
, mesh :: String
, fitscale :: Real
, fluidshape :: FluidShape
, fluidcoef :: Vec5 Real
, user :: Array Real
)
geom = tag @Props_geom "geom" :: Tag Props_geom
data SiteType = SiteSphere | SiteCapsule | SiteEllipsoid | SiteCylinder | SiteBox
instance Serialize SiteType where
serialize SiteSphere = "sphere"
serialize SiteCapsule = "capsule"
serialize SiteEllipsoid = "ellipsoid"
serialize SiteCylinder = "cylinder"
serialize SiteBox = "box"
type Props_site =
( name :: String
, class :: String
, type :: SiteType
, group :: Int
, material :: String
, rgba :: Vec4 Real
, size :: Vec Real
, fromto :: Array Real
, pos :: Vec Real
, quat :: Vec4 Real
, axisangle :: Vec4 Real
, xyaxes :: Array Real
, zaxis :: Vec Real
, euler :: Vec Real
, user :: Array Real
)
site = tagNoContent @Props_site "site" :: TagNoContent Props_site
data CameraMode = CamFixed | CamTrack | CamTrackcom | CamTargetbody | CamTargetbodycom
instance Serialize CameraMode where
serialize CamFixed = "fixed"
serialize CamTrack = "track"
serialize CamTrackcom = "trackcom"
serialize CamTargetbody = "targetbody"
serialize CamTargetbodycom = "targetbodycom"
data Projection = Perspective | Orthographic
instance Serialize Projection where
serialize Perspective = "perspective"
serialize Orthographic = "orthographic"
data CameraOutput = OutputRgb | OutputDepth | OutputDistance | OutputNormal | OutputSegmentation
instance Serialize CameraOutput where
serialize OutputRgb = "rgb"
serialize OutputDepth = "depth"
serialize OutputDistance = "distance"
serialize OutputNormal = "normal"
serialize OutputSegmentation = "segmentation"
type Props_camera =
( name :: String
, class :: String
, mode :: CameraMode
, target :: String
, projection :: Projection
, fovy :: Real
, resolution :: Int /\ Int
, output :: CameraOutput
, sensorsize :: Real /\ Real
, focal :: Real /\ Real
, focalpixel :: Real /\ Real
, principal :: Real /\ Real
, principalpixel :: Real /\ Real
, ipd :: Real
, pos :: Vec Real
, quat :: Vec4 Real
, axisangle :: Vec4 Real
, xyaxes :: Array Real
, zaxis :: Vec Real
, euler :: Vec Real
, user :: Array Real
)
camera = tagNoContent @Props_camera "camera" :: TagNoContent Props_camera
data LightType = LightSpot | LightDirectional | LightPoint | LightImage
instance Serialize LightType where
serialize LightSpot = "spot"
serialize LightDirectional = "directional"
serialize LightPoint = "point"
serialize LightImage = "image"
type Props_light =
( name :: String
, class :: String
, mode :: CameraMode
, target :: String
, type :: LightType
, directional :: Boolean
, castshadow :: Boolean
, active :: Boolean
, pos :: Vec Real
, dir :: Vec Real
, diffuse :: Vec Real
, texture :: String
, intensity :: Real
, ambient :: Vec Real
, specular :: Vec Real
, range :: Real
, bulbradius :: Real
, attenuation :: Vec Real
, cutoff :: Real
, exponent :: Real
)
light = tagNoContent @Props_light "light" :: TagNoContent Props_light
-- TODO: body/composite reuses row types of joint, geom, site, skin, plugin

View File

@@ -1,6 +1,32 @@
module Mujoco.MJCF where module Mujoco.MJCF
( Angle(..)
, Cone(..)
, Coordinate(..)
, Enable(..)
, InertiaFromGeom(..)
, Integrator(..)
, Jacobian(..)
, Props_compiler
, Props_flag
, Props_option
, Props_size
, Props_mujoco
, Props_statistic
, Solver(..)
, compiler
, flag
, mujoco
, option
, size
, statistic
, module X
)
where
import Mujoco.Prelude import Mujoco.Prelude
import Mujoco.MJCF.Asset as X
import Mujoco.MJCF.Body as X
import Mujoco.XML.Node (empty, text, fragment) as X
type Props_mujoco = (model :: String) type Props_mujoco = (model :: String)
mujoco = tag @Props_mujoco "mujoco" :: Tag Props_mujoco mujoco = tag @Props_mujoco "mujoco" :: Tag Props_mujoco
@@ -147,3 +173,13 @@ type Props_size =
, nuser_sensor :: Int , nuser_sensor :: Int
) )
size = tagNoContent @Props_size "size" :: TagNoContent Props_size size = tagNoContent @Props_size "size" :: TagNoContent Props_size
type Props_statistic =
( meanmass :: Real
, meaninertia :: Real
, meansize :: Real
, extent :: Real
, center :: Vec Real
)
statistic = tagNoContent @Props_statistic "statistic" :: TagNoContent Props_statistic

4
src/Mujoco.XML.Node.js Normal file
View File

@@ -0,0 +1,4 @@
import ReactDOM from 'react-dom/server.js'
/** @type {(node: import('react').ReactElement) => String} */
export const renderToString = el => ReactDOM.renderToStaticMarkup(el)

View File

@@ -23,6 +23,8 @@ import Prim.Row (class Union)
import Prim.RowList (class RowToList) import Prim.RowList (class RowToList)
import Unsafe.Coerce (unsafeCoerce) import Unsafe.Coerce (unsafeCoerce)
foreign import renderToString :: ReactElement -> String
type Tag props type Tag props
= forall r missing a propsrl = forall r missing a propsrl
. Children a . Children a
@@ -46,7 +48,7 @@ type TagNoContent props
foreign import data Node :: Type foreign import data Node :: Type
render :: Node -> String render :: Node -> String
render = React.renderToString <<< toReact render = renderToString <<< toReact
fromReact :: ReactElement -> Node fromReact :: ReactElement -> Node
fromReact = unsafeCoerce fromReact = unsafeCoerce

View File

@@ -4,10 +4,12 @@ import Prelude
import Effect (Effect) import Effect (Effect)
import Test.Mujoco.XML.Node.Prop as Test.Mujoco.XML.Node.Prop import Test.Mujoco.XML.Node.Prop as Test.Mujoco.XML.Node.Prop
import Test.Mujoco.MJCF as Test.Mujoco.MJCF
import Test.Spec.Reporter.Console (consoleReporter) import Test.Spec.Reporter.Console (consoleReporter)
import Test.Spec.Runner.Node (runSpecAndExitProcess) import Test.Spec.Runner.Node (runSpecAndExitProcess)
main :: Effect Unit main :: Effect Unit
main = main =
runSpecAndExitProcess [consoleReporter] do runSpecAndExitProcess [consoleReporter] do
Test.Mujoco.MJCF.spec
Test.Mujoco.XML.Node.Prop.spec Test.Mujoco.XML.Node.Prop.spec

26
test/Mujoco.MJCF.purs Normal file
View File

@@ -0,0 +1,26 @@
module Test.Mujoco.MJCF where
import Prelude
import Control.Monad.Error.Class (try)
import Data.Either (isLeft)
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Mujoco.MJCF as X
import Mujoco.Wasm (renderSpec)
import Mujoco.XML.Node (Node)
import Test.Assert (assertTrue)
import Test.Spec (Spec, describe, it)
ok :: Node -> Aff Unit
ok = void <<< renderSpec
fail :: Node -> Aff Unit
fail = (liftEffect <<< assertTrue <<< isLeft) <=< (try <<< renderSpec)
spec :: Spec Unit
spec =
describe "MJCF" do
it "</>" $ fail $ X.empty
it "<mujoco>" $ ok $ X.mujoco {} unit
it "<worldbody>" $ ok $ X.mujoco {} $ X.worldbody {} unit

10
test/Mujoco.Wasm.js Normal file
View File

@@ -0,0 +1,10 @@
import load_mujoco from 'mujoco_wasm/mujoco_wasm.js'
/** @typedef {import('mujoco_wasm').MainModule} Mujoco */
/** @typedef {import('mujoco_wasm').MjSpec} Spec */
/** @type {() => Promise<Mujoco>} */
export const loadMujoco = () => load_mujoco()
/** @type {(mj: Mujoco) => (xml: String) => () => Spec} */
export const parseXMLString = m => xml => () => m.parseXMLString(xml)

21
test/Mujoco.Wasm.purs Normal file
View File

@@ -0,0 +1,21 @@
module Mujoco.Wasm where
import Prelude
import Control.Promise (Promise)
import Control.Promise as Promise
import Effect (Effect)
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Mujoco.XML.Node as XML
foreign import data Mujoco :: Type
foreign import data Spec :: Type
foreign import loadMujoco :: Effect (Promise Mujoco)
foreign import parseXMLString :: Mujoco -> String -> Effect Spec
renderSpec :: XML.Node -> Aff Spec
renderSpec node = do
mj <- Promise.toAffE loadMujoco
liftEffect $ parseXMLString mj $ XML.render node