Skip to content

Commit

Permalink
feat: add three.js and the first 3D scene (#6)
Browse files Browse the repository at this point in the history
* feat: add a scene with an house, some trees and a garden
* fix(cypress): remove MirageJS to work with Cypress
  • Loading branch information
ReidyT authored Oct 18, 2024
1 parent 20d2e40 commit 123d5ba
Show file tree
Hide file tree
Showing 32 changed files with 1,227 additions and 116 deletions.
107 changes: 46 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<img style="text-align: center" src="https://graasp.org/favicon.svg" width=100 >
</div>

# Graasp App Template
# Graasp App Insulation Simulator

This repository hosts the template code for a **Graasp App** written with [Typescript](https://www.typescriptlang.org/) and [React](https://react.dev/). The bundler used is [Vite](https://vitejs.dev).
This repository hosts the code for the **Graasp App Insulation Simulator** written with [Typescript](https://www.typescriptlang.org/) and [React](https://react.dev/). The bundler used is [Vite](https://vitejs.dev).

<div style="gap:10px; display:flex; justify-content: center; align-items: center;">
<img src="https://upload.wikimedia.org/wikipedia/commons/4/4c/Typescript_logo_2020.svg" width=50 >
Expand All @@ -16,74 +16,18 @@ This repository hosts the template code for a **Graasp App** written with [Types
<span>❤️</span>
</div>

## Using this template
## Purpose

The recommended way to use this template is with the [Graasp CLI](https://github.com/graasp/graasp-cli) which provides a setup wizard and some convenience tools when creating your project.
The app's purpose is to help users understand and reduce heat loss through conduction in a house by testing various insulation materials, including advanced options like aerogel. Users can customize energy costs and the duration of the simulation (e.g., 1 year or 25 years) to see how these factors affect heat retention and energy efficiency over time. The app simulates heat loss scenarios based on user inputs, comparing them against a baseline to calculate energy savings and cost benefits. This allows users to visualize and quantify the impact of improved insulation on reducing heat loss by conduction.

Alternatively it is possible to create a new Github repo from this project using the Github Template function. In this case the local setup is left as an exercice to the reader.

### With the Graasp CLI

This template can be used with the [graasp CLI](https://www.npmjs.com/package/@graasp/cli?activeTab=readme) to setup your project in a single line:

```bash
npx @graasp/cli@latest new -s graasp/graasp-app-starter-ts-vite
```

The CLI will ask you a few questions to help you setup your project. It suggests sane defaults:

- Deploying using GitHubActions (recommended)
- Provide an appId
- Auto-install project dependencies
- Initialize a local git repository

<details >
<summary><h3>Directly from GitHub</h3></summary>

Should you choose to bootstrap your graasp app manually, you will need to execute the following steps.

#### Cloning the template

Get a copy of this repo.

##### Using the Template button

Click on the `Use this template` button. For more instructions head over to the [GitHub Docs on Using a template](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template)

##### Clone from the command line

With `git`:

```sh
git clone
```

With the [GitHub CLI](https://cli.github.com/):

```bash
gh repo clone graasp/graasp-app-starter-ts-vite
```

#### Adding Workflows

To deploy your app using github actions.

#### Renaming

You will have to look for the `Graasp App Template` string in yours project files and rename it to your project name

</details>

### GitHub Repo setup
## GitHub Repo setup

If you choose to deploy your app with the provided GitHubActions workflows you will need to create the following secrets:

- `APP_ID`: a UUID v4 that identifies your app for deployment
- `APP_KEY`: a UUID v4 that authorizes your app with the Graasp API
- `SENTRY_DSN`: your Sentry url to report issues and send telemetry

## Installation

## Running the app

Create a `.env.development` file with the following content:
Expand All @@ -110,3 +54,44 @@ VITE_VERSION=latest
# dont open browser
BROWSER=none
```

## Working with 3D Models

This project utilizes **Three.js** and **React-Three-Fiber** to streamline the loading and management of 3D models within a React environment. Additionally, the `gltfjsx` package is employed to optimize and compress GLB models, as well as to automatically generate the corresponding React components for easy integration and rendering of these models.

To enhance clarity for developers and maintainers, all files generated by the `gltfjsx` package include a standardized header, as shown below:

```js
/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx [email protected] OriginalHouse.glb --transform --types
Files: OriginalHouse.glb [250.7KB] > House.glb [16.05KB] (94%)
Model: "Residential Houses" from: https://www.sloyd.ai/
*/
```

In this example, we compressed the GLB file using the `--transform` flag, which optimizes the model for better performance, and generated a React component with the full command. The `--types` flag was used to generate TypeScript types for enhanced type safety. By using a compressed version of the GLB file, we consolidate the object's components into organized groups, making the React component easier to understand and improving browser rendering efficiency.

Indeed, the compressed component for the fir tree will consist of two meshes: one for the spines and one for the trunk. In contrast, the original component would have contained seven meshes for the spines and one for the trunk. This compression significantly reduces the number of meshes, which, in turn, minimizes the lines of code in our component, making it more efficient and easier to manage. The drawback of this compression is that we have less control over the different parts of the object. If this is what we want, we should avoid using the `--transform` flag.

Additionally, I used Blender to rename the object’s materials for clarity, such as changing `Material-001.002` to more descriptive names like `Wall`.

Lastly, each header contains the URL of the original model if it wasn't created in-house, providing proper attribution and reference for future modifications.

This approach simplifies the process of updating or adding a 3D model. To do so, simply place the model in the `public/models` folder and run the `npx gltfjsx` command. Afterward, move the original model into the `/models` folder (if we want to conserve the original) and store the generated code in the `src/modules/models` directory. It is also recommended to extract the logic from this component into a custom hook, making it modular and reusable while keeping the component clean and focused on rendering.

If an existing model is updated, we should replace the type and the React component group while leaving the logic hooks untouched. This approach ensures that the code remains valid and functional, as the underlying logic and behavior are preserved, reducing the risk of introducing errors.

The positions of the meshes within the group are auto-generated, and they should never be manually modified. If an object needs to be repositioned, adjust its `position` prop applied directly to the group instead. This maintains consistency and ensures the auto-generated structure remains intact while allowing for flexible positioning.

### Credits

The house and tree models are created by [Sloyd.ai](https://www.sloyd.ai). All rights reserved by Sloyd for these models.

## Known issues

Three.js and MUI can encounter conflicts when using the `Box` component from MUI. To resolve this issue, you can consider the following options:

- **Upgrade to MUI 6**: This may resolve the conflict, so it's worth testing.
- **Use the `Box` component with a `div`**: Replace Box with `<Box component="div">...</Box>` to mitigate the issue.
- **Avoid using `Box` altogether**: Instead, opt for the `Stack` component, which may provide a suitable alternative without conflicts.
5 changes: 1 addition & 4 deletions cypress/e2e/player/main.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ describe('Player View', () => {
});

it('App', () => {
cy.get(buildDataCy(PLAYER_VIEW_CY)).should(
'contain.text',
'Player as write',
);
cy.get(buildDataCy(PLAYER_VIEW_CY)).should('be.visible');
});
});
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export default [
rules: {
'react/prop-types': 'off',
'react/no-array-index-key': 'off',
'react/no-unknown-property': 'off', // disable to use React-Three
'react/jsx-props-no-spreading': 'off',
'react/destructuring-assignment': 'off',
'react/require-default-props': 'off',
Expand Down
6 changes: 3 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/graasp.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="title" content="Graasp App template" />
<meta name="title" content="Graasp App Insulation Simulator" />
<meta
name="description"
content="A Template project to create graasp apps."
content="A simulator for house's insulation efficiency."
/>
<meta
name="version-info"
Expand All @@ -19,7 +19,7 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
<title>Graasp App Template</title>
<title>Graasp App Insulation Simulator</title>
</head>
<body>
<div id="root"></div>
Expand Down
Binary file added models/OriginalFirTree.glb
Binary file not shown.
Binary file added models/OriginalHouse.glb
Binary file not shown.
Binary file added models/OriginalTree.glb
Binary file not shown.
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
"license": "AGPL-3.0-only",
"author": "Graasp",
"contributors": [
"Basile Spaenlehauer",
"Jérémy La Scala"
"Thibault Reidy"
],
"homepage": ".",
"type": "module",
Expand All @@ -18,6 +17,8 @@
"@mui/icons-material": "5.16.7",
"@mui/lab": "5.0.0-alpha.173",
"@mui/material": "5.16.7",
"@react-three/drei": "^9.114.4",
"@react-three/fiber": "^8.17.10",
"@sentry/react": "7.119.0",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-query-devtools": "^4.36.1",
Expand All @@ -29,6 +30,7 @@
"react-dom": "18.3.1",
"react-i18next": "14.1.3",
"react-toastify": "10.0.5",
"three": "^0.169.0",
"typescript": "5.6.2"
},
"scripts": {
Expand Down Expand Up @@ -62,6 +64,7 @@
"@eslint/js": "^9.12.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/i18n": "0.13.12",
"@types/three": "^0",
"@types/uuid": "9.0.8",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
Expand All @@ -84,10 +87,10 @@
"eslint-plugin-react-hooks": "4.6.2",
"globals": "^15.11.0",
"husky": "9.1.6",
"miragejs": "^0.1.48",
"nock": "^13.5.3",
"nyc": "17.1.0",
"prettier": "3.3.3",
"three-stdlib": "2.33.0",
"uuid": "9.0.1",
"vite": "^5.1.3",
"vite-plugin-checker": "^0.8.0",
Expand Down
Binary file added public/models/FirTree.glb
Binary file not shown.
Binary file added public/models/House.glb
Binary file not shown.
Binary file added public/models/Tree.glb
Binary file not shown.
1 change: 1 addition & 0 deletions src/config/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MODELS_3D_ROOT_PATH = 'public/models';
58 changes: 58 additions & 0 deletions src/context/SeasonContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
ReactNode,
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react';

import { AllSeasons, Season, Seasons } from '@/types/seasons';

type SeasonContextType = {
season: Season;
nextSeason: () => void;
};

const SeasonContext = createContext<SeasonContextType>({
season: Seasons.Spring,
nextSeason: () => {
throw new Error(
'The SeasonContext has been used before its initialization',
);
},
});

type Props = {
children: ReactNode;
};

export const SeasonProvider = ({ children }: Props): ReactNode => {
const [season, setSeason] = useState<Season>(Seasons.Spring);

const nextSeason = useCallback((): void => {
setSeason((prev) => {
const prevIdx = AllSeasons.findIndex((s) => s === prev);
const newIdx = (prevIdx + 1) % AllSeasons.length;

return Seasons[AllSeasons[newIdx]];
});
}, []);

const contextValue = useMemo(
() => ({
season,
nextSeason,
}),
[season, nextSeason],
);

return (
<SeasonContext.Provider value={contextValue}>
{children}
</SeasonContext.Provider>
);
};

export const useSeason = (): SeasonContextType =>
useContext<SeasonContextType>(SeasonContext);
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FC, ReactElement, createContext, useContext } from 'react';

import { hooks, mutations } from '../../config/queryClient';
import Loader from '../common/Loader';
import { hooks, mutations } from '@/config/queryClient';
import Loader from '@/modules/common/Loader';

// mapping between Setting names and their data type
// eslint-disable-next-line @typescript-eslint/ban-types
Expand Down
2 changes: 1 addition & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ if (MOCK_API) {
appContext: window.Cypress ? window.appContext : defaultMockContext,
database: window.Cypress ? window.database : buildDatabase(mockMembers),
},
window.Cypress ? MockSolution.MirageJS : MockSolution.ServiceWorker,
MockSolution.ServiceWorker,
);
}

Expand Down
8 changes: 4 additions & 4 deletions src/modules/common/Loader.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { FC } from 'react';
import { FC } from 'react';

import { Box } from '@mui/material';
import { Stack } from '@mui/material';
import CircularProgress from '@mui/material/CircularProgress';

const Loader: FC = () => (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Stack justifyContent="center">
<CircularProgress />
</Box>
</Stack>
);

export default Loader;
6 changes: 3 additions & 3 deletions src/modules/main/AnalyticsView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Button, Stack, Typography } from '@mui/material';
import { Button, Stack, Typography } from '@mui/material';

import { useLocalContext } from '@graasp/apps-query-client';

Expand Down Expand Up @@ -29,10 +29,10 @@ const AnalyticsView = (): JSX.Element => {
Post new App Action
</Button>
</Stack>
<Box p={2}>
<Stack p={2}>
<Typography>App Actions</Typography>
<pre>{JSON.stringify(appActions, null, 2)}</pre>
</Box>
</Stack>
</Stack>
</div>
);
Expand Down
3 changes: 2 additions & 1 deletion src/modules/main/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { useEffect } from 'react';
import { useLocalContext } from '@graasp/apps-query-client';
import { Context } from '@graasp/sdk';

import { SettingsProvider } from '@/context/SettingsContext';

import i18n, { DEFAULT_LANGUAGE } from '../../config/i18n';
import { SettingsProvider } from '../context/SettingsContext';
import AnalyticsView from './AnalyticsView';
import BuilderView from './BuilderView';
import PlayerView from './PlayerView';
Expand Down
Loading

0 comments on commit 123d5ba

Please sign in to comment.