dcascaval / scala-threejs-facades   0.131.0

GitHub

Facade Types for Three.js for use in Scala.js

Scala versions: 3.x
Scala.js versions: 1.x

Three.js Facades

Current @types/three Version: 131.0

This package provides a set of facade types for ScalaJS users to be able to use THREE.js from Scala. They are generated by this script, which uses the @types/three NPM package and programmatically converts the Typescript definitions into ScalaJS ones. The generated definitions target Scala 3, and ScalaJS 1.7.0+ compiled with JDK 11.

Note that this includes the entirety of the THREE.js core, but only OrbitControls from their included examples. The examples include a lot of useful code, and more should be ported over -- however, the type definitions are much less robust than those from the core, and this represents a larger lift.

Absolutely no guarantees are made about reliability here; steal 'em if they work for you.

Usage

Full example of project setup: https://github.com/dcascaval/scala-threejs-facades-example

  • Scala:

    • Add libraryDependencies += "io.github.dcascaval" %%% "three-typings" % "0.131.0" to build.sbt.
  • JS:

    • Include the output of ScalaJSBundler into your index.html.
      • By default, this is:
        <script type="text/javascript" src="./target/scala-3.0.0/scalajs-bundler/main/example-fastopt-bundle.js"></script>
      • If you use ScalaJSBundler's library bundling mode (see example project), this looks like:
          <script type="text/javascript" src="./target/scala-3.0.0/scalajs-bundler/main/example-fastopt-library.js"></script>
          <script type="text/javascript" src="./target/scala-3.0.0/scalajs-bundler/main/example-fastopt-loader.js"></script>
          <script type="text/javascript" src="./target/scala-3.0.0/scalajs-bundler/main/example-fastopt.js"></script>
    • (Potentially) including the following snippet is sometimes needed to make everything play nicely.
      <script>
        window.global = window;
      </script>
    • (Potentially) resolve any issues with package naming by adding: scalacOptions ++= Seq("-Yresolve-term-conflict:package"). (Discussion)

Goals

Other tools, such as ScalablyTyped exist and can usually handle this type of Typescript -> ScalaJS Facade conversion. In this case, however, the purely automatic conversion runs into trouble (several different conflicting versions of many base classes are generated, rendering it difficult to call objects in one module of THREE with objects from another). The goals of this package are to:

  • Generate facade types that are specifically designed to work with the way that THREE.js is laid out
  • Programatically apply special cases in such a way that keeping up-to-date involves minimal work, but that the ergonomics are close to that of a hand-written facade.
  • Target Scala 3 specifically, and ultimately provide helpful extension methods for working with THREE objects in a fluent style.

Drawbacks

This project is functional (compiles and runs!), and replicates several of the basic THREE.js examples with minimal casting and no namespacing issues. That said, this project does not resemble anything production-ready. Primarily this is because:

  • There are no tests, let alone automated tests, to verify that the API surface is covered correctly.
  • This project does not provide bindings for all of the examples ourside the core, though it's likely we will expand to more and more over time.
  • There are some cases where mutating super types can break type safety (see below.)

Special Cases

Interfaces to objects that serve as JS parameters are represented in TypeScript as buckets of potentially undefined fields.

export interface PointsMaterialParameters extends MaterialParameters {
  color?: ColorRepresentation | undefined;
  map?: Texture | null | undefined;
  alphaMap?: Texture | null | undefined;
  size?: number | undefined;
  sizeAttenuation?: boolean | undefined;
}

export class PointsMaterial extends Material {
  constructor(parameters?: PointsMaterialParameters);
  // ...
}

When instantiating these in scala, it's nice to be able to allow the following call site syntax, not mentioning the name of the interface or the other parameters.

new PointsMaterial(new { size = 0.1; color = "#FFF" })

Here Scala infers the type and instantiates an anonymous trait instance with the properties we want. However, naively translating the definition straight from TypeScript will allow the parameters argument to be optional, and type inference fails because PointsMaterial expects js.UndefOr[PointsMaterialParameters] instead of PointsMaterialParameters. In these bindings we special-case these points.

Subtyping Safety

A number of the THREE typings contain subtyping of the following form:

class Light {
  camera: Camera;
}

class SubCamera extends Camera {}

class SubLight extends Light {
  camera: SubCamera;
}

Naively this translates to:

class Light {
  var camera: Camera
}

class SubCamera extends Camera {}

class SubLight extends Light {
  override var camera: SubCamera
}

but Scala does not allow us to override vars with subtypes. To some degree this is fundamental: if it did, we could set the camera field to a different subtype of Camera than SubLight expects, breaking type safety. Instead the approach we currently take is to not override the method, and if you need to access it as a subtype, a hard cast must be performed. As follows:

class Light {
  var camera: Camera
}

class SubCamera extends Camera {}
class SubLight extends Light {}

// Usage
val l = SubLight()
val c : SubCamera = l.camera.asInstanceOf[SubCamera]

This isn't particularly typesafe either (we haven't changed the underlying JS API at all) but:

  • As opposed to the naive translation, it compiles and runs

  • It makes assumptions about types explicit -- and you can always return an Option or union type via an extension method instead of casting directly:

    extension (l: SubLight)
      def subCamera: Option[SubCamera] =
        if l.camera.isInstanceOf[SubCamera] then
          Some(l.camera.asInstanceOf[SubCamera])
        else None
    
    // Usage
    val l = SubLight()
    val c: Option[SubCamera] = l.subCamera

    I think it's reasonable for this library to generate code like the above in the future.

  • In practice, needing to access the subtype-specific attributes of this type of field seems rare. (Again, this library is not production material.)