diff --git a/ultraviolet/shared/src/main/scala/ultraviolet/macros/CreateShaderAST.scala b/ultraviolet/shared/src/main/scala/ultraviolet/macros/CreateShaderAST.scala index 83c2348..95e740c 100644 --- a/ultraviolet/shared/src/main/scala/ultraviolet/macros/CreateShaderAST.scala +++ b/ultraviolet/shared/src/main/scala/ultraviolet/macros/CreateShaderAST.scala @@ -1236,6 +1236,56 @@ class CreateShaderAST[Q <: Quotes](using val qq: Q) extends ShaderMacroUtils: case _ => throw ShaderError.Unsupported("Shaders do not support infix operator: " + op) + case Apply( + Apply(Ident(op), List(Apply(_, List(Typed(Repeated(List(Literal(StringConstant(value))), _), _))))), + _ + ) => + op match + case "hex" => + import ultraviolet.syntax.interpolators.hex.* + value.toVec3.map(v => ShaderAST.DataTypes.vec3(v.x, v.y, v.z)).getOrElse { + throw ShaderError.Unsupported( + "Hex values must be 6 or 8 characters long using the `hex` or `hexa` interpolators, respectively (e.g. #FF00FF)." + ) + } + + case "hexa" => + import ultraviolet.syntax.interpolators.hex.* + value.toVec4.map(v => ShaderAST.DataTypes.vec4(v.x, v.y, v.z, v.a)).getOrElse { + throw ShaderError.Unsupported( + "Hex values must be 6 or 8 characters long using the `hex` or `hexa` interpolators, respectively (e.g. #FF00FF00)." + ) + } + + case "rgb" => + import ultraviolet.syntax.interpolators.rgb.* + value.toVec3 + .map(v => ShaderAST.DataTypes.vec3(v.x, v.y, v.z)) + .getOrElse { + throw ShaderError.Unsupported( + "RGB values must be 3 or 4 integers long using the `rgb` or `rgba` interpolators, respectively (e.g. 255,0,255)." + ) + } + + case "rgba" => + import ultraviolet.syntax.interpolators.rgb.* + value.toVec4 + .map(v => ShaderAST.DataTypes.vec4(v.x, v.y, v.z, v.a)) + .getOrElse { + throw ShaderError.Unsupported( + "RGB values must be 3 or 4 integers long using the `rgb` or `rgba` interpolators, respectively (e.g. 255,0,255,0)." + ) + } + + case _ => + throw ShaderError.Unsupported("Shaders do not support interpolators of type: " + op) + + case Apply( + Apply(Ident(op), List(Apply(_, List(Typed(Repeated(_, _), _))))), + _ + ) => + throw ShaderError.Unsupported("Shader interpolated colours must be string literals, e.g. #ff0000") + case Apply(Apply(Ident(op), List(l)), List(r)) => op match case "<" | "<=" | ">" | ">=" | "==" | "!=" => diff --git a/ultraviolet/shared/src/main/scala/ultraviolet/syntax.scala b/ultraviolet/shared/src/main/scala/ultraviolet/syntax.scala index 1b437c9..c58635c 100644 --- a/ultraviolet/shared/src/main/scala/ultraviolet/syntax.scala +++ b/ultraviolet/shared/src/main/scala/ultraviolet/syntax.scala @@ -6,6 +6,7 @@ import ultraviolet.macros.UBOReader import scala.annotation.StaticAnnotation import scala.annotation.nowarn import scala.deriving.Mirror +import scala.util.matching.Regex object syntax extends ShaderDSLOps: type WebGL1 = ultraviolet.datatypes.ShaderPrinter.WebGL1 @@ -91,4 +92,76 @@ object syntax extends ShaderDSLOps: def PrecisionMediumPFloat: ShaderHeader = ShaderHeader.PrecisionMediumPFloat def PrecisionLowPFloat: ShaderHeader = ShaderHeader.PrecisionLowPFloat + private[ultraviolet] object interpolators: + + object hex: + private val hexGroup: String = "([0-9A-F]{2})" + private val hex3: Regex = List.fill(3)(hexGroup).mkString("(?i)#", "", "").r + private val hex4: Regex = List.fill(4)(hexGroup).mkString("(?i)#", "", "").r + + private def toScaledFloat(string: String): Float = Integer.parseInt(string, 16) / 255f + + extension (string: String) { + def toVec3: Option[vec3] = Option(string).collect { case hex3(r, g, b) => + vec3(toScaledFloat(r), toScaledFloat(g), toScaledFloat(b)) + } + + def toVec4: Option[vec4] = Option(string).collect { case hex4(r, g, b, a) => + vec4(toScaledFloat(r), toScaledFloat(g), toScaledFloat(b), toScaledFloat(a)) + } + } + + object rgb: + private def is8bit(i: Int): Boolean = i >= 0 && i < 256 + private def toScaledFloat(string: String): Option[Float] = string.toIntOption.filter(is8bit).map(_ / 255f) + + extension (string: String) { + def toVec3: Option[vec3] = Option(string.split(",").toList.map(toScaledFloat)).collect { + case Some(r) :: Some(g) :: Some(b) :: Nil => vec3(r, g, b) + } + + def toVec4: Option[vec4] = Option(string.split(",").toList.map(toScaledFloat)).collect { + case Some(r) :: Some(g) :: Some(b) :: Some(a) :: Nil => vec4(r, g, b, a) + } + } + + extension (sc: StringContext) { + + @SuppressWarnings(Array("scalafix:DisableSyntax.throw")) + def hex(args: Any*): vec3 = + import interpolators.hex.* + sc.s(args*).toVec3.getOrElse { + throw IllegalArgumentException( + s"Invalid hex values ${args.mkString}. Supported formats are #00ff00 and #00ff00ff (case insensitive), using the 'hex' and 'hexa' interpolators, respectively" + ) + } + + @SuppressWarnings(Array("scalafix:DisableSyntax.throw")) + def hexa(args: Any*): vec4 = + import interpolators.hex.* + sc.s(args*).toVec4.getOrElse { + throw IllegalArgumentException( + s"Invalid hexa values ${args.mkString}. Supported formats are #00ff00 and #00ff00ff (case insensitive), using the 'hex' and 'hexa' interpolators, respectively" + ) + } + + @SuppressWarnings(Array("scalafix:DisableSyntax.throw")) + def rgb(args: Int*): vec3 = + import interpolators.rgb.* + sc.s(args*).toVec3.getOrElse { + throw IllegalArgumentException( + s"Invalid rgb values ${args.mkString}. Supported formats are 0,255,0 and 0,255,0,255, using the 'rgb' and 'rgba' interpolators, respectively" + ) + } + + @SuppressWarnings(Array("scalafix:DisableSyntax.throw")) + def rgba(args: Int*): vec4 = + import interpolators.rgb.* + sc.s(args*).toVec4.getOrElse { + throw IllegalArgumentException( + s"Invalid rgba values ${args.mkString}. Supported formats are 0,255,0 and 0,255,0,255, using the 'rgb' and 'rgba' interpolators, respectively" + ) + } + } + end syntax diff --git a/ultraviolet/shared/src/test/scala/ultraviolet/SyntaxTests.scala b/ultraviolet/shared/src/test/scala/ultraviolet/SyntaxTests.scala new file mode 100644 index 0000000..d9a59d3 --- /dev/null +++ b/ultraviolet/shared/src/test/scala/ultraviolet/SyntaxTests.scala @@ -0,0 +1,56 @@ +package ultraviolet + +import scala.util.Random + +import syntax.* + +class SyntaxTests extends munit.FunSuite { + + test("hex interpolator") { + assertEquals(hex"#00FF00", vec3(0f, 1f, 0f)) + assertEquals(hex"#ff00ff", vec3(1f, 0f, 1f)) + val (hex1, hex2, hex3) = ("00", "ff", "00") + assertEquals(hex"#$hex1$hex2$hex3", vec3(0f, 1f, 0f)) + + intercept[IllegalArgumentException](hex"#00000"): Unit + intercept[IllegalArgumentException](hex"#0000000"): Unit + intercept[IllegalArgumentException](hex"#gggggg"): Unit + } + + test("hexa interpolator") { + assertEquals(hexa"#00FF00FF", vec4(0f, 1f, 0f, 1f)) + assertEquals(hexa"#ff00ff00", vec4(1f, 0f, 1f, 0f)) + val (hex1, hex2, hex3, hex4) = ("00", "ff", "00", "ff") + assertEquals(hexa"#$hex1$hex2$hex3$hex4", vec4(0f, 1f, 0f, 1f)) + + intercept[IllegalArgumentException](hexa"#0000000"): Unit + intercept[IllegalArgumentException](hexa"#000000000"): Unit + intercept[IllegalArgumentException](hexa"#gggggggg"): Unit + } + + test("rgb interpolator") { + assertEquals(rgb"0,0,0", vec3(0f, 0f, 0f)) + assertEquals(rgb"255,0,255", vec3(1f, 0f, 1f)) + val (int1, int2, int3) = (0, 255, 0) + assertEquals(rgb"$int1,$int2,$int3", vec3(0f, 1f, 0f)) + + intercept[IllegalArgumentException](rgb"0,0"): Unit + intercept[IllegalArgumentException](rgb"0,0,0,0"): Unit + intercept[IllegalArgumentException](rgb"0, 0, 0"): Unit + intercept[IllegalArgumentException](rgb"-1,0,0"): Unit + intercept[IllegalArgumentException](rgb"256,0,0"): Unit + } + + test("rgba interpolator") { + assertEquals(rgba"0,0,0,0", vec4(0f, 0f, 0f, 0f)) + assertEquals(rgba"255,0,255,0", vec4(1f, 0f, 1f, 0f)) + val (int1, int2, int3, int4) = (0, 255, 0, 255) + assertEquals(rgba"$int1,$int2,$int3,$int4", vec4(0f, 1f, 0f, 1f)) + + intercept[IllegalArgumentException](rgba"0,0,0"): Unit + intercept[IllegalArgumentException](rgba"0,0,0,0,0"): Unit + intercept[IllegalArgumentException](rgba"0, 0, 0, 0"): Unit + intercept[IllegalArgumentException](rgba"-1,0,0,0"): Unit + intercept[IllegalArgumentException](rgba"256,0,0,0"): Unit + } +} diff --git a/ultraviolet/shared/src/test/scala/ultraviolet/acceptance/GLSLInterpolatorTests.scala b/ultraviolet/shared/src/test/scala/ultraviolet/acceptance/GLSLInterpolatorTests.scala new file mode 100644 index 0000000..c71526a --- /dev/null +++ b/ultraviolet/shared/src/test/scala/ultraviolet/acceptance/GLSLInterpolatorTests.scala @@ -0,0 +1,46 @@ +package ultraviolet.acceptance + +import ultraviolet.DebugAST +import ultraviolet.syntax.* + +class GLSLInterpolatorTests extends munit.FunSuite { + test("hex interpolator") { + inline def fragment: Shader[Unit, vec4] = Shader(_ => vec4(hex"#ff00ff", 0f)) + + // println(DebugAST.toAST(fragment)) + + val actual = fragment.toGLSL[WebGL2].toOutput.code + + assertEquals(actual, "vec4(vec3(1.0,0.0,1.0),0.0);") + } + + test("hexa interpolator") { + inline def fragment: Shader[Unit, vec4] = Shader(_ => hexa"#ff00ff00") + + // println(DebugAST.toAST(fragment)) + + val actual = fragment.toGLSL[WebGL2].toOutput.code + + assertEquals(actual, "vec4(1.0,0.0,1.0,0.0);") + } + + test("rgb interpolator") { + inline def fragment: Shader[Unit, vec4] = Shader(_ => vec4(rgb"255,0,255", 0f)) + + // println(DebugAST.toAST(fragment)) + + val actual = fragment.toGLSL[WebGL2].toOutput.code + + assertEquals(actual, "vec4(vec3(1.0,0.0,1.0),0.0);") + } + + test("rgba interpolator") { + inline def fragment: Shader[Unit, vec4] = Shader(_ => rgba"255,0,255,0") + + // println(DebugAST.toAST(fragment)) + + val actual = fragment.toGLSL[WebGL2].toOutput.code + + assertEquals(actual, "vec4(1.0,0.0,1.0,0.0);") + } +}