Skip to content
This repository has been archived by the owner on Apr 4, 2022. It is now read-only.

Commit

Permalink
Merge branch 'main' into dom-references
Browse files Browse the repository at this point in the history
* main:
  "...deg" transforms (#33) (fixes #31)
  • Loading branch information
hmans committed Jan 28, 2021
2 parents c18b21c + 6194c85 commit 03fde0e
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 64 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **Changed:** By popular request, three-elements will no longer log to the console on startup. Enjoy the quiet!

- **New:** When working with plain string attributes, you can now use the `deg` suffix to convert the specified value into radians. This is very useful in 100% HTML-based projects where you don't have access to JavaScript's `Math.PI`:

```html
<three-mesh rotation.x="-90deg">...</three-mesh>
```

- **Changed:** The core ticker loop now makes use of `setAnimationLoop` instead of `requestAnimationFrame`, which is a critical prerequisite for making your three-elements project [WebXR-ready](https://three-elements.hmans.co/advanced/webxr.html).

- **New:** `<three-game>` now can receive an `xr` attribute to enable WebXR features.
Expand Down Expand Up @@ -43,6 +49,8 @@ If you've been extending ThreeElement in your own code, or hacking on the codeba

- **Breaking Change:** Ticker events are now emitted by the three-game's `emitter`. Since we're no longer using DOM events, this means we also no longer need the `ticking` property/attribute, so it has been removed.

- **Holy crap:** `applyProps` was refactored to use `if` instead of `switch (true)`. All you Senior JavaScript Architects can finally calm down, for I am no longer impeding upon your creed!

- **Changed:** `yarn dev` now makes use of the excellent [@web/dev-server](https://modern-web.dev/docs/dev-server/overview/). This allows us to get rid of the importmap shim we had been using so far, load additional dependencies straight from our own `node_modules`, and greatly increase iteration speed during development.

## [0.2.1] - 2021-01-27
Expand Down
2 changes: 1 addition & 1 deletion examples/static.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<three-orbit-controls></three-orbit-controls>

<!-- Scene Contents -->
<three-mesh rotation="[-1.5707, 0, 0]" position.y="-8" receive-shadow>
<three-mesh rotation="-90deg, 0, 0" position.y="-8" receive-shadow>
<three-plane-buffer-geometry args="[1000, 1000]"></three-plane-buffer-geometry>
<three-mesh-standard-material color="#ccc"></three-mesh-standard-material>
</three-mesh>
Expand Down
2 changes: 1 addition & 1 deletion examples/vr.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
></three-directional-light>

<!-- Scene Contents -->
<three-mesh rotation="[-1.5707, 0, 0]" position.y="-8" receive-shadow>
<three-mesh rotation="-90deg, 0, 0" position.y="-8" receive-shadow>
<three-plane-buffer-geometry args="[1000, 1000]"></three-plane-buffer-geometry>
<three-mesh-standard-material color="#ccc"></three-mesh-standard-material>
</three-mesh>
Expand Down
131 changes: 71 additions & 60 deletions src/util/applyProps.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IStringIndexable } from "../types"
import { camelize } from "./camelize"
import { MathUtils } from "three"

const IGNORED_KEYS = ["id"]

Expand All @@ -10,66 +11,76 @@ export const applyProps = (object: IStringIndexable, props: IStringIndexable) =>

const key = camelize(firstKey)

/* Attempt to parse the value */
let parsed = undefined
try {
parsed = JSON.parse(value)
} catch (e) {}

switch (true) {
/* Skip ignored keys */
case IGNORED_KEYS.includes(key):
break

/* Ignore all data- keys */
case firstKey.startsWith("data-"):
break

/* Handle nested keys, ie. position-x */
case key in object && rest.length > 0:
applyProps(object[key], { [rest.join(".")]: value })
break

/*
Handle boolean properties. We will check against the only values that we consider falsey here,
taking into account that they might be coming from string-based HTML element attributes, where a
stand-alone boolean attribute like "cast-shadow" will emit a value of "". Eh!
*/
case typeof object[key] === "boolean":
object[key] = ![undefined, null, false, "no", "false"].includes(value)
break

/* Handle properties that provide .set methods */
case object[key]?.set !== undefined:
switch (true) {
/* If the value is an array, feed its destructured representation to the set method. */
case Array.isArray(parsed):
object[key].set(...parsed)
break

/* A bit of special handling for "scale" properties, where we'll also accept a single numerical value */
case key === "scale" && typeof parsed === "number":
object[key].setScalar(parsed)
break

/* If we have a parsed value, set it directly */
case parsed:
object[key].set(parsed)
break

/* Otherwise, set the original string value, but split by commas */
default:
const list = value.split(",").map((el: string) => parseFloat(el) || el)
object[key].set(...list)
}
break

default:
/*
If we've reached this point, we're finally able to set a property on the object.
Amazing! But let's only do it if the property key is actually known.
*/
if (key in object) object[key] = parsed !== undefined ? parsed : value
/* Skip all ignored keys. */
if (IGNORED_KEYS.includes(key)) return

/* Skip all data attributes. */
if (firstKey.startsWith("data-")) return

/* Recursively handle nested keys, eg. position.x */
if (key in object && rest.length > 0) {
applyProps(object[key], { [rest.join(".")]: value })
return
}

/*
Handle boolean properties. We will check against the only values that we consider falsey here,
taking into account that they might be coming from string-based HTML element attributes, where a
stand-alone boolean attribute like "cast-shadow" will emit a value of "". Eh!
*/
if (typeof object[key] === "boolean") {
object[key] = ![undefined, null, false, "no", "false"].includes(value)
return
}

/* It is attribute-setting time! Let's try to parse the value. */
const parsed = parseJson(value) ?? parseDeg(value)

/* Handle properties that provide .set methods */
if (object[key]?.set !== undefined) {
/* If the value is an array, feed its destructured representation to the set method. */
if (Array.isArray(parsed)) {
object[key].set(...parsed)
return
}

/* A bit of special handling for "scale" properties, where we'll also accept a single numerical value */
if (key === "scale" && typeof parsed === "number") {
object[key].setScalar(parsed)
return
}

/* If we have a parsed value, set it directly */
if (parsed) {
object[key].set(parsed)
return
}

/* Otherwise, set the original string value, but split by commas */
const list = value.split(",").map((el: string) => parseJson(el) ?? parseDeg(el) ?? el)
object[key].set(...list)
return
}

/*
If we've reached this point, we're finally able to set a property on the object.
Amazing! But let's only do it if the property key is actually known.
*/
if (key in object) object[key] = parsed !== undefined ? parsed : value
}
}

const parseJson = (value: string) => {
let parsed = undefined

try {
parsed = JSON.parse(value)
} catch (e) {}

return parsed
}

const parseDeg = (value: string) => {
const r = value.trim().match(/^([0-9\.\- ]+)deg$/)
if (r) return MathUtils.degToRad(parseFloat(r[1]))
}
32 changes: 32 additions & 0 deletions test/three-element.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,38 @@ describe("<three-*> powered by ThreeElement", () => {
expect(el.object.scale.y).to.equal(1)
expect(el.object.scale.z).to.equal(1)
})

describe("the ...deg suffix", () => {
it("converts the value to radians", async () => {
const el = await renderMeshElement()

el.setAttribute("rotation.x", "90deg")
await nextFrame()
expect(el.object.rotation.x).to.equal(Math.PI / 2)

el.setAttribute("rotation.x", "-90deg")
await nextFrame()
expect(el.object.rotation.x).to.equal(Math.PI / -2)

el.setAttribute("rotation.x", "0deg")
await nextFrame()
expect(el.object.rotation.x).to.equal(0)

el.setAttribute("rotation.x", "-0.5deg")
await nextFrame()
expect(el.object.rotation.x).to.equal((Math.PI / 180) * -0.5)
})

it("works within lists", async () => {
const el = await renderMeshElement()

el.setAttribute("rotation", "90deg, 0, -90deg")
await nextFrame()
expect(el.object.rotation.x).to.equal(Math.PI / 2)
expect(el.object.rotation.y).to.equal(0)
expect(el.object.rotation.z).to.equal(Math.PI / -2)
})
})
})

