diff --git a/package-lock.json b/package-lock.json
index e823a05b4..e587038f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,7 +1,7 @@
{
"name": "lit.dev",
"version": "0.0.0",
- "lockfileVersion": 2,
+ "lockfileVersion": 1,
"requires": true,
"packages": {
"": {
@@ -8441,6 +8441,290 @@
"write-file-atomic": "^3.0.3"
}
},
+ "@lit/reactive-element": {
+ "version": "1.0.0-rc.4",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0-rc.4.tgz",
+ "integrity": "sha512-dJMha+4NFYdpnUJzRrWTFV5Hdp9QHWFuPnaoqonrKl4lGJVnYez9mu8ev9F/5KM47tjAjh22DuRHrdFDHfOijA==",
+ "dev": true
+ },
+ "@material/animation": {
+ "version": "14.0.0-canary.261f2db59.0",
+ "resolved": "https://registry.npmjs.org/@material/animation/-/animation-14.0.0-canary.261f2db59.0.tgz",
+ "integrity": "sha512-OjxWJYSRNs4vnPe8NclaNn+TsNc8TR/wHusGtezF5F+wl+5mh+K69BMXAmURtq3idoRg4XaOSC/Ohk1ovD1fMQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^2.1.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/base": {
+ "version": "14.0.0-canary.261f2db59.0",
+ "resolved": "https://registry.npmjs.org/@material/base/-/base-14.0.0-canary.261f2db59.0.tgz",
+ "integrity": "sha512-vy5SQt+jcwwdRFfBvtpVdpULUBujecVUKOXcopaQoi2XIzI5EBHuR4gPN0cd1yfmVEucD6p2fvVv2FJ3Ngr61w==",
+ "dev": true,
+ "requires": {
+ "tslib": "^2.1.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/dom": {
+ "version": "14.0.0-canary.261f2db59.0",
+ "resolved": "https://registry.npmjs.org/@material/dom/-/dom-14.0.0-canary.261f2db59.0.tgz",
+ "integrity": "sha512-iUpZG6Bb2l/PfNV2Fb/pXfG1p4Bz4PC9A7ATPlKfcU5HioObcnYVc/+Hrtaw8eu28BNIc+VVROtbfpqG/YgKSQ==",
+ "dev": true,
+ "requires": {
+ "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+ "tslib": "^2.1.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/elevation": {
+ "version": "14.0.0-canary.261f2db59.0",
+ "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-14.0.0-canary.261f2db59.0.tgz",
+ "integrity": "sha512-AqN/tsTGGyBzZ7CtoSMBY9bDYvCuUt98EUfiGjZGXcf4HgoHV3Cn/JSLrhru5Cq8Nx6HF6AmHh3dQCfNCQduew==",
+ "dev": true,
+ "requires": {
+ "@material/animation": "14.0.0-canary.261f2db59.0",
+ "@material/base": "14.0.0-canary.261f2db59.0",
+ "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+ "@material/rtl": "14.0.0-canary.261f2db59.0",
+ "@material/theme": "14.0.0-canary.261f2db59.0",
+ "tslib": "^2.1.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/feature-targeting": {
+ "version": "14.0.0-canary.261f2db59.0",
+ "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-14.0.0-canary.261f2db59.0.tgz",
+ "integrity": "sha512-CrVoGNu0ym52OPEKy3kgeNL2oSWOCBYbYxSH3GhERxCq5FwGBN+XmK/ZDLFVQlHYy3v8x4TqVEwXviCeumNTxQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^2.1.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/mwc-base": {
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@material/mwc-base/-/mwc-base-0.25.3.tgz",
+ "integrity": "sha512-4wvxZ9dhPr0O4jjOHPmFyn77pafe+h1gHPlT9sbQ+ly8NY/fSn/TXn7/PbxgL8g4ZHxMvD3o7PJopg+6cbHp8Q==",
+ "dev": true,
+ "requires": {
+ "@lit/reactive-element": "1.0.0-rc.4",
+ "@material/base": "=14.0.0-canary.261f2db59.0",
+ "@material/dom": "=14.0.0-canary.261f2db59.0",
+ "lit": "^2.0.0",
+ "tslib": "^2.0.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/mwc-icon-button": {
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@material/mwc-icon-button/-/mwc-icon-button-0.25.3.tgz",
+ "integrity": "sha512-FexkMpK3ZSHh7NF+PIqvVhvAbBOgFDYPck/lqnxIDC3VGJ0rjD/1MqevDy2fY6IcHGlc8Ai7VuYbdQ6Cvw8WcQ==",
+ "dev": true,
+ "requires": {
+ "@material/mwc-ripple": "^0.25.3",
+ "lit": "^2.0.0",
+ "tslib": "^2.0.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/mwc-ripple": {
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@material/mwc-ripple/-/mwc-ripple-0.25.3.tgz",
+ "integrity": "sha512-G/gt/csxgME6/sAku3GiuB0O2LLvoPWsRTLq/9iABpaGLJjqaKHvNg/IVzNDdF3YZT7EORgR9cBWWl7umA4i4Q==",
+ "dev": true,
+ "requires": {
+ "@material/dom": "=14.0.0-canary.261f2db59.0",
+ "@material/mwc-base": "^0.25.3",
+ "@material/ripple": "=14.0.0-canary.261f2db59.0",
+ "lit": "^2.0.0",
+ "tslib": "^2.0.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/mwc-slider": {
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@material/mwc-slider/-/mwc-slider-0.25.3.tgz",
+ "integrity": "sha512-HhjOwi9s/ssGrMaeOwvVXAZQJ3RQc7qKBe7KBA4diRF3Fe4UvjtFsXJN5eZAXmi0LwZPWNnzxKP1JAQBcJWszQ==",
+ "dev": true,
+ "requires": {
+ "@material/dom": "=14.0.0-canary.261f2db59.0",
+ "@material/mwc-base": "^0.25.3",
+ "@material/mwc-ripple": "^0.25.3",
+ "@material/slider": "=14.0.0-canary.261f2db59.0",
+ "lit": "^2.0.0",
+ "tslib": "^2.0.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/ripple": {
+ "version": "14.0.0-canary.261f2db59.0",
+ "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-14.0.0-canary.261f2db59.0.tgz",
+ "integrity": "sha512-3FLCLj8X7KrFfuYBHJg1b7Odb3V/AW7fxk3m1i1zhDnygKmlQ/abVucH1s2qbX3Y+JIiq+5/C5407h9BFtOf+A==",
+ "dev": true,
+ "requires": {
+ "@material/animation": "14.0.0-canary.261f2db59.0",
+ "@material/base": "14.0.0-canary.261f2db59.0",
+ "@material/dom": "14.0.0-canary.261f2db59.0",
+ "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+ "@material/rtl": "14.0.0-canary.261f2db59.0",
+ "@material/theme": "14.0.0-canary.261f2db59.0",
+ "tslib": "^2.1.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/rtl": {
+ "version": "14.0.0-canary.261f2db59.0",
+ "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-14.0.0-canary.261f2db59.0.tgz",
+ "integrity": "sha512-bVnXBbUsHs57+EXdeFbcwaKy3lT/itI/qTLmJ88ar0qaGEujO1GmESHm3ioqkeo4kQpTfDhBwQGeEi1aDaTdFg==",
+ "dev": true,
+ "requires": {
+ "@material/theme": "14.0.0-canary.261f2db59.0",
+ "tslib": "^2.1.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/slider": {
+ "version": "14.0.0-canary.261f2db59.0",
+ "resolved": "https://registry.npmjs.org/@material/slider/-/slider-14.0.0-canary.261f2db59.0.tgz",
+ "integrity": "sha512-JoTlKGDf5+Ao0JNNlpiNM1h3TezhOELpM8dmCs1IwCzw7pWepihSa4oXemdDadHKAYM0HOIo2OQVcark96/WSQ==",
+ "dev": true,
+ "requires": {
+ "@material/animation": "14.0.0-canary.261f2db59.0",
+ "@material/base": "14.0.0-canary.261f2db59.0",
+ "@material/dom": "14.0.0-canary.261f2db59.0",
+ "@material/elevation": "14.0.0-canary.261f2db59.0",
+ "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+ "@material/ripple": "14.0.0-canary.261f2db59.0",
+ "@material/rtl": "14.0.0-canary.261f2db59.0",
+ "@material/theme": "14.0.0-canary.261f2db59.0",
+ "@material/typography": "14.0.0-canary.261f2db59.0",
+ "tslib": "^2.1.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/theme": {
+ "version": "14.0.0-canary.261f2db59.0",
+ "resolved": "https://registry.npmjs.org/@material/theme/-/theme-14.0.0-canary.261f2db59.0.tgz",
+ "integrity": "sha512-bUqyFT0QF8Nxx02fekt3CXIfC9DEPOPdo2hjgdtvhrNP+vftbkI2tKZ5/uRUnVA+zqQAOyIl5z6FOMg4fyemCA==",
+ "dev": true,
+ "requires": {
+ "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+ "tslib": "^2.1.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
+ "@material/typography": {
+ "version": "14.0.0-canary.261f2db59.0",
+ "resolved": "https://registry.npmjs.org/@material/typography/-/typography-14.0.0-canary.261f2db59.0.tgz",
+ "integrity": "sha512-WOCdcNkD5KBRAwICcRqWBRG3cDkyrwK5USTNmG0oxnwnZAN7daOpPTdLppVAhadE7faj8d67ON+V9pH7+T62FQ==",
+ "dev": true,
+ "requires": {
+ "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+ "@material/theme": "14.0.0-canary.261f2db59.0",
+ "tslib": "^2.1.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ }
+ }
+ },
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -8646,8 +8930,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
"integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"@octokit/plugin-rest-endpoint-methods": {
"version": "5.13.0",
@@ -8735,6 +9018,22 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
+ "@types/trusted-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
+ "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
+ "dev": true
+ },
+ "JSONStream": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
+ "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
+ "dev": true,
+ "requires": {
+ "jsonparse": "^1.2.0",
+ "through": ">=2.2.7 <3"
+ }
+ },
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -9362,8 +9661,8 @@
"integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==",
"dev": true,
"requires": {
- "is-text-path": "^1.0.1",
"JSONStream": "^1.0.4",
+ "is-text-path": "^1.0.1",
"lodash": "^4.17.15",
"meow": "^8.0.0",
"split2": "^3.0.0",
@@ -10724,16 +11023,6 @@
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
"dev": true
},
- "JSONStream": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
- "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
- "dev": true,
- "requires": {
- "jsonparse": "^1.2.0",
- "through": ">=2.2.7 <3"
- }
- },
"jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -10911,6 +11200,52 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
+ "lit": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-2.2.1.tgz",
+ "integrity": "sha512-dSe++R50JqrvNGXmI9OE13de1z5U/Y3J2dTm/9GC86vedI8ILoR8ZGnxfThFpvQ9m0lR0qRnIR4IiKj/jDCfYw==",
+ "dev": true,
+ "requires": {
+ "@lit/reactive-element": "^1.3.0",
+ "lit-element": "^3.2.0",
+ "lit-html": "^2.2.0"
+ },
+ "dependencies": {
+ "@lit/reactive-element": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.3.1.tgz",
+ "integrity": "sha512-nOJARIr3pReqK3hfFCSW2Zg/kFcFsSAlIE7z4a0C9D2dPrgD/YSn3ZP2ET/rxKB65SXyG7jJbkynBRm+tGlacw==",
+ "dev": true
+ }
+ }
+ },
+ "lit-element": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.2.0.tgz",
+ "integrity": "sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g==",
+ "dev": true,
+ "requires": {
+ "@lit/reactive-element": "^1.3.0",
+ "lit-html": "^2.2.0"
+ },
+ "dependencies": {
+ "@lit/reactive-element": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.3.1.tgz",
+ "integrity": "sha512-nOJARIr3pReqK3hfFCSW2Zg/kFcFsSAlIE7z4a0C9D2dPrgD/YSn3ZP2ET/rxKB65SXyG7jJbkynBRm+tGlacw==",
+ "dev": true
+ }
+ }
+ },
+ "lit-html": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.2.1.tgz",
+ "integrity": "sha512-AiJ/Rs0awjICs2FioTnHSh+Np5dhYSkyRczKy3wKjp8qjLhr1Ov+GiHrUQNdX8ou1LMuznpIME990AZsa/tR8g==",
+ "dev": true,
+ "requires": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
"load-json-file": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz",
@@ -12857,15 +13192,6 @@
"integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=",
"dev": true
},
- "string_decoder": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "dev": true,
- "requires": {
- "safe-buffer": "~5.2.0"
- }
- },
"string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -12908,6 +13234,15 @@
"define-properties": "^1.1.3"
}
},
+ "string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
"strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
diff --git a/package.json b/package.json
index 57300da63..c2c31e896 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,8 @@
}
},
"devDependencies": {
+ "@material/mwc-icon-button": "^0.25.3",
+ "@material/mwc-slider": "^0.25.3",
"lerna": "^4.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.2",
diff --git a/packages/lit-dev-content/package.json b/packages/lit-dev-content/package.json
index f7f207e56..f40358a59 100644
--- a/packages/lit-dev-content/package.json
+++ b/packages/lit-dev-content/package.json
@@ -182,7 +182,8 @@
"rollup-plugin-minify-html-literals": "^1.2.6",
"rollup-plugin-summary": "^1.2.3",
"rollup-plugin-terser": "^7.0.2",
- "slugify": "^1.3.6"
+ "slugify": "^1.3.6",
+ "three": "^0.139.0"
},
"dependencies": {
"@lion/combobox": "^0.8.3",
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/00/after/brick-viewer.ts b/packages/lit-dev-content/samples/tutorials/brick-viewer/00/after/brick-viewer.ts
new file mode 100644
index 000000000..c1e3b545d
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/00/after/brick-viewer.ts
@@ -0,0 +1,204 @@
+import { ifDefined } from "lit/directives/if-defined.js";
+import { LitElement,PropertyValues, css, html } from 'lit';
+import { customElement, query, property } from 'lit/decorators.js';
+
+// @ts-ignore
+import * as THREE from "three";
+// @ts-ignore
+import { LDrawLoader } from "three/examples/jsm/loaders/LDrawLoader.js";
+// @ts-ignore
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
+
+import "@material/mwc-icon-button";
+import "@material/mwc-slider";
+// @ts-ignore
+import { Slider } from "@material/mwc-slider";
+
+@customElement("brick-viewer")
+export class BrickViewer extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ position: relative;
+ }
+ #controls {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ display: flex;
+ }
+ mwc-slider {
+ flex-grow: 1;
+ }
+ `;
+
+ @property({ type: String })
+ src: string | null = null;
+
+ @property({ type: Number, reflect: true })
+ step: number = 1;
+
+ @query("mwc-slider")
+ slider!: Slider | null;
+
+ private _scene = new THREE.Scene();
+ private _renderer = new THREE.WebGLRenderer({ antialias: true });
+ private _camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
+ private _controls = new OrbitControls(this._camera, this._renderer.domElement);
+ private _loader = new LDrawLoader();
+ private _model: any;
+ private _numConstructionSteps?: number;
+
+ async firstUpdated() {
+ this._camera = new THREE.PerspectiveCamera(
+ 45,
+ this.clientWidth / this.clientHeight,
+ 1,
+ 10000
+ );
+ this._camera.position.set(150, 200, 250);
+
+ this._scene = new THREE.Scene();
+ this._scene.background = new THREE.Color(0xdeebed);
+
+ const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
+ this._scene.add(ambientLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(-1000, 1200, 1500);
+ this._scene.add(directionalLight);
+
+ this._renderer = new THREE.WebGLRenderer({ antialias: true });
+ this._renderer.setPixelRatio(window.devicePixelRatio);
+ this._renderer.setSize(this.offsetWidth, this.offsetHeight);
+
+ this._controls = new OrbitControls(this._camera, this._renderer.domElement);
+ this._controls.addEventListener("change", () =>
+ requestAnimationFrame(this._animate)
+ );
+
+ (this._loader as any).separateObjects = true;
+
+ this._animate();
+
+ const resizeObserver = new ResizeObserver(this._onResize);
+ resizeObserver.observe(this);
+
+ // Buttons are loading after slider, so slider's initial width calculation is wrong.
+ if (this.slider) {
+ await this.slider.updateComplete;
+ this.slider.layout();
+ }
+ }
+
+ private _onResize = (entries: ResizeObserverEntry[]) => {
+ const { width, height } = entries[0].contentRect;
+ this._renderer.setSize(width, height);
+ this._camera.aspect = width / height;
+ this._camera.updateProjectionMatrix();
+ requestAnimationFrame(this._animate);
+ };
+
+ private _restart() {
+ this.step! = 1;
+ }
+
+ private _stepBack() {
+ this.step! -= 1;
+ }
+
+ private _stepForward() {
+ this.step! += 1;
+ }
+
+ private _resetCamera() {
+ this._controls.reset();
+ }
+
+ render() {
+ return html`
+ ${this._renderer.domElement}
+
+
+
+
+ (this.step = e.detail.value)}
+ >
+
+
+
+ `;
+ }
+
+ update(changedProperties: PropertyValues) {
+ if (changedProperties.has("src")) {
+ this._loadModel();
+ }
+ if (changedProperties.has("step")) {
+ this._updateBricksVisibility();
+ }
+ super.update(changedProperties);
+ }
+
+ private _loadModel() {
+ if (this.src === null) {
+ return;
+ }
+ // @ts-ignore
+ this._loader.setPath("").load(this.src, newModel => {
+ if (this._model !== undefined) {
+ this._scene.remove(this._model);
+ this._model = undefined;
+ }
+
+ this._model = newModel;
+
+ // Convert from LDraw coordinates: rotate 180 degrees around OX
+ this._model.rotation.x = Math.PI;
+ this._scene.add(this._model);
+
+ this._numConstructionSteps = this._model.userData.numConstructionSteps;
+ this.step = this._numConstructionSteps!;
+
+ // Adjust camera
+ const bbox = new THREE.Box3().setFromObject(this._model);
+ this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
+ this._controls.update();
+ this._controls.saveState();
+ });
+ }
+
+ private _updateBricksVisibility() {
+ this._model &&
+ this._model.traverse((c: any) => {
+ if (c.isGroup && this.step) {
+ c.visible = c.userData.constructionStep <= this.step;
+ }
+ });
+ requestAnimationFrame(this._animate);
+ }
+
+ private _animate = () => {
+ this._renderer.render(this._scene, this._camera);
+ };
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/00/after/index.html b/packages/lit-dev-content/samples/tutorials/brick-viewer/00/after/index.html
index a51165714..c522266bb 100644
--- a/packages/lit-dev-content/samples/tutorials/brick-viewer/00/after/index.html
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/00/after/index.html
@@ -1 +1,54 @@
-Hello world step 1 completed!
\ No newline at end of file
+
+
+
+
+
+
+
+
+ Brick Viewer
+
+
+ Car
+ Bulldozer
+ Radar Truck
+
+
+
+
+
+
+ The <brick-viewer> element sits in your HTML like any other HTML
+ element!
+
+
+
+
+
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/00/after/project.json b/packages/lit-dev-content/samples/tutorials/brick-viewer/00/after/project.json
index 7aee5eb4f..cfc7995c0 100644
--- a/packages/lit-dev-content/samples/tutorials/brick-viewer/00/after/project.json
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/00/after/project.json
@@ -1,6 +1,7 @@
{
"extends": "/samples/base.json",
"files": {
+ "brick-viewer.ts": {},
"index.html": {}
}
}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/00/before/brick-viewer.ts b/packages/lit-dev-content/samples/tutorials/brick-viewer/00/before/brick-viewer.ts
new file mode 100644
index 000000000..45ee38f8f
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/00/before/brick-viewer.ts
@@ -0,0 +1,9 @@
+import { LitElement, html } from 'lit';
+import { customElement } from 'lit/decorators.js';
+
+@customElement('brick-viewer')
+class BrickViewer extends LitElement {
+ render() {
+ return html`Brick viewer
`;
+ }
+}
\ No newline at end of file
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/00/before/index.html b/packages/lit-dev-content/samples/tutorials/brick-viewer/00/before/index.html
index dcb9fa664..4d42a0baa 100644
--- a/packages/lit-dev-content/samples/tutorials/brick-viewer/00/before/index.html
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/00/before/index.html
@@ -1 +1,19 @@
-Hello world step 1!
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/00/before/project.json b/packages/lit-dev-content/samples/tutorials/brick-viewer/00/before/project.json
index 7aee5eb4f..cfc7995c0 100644
--- a/packages/lit-dev-content/samples/tutorials/brick-viewer/00/before/project.json
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/00/before/project.json
@@ -1,6 +1,7 @@
{
"extends": "/samples/base.json",
"files": {
+ "brick-viewer.ts": {},
"index.html": {}
}
}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/01/after/brick-viewer.ts b/packages/lit-dev-content/samples/tutorials/brick-viewer/01/after/brick-viewer.ts
new file mode 100644
index 000000000..52d597e9a
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/01/after/brick-viewer.ts
@@ -0,0 +1,12 @@
+import { LitElement, html } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+
+@customElement('brick-viewer')
+class BrickViewer extends LitElement {
+ @property({type: String})
+ src: string|null = null;
+
+ render() {
+ return html`Brick model: ${this.src}
`;
+ }
+}
\ No newline at end of file
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/01/after/index.html b/packages/lit-dev-content/samples/tutorials/brick-viewer/01/after/index.html
index 04115e5f0..4d677c6ec 100644
--- a/packages/lit-dev-content/samples/tutorials/brick-viewer/01/after/index.html
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/01/after/index.html
@@ -1 +1,19 @@
-Hello world completed!
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/01/after/project.json b/packages/lit-dev-content/samples/tutorials/brick-viewer/01/after/project.json
index 7aee5eb4f..cfc7995c0 100644
--- a/packages/lit-dev-content/samples/tutorials/brick-viewer/01/after/project.json
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/01/after/project.json
@@ -1,6 +1,7 @@
{
"extends": "/samples/base.json",
"files": {
+ "brick-viewer.ts": {},
"index.html": {}
}
}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/01/before/brick-viewer.ts b/packages/lit-dev-content/samples/tutorials/brick-viewer/01/before/brick-viewer.ts
new file mode 100644
index 000000000..45ee38f8f
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/01/before/brick-viewer.ts
@@ -0,0 +1,9 @@
+import { LitElement, html } from 'lit';
+import { customElement } from 'lit/decorators.js';
+
+@customElement('brick-viewer')
+class BrickViewer extends LitElement {
+ render() {
+ return html`Brick viewer
`;
+ }
+}
\ No newline at end of file
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/01/before/index.html b/packages/lit-dev-content/samples/tutorials/brick-viewer/01/before/index.html
index 288c0ea86..4d42a0baa 100644
--- a/packages/lit-dev-content/samples/tutorials/brick-viewer/01/before/index.html
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/01/before/index.html
@@ -1 +1,19 @@
-Hello world!
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/01/before/project.json b/packages/lit-dev-content/samples/tutorials/brick-viewer/01/before/project.json
index 7aee5eb4f..cfc7995c0 100644
--- a/packages/lit-dev-content/samples/tutorials/brick-viewer/01/before/project.json
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/01/before/project.json
@@ -1,6 +1,7 @@
{
"extends": "/samples/base.json",
"files": {
+ "brick-viewer.ts": {},
"index.html": {}
}
}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/02/after/brick-viewer.ts b/packages/lit-dev-content/samples/tutorials/brick-viewer/02/after/brick-viewer.ts
new file mode 100644
index 000000000..316859bd0
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/02/after/brick-viewer.ts
@@ -0,0 +1,122 @@
+import { ifDefined } from "lit/directives/if-defined.js";
+import { LitElement,PropertyValues, css, html } from 'lit';
+import { customElement, query, property } from 'lit/decorators.js';
+
+// @ts-ignore
+import * as THREE from "three";
+// @ts-ignore
+import { LDrawLoader } from "three/examples/jsm/loaders/LDrawLoader.js";
+// @ts-ignore
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
+
+@customElement("brick-viewer")
+export class BrickViewer extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ }
+ `;
+
+ @property({ type: String })
+ src: string | null = null;
+
+ @property({ type: Number, reflect: true })
+ step: number = 1;
+
+ private _scene = new THREE.Scene();
+ private _renderer = new THREE.WebGLRenderer({ antialias: true });
+ private _camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
+ private _controls = new OrbitControls(this._camera, this._renderer.domElement);
+ private _loader = new LDrawLoader();
+ private _model: any;
+ private _numConstructionSteps?: number;
+
+ async firstUpdated() {
+ this._camera = new THREE.PerspectiveCamera(
+ 45,
+ this.clientWidth / this.clientHeight,
+ 1,
+ 10000
+ );
+ this._camera.position.set(150, 200, 250);
+
+ this._scene = new THREE.Scene();
+ this._scene.background = new THREE.Color(0xdeebed);
+
+ const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
+ this._scene.add(ambientLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(-1000, 1200, 1500);
+ this._scene.add(directionalLight);
+
+ this._renderer = new THREE.WebGLRenderer({ antialias: true });
+ this._renderer.setPixelRatio(window.devicePixelRatio);
+ this._renderer.setSize(this.offsetWidth, this.offsetHeight);
+
+ this._controls = new OrbitControls(this._camera, this._renderer.domElement);
+ this._controls.addEventListener("change", () =>
+ requestAnimationFrame(this._animate)
+ );
+
+ (this._loader as any).separateObjects = true;
+
+ this._animate();
+
+ const resizeObserver = new ResizeObserver(this._onResize);
+ resizeObserver.observe(this);
+ }
+
+ private _onResize = (entries: ResizeObserverEntry[]) => {
+ const { width, height } = entries[0].contentRect;
+ this._renderer.setSize(width, height);
+ this._camera.aspect = width / height;
+ this._camera.updateProjectionMatrix();
+ requestAnimationFrame(this._animate);
+ };
+
+ render() {
+ return html`
+ ${this._renderer.domElement}
+ `;
+ }
+
+ update(changedProperties: PropertyValues) {
+ if (changedProperties.has("src")) {
+ this._loadModel();
+ }
+ super.update(changedProperties);
+ }
+
+ private _loadModel() {
+ if (this.src === null) {
+ return;
+ }
+ // @ts-ignore
+ this._loader.setPath("").load(this.src, newModel => {
+ if (this._model !== undefined) {
+ this._scene.remove(this._model);
+ this._model = undefined;
+ }
+
+ this._model = newModel;
+
+ // Convert from LDraw coordinates: rotate 180 degrees around OX
+ this._model.rotation.x = Math.PI;
+ this._scene.add(this._model);
+
+ this._numConstructionSteps = this._model.userData.numConstructionSteps;
+ this.step = this._numConstructionSteps!;
+
+ // Adjust camera
+ const bbox = new THREE.Box3().setFromObject(this._model);
+ this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
+ this._controls.update();
+ this._controls.saveState();
+ });
+ }
+
+ private _animate = () => {
+ this._renderer.render(this._scene, this._camera);
+ };
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/02/after/index.html b/packages/lit-dev-content/samples/tutorials/brick-viewer/02/after/index.html
new file mode 100644
index 000000000..48b32de1f
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/02/after/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/02/after/project.json b/packages/lit-dev-content/samples/tutorials/brick-viewer/02/after/project.json
new file mode 100644
index 000000000..cfc7995c0
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/02/after/project.json
@@ -0,0 +1,7 @@
+{
+ "extends": "/samples/base.json",
+ "files": {
+ "brick-viewer.ts": {},
+ "index.html": {}
+ }
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/02/before/brick-viewer.ts b/packages/lit-dev-content/samples/tutorials/brick-viewer/02/before/brick-viewer.ts
new file mode 100644
index 000000000..9442b6291
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/02/before/brick-viewer.ts
@@ -0,0 +1,16 @@
+import { LitElement, html, css } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+// @ts-ignore
+import * as THREE from 'three';
+// @ts-ignore
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+
+@customElement('brick-viewer')
+class BrickViewer extends LitElement {
+ @property({type: String})
+ src: string|null = null;
+
+ render() {
+ return html`Brick model: ${this.src}
`;
+ }
+}
\ No newline at end of file
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/02/before/index.html b/packages/lit-dev-content/samples/tutorials/brick-viewer/02/before/index.html
new file mode 100644
index 000000000..4d42a0baa
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/02/before/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/02/before/project.json b/packages/lit-dev-content/samples/tutorials/brick-viewer/02/before/project.json
new file mode 100644
index 000000000..cfc7995c0
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/02/before/project.json
@@ -0,0 +1,7 @@
+{
+ "extends": "/samples/base.json",
+ "files": {
+ "brick-viewer.ts": {},
+ "index.html": {}
+ }
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/03/after/brick-viewer.ts b/packages/lit-dev-content/samples/tutorials/brick-viewer/03/after/brick-viewer.ts
new file mode 100644
index 000000000..a215207b0
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/03/after/brick-viewer.ts
@@ -0,0 +1,138 @@
+import { LitElement,PropertyValues, css, html } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+
+// @ts-ignore
+import * as THREE from "three";
+// @ts-ignore
+import { LDrawLoader } from "three/examples/jsm/loaders/LDrawLoader.js";
+// @ts-ignore
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
+
+@customElement("brick-viewer")
+export class BrickViewer extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ }
+ `;
+
+ @property({ type: String })
+ src: string | null = null;
+
+ @property({ type: Number, reflect: true })
+ step?: number;
+
+ private _scene = new THREE.Scene();
+ private _renderer = new THREE.WebGLRenderer({ antialias: true });
+ private _camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
+ private _controls = new OrbitControls(this._camera, this._renderer.domElement);
+ private _loader = new LDrawLoader();
+ private _model: any;
+ private _numConstructionSteps?: number;
+
+ async firstUpdated() {
+ this._camera = new THREE.PerspectiveCamera(
+ 45,
+ this.clientWidth / this.clientHeight,
+ 1,
+ 10000
+ );
+ this._camera.position.set(150, 200, 250);
+
+ this._scene = new THREE.Scene();
+ this._scene.background = new THREE.Color(0xdeebed);
+
+ const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
+ this._scene.add(ambientLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(-1000, 1200, 1500);
+ this._scene.add(directionalLight);
+
+ this._renderer = new THREE.WebGLRenderer({ antialias: true });
+ this._renderer.setPixelRatio(window.devicePixelRatio);
+ this._renderer.setSize(this.offsetWidth, this.offsetHeight);
+
+ this._controls = new OrbitControls(this._camera, this._renderer.domElement);
+ this._controls.addEventListener("change", () =>
+ requestAnimationFrame(this._animate)
+ );
+
+ (this._loader as any).separateObjects = true;
+
+ this._animate();
+
+ const resizeObserver = new ResizeObserver(this._onResize);
+ resizeObserver.observe(this);
+ }
+
+ private _onResize = (entries: ResizeObserverEntry[]) => {
+ const { width, height } = entries[0].contentRect;
+ this._renderer.setSize(width, height);
+ this._camera.aspect = width / height;
+ this._camera.updateProjectionMatrix();
+ requestAnimationFrame(this._animate);
+ };
+
+ render() {
+ return html`
+ ${this._renderer.domElement}
+ `;
+ }
+
+ update(changedProperties: PropertyValues) {
+ if (changedProperties.has('src')) {
+ this._loadModel();
+ }
+ if (changedProperties.has('step')) {
+ this._updateBricksVisibility();
+ }
+ super.update(changedProperties);
+ }
+
+ private _updateBricksVisibility() {
+ this._model && this._model.traverse((c: any) => {
+ if (c.isGroup && this.step) {
+ c.visible = c.userData.constructionStep <= this.step;
+ }
+ });
+ requestAnimationFrame(this._animate.bind(this));
+ this.requestUpdate();
+ }
+
+ private _loadModel() {
+ if (this.src === null) {
+ return;
+ }
+ this._loader
+ .setPath('')
+ // Using our src property!
+ .load(this.src, (newModel: any) => {
+
+ if (this._model !== undefined) {
+ this._scene.remove(this._model);
+ this._model = undefined;
+ }
+
+ this._model = newModel;
+
+ // Convert from LDraw coordinates: rotate 180 degrees around OX
+ this._model.rotation.x = Math.PI;
+ this._scene.add(this._model);
+
+ this._numConstructionSteps = this._model.userData.numConstructionSteps;
+
+ const bbox = new THREE.Box3().setFromObject(this._model);
+ this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
+ this._controls.update();
+ this._controls.saveState();
+
+ this.step ??= this._numConstructionSteps!;
+ this._updateBricksVisibility();
+ });
+ }
+
+ private _animate = () => {
+ this._renderer.render(this._scene, this._camera);
+ };
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/03/after/index.html b/packages/lit-dev-content/samples/tutorials/brick-viewer/03/after/index.html
new file mode 100644
index 000000000..c01f57338
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/03/after/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/03/after/project.json b/packages/lit-dev-content/samples/tutorials/brick-viewer/03/after/project.json
new file mode 100644
index 000000000..cfc7995c0
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/03/after/project.json
@@ -0,0 +1,7 @@
+{
+ "extends": "/samples/base.json",
+ "files": {
+ "brick-viewer.ts": {},
+ "index.html": {}
+ }
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/03/before/brick-viewer.ts b/packages/lit-dev-content/samples/tutorials/brick-viewer/03/before/brick-viewer.ts
new file mode 100644
index 000000000..316859bd0
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/03/before/brick-viewer.ts
@@ -0,0 +1,122 @@
+import { ifDefined } from "lit/directives/if-defined.js";
+import { LitElement,PropertyValues, css, html } from 'lit';
+import { customElement, query, property } from 'lit/decorators.js';
+
+// @ts-ignore
+import * as THREE from "three";
+// @ts-ignore
+import { LDrawLoader } from "three/examples/jsm/loaders/LDrawLoader.js";
+// @ts-ignore
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
+
+@customElement("brick-viewer")
+export class BrickViewer extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ }
+ `;
+
+ @property({ type: String })
+ src: string | null = null;
+
+ @property({ type: Number, reflect: true })
+ step: number = 1;
+
+ private _scene = new THREE.Scene();
+ private _renderer = new THREE.WebGLRenderer({ antialias: true });
+ private _camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
+ private _controls = new OrbitControls(this._camera, this._renderer.domElement);
+ private _loader = new LDrawLoader();
+ private _model: any;
+ private _numConstructionSteps?: number;
+
+ async firstUpdated() {
+ this._camera = new THREE.PerspectiveCamera(
+ 45,
+ this.clientWidth / this.clientHeight,
+ 1,
+ 10000
+ );
+ this._camera.position.set(150, 200, 250);
+
+ this._scene = new THREE.Scene();
+ this._scene.background = new THREE.Color(0xdeebed);
+
+ const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
+ this._scene.add(ambientLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(-1000, 1200, 1500);
+ this._scene.add(directionalLight);
+
+ this._renderer = new THREE.WebGLRenderer({ antialias: true });
+ this._renderer.setPixelRatio(window.devicePixelRatio);
+ this._renderer.setSize(this.offsetWidth, this.offsetHeight);
+
+ this._controls = new OrbitControls(this._camera, this._renderer.domElement);
+ this._controls.addEventListener("change", () =>
+ requestAnimationFrame(this._animate)
+ );
+
+ (this._loader as any).separateObjects = true;
+
+ this._animate();
+
+ const resizeObserver = new ResizeObserver(this._onResize);
+ resizeObserver.observe(this);
+ }
+
+ private _onResize = (entries: ResizeObserverEntry[]) => {
+ const { width, height } = entries[0].contentRect;
+ this._renderer.setSize(width, height);
+ this._camera.aspect = width / height;
+ this._camera.updateProjectionMatrix();
+ requestAnimationFrame(this._animate);
+ };
+
+ render() {
+ return html`
+ ${this._renderer.domElement}
+ `;
+ }
+
+ update(changedProperties: PropertyValues) {
+ if (changedProperties.has("src")) {
+ this._loadModel();
+ }
+ super.update(changedProperties);
+ }
+
+ private _loadModel() {
+ if (this.src === null) {
+ return;
+ }
+ // @ts-ignore
+ this._loader.setPath("").load(this.src, newModel => {
+ if (this._model !== undefined) {
+ this._scene.remove(this._model);
+ this._model = undefined;
+ }
+
+ this._model = newModel;
+
+ // Convert from LDraw coordinates: rotate 180 degrees around OX
+ this._model.rotation.x = Math.PI;
+ this._scene.add(this._model);
+
+ this._numConstructionSteps = this._model.userData.numConstructionSteps;
+ this.step = this._numConstructionSteps!;
+
+ // Adjust camera
+ const bbox = new THREE.Box3().setFromObject(this._model);
+ this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
+ this._controls.update();
+ this._controls.saveState();
+ });
+ }
+
+ private _animate = () => {
+ this._renderer.render(this._scene, this._camera);
+ };
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/03/before/index.html b/packages/lit-dev-content/samples/tutorials/brick-viewer/03/before/index.html
new file mode 100644
index 000000000..48b32de1f
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/03/before/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/03/before/project.json b/packages/lit-dev-content/samples/tutorials/brick-viewer/03/before/project.json
new file mode 100644
index 000000000..cfc7995c0
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/03/before/project.json
@@ -0,0 +1,7 @@
+{
+ "extends": "/samples/base.json",
+ "files": {
+ "brick-viewer.ts": {},
+ "index.html": {}
+ }
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/04/after/brick-viewer.ts b/packages/lit-dev-content/samples/tutorials/brick-viewer/04/after/brick-viewer.ts
new file mode 100644
index 000000000..c1e3b545d
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/04/after/brick-viewer.ts
@@ -0,0 +1,204 @@
+import { ifDefined } from "lit/directives/if-defined.js";
+import { LitElement,PropertyValues, css, html } from 'lit';
+import { customElement, query, property } from 'lit/decorators.js';
+
+// @ts-ignore
+import * as THREE from "three";
+// @ts-ignore
+import { LDrawLoader } from "three/examples/jsm/loaders/LDrawLoader.js";
+// @ts-ignore
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
+
+import "@material/mwc-icon-button";
+import "@material/mwc-slider";
+// @ts-ignore
+import { Slider } from "@material/mwc-slider";
+
+@customElement("brick-viewer")
+export class BrickViewer extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ position: relative;
+ }
+ #controls {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ display: flex;
+ }
+ mwc-slider {
+ flex-grow: 1;
+ }
+ `;
+
+ @property({ type: String })
+ src: string | null = null;
+
+ @property({ type: Number, reflect: true })
+ step: number = 1;
+
+ @query("mwc-slider")
+ slider!: Slider | null;
+
+ private _scene = new THREE.Scene();
+ private _renderer = new THREE.WebGLRenderer({ antialias: true });
+ private _camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
+ private _controls = new OrbitControls(this._camera, this._renderer.domElement);
+ private _loader = new LDrawLoader();
+ private _model: any;
+ private _numConstructionSteps?: number;
+
+ async firstUpdated() {
+ this._camera = new THREE.PerspectiveCamera(
+ 45,
+ this.clientWidth / this.clientHeight,
+ 1,
+ 10000
+ );
+ this._camera.position.set(150, 200, 250);
+
+ this._scene = new THREE.Scene();
+ this._scene.background = new THREE.Color(0xdeebed);
+
+ const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
+ this._scene.add(ambientLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(-1000, 1200, 1500);
+ this._scene.add(directionalLight);
+
+ this._renderer = new THREE.WebGLRenderer({ antialias: true });
+ this._renderer.setPixelRatio(window.devicePixelRatio);
+ this._renderer.setSize(this.offsetWidth, this.offsetHeight);
+
+ this._controls = new OrbitControls(this._camera, this._renderer.domElement);
+ this._controls.addEventListener("change", () =>
+ requestAnimationFrame(this._animate)
+ );
+
+ (this._loader as any).separateObjects = true;
+
+ this._animate();
+
+ const resizeObserver = new ResizeObserver(this._onResize);
+ resizeObserver.observe(this);
+
+ // Buttons are loading after slider, so slider's initial width calculation is wrong.
+ if (this.slider) {
+ await this.slider.updateComplete;
+ this.slider.layout();
+ }
+ }
+
+ private _onResize = (entries: ResizeObserverEntry[]) => {
+ const { width, height } = entries[0].contentRect;
+ this._renderer.setSize(width, height);
+ this._camera.aspect = width / height;
+ this._camera.updateProjectionMatrix();
+ requestAnimationFrame(this._animate);
+ };
+
+ private _restart() {
+ this.step! = 1;
+ }
+
+ private _stepBack() {
+ this.step! -= 1;
+ }
+
+ private _stepForward() {
+ this.step! += 1;
+ }
+
+ private _resetCamera() {
+ this._controls.reset();
+ }
+
+ render() {
+ return html`
+ ${this._renderer.domElement}
+
+
+
+
+ (this.step = e.detail.value)}
+ >
+
+
+
+ `;
+ }
+
+ update(changedProperties: PropertyValues) {
+ if (changedProperties.has("src")) {
+ this._loadModel();
+ }
+ if (changedProperties.has("step")) {
+ this._updateBricksVisibility();
+ }
+ super.update(changedProperties);
+ }
+
+ private _loadModel() {
+ if (this.src === null) {
+ return;
+ }
+ // @ts-ignore
+ this._loader.setPath("").load(this.src, newModel => {
+ if (this._model !== undefined) {
+ this._scene.remove(this._model);
+ this._model = undefined;
+ }
+
+ this._model = newModel;
+
+ // Convert from LDraw coordinates: rotate 180 degrees around OX
+ this._model.rotation.x = Math.PI;
+ this._scene.add(this._model);
+
+ this._numConstructionSteps = this._model.userData.numConstructionSteps;
+ this.step = this._numConstructionSteps!;
+
+ // Adjust camera
+ const bbox = new THREE.Box3().setFromObject(this._model);
+ this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
+ this._controls.update();
+ this._controls.saveState();
+ });
+ }
+
+ private _updateBricksVisibility() {
+ this._model &&
+ this._model.traverse((c: any) => {
+ if (c.isGroup && this.step) {
+ c.visible = c.userData.constructionStep <= this.step;
+ }
+ });
+ requestAnimationFrame(this._animate);
+ }
+
+ private _animate = () => {
+ this._renderer.render(this._scene, this._camera);
+ };
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/04/after/index.html b/packages/lit-dev-content/samples/tutorials/brick-viewer/04/after/index.html
new file mode 100644
index 000000000..7059296d8
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/04/after/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/04/after/project.json b/packages/lit-dev-content/samples/tutorials/brick-viewer/04/after/project.json
new file mode 100644
index 000000000..cfc7995c0
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/04/after/project.json
@@ -0,0 +1,7 @@
+{
+ "extends": "/samples/base.json",
+ "files": {
+ "brick-viewer.ts": {},
+ "index.html": {}
+ }
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/04/before/brick-viewer.ts b/packages/lit-dev-content/samples/tutorials/brick-viewer/04/before/brick-viewer.ts
new file mode 100644
index 000000000..a215207b0
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/04/before/brick-viewer.ts
@@ -0,0 +1,138 @@
+import { LitElement,PropertyValues, css, html } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+
+// @ts-ignore
+import * as THREE from "three";
+// @ts-ignore
+import { LDrawLoader } from "three/examples/jsm/loaders/LDrawLoader.js";
+// @ts-ignore
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
+
+@customElement("brick-viewer")
+export class BrickViewer extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ }
+ `;
+
+ @property({ type: String })
+ src: string | null = null;
+
+ @property({ type: Number, reflect: true })
+ step?: number;
+
+ private _scene = new THREE.Scene();
+ private _renderer = new THREE.WebGLRenderer({ antialias: true });
+ private _camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
+ private _controls = new OrbitControls(this._camera, this._renderer.domElement);
+ private _loader = new LDrawLoader();
+ private _model: any;
+ private _numConstructionSteps?: number;
+
+ async firstUpdated() {
+ this._camera = new THREE.PerspectiveCamera(
+ 45,
+ this.clientWidth / this.clientHeight,
+ 1,
+ 10000
+ );
+ this._camera.position.set(150, 200, 250);
+
+ this._scene = new THREE.Scene();
+ this._scene.background = new THREE.Color(0xdeebed);
+
+ const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
+ this._scene.add(ambientLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(-1000, 1200, 1500);
+ this._scene.add(directionalLight);
+
+ this._renderer = new THREE.WebGLRenderer({ antialias: true });
+ this._renderer.setPixelRatio(window.devicePixelRatio);
+ this._renderer.setSize(this.offsetWidth, this.offsetHeight);
+
+ this._controls = new OrbitControls(this._camera, this._renderer.domElement);
+ this._controls.addEventListener("change", () =>
+ requestAnimationFrame(this._animate)
+ );
+
+ (this._loader as any).separateObjects = true;
+
+ this._animate();
+
+ const resizeObserver = new ResizeObserver(this._onResize);
+ resizeObserver.observe(this);
+ }
+
+ private _onResize = (entries: ResizeObserverEntry[]) => {
+ const { width, height } = entries[0].contentRect;
+ this._renderer.setSize(width, height);
+ this._camera.aspect = width / height;
+ this._camera.updateProjectionMatrix();
+ requestAnimationFrame(this._animate);
+ };
+
+ render() {
+ return html`
+ ${this._renderer.domElement}
+ `;
+ }
+
+ update(changedProperties: PropertyValues) {
+ if (changedProperties.has('src')) {
+ this._loadModel();
+ }
+ if (changedProperties.has('step')) {
+ this._updateBricksVisibility();
+ }
+ super.update(changedProperties);
+ }
+
+ private _updateBricksVisibility() {
+ this._model && this._model.traverse((c: any) => {
+ if (c.isGroup && this.step) {
+ c.visible = c.userData.constructionStep <= this.step;
+ }
+ });
+ requestAnimationFrame(this._animate.bind(this));
+ this.requestUpdate();
+ }
+
+ private _loadModel() {
+ if (this.src === null) {
+ return;
+ }
+ this._loader
+ .setPath('')
+ // Using our src property!
+ .load(this.src, (newModel: any) => {
+
+ if (this._model !== undefined) {
+ this._scene.remove(this._model);
+ this._model = undefined;
+ }
+
+ this._model = newModel;
+
+ // Convert from LDraw coordinates: rotate 180 degrees around OX
+ this._model.rotation.x = Math.PI;
+ this._scene.add(this._model);
+
+ this._numConstructionSteps = this._model.userData.numConstructionSteps;
+
+ const bbox = new THREE.Box3().setFromObject(this._model);
+ this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
+ this._controls.update();
+ this._controls.saveState();
+
+ this.step ??= this._numConstructionSteps!;
+ this._updateBricksVisibility();
+ });
+ }
+
+ private _animate = () => {
+ this._renderer.render(this._scene, this._camera);
+ };
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/04/before/index.html b/packages/lit-dev-content/samples/tutorials/brick-viewer/04/before/index.html
new file mode 100644
index 000000000..4e3ea01b3
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/04/before/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/04/before/project.json b/packages/lit-dev-content/samples/tutorials/brick-viewer/04/before/project.json
new file mode 100644
index 000000000..cfc7995c0
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/04/before/project.json
@@ -0,0 +1,7 @@
+{
+ "extends": "/samples/base.json",
+ "files": {
+ "brick-viewer.ts": {},
+ "index.html": {}
+ }
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/05/before/brick-viewer.ts b/packages/lit-dev-content/samples/tutorials/brick-viewer/05/before/brick-viewer.ts
new file mode 100644
index 000000000..c15aee363
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/05/before/brick-viewer.ts
@@ -0,0 +1,207 @@
+import { ifDefined } from "lit/directives/if-defined.js";
+import { LitElement,PropertyValues, css, html } from 'lit';
+import { customElement, query, property } from 'lit/decorators.js';
+
+// @ts-ignore
+import * as THREE from "three";
+// @ts-ignore
+import { LDrawLoader } from "three/examples/jsm/loaders/LDrawLoader.js";
+// @ts-ignore
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
+
+import "@material/mwc-icon-button";
+import "@material/mwc-slider";
+// @ts-ignore
+import { Slider } from "@material/mwc-slider";
+
+@customElement("brick-viewer")
+export class BrickViewer extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ position: relative;
+ }
+ #controls {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ display: flex;
+ }
+ mwc-slider {
+ flex-grow: 1;
+ }
+ `;
+
+ @property({ type: String })
+ src: string | null = null;
+
+ @property({ type: Number, reflect: true })
+ step: number = 1;
+
+ @query("mwc-slider")
+ slider!: Slider | null;
+
+ private _scene = new THREE.Scene();
+ private _renderer = new THREE.WebGLRenderer({ antialias: true });
+ private _camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
+ private _controls = new OrbitControls(this._camera, this._renderer.domElement);
+ private _loader = new LDrawLoader();
+ private _model: any;
+ private _numConstructionSteps?: number;
+
+ async firstUpdated() {
+ this._camera = new THREE.PerspectiveCamera(
+ 45,
+ this.clientWidth / this.clientHeight,
+ 1,
+ 10000
+ );
+ this._camera.position.set(150, 200, 250);
+
+ this._scene = new THREE.Scene();
+ this._scene.background = new THREE.Color(0xdeebed);
+
+ const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
+ this._scene.add(ambientLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(-1000, 1200, 1500);
+ this._scene.add(directionalLight);
+
+ this._renderer = new THREE.WebGLRenderer({ antialias: true });
+ this._renderer.setPixelRatio(window.devicePixelRatio);
+ this._renderer.setSize(this.offsetWidth, this.offsetHeight);
+
+ this._controls = new OrbitControls(this._camera, this._renderer.domElement);
+ this._controls.addEventListener("change", () =>
+ requestAnimationFrame(this._animate)
+ );
+
+ (this._loader as any).separateObjects = true;
+
+ this._animate();
+
+ const resizeObserver = new ResizeObserver(this._onResize);
+ resizeObserver.observe(this);
+
+ // Buttons are loading after slider, so slider's initial width calculation is wrong.
+ if (this.slider) {
+ await this.slider.updateComplete;
+ this.slider.layout();
+ }
+ }
+
+ private _onResize = (entries: ResizeObserverEntry[]) => {
+ const { width, height } = entries[0].contentRect;
+ this._renderer.setSize(width, height);
+ this._camera.aspect = width / height;
+ this._camera.updateProjectionMatrix();
+ requestAnimationFrame(this._animate);
+ };
+
+ private _restart() {
+ this.step! = 1;
+ }
+
+ private _stepBack() {
+ this.step! -= 1;
+ }
+
+ private _stepForward() {
+ this.step! += 1;
+ }
+
+ private _resetCamera() {
+ this._controls.reset();
+ }
+
+ render() {
+ return html`
+ ${this._renderer.domElement}
+
+
+
+
+ (this.step = e.detail.value)}
+ >
+
+
+
+ `;
+ }
+
+ update(changedProperties: PropertyValues) {
+ if (changedProperties.has("src")) {
+ this._loadModel();
+ }
+ if (changedProperties.has("step")) {
+ this._updateBricksVisibility();
+ }
+ super.update(changedProperties);
+ }
+
+ private _loadModel() {
+ if (this.src === null) {
+ return;
+ }
+ // @ts-ignore
+ this._loader.setPath("").load(this.src, newModel => {
+ if (this._model !== undefined) {
+ this._scene.remove(this._model);
+ this._model = undefined;
+ }
+
+ this._model = newModel;
+
+ // Convert from LDraw coordinates: rotate 180 degrees around OX
+ this._model.rotation.x = Math.PI;
+ this._scene.add(this._model);
+
+ this._numConstructionSteps = this._model.userData.numConstructionSteps;
+
+ // Adjust camera
+ const bbox = new THREE.Box3().setFromObject(this._model);
+ this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
+ this._controls.update();
+ this._controls.saveState();
+
+ this.step ??= this._numConstructionSteps!;
+ this._updateBricksVisibility();
+ });
+ }
+
+ private _updateBricksVisibility() {
+ this._model &&
+ this._model.traverse((c: any) => {
+ if (c.isGroup && this.step) {
+ c.visible = c.userData.constructionStep <= this.step;
+ }
+ });
+ requestAnimationFrame(this._animate);
+ this.requestUpdate();
+ }
+
+ private _animate = () => {
+ this._renderer.render(this._scene, this._camera);
+ };
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/05/before/index.html b/packages/lit-dev-content/samples/tutorials/brick-viewer/05/before/index.html
new file mode 100644
index 000000000..7059296d8
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/05/before/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/05/before/project.json b/packages/lit-dev-content/samples/tutorials/brick-viewer/05/before/project.json
new file mode 100644
index 000000000..cfc7995c0
--- /dev/null
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/05/before/project.json
@@ -0,0 +1,7 @@
+{
+ "extends": "/samples/base.json",
+ "files": {
+ "brick-viewer.ts": {},
+ "index.html": {}
+ }
+}
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/description.md b/packages/lit-dev-content/samples/tutorials/brick-viewer/description.md
index 699b0e63e..cba839535 100644
--- a/packages/lit-dev-content/samples/tutorials/brick-viewer/description.md
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/description.md
@@ -1,2 +1 @@
-Learn how to build a 3d brick viewer combining canvas and the best parts of Web
-Components!
\ No newline at end of file
+In this tutorial, you'll build a Brick Viewer web component with the help of lit.
\ No newline at end of file
diff --git a/packages/lit-dev-content/samples/tutorials/brick-viewer/tutorial.json b/packages/lit-dev-content/samples/tutorials/brick-viewer/tutorial.json
index 639c6c654..68f291faf 100644
--- a/packages/lit-dev-content/samples/tutorials/brick-viewer/tutorial.json
+++ b/packages/lit-dev-content/samples/tutorials/brick-viewer/tutorial.json
@@ -2,18 +2,34 @@
"header": "Build a Brick Viewer",
"difficulty": "Intermediate",
"size": "medium",
- "duration": 70,
+ "duration": 60,
"category": "Build",
- "imgSrc": "images/logo-whitebg-padded-1600x800.png",
- "imgAlt": "This is the Lit icon",
+ "imgSrc": "images/tutorials/brick-viewer/preview.gif",
+ "imgAlt": "This is the demo of the brick viewer component assembling the lego bricks",
"steps": [
{
- "title": "Build a Brick Viewer",
+ "title": "Define a Custom Element",
"hasAfter": true
},
{
- "title": "Ya Bricked it!",
+ "title": "Specifying the LDraw File",
"hasAfter": true
+ },
+ {
+ "title": "Set the Scene with Three.js",
+ "hasAfter": true
+ },
+ {
+ "title": "Displaying Partial Models",
+ "hasAfter": true
+ },
+ {
+ "title": "Brick Set Navigation",
+ "hasAfter": true
+ },
+ {
+ "title": "Conclusion",
+ "hasAfter": false
}
]
}
\ No newline at end of file
diff --git a/packages/lit-dev-content/samples/tutorials/intro-to-lit/tutorial.json b/packages/lit-dev-content/samples/tutorials/intro-to-lit/tutorial.json
index a4f157988..f1d22cced 100644
--- a/packages/lit-dev-content/samples/tutorials/intro-to-lit/tutorial.json
+++ b/packages/lit-dev-content/samples/tutorials/intro-to-lit/tutorial.json
@@ -5,10 +5,6 @@
"duration": 20,
"category": "Learn",
"steps": [
- {
- "title": "Lit tutorial",
- "hasAfter": true
- },
{
"title": "Define a component",
"hasAfter": true
diff --git a/packages/lit-dev-content/site/images/tutorials/brick-viewer/buttons.png b/packages/lit-dev-content/site/images/tutorials/brick-viewer/buttons.png
new file mode 100644
index 000000000..45c41ce9b
Binary files /dev/null and b/packages/lit-dev-content/site/images/tutorials/brick-viewer/buttons.png differ
diff --git a/packages/lit-dev-content/site/images/tutorials/brick-viewer/final.gif b/packages/lit-dev-content/site/images/tutorials/brick-viewer/final.gif
new file mode 100644
index 000000000..e4df720d1
Binary files /dev/null and b/packages/lit-dev-content/site/images/tutorials/brick-viewer/final.gif differ
diff --git a/packages/lit-dev-content/site/images/tutorials/brick-viewer/preview.gif b/packages/lit-dev-content/site/images/tutorials/brick-viewer/preview.gif
new file mode 100644
index 000000000..e4df720d1
Binary files /dev/null and b/packages/lit-dev-content/site/images/tutorials/brick-viewer/preview.gif differ
diff --git a/packages/lit-dev-content/site/tutorials/content/brick-viewer/00.md b/packages/lit-dev-content/site/tutorials/content/brick-viewer/00.md
index 1862000d8..8be086f63 100644
--- a/packages/lit-dev-content/site/tutorials/content/brick-viewer/00.md
+++ b/packages/lit-dev-content/site/tutorials/content/brick-viewer/00.md
@@ -1,44 +1,25 @@
-This is the intro that describes the codelab.
+We will be going over how to build a `` component to render a [LDraw](https://threejs.org/docs/#examples/en/loaders/LDrawLoader) model in [THREE.js](https://threejs.org/).
-In this codelab we are going to cover
-
-* canvases
-* custom elements
-* 3d stuff
-
-Here is a TS / JS switchable sample
+To get started you'll need to create a basic lit component and give it the tag name of `brick-viewer` while returning a simple `div`.
{% switchable-sample %}
```ts
-@customElement('my-element')
-class MyElement extends LitElement {
- @property({attribute: false}) items = [1,2,3];
+@customElement('brick-viewer')
+class BrickViewer extends LitElement {
render() {
- html`
-
- ${this.items.map(item => html`${item} `)}
- `;
+ return html`Brick viewer
`;
}
}
```
```js
-class MyElement extends LitElement {
+class BrickViewer extends LitElement {
render() {
- static properties = {items: {attribute: false}};
- constructor() {
- super();
- this.items = [1,2,3];
- }
-
- html`
-
- ${this.items.map(item => html`${item} `)}
- `;
+ return html`Brick viewer
`;
}
}
-customElements.define('my-element', MyElement);
+customElements.define('brick-viewer', BrickViewer);
```
{% endswitchable-sample %}
\ No newline at end of file
diff --git a/packages/lit-dev-content/site/tutorials/content/brick-viewer/01.md b/packages/lit-dev-content/site/tutorials/content/brick-viewer/01.md
index abe09c0fd..867b278e3 100644
--- a/packages/lit-dev-content/site/tutorials/content/brick-viewer/01.md
+++ b/packages/lit-dev-content/site/tutorials/content/brick-viewer/01.md
@@ -1,7 +1,53 @@
-You are DONE-ZO! Read more about [X](https://lit.dev) [Y](https://google.com) and [Z](https://web.dev).
+### Define a property
-See also these tutorials maybe?
+It would be great if a user of the `` could specify which brick model to display using an attribute, like this:
-* list
-* list
-* list
\ No newline at end of file
+```html
+
+```
+
+Since you are building an HTML element, you can take advantage of the declarative API and define a source attribute, just like an ` ` or `` tag. With lit-element, it's as easy as decorating a class property with `@property`. The `type` option lets you specify how lit-element parses the property for use as an HTML attribute.
+
+Define the `src` property and attribute:
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ @property({type: String})
+ src: string|null = null;
+}
+```
+
+```js
+class BrickViewer extends LitElement {
+ static properties = {
+ src: {type: String},
+ };
+}
+customElements.define('brick-viewer', BrickViewer);
+```
+
+{% endswitchable-sample %}
+
+### Displaying values
+
+You can display the value of the `src` attribute by using it in the render method's template literal. Interpolate values into template literals using `${value}` syntax.
+
+```js
+export class BrickViewer extends LitElement {
+ render() {
+ return html`Brick model: ${this.src}
`;
+ }
+}
+```
+
+Now, you can see the value of the src attribute in the `` element in the window. Try this: open your browser's developer tools and manually change the src attribute. Go ahead, try it...
+
+...Did you notice that the text in the element updates automatically? lit observes the class properties decorated with `@property` and re-render the view for you! lit does the heavy lifting so you don't have to.
+
+
+
+Wondering about re-rendering efficiency? [lit](https://lit.dev/docs/components/rendering/) will only update the parts of your templates that change, without DOM diffing.
+
+
diff --git a/packages/lit-dev-content/site/tutorials/content/brick-viewer/02.md b/packages/lit-dev-content/site/tutorials/content/brick-viewer/02.md
new file mode 100644
index 000000000..1e550f1f5
--- /dev/null
+++ b/packages/lit-dev-content/site/tutorials/content/brick-viewer/02.md
@@ -0,0 +1,319 @@
+### Lights, Camera, Render!
+
+Our custom element will use three.js to render our 3D brick models. There are some things we want to do just once for each instance of a `` element, such as set up the three.js scene, camera, and lighting. We'll add these to the constructor the BrickViewer class. We'll keep some objects as class properties so we can use them later: camera, scene, controls, and renderer.
+
+Add in the three.js scene setup:
+
+
+{% switchable-sample %}
+
+
+```ts
+export class BrickViewer extends LitElement {
+ private _scene = new THREE.Scene();
+ private _renderer = new THREE.WebGLRenderer({ antialias: true });
+ private _camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
+ private _controls = new OrbitControls(this._camera, this._renderer.domElement);
+
+ /**
+ * Resize the canvas when the window is resized.
+ */
+ private _onResize = (entries: ResizeObserverEntry[]) => {
+ const { width, height } = entries[0].contentRect;
+ this._renderer.setSize(width, height);
+ this._camera.aspect = width / height;
+ this._camera.updateProjectionMatrix();
+ requestAnimationFrame(this._animate.bind(this));
+ };
+
+ /**
+ * Animate the scene.
+ */
+ private _animate = (time: number = 0) => {
+ // Render the scene
+ this._renderer.render(this._scene, this._camera);
+ };
+
+ /**
+ * Initialize the scene.
+ */
+ firstUpdated() {
+ // Create the camera
+ this._camera = new THREE.PerspectiveCamera(45, this.clientWidth/this.clientHeight, 1, 10000);
+ this._camera.position.set(150, 200, 250);
+
+ // Initialize the renderer
+ this._renderer.setPixelRatio(window.devicePixelRatio);
+ this._renderer.setSize(this.offsetWidth, this.offsetHeight);
+
+ // Add orbit controls to the camera
+ this._controls = new OrbitControls(this._camera, this._renderer.domElement);
+ this._controls.addEventListener("change", () => {
+ requestAnimationFrame(this._animate.bind(this));
+ });
+
+ // Set the background color
+ this._scene.background = new THREE.Color(0xdeebed);
+
+ // Add an ambient light
+ const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
+ this._scene.add(ambientLight);
+
+ // Add a directional light
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(-1000, 1200, 1500);
+ this._scene.add(directionalLight);
+
+ // Add a resize observer for the element
+ const resizeObserver = new ResizeObserver(this._onResize);
+ resizeObserver.observe(this);
+
+ // Start the animation loop
+ this._renderer.setAnimationLoop(this._animate.bind(this));
+ }
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ _scene = new THREE.Scene();
+ _renderer = new THREE.WebGLRenderer({ antialias: true });
+ _camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
+ _controls = new OrbitControls(this._camera, this._renderer.domElement);
+
+ /**
+ * Resize the canvas when the window is resized.
+ */
+ private _onResize = (entries) => {
+ const { width, height } = entries[0].contentRect;
+ this._renderer.setSize(width, height);
+ this._camera.aspect = width / height;
+ this._camera.updateProjectionMatrix();
+ requestAnimationFrame(this._animate.bind(this));
+ };
+
+ /**
+ * Animate the scene.
+ */
+ private _animate = (time = 0) => {
+ // Render the scene
+ this._renderer.render(this._scene, this._camera);
+ };
+
+ /**
+ * Initialize the scene.
+ */
+ firstUpdated() {
+ // Create the camera
+ this._camera = new THREE.PerspectiveCamera(45, this.clientWidth/this.clientHeight, 1, 10000);
+ this._camera.position.set(150, 200, 250);
+
+ // Initialize the renderer
+ this._renderer.setPixelRatio(window.devicePixelRatio);
+ this._renderer.setSize(this.offsetWidth, this.offsetHeight);
+
+ // Add orbit controls to the camera
+ this._controls = new OrbitControls(this._camera, this._renderer.domElement);
+ this._controls.addEventListener("change", () => {
+ requestAnimationFrame(this._animate.bind(this));
+ });
+
+ // Set the background color
+ this._scene.background = new THREE.Color(0xdeebed);
+
+ // Add an ambient light
+ const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
+ this._scene.add(ambientLight);
+
+ // Add a directional light
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(-1000, 1200, 1500);
+ this._scene.add(directionalLight);
+
+ // Add a resize observer for the element
+ const resizeObserver = new ResizeObserver(this._onResize);
+ resizeObserver.observe(this);
+
+ // Start the animation loop
+ this._renderer.setAnimationLoop(this._animate.bind(this));
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+The `WebGLRenderer` object provides a DOM element that displays the rendered three.js scene. It's accessed via the `domElement` property. We can interpolate this value into the render template literal, using `${value}` syntax.
+
+Remove the `src` message we had in the template, and insert the renderer's DOM element:
+
+```ts
+export class BrickViewer extends LitElement {
+ render() {
+ return html`
+ ${this._renderer.domElement}
+ `;
+ }
+}
+```
+
+To allow the renderer's dom element to be shown in its entirety, we also need to set the `` element itself to `display: block`. We can provide styles in a static property called `styles`, set to a `css` template literal.
+
+Add this styling to the class:
+
+```ts
+export class BrickViewer extends LitElement {
+ static styles = css`
+ /* The :host selector styles the brick-viewer itself! */
+ :host {
+ display: block;
+ }
+ `;
+}
+```
+
+Now `` is displaying a rendered three.js scene!
+
+But... it's empty. Let's provide it with a model.
+
+### Brick loader
+
+We'll pass the `src` property we defined earlier to the LDrawLoader, which is shipped with three.js.
+
+LDraw files can separate a Brick model into separate building steps. Total number of steps and individual brick visibility are accessible through the LDrawLoader API.
+
+Copy these properties, the new `_loadModel` method, and the new line in constructor:
+
+{% switchable-sample %}
+
+```ts
+@customElement('brick-viewer')
+export class BrickViewer extends LitElement {
+ private _loader = new LDrawLoader();
+ private _model: any;
+ private _numConstructionSteps?: number;
+ step?: number;
+
+ constructor() {
+ // ...
+ // Add this line right before this._animate();
+ (this._loader as any).separateObjects = true;
+ this._animate();
+ }
+
+ private _loadModel() {
+ if (this.src === null) {
+ return;
+ }
+ this._loader
+ .setPath('')
+ // Using our src property!
+ .load(this.src, (newModel) => {
+
+ if (this._model !== undefined) {
+ this._scene.remove(this._model);
+ this._model = undefined;
+ }
+
+ this._model = newModel;
+
+ // Convert from LDraw coordinates: rotate 180 degrees around OX
+ this._model.rotation.x = Math.PI;
+ this._scene.add(this._model);
+
+ this._numConstructionSteps = this._model.userData.numConstructionSteps;
+ this.step = this._numConstructionSteps!;
+
+ const bbox = new THREE.Box3().setFromObject(this._model);
+ this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
+ this._controls.update();
+ this._controls.saveState();
+ });
+ }
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ _loader = new LDrawLoader();
+ _model;
+ _numConstructionSteps?;
+ step = 1;
+
+ constructor() {
+ // ...
+ // Add this line right before this._animate();
+ (this._loader as any).separateObjects = true;
+ this._animate();
+ }
+
+ _loadModel() {
+ if (this.src === null) {
+ return;
+ }
+ this._loader
+ .setPath('')
+ // Using our src property!
+ .load(this.src, (newModel) => {
+
+ if (this._model !== undefined) {
+ this._scene.remove(this._model);
+ this._model = undefined;
+ }
+
+ this._model = newModel;
+
+ // Convert from LDraw coordinates: rotate 180 degrees around OX
+ this._model.rotation.x = Math.PI;
+ this._scene.add(this._model);
+
+ this._numConstructionSteps = this._model.userData.numConstructionSteps;
+ this.step = this._numConstructionSteps!;
+
+ const bbox = new THREE.Box3().setFromObject(this._model);
+ this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
+ this._controls.update();
+ this._controls.saveState();
+ });
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+When should `_loadModel` be called? It needs to be invoked every time the src attribute changes. By decorating the `src` property with `@property`, we have opted the property into the lit-element update lifecycle. Whenever one of these decorated properties' value changes, a series of methods are called that can access the new and old values of the properties. The lifecycle method we're interested in is called `update`. The `update` method takes a `PropertyValues` argument, which will contain information about any properties that have just changed. This is the perfect place to call `_loadModel`.
+
+Add the `update` method:
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ update(changedProperties: PropertyValues) {
+ if (changedProperties.has('src')) {
+ this._loadModel();
+ }
+ super.update(changedProperties);
+ }
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ update(changedProperties) {
+ if (changedProperties.has('src')) {
+ this._loadModel();
+ }
+ super.update(changedProperties);
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+Our `` element can now display a brick file, specified with the `src` attribute.
+
+
+
+Learn more: You can read about the lit-element update lifecycle at [lit.dev](https://lit.dev/docs/components/lifecycle/).
+
+
\ No newline at end of file
diff --git a/packages/lit-dev-content/site/tutorials/content/brick-viewer/03.md b/packages/lit-dev-content/site/tutorials/content/brick-viewer/03.md
new file mode 100644
index 000000000..4b3bb2248
--- /dev/null
+++ b/packages/lit-dev-content/site/tutorials/content/brick-viewer/03.md
@@ -0,0 +1,88 @@
+Now, let's make the current construction step configurable. We'd like to be able to specify ` `, and we should see what the brick model looks like on the 5th construction step. To do that, let's make the `step` property an observed property by decorating it with `@property`.
+
+Decorate the `step` property:
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ @property({type: Number})
+ step?: number;
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ static get properties() {
+ return {
+ step: {type: Number},
+ };
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+Now, we'll add a helper method that makes only the bricks up to the current build step visible. We'll call the helper in the update method so that it runs every time the `step` property is changed.
+
+Update the `update` method, and add the new `_updateBricksVisibility` method:
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ update(changedProperties: PropertyValues) {
+ if (changedProperties.has('src')) {
+ this._loadModel();
+ }
+ if (changedProperties.has('step')) {
+ this._updateBricksVisibility();
+ }
+ super.update(changedProperties);
+ }
+
+ private _updateBricksVisibility() {
+ this._model && this._model.traverse((c: any) => {
+ if (c.isGroup && this.step) {
+ c.visible = c.userData.constructionStep <= this.step;
+ }
+ });
+ requestAnimationFrame(this._animate);
+ }
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ static get properties() {
+ return {
+ step: {type: Number},
+ };
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has('src')) {
+ this._loadModel();
+ }
+ if (changedProperties.has('step')) {
+ this._updateBricksVisibility();
+ }
+ super.update(changedProperties);
+ }
+
+ private _updateBricksVisibility() {
+ this._model && this._model.traverse((c) => {
+ if (c.isGroup && this.step) {
+ c.visible = c.userData.constructionStep <= this.step;
+ }
+ });
+ requestAnimationFrame(this._animate);
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+Okay, now open up your browser's **devtools**, and inspect the `` element. Add a `step` attribute to it.
+
+Watch what happens to the rendered model! We can use the `step` attribute to control how much of the model is shown. Here's what it should look like when the `step` attribute is set to `"10"`:
diff --git a/packages/lit-dev-content/site/tutorials/content/brick-viewer/04.md b/packages/lit-dev-content/site/tutorials/content/brick-viewer/04.md
new file mode 100644
index 000000000..98a96d75a
--- /dev/null
+++ b/packages/lit-dev-content/site/tutorials/content/brick-viewer/04.md
@@ -0,0 +1,511 @@
+### mwc-icon-button
+
+The end-user of our `` should also be able to navigate the build steps via UI. Let's add buttons for going to the next step, previous step, and first step. We'll use Material Design's button web component to make it easy. Since `@material/mwc-icon-button` is already imported, we're ready to drop in ` `. We can specify the icon we'd like to use with the icon attribute, like this: ` `. All possible icons can be found here: [material.io/resources/icons](https://material.io/resources/icons).
+
+Let's add some icon buttons to the render method:
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ render() {
+ return html`
+ ${this._renderer.domElement}
+
+
+
+
+
+ `;
+ }
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ render() {
+ return html`
+ ${this._renderer.domElement}
+
+
+
+
+
+ `;
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+Using Material Design on our page is that easy, thanks to web components!
+
+### Event bindings
+
+These buttons should actually do something. The "reply" button should reset the construction step to 1. The "navigate_before" button should decrement the construction step, and the "navigate_next" button should increment it. lit-element makes it easy to add this functionality, with event bindings. In your html template literal, use the syntax `@eventname=${eventHandler}` as an element attribute. `eventHandler` will now run when an `eventname` event is detected on that element! As an example, let's add click event handlers to our three icon buttons:
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ private _restart() {
+ this.step! = 1;
+ }
+
+ private _stepBack() {
+ this.step! -= 1;
+ }
+
+ private _stepForward() {
+ this.step! += 1;
+ }
+
+ render() {
+ return html`
+ ${this._renderer.domElement}
+
+
+
+
+
+ `;
+ }
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ private _restart() {
+ this.step = 1;
+ }
+
+ private _stepBack() {
+ this.step -= 1;
+ }
+
+ private _stepForward() {
+ this.step += 1;
+ }
+
+ render() {
+ return html`
+ ${this._renderer.domElement}
+
+
+
+
+
+ `;
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+Try clicking the buttons now. Nice job!
+
+### Styles
+
+The buttons work, but they don't look good. They're all huddled at the bottom. Let's style them to overlay them on the scene.
+
+To apply styles to these buttons, we return to the `static styles` property. These styles are scoped, which means they'll only apply to elements within this web component. That's one of the joys of writing web components: selectors can be simpler, and CSS will be easier to read and write. Bye-bye, [BEM](https://css-tricks.com/bem-101/)!
+
+Update the styles so they look like this:
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ position: relative;
+ }
+ #controls {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ display: flex;
+ }
+ `;
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ position: relative;
+ }
+ #controls {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ display: flex;
+ }
+ `;
+}
+```
+
+{% endswitchable-sample %}
+
+
+
+### Reset camera button
+
+End-users of our `` can rotate the scene using mouse controls. While we're adding buttons, let's add one for resetting the camera to its default position. Another `` with a click event binding will get the job done.
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ private _resetCamera() {
+ this._controls.reset();
+ }
+
+ render() {
+ return html`
+
+
+
+
+
+ `;
+ }
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ private _resetCamera() {
+ this._controls.reset();
+ }
+
+ render() {
+ return html`
+
+
+
+
+
+ `;
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+### Quicker navigation
+
+Some brick sets have lots of steps. A user may want to skip to a specific step. Adding a slider with step numbers can help with quick navigation. We'll use the `` element for this.
+
+### mwc-slider
+
+The slider element needs a few pieces of important data, like the minimum and maximum slider value. The minimum slider value can always be "1". The maximum slider value should be `this._numConstructionSteps`, if the model has loaded. We can tell `` these values through its attributes. We can also use the `ifDefined` lit-html _directive_ to avoid setting the `max` attribute if the `_numConstructionSteps` property hasn't been defined.
+
+Add an `` between the "back" and "forward" buttons:
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ render() {
+ return html`
+
+
+
+
+
+
+ `;
+ }
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ render() {
+ return html`
+
+
+
+
+
+
+ `;
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+### Data "up"
+
+When a user moves the slider, the current construction step should change, and the model's visibility should be updated accordingly. The slider element will emit an input event whenever the slider is dragged. Add an event binding on the slider itself to catch this event and change the construction step.
+
+Add the event binding:
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ render() {
+ return html`
+
+
+
+ this.step = e.detail.value}
+ >
+
+
+ `;
+ }
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ render() {
+ return html`
+
+
+
+ this.step = e.detail.value}
+ >
+
+
+ `;
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+Woo! We can use the slider to change which step is displayed.
+
+### Data "down"
+
+There's one more thing. When the "back" and "next" buttons are used to change the step, the _slider_ handle needs to be updated. Bind ``'s value attribute to `this.step`.
+
+
+
+When using web components, use events to propagate data "up" from elements, and use attributes to propagate data "down" to children.
+
+
+
+Add the `value` binding:
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ render() {
+ return html`
+
+
+
+
+
+
+ `;
+ }
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ render() {
+ return html`
+
+
+
+
+
+
+ `;
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+We're almost done with the slider. Add a flex style to make it play nicely with the other controls:
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ static styles = css`
+ /* ... */
+ mwc-slider {
+ flex-grow: 1;
+ }
+ `;
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ static styles = css`
+ /* ... */
+ mwc-slider {
+ flex-grow: 1;
+ }
+ `;
+}
+```
+
+{% endswitchable-sample %}
+
+Also, we need to call `layout` on the slider element itself. We'll do that in the `firstUpdated` lifecycle method, which is called once the DOM is first laid out. The `query` decorator can help us get a reference to the slider element in the template.
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ @query('mwc-slider')
+ slider!: Slider|null;
+
+ async firstUpdated() {
+ if (this.slider) {
+ await this.slider.updateComplete
+ this.slider.layout();
+ }
+ }
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ @query('mwc-slider')
+ slider!: Slider|null;
+
+ async firstUpdated() {
+ if (this.slider) {
+ await this.slider.updateComplete
+ this.slider.layout();
+ }
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+Here's all of the slider additions put together (with extra `pin` and `markers` attributes on the slider to make it look cool):
+
+{% switchable-sample %}
+
+```ts
+export class BrickViewer extends LitElement {
+ @query('mwc-slider')
+ slider!: Slider|null;
+
+ static styles = css`
+ /* ... */
+ mwc-slider {
+ flex-grow: 1;
+ }
+ `;
+
+ async firstUpdated() {
+ if (this.slider) {
+ await this.slider.updateComplete
+ this.slider.layout();
+ }
+ }
+
+ render() {
+ return html`
+ ${this._renderer.domElement}
+
+
+
+ this.constructionStep = e.detail.value}
+ >
+
+
+
+ `;
+ }
+}
+```
+
+```js
+export class BrickViewer extends LitElement {
+ @query('mwc-slider')
+ slider;
+
+ static styles = css`
+ /* ... */
+ mwc-slider {
+ flex-grow: 1;
+ }
+ `;
+
+ async firstUpdated() {
+ if (this.slider) {
+ await this.slider.updateComplete
+ this.slider.layout();
+ }
+ }
+
+ render() {
+ return html`
+ ${this._renderer.domElement}
+
+
+
+ this.constructionStep = e.detail.value}
+ >
+
+
+
+ `;
+ }
+}
+```
+
+{% endswitchable-sample %}
+
+Here's the final product!
+
+
diff --git a/packages/lit-dev-content/site/tutorials/content/brick-viewer/05.md b/packages/lit-dev-content/site/tutorials/content/brick-viewer/05.md
new file mode 100644
index 000000000..db075a068
--- /dev/null
+++ b/packages/lit-dev-content/site/tutorials/content/brick-viewer/05.md
@@ -0,0 +1,18 @@
+We learned a lot about how to use lit-element to build our very own HTML element. We learned how to:
+
+- Define a custom element
+- Declare an attribute API
+- Render a view for a custom element
+- Encapsulate styles
+- Use events and properties to pass data
+
+If you want to learn more about lit-element, you can read more at its [official site](https://lit-element.polymer-project.org/).
+
+You can view a completed brick-viewer element at [stackblitz.com/edit/brick-viewer-complete](https://stackblitz.com/edit/brick-viewer-complete?file=brick-viewer.ts).
+
+brick-viewer is also shipped on NPM, and you can view the source here: [Github repo](https://github.com/PolymerLabs/brick-viewer).
+
+### More models to test
+- https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/ldraw/officialLibrary/models/car.ldr_Packed.mpd
+- https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/ldraw/officialLibrary/models/4915-1-MiniConstruction.mpd_Packed.mpd
+- https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/ldraw/officialLibrary/models/889-1-RadarTruck.mpd_Packed.mpd