From 12e1d11ee8fe574e37ff7f3fbf39df977f89ccc5 Mon Sep 17 00:00:00 2001 From: Rhiannan Berry Date: Sun, 4 Apr 2021 13:00:02 -0400 Subject: [PATCH] Linting galore --- .eslintrc.js | 32 ++- .prettierrc.js | 7 + .prettierrc.json | 4 - package.json | 3 +- src/components/avatar_part_radio_group.tsx | 59 ++--- src/components/color.tsx | 61 ++--- src/components/color_radio_group.tsx | 95 ++++--- src/components/editor.tsx | 169 ++++++------ src/components/material_radio_group.tsx | 73 +++--- src/components/page_radio_group.tsx | 42 ++- src/components/radio.tsx | 46 ++-- src/custom_types/import-png.d.ts | 4 +- src/main.js | 285 +++++++++------------ src/models/avatar_base.ts | 81 +++--- src/models/avatar_part.ts | 57 ++--- src/models/materials/base_material.ts | 6 +- src/models/materials/material.ts | 44 ++-- src/models/materials/texture.ts | 45 +--- src/util.js | 13 - src/util.ts | 34 +++ tsconfig.json | 6 +- 21 files changed, 553 insertions(+), 613 deletions(-) create mode 100644 .prettierrc.js delete mode 100644 .prettierrc.json delete mode 100644 src/util.js create mode 100644 src/util.ts diff --git a/.eslintrc.js b/.eslintrc.js index 5d8a57af7..63105ce1d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,17 @@ module.exports = { - parser: "babel-eslint", + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + ecmaFeatures: { + jsx: true + } + }, + settings: { + react: { + version: "detect" + } + }, env: { browser: true, es6: true, @@ -10,15 +22,13 @@ module.exports = { AFRAME: true, NAF: true }, - plugins: ["prettier", "react"], + extends: [ + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "prettier/@typescript-eslint", + "plugin:prettier/recommended" + ], rules: { - "prettier/prettier": "error", - "prefer-const": "error", - "no-use-before-define": "error", - "no-var": "error", - "no-throw-literal": "error", - // Light console usage is useful but remove debug logs before merging to master. - "no-console": "off" - }, - extends: ["prettier", "plugin:react/recommended", "eslint:recommended"] + + } }; \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..787cabf42 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,7 @@ +module.exports = { + semi: true, + trailingComma: "all", + singleQuote: true, + printWidth: 120, + tabWidth: 4 +} \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index a5fce4872..000000000 --- a/.prettierrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "printWidth": 120, - "parser": "babel" - } \ No newline at end of file diff --git a/package.json b/package.json index 5cbbb51e2..5da14c390 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ }, "scripts": { "prettier": "prettier --write src/**/*.js", - "lint:js": "eslint src/**/*.js", + "lint": "eslint 'src/**/*.{js,ts,tsx}'", + "lint:fix": "eslint 'src/**/*.{js,ts,tsx}' --fix", "start": "webpack-dev-server --mode=development", "build": "webpack --mode=production " }, diff --git a/src/components/avatar_part_radio_group.tsx b/src/components/avatar_part_radio_group.tsx index 490d955a0..e90200ec0 100644 --- a/src/components/avatar_part_radio_group.tsx +++ b/src/components/avatar_part_radio_group.tsx @@ -1,11 +1,11 @@ -import React, { Component, } from "react"; -import * as PropTypes from "prop-types"; +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faBan } from "@fortawesome/free-solid-svg-icons/faBan"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBan } from '@fortawesome/free-solid-svg-icons/faBan'; -import AvatarPart from "../models/avatar_part"; -import Radio from "./radio"; +import AvatarPart from '../models/avatar_part'; +import Radio from './radio'; interface AvatarPartRadioGroupProps { avatarPart: AvatarPart; @@ -13,14 +13,14 @@ interface AvatarPartRadioGroupProps { } export default class AvatarPartRadioGroup extends Component { - disabled: Boolean; - isRequired: Boolean; + disabled: boolean; + isRequired: boolean; props: AvatarPartRadioGroupProps; static propTypes = { avatarPart: PropTypes.instanceOf(AvatarPart), - iconPaths: PropTypes.arrayOf(PropTypes.string) - } + iconPaths: PropTypes.arrayOf(PropTypes.string), + }; constructor(props: AvatarPartRadioGroupProps) { super(props); @@ -31,44 +31,45 @@ export default class AvatarPartRadioGroup extends Component { this.togglePart = this.togglePart.bind(this); } - componentDidMount() { + componentDidMount(): void { //random start const count = this.props.iconPaths.length; const rand = Math.floor(Math.random() * count); - - if (this.isRequired || Math.random() < (1 - (1/count))) { + + if (this.isRequired || Math.random() < 1 - 1 / count) { this.togglePart(rand); } else { this.disablePart(); } } - disablePart() { + disablePart(): void { this.props.avatarPart.disable(); this.forceUpdate(); } - togglePart(partIndex: number) { + togglePart(partIndex: number): void { this.props.avatarPart.toggleMesh(partIndex); this.forceUpdate(); } - render() { - const disableButton = this.isRequired - ? null - : - - ; + render(): JSX.Element { + const disableButton = this.isRequired ? null : ( + + + + ); - const parts = this.props.iconPaths.map((path, i) => - ( + - + selected={!this.props.avatarPart.disabled && this.props.avatarPart.isSelected(i)} + > + - ); + )); return (
@@ -77,4 +78,4 @@ export default class AvatarPartRadioGroup extends Component {
); } -} \ No newline at end of file +} diff --git a/src/components/color.tsx b/src/components/color.tsx index d9550673a..3aa8afdc6 100644 --- a/src/components/color.tsx +++ b/src/components/color.tsx @@ -1,9 +1,9 @@ -import React, {Component,} from 'react'; -import * as PropTypes from "prop-types"; -import { CustomPicker } from 'react-color'; -import * as tinycolor from 'tinycolor2'; +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; +import { CustomPicker, ColorResult } from 'react-color'; +import tinycolor from 'tinycolor2'; -import {Hue, Saturation} from 'react-color/lib/components/common'; +import { Hue, Saturation } from 'react-color/lib/components/common'; interface MyColorPickerProps { color: string; @@ -15,13 +15,13 @@ class MyColorPicker extends Component { state = { hsl: { h: 0, s: 0, l: 0 }, hsv: { h: 0, s: 0, v: 0 }, - hex: 'aaaaaa' + hex: 'aaaaaa', }; static propTypes = { color: PropTypes.string, - onChange: PropTypes.func - } + onChange: PropTypes.func, + }; constructor(props: MyColorPickerProps) { super(props); @@ -29,18 +29,19 @@ class MyColorPicker extends Component { this.onChange = this.onChange.bind(this); } - componentDidMount() { + componentDidMount(): void { const color = tinycolor(this.props.color); this.setState({ hsv: color.toHsv(), hsl: color.toHsl(), hex: color.toHex(), - }) + }); } - // @ts-ignore - onChange(e) { - const color = tinycolor(e); + // e actually takes the form of hsl or hsv exclusively. do not believe the lies!! + onChange(c: ColorResult): void { + // @ts-ignore + const color = tinycolor(c); this.setState({ hsv: color.toHsv(), hsl: color.toHsl(), @@ -50,43 +51,45 @@ class MyColorPicker extends Component { this.props.onChange(color.toHex()); } - render() { + render(): JSX.Element { const style = { width: '100%', position: 'relative', display: 'block', - height: '60px' + height: '60px', } as React.CSSProperties; - + const saturationStyle = { height: '80%', - position: 'relative' + position: 'relative', } as React.CSSProperties; const hueStyle = { height: '20%', - position: 'relative' + position: 'relative', } as React.CSSProperties; - return( - + return ( +
- + hsl={this.state.hsl} + hsv={this.state.hsv} + onChange={this.onChange} + />
- + hsl={this.state.hsl} + onChange={this.onChange} + />
- ) + ); } } //@ts-ignore -export default CustomPicker(MyColorPicker); \ No newline at end of file +export default CustomPicker(MyColorPicker); diff --git a/src/components/color_radio_group.tsx b/src/components/color_radio_group.tsx index 420fb4234..32fd72e25 100644 --- a/src/components/color_radio_group.tsx +++ b/src/components/color_radio_group.tsx @@ -1,16 +1,16 @@ -import React, { Component, } from "react"; -import * as PropTypes from "prop-types"; -import * as tinycolor from 'tinycolor2'; +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; +import tinycolor from 'tinycolor2'; +import { ColorResult } from 'react-color'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faBan } from "@fortawesome/free-solid-svg-icons/faBan"; -import { faPalette } from "@fortawesome/free-solid-svg-icons/faPalette"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBan } from '@fortawesome/free-solid-svg-icons/faBan'; +import { faPalette } from '@fortawesome/free-solid-svg-icons/faPalette'; -import { Material } from "../models/materials/material"; - -import Radio from "./radio"; -import MyColorPicker from "./color"; +import { Material } from '../models/materials/material'; +import Radio from './radio'; +import MyColorPicker from './color'; interface ColorRadioGroupProps { materials: Material[]; @@ -19,18 +19,18 @@ interface ColorRadioGroupProps { export default class ColorRadioGroup extends Component { props: ColorRadioGroupProps; - disabled: Boolean; + disabled: boolean; selectedColor: string; customColor: string; static propTypes = { materials: PropTypes.arrayOf(PropTypes.instanceOf(Material)), - colors: PropTypes.arrayOf(PropTypes.string) - } + colors: PropTypes.arrayOf(PropTypes.string), + }; constructor(props: ColorRadioGroupProps) { super(props); - + this.customColor = tinycolor.random().toHexString(); this.disableMaterial = this.disableMaterial.bind(this); @@ -38,83 +38,80 @@ export default class ColorRadioGroup extends Component { this.setCustomColor = this.setCustomColor.bind(this); this.setToCustomColor = this.setToCustomColor.bind(this); } - - componentDidMount() { //random start values + + componentDidMount(): void { + //random start values const count = this.props.colors.length; const index = Math.floor(Math.random() * Math.floor(count)); this.setColor(this.props.colors[index]); } - setColor(color: string) { + setColor(color: string): void { this.disabled = false; this.selectedColor = color; this.props.materials.forEach(material => { material.material.visible = true; material.material.color.setStyle(color); - }) + }); this.forceUpdate(); } - setCustomColor(color: object) { - // @ts-ignore + setCustomColor(color: ColorResult): void { this.customColor = color.hex; - //@ts-ignore this.setColor(color.hex); } - setToCustomColor() { + setToCustomColor(): void { this.setColor(this.customColor); } - disableMaterial() { + disableMaterial(): void { this.disabled = true; this.props.materials.forEach(material => { material.material.visible = false; - }) + }); this.forceUpdate(); } - render() { + render(): JSX.Element { const isRequired = this.props.materials[0].isRequired; let isSelected = this.disabled; - - const disableButton = isRequired - ? null - : - - ; - this.props.colors.forEach(color => { - isSelected = isSelected || (this.selectedColor == color); - }) - - const colors = this.props.colors.map((color, i) => - + const disableButton = isRequired ? null : ( + + + ); + this.props.colors.forEach(color => { + isSelected = isSelected || this.selectedColor == color; + }); + + const colors = this.props.colors.map((color, i) => ( + + )); + //TODO: make setting title on custom color work const customColorButton = ( - + - ) + ); return (
{disableButton} {colors} {customColorButton} - +
); } -} \ No newline at end of file +} diff --git a/src/components/editor.tsx b/src/components/editor.tsx index e81e210d9..3c306a444 100644 --- a/src/components/editor.tsx +++ b/src/components/editor.tsx @@ -1,36 +1,41 @@ -import React, {Component, FormEventHandler} from "react"; -import * as PropTypes from "prop-types"; +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; -import AvatarPart from "../models/avatar_part"; -import { Material } from "../models/materials/material"; -import { BaseMaterial } from "../models/materials/base_material"; -import AvatarPartRadioGroup from "./avatar_part_radio_group"; -import ColorRadioGroup from "./color_radio_group"; -import MaterialRadioGroup from "./material_radio_group"; -import PageRadioGroup from "./page_radio_group"; -import Texture from "../models/materials/texture"; +import AvatarPart from '../models/avatar_part'; +import { Material } from '../models/materials/material'; +import { BaseMaterial } from '../models/materials/base_material'; +import AvatarPartRadioGroup from './avatar_part_radio_group'; +import ColorRadioGroup from './color_radio_group'; +import MaterialRadioGroup from './material_radio_group'; +import PageRadioGroup from './page_radio_group'; +import Texture from '../models/materials/texture'; -import blush from "../images/textures/blush_default.png"; -import eyes from "../images/textures/eyes_default.png"; -import eyebrows from "../images/textures/eyebrows_default.png"; -import eyeWhites from "../images/textures/eye_whites.png"; -import hair from "../images/textures/hair_default.png"; -import jacket from "../images/textures/jacket_default.png"; -import shirt from "../images/textures/shirt_default.png"; -import skin from "../images/textures/skin_default.png"; +import blush from '../images/textures/blush_default.png'; +import eyes from '../images/textures/eyes_default.png'; +import eyebrows from '../images/textures/eyebrows_default.png'; +import eyeWhites from '../images/textures/eye_whites.png'; +import hair from '../images/textures/hair_default.png'; +import jacket from '../images/textures/jacket_default.png'; +import shirt from '../images/textures/shirt_default.png'; +import skin from '../images/textures/skin_default.png'; -import duck from "../images/textures/duck.png"; +import duck from '../images/textures/duck.png'; -import bodyPage from "../images/icons/icons_body.png"; -import headPage from "../images/icons/icons_head.png"; -import shirtPage from "../images/icons//icons_shirt.png"; +import bodyPage from '../images/icons/icons_body.png'; +import headPage from '../images/icons/icons_head.png'; +import shirtPage from '../images/icons//icons_shirt.png'; //import bodyPage from "../images/icons/icons_body.png"; -import curvy from "../images/icons/icons_curvy.png"; -import straight from "../images/icons/icons_straight.png"; -import blair from "../images/icons/icons_hair_blair.png"; -import long from "../images/icons/icons_hair_long.png"; -import messy from "../images/icons/icons_hair_messy.png"; +import curvy from '../images/icons/icons_curvy.png'; +import straight from '../images/icons/icons_straight.png'; +import blair from '../images/icons/icons_hair_blair.png'; +import long from '../images/icons/icons_hair_long.png'; +import messy from '../images/icons/icons_hair_messy.png'; + +interface EditorProps { + bodyPart: AvatarPart; + hairPart: AvatarPart; +} export default class Editor extends Component { blushMaterial: Material; @@ -47,23 +52,23 @@ export default class Editor extends Component { frontLogoTextures: Texture[]; backLogoMaterial: Material; frontLogoMaterial: Material; - props: BodyProps; + props: EditorProps; static propTypes = { bodyPart: PropTypes.instanceOf(AvatarPart), - hairPart: PropTypes.instanceOf(AvatarPart) - } + hairPart: PropTypes.instanceOf(AvatarPart), + }; - constructor(props:Object) { + constructor(props: EditorProps) { super(props); this.logoPaths = [duck]; - this.backLogoTextures = this.logoPaths.map (path => { + this.backLogoTextures = this.logoPaths.map(path => { return new Texture(path, 662); - }) - this.frontLogoTextures = this.logoPaths.map (path => { + }); + this.frontLogoTextures = this.logoPaths.map(path => { return new Texture(path, 148); - }) + }); this.blushMaterial = new Material(blush); this.eyesMaterial = new BaseMaterial(eyes); @@ -76,105 +81,91 @@ export default class Editor extends Component { this.shirtMaterial = new BaseMaterial(shirt); this.skinMaterial = new BaseMaterial(skin); - this.props.bodyPart.assignNewMaterials( - [ - this.skinMaterial, - this.eyeWhitesMaterial, - this.eyesMaterial, - this.eyebrowsMaterial, - this.blushMaterial, - this.shirtMaterial, - this.frontLogoMaterial, - this.jacketMaterial, - this.backLogoMaterial - ]); - this.props.hairPart.assignNewMaterials( - [ - this.hairMaterial - ] - ) - - this.selectedPage = "Body"; + this.props.bodyPart.assignNewMaterials([ + this.skinMaterial, + this.eyeWhitesMaterial, + this.eyesMaterial, + this.eyebrowsMaterial, + this.blushMaterial, + this.shirtMaterial, + this.frontLogoMaterial, + this.jacketMaterial, + this.backLogoMaterial, + ]); + this.props.hairPart.assignNewMaterials([this.hairMaterial]); + + this.selectedPage = 'Body'; this.changePage = this.changePage.bind(this); } - changePage(selectedPage: string) { + changePage(selectedPage: string): void { this.selectedPage = selectedPage; this.forceUpdate(); } - render() { + render(): JSX.Element { return (
- -
+
Body Shape - + Skin Color + colors={['#503335', '#592f2a', '#a1665e', '#c58c85', '#d1a3a4', '#ecbcb4', '#FFE2DC']} + /> Blush Color + colors={['#551F25', '#82333C', '#983E38', '#DC6961', '#e3b9a1']} + />
-
+
Hair Style - + Hair Color + colors={['#2F2321', '#5C4033', '#C04532', '#B9775A', '#E6C690', '#FCE3B8', '#E6E6E6']} + /> Eye Color + colors={['#552919', '#915139', '#917839', '#718233', '#338251', '#335A82']} + />
-
+
Shirt Color + colors={['#f2f2f2', '#cedded', '#92a1b1', '#3479b7', '#7d0c1e', '#262525']} + /> Jacket + colors={['#f2f2f2', '#cedded', '#92a1b1', '#3479b7', '#7d0c1e', '#262525']} + /> Front Logo + xPosition={148} + /> Back Logo + xPosition={662} + />
- ) + ); } } - -interface BodyProps { - bodyPart: AvatarPart - hairPart: AvatarPart -} \ No newline at end of file diff --git a/src/components/material_radio_group.tsx b/src/components/material_radio_group.tsx index e514f6c7b..cb053b4e1 100644 --- a/src/components/material_radio_group.tsx +++ b/src/components/material_radio_group.tsx @@ -1,13 +1,13 @@ -import React, { Component, } from "react"; -import * as PropTypes from "prop-types"; +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faBan } from "@fortawesome/free-solid-svg-icons/faBan"; -import { faUpload } from "@fortawesome/free-solid-svg-icons/faUpload"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBan } from '@fortawesome/free-solid-svg-icons/faBan'; +import { faUpload } from '@fortawesome/free-solid-svg-icons/faUpload'; -import { Material } from "../models/materials/material"; -import Texture from "../models/materials/texture"; -import Radio from "./radio"; +import { Material } from '../models/materials/material'; +import Texture from '../models/materials/texture'; +import Radio from './radio'; interface MaterialRadioGroupProps { material: Material; @@ -18,23 +18,22 @@ interface MaterialRadioGroupProps { export default class MaterialRadioGroup extends Component { props: MaterialRadioGroupProps; - disabled: Boolean; - customSelected: Boolean; - selected: number;//just gonna go with key here - //@ts-ignore - file; + disabled: boolean; + customSelected: boolean; + selected: number; //just gonna go with key here + file: React.RefObject; static propTypes = { material: PropTypes.instanceOf(Material), textures: PropTypes.arrayOf(PropTypes.instanceOf(Texture)), texturePaths: PropTypes.arrayOf(PropTypes.string), - xPosition: PropTypes.number - } + xPosition: PropTypes.number, + }; constructor(props: MaterialRadioGroupProps) { super(props); - this.file = React.createRef(); + this.file = React.createRef(); this.selected = -1; this.disabled = true; @@ -47,7 +46,7 @@ export default class MaterialRadioGroup extends Component { this.triggerClick = this.triggerClick.bind(this); } - disable() { + disable(): void { this.selected = -1; this.disabled = true; this.customSelected = false; @@ -55,7 +54,7 @@ export default class MaterialRadioGroup extends Component { this.forceUpdate(); } - async toggleSelected(selected: number) { + async toggleSelected(selected: number): Promise { if (selected == this.selected) { this.disable(); } else { @@ -69,12 +68,11 @@ export default class MaterialRadioGroup extends Component { this.forceUpdate(); } } - triggerClick() { - //@ts-ignore + triggerClick(): void { this.file.current.click(); } - async setCustom(e: React.ChangeEvent) { + async setCustom(e: React.ChangeEvent): Promise { const path = window.URL.createObjectURL(e.target.files[0]); const texture = new Texture(path, this.props.xPosition); this.disabled = false; @@ -86,37 +84,34 @@ export default class MaterialRadioGroup extends Component { this.forceUpdate(); } - render() { + render(): JSX.Element { const disableButton = ( - - + + ); - const textures = this.props.texturePaths.map((path, i) => - ( + - + selected={!this.disabled && !this.customSelected && this.selected == i} + > + - ) + )); const customButton = ( - - + this.file.current.click()} selected={this.customSelected}> + - ) + ); return (
{disableButton} {textures} {customButton} - +
- ) + ); } -} \ No newline at end of file +} diff --git a/src/components/page_radio_group.tsx b/src/components/page_radio_group.tsx index 5234adf85..75df11b2a 100644 --- a/src/components/page_radio_group.tsx +++ b/src/components/page_radio_group.tsx @@ -1,12 +1,12 @@ -import React, { Component, } from "react"; -import * as PropTypes from "prop-types"; +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; -import Radio from "./radio"; +import Radio from './radio'; interface PageRadioGroupProps { - iconPaths: string[], - pageNames: string[], - onClickCallback: Function + iconPaths: string[]; + pageNames: string[]; + onClickCallback: Function; } export default class PageRadioGroup extends Component { props: PageRadioGroupProps; @@ -15,8 +15,8 @@ export default class PageRadioGroup extends Component { static propTypes = { iconPaths: PropTypes.arrayOf(PropTypes.string), pageNames: PropTypes.arrayOf(PropTypes.string), - onClickCallback: PropTypes.func - } + onClickCallback: PropTypes.func, + }; constructor(props: PageRadioGroupProps) { super(props); @@ -25,27 +25,25 @@ export default class PageRadioGroup extends Component { this.togglePage = this.togglePage.bind(this); } - togglePage(pageName: string) { + togglePage(pageName: string): void { this.selectedPage = pageName; this.props.onClickCallback(pageName); this.forceUpdate(); } - render() { - const buttons = this.props.iconPaths.map((path, i) => - ( + - + setTitle + > + - ); + )); - return ( -
- {buttons} -
- ); + return
{buttons}
; } -} \ No newline at end of file +} diff --git a/src/components/radio.tsx b/src/components/radio.tsx index c459140e9..f1c338e7f 100644 --- a/src/components/radio.tsx +++ b/src/components/radio.tsx @@ -1,40 +1,32 @@ -import React, { Component, } from "react"; -import * as PropTypes from "prop-types"; +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; interface RadioProps { - children: Object; + children?: JSX.Element[] | JSX.Element | null; color: string; onClickCallback: Function; - selected: Boolean; - setTitle: Boolean; - value: string|number; + selected: boolean; + setTitle: boolean; + value?: string | number; } export default class Radio extends Component { props: RadioProps; - value: string|number; + value: string | number; static propTypes = { - children: PropTypes.object, color: PropTypes.string, onClickCallback: PropTypes.func, selected: PropTypes.bool, setTitle: PropTypes.bool, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number - ]) - } + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }; static defaultProps = { - // @ts-ignore - children: null, - color: "#b8b8b8", + color: '#b8b8b8', selected: false, setTitle: false, - // @ts-ignore - value: null - } + }; constructor(props: RadioProps) { super(props); @@ -43,22 +35,26 @@ export default class Radio extends Component { this.onClickValue = this.onClickValue.bind(this); } - onClickValue() { + onClickValue(): void { this.props.onClickCallback(this.value); //only send back up if SUCCESSFUL for texture upload attempts } - render() { - var swatchStyle = { + render(): JSX.Element { + const swatchStyle = { backgroundColor: this.props.color, - boxShadow: this.props.selected ? `0 0 6px ${this.props.color}` : null + boxShadow: this.props.selected ? `0 0 6px ${this.props.color}` : null, }; return ( - +
{this.props.children}
- ) + ); } } diff --git a/src/custom_types/import-png.d.ts b/src/custom_types/import-png.d.ts index 3f6620592..099473d77 100644 --- a/src/custom_types/import-png.d.ts +++ b/src/custom_types/import-png.d.ts @@ -1,4 +1,4 @@ -declare module "*.png" { +declare module '*.png' { const value: any; export default value; -} \ No newline at end of file +} diff --git a/src/main.js b/src/main.js index 63aec0c37..5043c9acc 100644 --- a/src/main.js +++ b/src/main.js @@ -1,164 +1,135 @@ -import * as THREE from "three"; -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; -import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; -import {GLTFExporter} from "three/examples/jsm/exporters/GLTFExporter" -import styles from "./stylesheets/main.scss"; -import buttonStyle from "./stylesheets/buttons.scss"; -import editorStyle from "./stylesheets/editor.scss"; - -import Editor from "./components/editor"; - -import bdy from "../includes/models/merged/model_body.glb" -import hr from "../includes/models/merged/model_hair.glb" - -import AvatarBase from "./models/avatar_base"; -import AvatarPart from "./models/avatar_part"; - -var mixer= null; -var clock = new THREE.Clock(); - -function applyExtenstions() { - THREE.Color.prototype.getHexStringFull = function getHexStringFull() { - return "#" + this.getHexString(); - }; - - THREE.Color.prototype.randomize = function randomize() { - this.setHex(Math.random() * 0xffffff); - return this; - }; - THREE.Color.prototype.createRandom = function createRandomColor() { - return new THREE.Color().randomize(); - }; -} +import * as THREE from 'three'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; +import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter'; +import styles from './stylesheets/main.scss'; +import buttonStyle from './stylesheets/buttons.scss'; +import editorStyle from './stylesheets/editor.scss'; + +import Editor from './components/editor'; + +import bdy from '../includes/models/merged/model_body.glb'; +import hr from '../includes/models/merged/model_hair.glb'; + +import AvatarBase from './models/avatar_base'; +import AvatarPart from './models/avatar_part'; +import { loadGLTF } from './util'; + +let mixer = null; +const clock = new THREE.Clock(); async function init() { - applyExtenstions(); - - const size = { - width: 500, - height: 400 - }; - - const scene = new THREE.Scene(); - const camera = new THREE.PerspectiveCamera(75, size.width / size.height, 0.1, 1000); - - const renderer = new THREE.WebGLRenderer(); - - renderer.setSize(size.width, size.height); - document.getElementById("container").prepend(renderer.domElement); - const controls = new OrbitControls(camera, renderer.domElement); - controls.enablePan = false; - - // ambient - scene.add(new THREE.AmbientLight(0xffffff)); - scene.background = new THREE.Color(0x413b45); - - // light - camera.position.set(0, 15, 20); - camera.matrixAutoUpdate = true; - - controls.target.set(0, 15, 0); - controls.update(); - - function render() { - var delta = clock.getDelta(); - mixer.update(delta); - renderer.render(scene, camera); - } - - const bodyScene = await load(bdy); - const hairScene = await load(hr); - - const sc = bodyScene.scene; - const avatarRoot = bodyScene.scene.children[0]; - - sc.scale.x = 30; - sc.scale.y = 30; - sc.scale.z = 30; - //TODO: actually scour the whole model for extensions and delete, then re-add in correct spot - // OR fix model. idk. - //TODO: make little editor for adding gltf extensions easily - if (avatarRoot.userData.gltfExtensions) { - delete avatarRoot.userData.gltfExtensions.MOZ_hubs_components['scale-audio-feedback']; - } - avatarRoot.traverse(node => { - if (node.name == "Neck") { - node.userData.gltfExtensions= { - MOZ_hubs_components: { - version:4, - "scale-audio-feedback": { - maxScale: 1.5, - minScale: 1 - } + const size = { + width: 500, + height: 400, + }; + + const scene = new THREE.Scene(); + const camera = new THREE.PerspectiveCamera(75, size.width / size.height, 0.1, 1000); + + const renderer = new THREE.WebGLRenderer(); + + renderer.setSize(size.width, size.height); + document.getElementById('container').prepend(renderer.domElement); + const controls = new OrbitControls(camera, renderer.domElement); + controls.enablePan = false; + + // ambient + scene.add(new THREE.AmbientLight(0xffffff)); + scene.background = new THREE.Color(0x413b45); + + // light + camera.position.set(0, 15, 20); + camera.matrixAutoUpdate = true; + + controls.target.set(0, 15, 0); + controls.update(); + + function render() { + const delta = clock.getDelta(); + mixer.update(delta); + renderer.render(scene, camera); + } + + const bodyScene = await loadGLTF(bdy); + const hairScene = await loadGLTF(hr); + + const sc = bodyScene.scene; + console.log(bodyScene); + const avatarRoot = bodyScene.scene.children[0]; + + sc.scale.x = 30; + sc.scale.y = 30; + sc.scale.z = 30; + //TODO: actually scour the whole model for extensions and delete, then re-add in correct spot + // OR fix model. idk. + //TODO: make little editor for adding gltf extensions easily + if (avatarRoot.userData.gltfExtensions) { + delete avatarRoot.userData.gltfExtensions.MOZ_hubs_components['scale-audio-feedback']; + } + avatarRoot.traverse(node => { + if (node.name == 'Neck') { + node.userData.gltfExtensions = { + MOZ_hubs_components: { + version: 4, + 'scale-audio-feedback': { + maxScale: 1.5, + minScale: 1, + }, + }, + }; } - } + }); + const skeleton = avatarRoot.children[1].skeleton; + const bodySkinnedMeshes = avatarRoot.children.slice(1); + + avatarRoot.children.splice(1, avatarRoot.children.length - 1); + const avatarBase = new AvatarBase(bodyScene, skeleton); + const avatarPart = new AvatarPart(true, true, bodySkinnedMeshes); + avatarBase.addAvatarPart(avatarPart); + + const hairSkinnedMeshes = hairScene.scene.children[0].children.slice(1); + + const hairPart = new AvatarPart(false, true, hairSkinnedMeshes); + avatarBase.addAvatarPart(hairPart); + + mixer = new THREE.AnimationMixer(sc); + //const action = mixer.clipAction(bodyScene.animations[13]) + //action.play(); + scene.add(sc); + + function exportGLB() { + const exporter = new GLTFExporter(); + avatarBase.getMergedGLTF().then(val => { + exporter.parse( + val.scene, + glb => { + avatarBase.postExportRestore(); + const blob = new Blob([glb], { type: 'model/gltf-binary' }); + const el = document.createElement('a'); + el.style.display = 'none'; + el.href = URL.createObjectURL(blob); + el.download = 'custom_avatar.glb'; + el.click(); + el.remove(); + }, + { animations: val.animations, binary: true, includeCustomExtensions: true }, + ); + }); } - }) - const skeleton = avatarRoot.children[1].skeleton; - const bodySkinnedMeshes = avatarRoot.children.slice(1); - - avatarRoot.children.splice(1, avatarRoot.children.length - 1); - const avatarBase = new AvatarBase(bodyScene, skeleton); - const avatarPart = new AvatarPart(true, true, bodySkinnedMeshes); - avatarBase.addAvatarPart(avatarPart); - - const hairSkinnedMeshes = hairScene.scene.children[0].children.slice(1); - - const hairPart = new AvatarPart(false,true, hairSkinnedMeshes); - avatarBase.addAvatarPart(hairPart); - - mixer = new THREE.AnimationMixer(sc); - //const action = mixer.clipAction(bodyScene.animations[13]) - //action.play(); - scene.add(sc); - - function exportGLB() { - const exporter = new GLTFExporter(); - avatarBase.getMergedGLTF().then(val => { - exporter.parse(val.scene, (glb) => { - avatarBase.postExportRestore(); - const blob = new Blob([glb], {type: 'model/gltf-binary'}); - const el = document.createElement("a"); - el.style.display = "none"; - el.href = URL.createObjectURL(blob); - el.download = "custom_avatar.glb" - el.click(); - el.remove(); - }, {animations: val.animations, binary: true, includeCustomExtensions: true}) - - }) - } - - ReactDOM.render( - <> - - - , - document.getElementById("options") - ) - - setInterval(() => { - render(); - }, 100); + + ReactDOM.render( + <> + + + , + document.getElementById('options'), + ); + + setInterval(() => { + render(); + }, 100); } window.onload = init; - - -function load(src) { - return new Promise((resolve, reject) => { - const _loader = new GLTFLoader(); - _loader.load( - src, - (val) => { - resolve(val); - }, - undefined, - (err) => { - reject(err); - } - ) - }) -} \ No newline at end of file diff --git a/src/models/avatar_base.ts b/src/models/avatar_base.ts index 606f9d436..6b57875c8 100644 --- a/src/models/avatar_base.ts +++ b/src/models/avatar_base.ts @@ -1,43 +1,44 @@ -import * as THREE from "three"; -import { BufferGeometryUtils } from "three/examples/jsm/utils/BufferGeometryUtils"; -import { cloneDeep } from "lodash"; -import AvatarPart from "./avatar_part"; -import { Material } from "./materials/material"; +import * as THREE from 'three'; +import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils'; +import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'; +import load from '../util'; +import AvatarPart from './avatar_part'; +import { Material } from './materials/material'; export default class AvatarBase { - private fullScene: Object; + private fullScene: GLTF; private skeleton: THREE.Skeleton; private materials: Material[]; private avatarParts: AvatarPart[] = []; private avatarRootChildren: THREE.Object3D[]; - constructor(fullScene: Object, skeleton: THREE.Skeleton) { //should be a scene with "avatar root" + constructor(fullScene: GLTF, skeleton: THREE.Skeleton) { + //should be a scene with "avatar root" this.fullScene = fullScene; this.skeleton = skeleton; } - addAvatarPart(avatarPart: AvatarPart) { + addAvatarPart(avatarPart: AvatarPart): void { //avatarPart.assignSkeleton(this.skeleton); //TODO: Figure out why this is insane this.avatarParts.push(avatarPart); - console.log(this.avatarRoot) - avatarPart.meshes.forEach((mesh) => { + console.log(this.avatarRoot); + avatarPart.meshes.forEach(mesh => { this.avatarRoot.add(mesh); - }) + }); } - async getMergedGLTF() { + async getMergedGLTF(): Promise { const material = await this.getMergedMaterial(); - let geometries:THREE.BufferGeometry[] = []; + let geometries: THREE.BufferGeometry[] = []; this.avatarParts.forEach(part => { geometries = geometries.concat(part.getSelectedMeshes().map(mesh => mesh.geometry)); }); const skinnedMesh = new THREE.SkinnedMesh(BufferGeometryUtils.mergeBufferGeometries(geometries), material); skinnedMesh.skeleton = this.skeleton; - skinnedMesh.name = "Avatar"; + skinnedMesh.name = 'Avatar'; - //@ts-ignore - const sc = this.fullScene.scene as THREE.Scene; + const sc = this.fullScene.scene; sc.scale.x = 1; sc.scale.y = 1; sc.scale.z = 1; @@ -49,64 +50,46 @@ export default class AvatarBase { avatarRoot.remove(...avatarRoot.children); avatarRoot.add(bones); - avatarRoot.add(skinnedMesh) + avatarRoot.add(skinnedMesh); console.log(sc); - //@ts-ignore return this.fullScene; } - private async getMergedMaterial() { - let texture = await load(this.getMergedTexture()); - const mergedMaterial = new THREE.MeshStandardMaterial({map: texture as THREE.Texture}); + private async getMergedMaterial(): Promise { + const texture = await load(this.getMergedTexture()); + const mergedMaterial = new THREE.MeshStandardMaterial({ map: texture as THREE.Texture }); mergedMaterial.map.flipY = false; return mergedMaterial; } - postExportRestore() { - //@ts-ignore - const sc = this.fullScene.scene as THREE.Scene; + postExportRestore(): void { + const sc = this.fullScene.scene; sc.scale.x = 30; sc.scale.y = 30; sc.scale.z = 30; this.avatarRoot.remove(...this.avatarRoot.children); this.avatarRootChildren.forEach(child => { this.avatarRoot.add(child); - }) + }); } - getMergedTexture() : string { - const canvas = window.document.createElement("canvas"); + getMergedTexture(): string { + const canvas = window.document.createElement('canvas'); canvas.width = 1024; canvas.height = 1024; - const ctx = canvas.getContext("2d"); - this.avatarParts.forEach( avatarPart => { - avatarPart.materials.forEach( material => { + const ctx = canvas.getContext('2d'); + this.avatarParts.forEach(avatarPart => { + avatarPart.materials.forEach(material => { if (material.material.visible) { ctx.drawImage(material.getFlattenedTexture(), 0, 0); } - }) + }); }); - return canvas.toDataURL("image/png", 1.0); + return canvas.toDataURL('image/png', 1.0); } - get avatarRoot() { - // @ts-ignore + get avatarRoot(): THREE.Object3D { return this.fullScene.scene.children[0]; } } -function load(src : string) { - return new Promise((resolve, reject) => { - const _loader = new THREE.TextureLoader(); - _loader.load( - src, - (val) => { - resolve(val); - }, - undefined, - (err) => { - reject(err); - } - ) - }) -} \ No newline at end of file diff --git a/src/models/avatar_part.ts b/src/models/avatar_part.ts index 75456609f..2b80e3617 100644 --- a/src/models/avatar_part.ts +++ b/src/models/avatar_part.ts @@ -1,15 +1,15 @@ -import * as THREE from "three"; -import { Material } from "./materials/material"; +import * as THREE from 'three'; +import { Material } from './materials/material'; export default class AvatarPart { - isRequired: Boolean; - isSingular: Boolean; + isRequired: boolean; + isSingular: boolean; //private sharesMaterials: Boolean = true; private selectedSkinnedMeshes: number[] = []; private skinnedMeshes: THREE.SkinnedMesh[]; materials: Material[]; - constructor(isRequired: Boolean, isSingular:Boolean, skinnedMeshes: THREE.SkinnedMesh[]) { + constructor(isRequired: boolean, isSingular: boolean, skinnedMeshes: THREE.SkinnedMesh[]) { this.isRequired = isRequired; this.isSingular = isSingular; this.skinnedMeshes = skinnedMeshes; @@ -18,59 +18,58 @@ export default class AvatarPart { } this.skinnedMeshes.forEach((mesh, i) => { mesh.visible = this.isSelected(i); - }) + }); } - assignSkeleton(skeleton: THREE.Skeleton) { + assignSkeleton(skeleton: THREE.Skeleton): void { this.skinnedMeshes.forEach(mesh => { mesh.bind(skeleton); - }) + }); } - assignNewMaterials(materials: Material[]) { + assignNewMaterials(materials: Material[]): void { this.skinnedMeshes.forEach(mesh => { mesh.material = []; mesh.geometry.clearGroups(); materials.forEach((material: Material, i: number) => { mesh.geometry.addGroup(0, Infinity, i); // @ts-ignore - mesh.material.push(material.material); - }) + mesh.material.push(material.material); + }); this.materials = materials; - }) + }); } - disable() { + disable(): void { if (!this.isRequired) { - this.selectedSkinnedMeshes.forEach((i) => { + this.selectedSkinnedMeshes.forEach(i => { this.toggleMesh(i); - }) + }); } } - isSelected(index: number) { + isSelected(index: number): boolean { return this.selectedSkinnedMeshes.includes(index); } - toggleMesh(toToggle : number | string) { - var value : null | number; - if (typeof toToggle === "string") { + toggleMesh(toToggle: number | string): void { + let value: null | number; + if (typeof toToggle === 'string') { this.skinnedMeshes.forEach((mesh, i) => { - if(mesh.name == toToggle) { + if (mesh.name == toToggle) { value = i; } }); } else { value = toToggle as number; } - + const selectedCount = this.selectedSkinnedMeshes.length; const meshCount = this.skinnedMeshes.length; if (value !== null && value < meshCount) { - const alreadySelected = this.selectedSkinnedMeshes.includes(value); - if(alreadySelected) { + if (alreadySelected) { if (this.isRequired && (this.isSingular || selectedCount == 1)) { return; //dont do anything } @@ -84,25 +83,25 @@ export default class AvatarPart { } } - getSelectedMeshes() : THREE.SkinnedMesh[] { + getSelectedMeshes(): THREE.SkinnedMesh[] { return this.selectedSkinnedMeshes.map(i => this.skinnedMeshes[i]); } - get disabled() { + get disabled(): boolean { return this.selectedSkinnedMeshes.length == 0; } - get meshes() { + get meshes(): THREE.SkinnedMesh[] { return this.skinnedMeshes; } - private selectMesh(toSelect : number) { + private selectMesh(toSelect: number): void { this.skinnedMeshes[toSelect].visible = true; this.selectedSkinnedMeshes.push(toSelect); } - private deselectMesh(toDeselect : number) { + private deselectMesh(toDeselect: number): void { this.skinnedMeshes[toDeselect].visible = false; this.selectedSkinnedMeshes = this.selectedSkinnedMeshes.filter(item => item !== toDeselect); } -} \ No newline at end of file +} diff --git a/src/models/materials/base_material.ts b/src/models/materials/base_material.ts index 57e78d180..26ab6a43f 100644 --- a/src/models/materials/base_material.ts +++ b/src/models/materials/base_material.ts @@ -1,7 +1,7 @@ -import { Material } from "./material"; +import { Material } from './material'; export class BaseMaterial extends Material { - constructor(textureURL : string = null) { + constructor(textureURL: string = null) { super(textureURL, true); } -} \ No newline at end of file +} diff --git a/src/models/materials/material.ts b/src/models/materials/material.ts index 0c0ec3026..8567c55c5 100644 --- a/src/models/materials/material.ts +++ b/src/models/materials/material.ts @@ -1,9 +1,10 @@ -import * as THREE from "three"; +import * as THREE from 'three'; +import load from '../../util'; export class Material { - material : THREE.MeshStandardMaterial; - isRequired: Boolean; + material: THREE.MeshStandardMaterial; + isRequired: boolean; - constructor(textureURL : string = null, isRequired: Boolean = false) { + constructor(textureURL: string = null, isRequired = false) { this.isRequired = isRequired; this.material = new THREE.MeshStandardMaterial(); this.material.transparent = true; @@ -13,52 +14,37 @@ export class Material { this.material.map = texture as THREE.Texture; this.material.map.flipY = false; this.material.needsUpdate = true; - }) + }); } } - getFlattenedTexture(): null | HTMLCanvasElement { //figure out return type + getFlattenedTexture(): null | HTMLCanvasElement { + //figure out return type if (!this.material.visible) { return null; } const texture = this.material.map.image; - const color : THREE.Color = this.material.color; + const color: THREE.Color = this.material.color; - const canvas = window.document.createElement("canvas"); + const canvas = window.document.createElement('canvas'); canvas.width = 1024; canvas.height = 1024; - const ctx = canvas.getContext("2d"); - ctx.globalCompositeOperation = "copy"; + const ctx = canvas.getContext('2d'); + ctx.globalCompositeOperation = 'copy'; ctx.drawImage(texture, 0, 0); - if (color.equals(new THREE.Color("white"))) { + if (color.equals(new THREE.Color('white'))) { return canvas; } - ctx.globalCompositeOperation = "multiply"; + ctx.globalCompositeOperation = 'multiply'; ctx.fillStyle = color.getStyle(); ctx.fillRect(0, 0, 1024, 1024); - ctx.globalCompositeOperation = "destination-atop"; + ctx.globalCompositeOperation = 'destination-atop'; ctx.drawImage(texture, 0, 0); return canvas; } } - -function load(src : string) { - return new Promise((resolve, reject) => { - const _loader = new THREE.TextureLoader(); - _loader.load( - src, - (val) => { - resolve(val); - }, - undefined, - (err) => { - reject(err); - } - ) - }) -} \ No newline at end of file diff --git a/src/models/materials/texture.ts b/src/models/materials/texture.ts index ada4f7379..a6ed9b108 100644 --- a/src/models/materials/texture.ts +++ b/src/models/materials/texture.ts @@ -1,24 +1,23 @@ -import * as THREE from "three"; - +import * as THREE from 'three'; +import load from '../../util'; export default class Texture { private texture: THREE.Texture; - // @ts-ignore - private texturePromise: Promise; - private textureLoaded: Boolean = false; - private x:number; - private y:number; - private width:number; - private height:number; - - constructor(path:string, x:number, y:number=476, width:number=220, height: number = 270) { + private texturePromise: Promise; + private textureLoaded = false; + private x: number; + private y: number; + private width: number; + private height: number; + + constructor(path: string, x: number, y = 476, width = 220, height = 270) { this.texturePromise = load(path); this.x = x; this.y = y; this.width = width; this.height = height; } - async getTexture() { + async getTexture(): Promise { if (this.textureLoaded) { return this.texture; } else { @@ -30,7 +29,7 @@ export default class Texture { let w = this.width; let h = this.height; - if(differentSizes || !this.x || !this.y) { + if (differentSizes || !this.x || !this.y) { const currentAspect = img.width / img.height; const desiredAspect = this.width / this.height; const fitToX = desiredAspect < currentAspect; @@ -43,7 +42,7 @@ export default class Texture { canvas.height = 1024; const ctx = canvas.getContext('2d'); ctx.drawImage(img, this.x, this.y, w, h); - + const actualVal = await load(canvas.toDataURL()); const actualTexture = actualVal as THREE.Texture; actualTexture.flipY = false; @@ -53,21 +52,3 @@ export default class Texture { } } } - - - -function load(src : string) { - return new Promise((resolve, reject) => { - const _loader = new THREE.TextureLoader(); - _loader.load( - src, - (val) => { - resolve(val); - }, - undefined, - (err) => { - reject(err); - } - ) - }) -} \ No newline at end of file diff --git a/src/util.js b/src/util.js deleted file mode 100644 index 80a95b797..000000000 --- a/src/util.js +++ /dev/null @@ -1,13 +0,0 @@ -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -export async function until(fn) { - while (!fn()) { - await sleep(0); - } -} - -export function getRandom( arr ) { - arr[Math.floor(Math.random() * Math.floor(arr.length))] -} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 000000000..71951ebe4 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,34 @@ +import * as THREE from 'three'; +import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'; + +export default function load(src: string): Promise { + return new Promise((resolve, reject) => { + const _loader = new THREE.TextureLoader(); + _loader.load( + src, + val => { + resolve(val); + }, + undefined, + err => { + reject(err); + }, + ); + }); +} + +export function loadGLTF(src: string): Promise { + return new Promise((resolve, reject) => { + const _loader = new GLTFLoader(); + _loader.load( + src, + val => { + resolve(val); + }, + undefined, + err => { + reject(err); + }, + ); + }); +} diff --git a/tsconfig.json b/tsconfig.json index 72c86ed1c..7a2c8667e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,10 @@ "jsx": "react", "baseUrl" : "./", "moduleResolution": "node", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "typeRoots": [ + "./node_modules/@types", + "./src/custom_types" + ] } } \ No newline at end of file