describe("the `attach` attribute", () => {
Expand Down
23 changes: 21 additions & 2 deletions test/util/applyProps.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from "@open-wc/testing"
import { applyProps } from "../../src/util/applyProps"
import * as THREE from "three"

describe("applyProps", () => {
it("can directly assign root-level properties", () => {
Expand All @@ -20,19 +21,37 @@ describe("applyProps", () => {
expect(object.foo.bar).to.equal(1)
})

it("can parses numerical values as floats", () => {
it("parses numerical values as floats", () => {
const object = {
foo: 0
}
applyProps(object, { foo: "1.5" })
expect(object.foo).to.equal(1.5)
})

it("can correctly parses '0' to a 0", () => {
it("parses '0' as 0", () => {
const object = {
foo: 1
}
applyProps(object, { foo: "0" })
expect(object.foo).to.equal(0)
})

it("parses '123deg' to the corresponding radian value", () => {
const object = {
foo: 0
}
applyProps(object, { foo: "90deg" })
expect(object.foo).to.equal(Math.PI / 2)
})

it("handles a list of '...deg' values correctly if the assigned property has a .set method", () => {
const object = {
foo: new THREE.Vector3()
}
applyProps(object, { foo: "90deg, 1.23, -90deg" })
expect(object.foo.x).to.equal(Math.PI / 2)
expect(object.foo.y).to.equal(1.23)
expect(object.foo.z).to.equal(Math.PI / -2)
})
})

0 comments on commit 03fde0e

Please sign in to comment.