├── public
├── robots.txt
├── logo512x512.png
├── favicon512x512.ico
├── manifest.json
└── index.html
├── .prettierrc.yaml
├── .gitignore
├── src
├── index.js
├── demoProps.js
├── templates
│ ├── cameraViewParams.js
│ └── boxModelParams.js
├── components
│ ├── NumericInputField.js
│ └── layouts.js
├── App.js
├── serviceWorker.js
└── box.js
├── package.json
├── LICENSE
├── README.md
└── prototype-script.py
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/logo512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mithi/hello-tiny-box/HEAD/public/logo512x512.png
--------------------------------------------------------------------------------
/public/favicon512x512.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mithi/hello-tiny-box/HEAD/public/favicon512x512.ico
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | printWidth: 90
2 | tabWidth: 4
3 | quoteProps: "consistent"
4 | arrowParens: "avoid"
5 | semi: false
6 | singleQuote: false
7 | jsxSingleQuote: false
8 | trailingComma: "es5"
9 | bracketSpacing: true
10 | jsxBracketSameLine: false
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import App from "./App"
4 | import * as serviceWorker from "./serviceWorker"
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById("root")
11 | )
12 |
13 | // If you want your app to work offline and load faster, you can change
14 | // unregister() to register() below. Note this comes with some pitfalls.
15 | // Learn more about service workers: https://bit.ly/CRA-PWA
16 | serviceWorker.unregister()
17 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Tiny Box",
3 | "name": "Hello Tiny Box !",
4 | "icons": [
5 | {
6 | "src": "favicon64x64.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo512x512.png",
12 | "type": "image/png",
13 | "sizes": "512x512"
14 | }
15 | ],
16 | "start_url": ".",
17 | "display": "standalone",
18 | "theme_color": "#000000",
19 | "background_color": "#ffffff"
20 | }
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hello-tiny-box",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.11.0",
7 | "@material-ui/icons": "^4.9.1",
8 | "bare-minimum-2d": "^0.2.0",
9 | "gh-pages": "^3.1.0",
10 | "react": "^16.13.1",
11 | "react-dom": "^16.13.1",
12 | "react-scripts": "3.4.2"
13 | },
14 | "homepage": "http://mithi.github.io/hello-tiny-box",
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "predeploy": "npm run build",
19 | "deploy": "gh-pages -d build"
20 | },
21 | "eslintConfig": {
22 | "extends": "react-app"
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Hello Tiny Box! Manipulate a 3d Box
12 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Mithi Sevilla
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/demoProps.js:
--------------------------------------------------------------------------------
1 | const container = {
2 | color: "#333333",
3 | opacity: 1.0,
4 | xRange: 200,
5 | yRange: 200,
6 | }
7 |
8 | const newBoxPoint = (x, y, z, id) => {
9 | return {
10 | x: [x],
11 | y: [50 + y],
12 | color: "#FF0000",
13 | opacity: 1.0,
14 | size: (10 * z) / 100,
15 | type: "points",
16 | id,
17 | }
18 | }
19 |
20 | const newCamPoint = (x, y, z, zoom, id) => {
21 | return {
22 | x: [x],
23 | y: [-30 + y],
24 | color: "#FFFFFF",
25 | opacity: zoom / 10,
26 | size: (10 * z) / 100,
27 | type: "points",
28 | id,
29 | }
30 | }
31 | const newPlotParams = (cam, box) => {
32 | const { rx, ry, rz, tx, ty, tz, sx, sy, sz } = box
33 |
34 | const rBox = newBoxPoint(rx + 50, ry, rz, "rbox")
35 | const sBox = newBoxPoint(sx, sy, sz, "sbox")
36 | const tBox = newBoxPoint(tx - 50, ty, tz, "tbox")
37 |
38 | const { rx: crx, ry: cry, rz: crz, tx: ctx, ty: cty, tz: ctz, zoom } = cam
39 | const rCam = newCamPoint(crx + 30, cry, crz, zoom, "rcam")
40 | const tCam = newCamPoint(ctx - 30, cty, ctz, zoom, "tcam")
41 | const data = [rBox, sBox, tBox, rCam, tCam]
42 | return { data, container }
43 | }
44 |
45 | export { newPlotParams }
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hello Tiny Box 📦
2 |
3 | > Manipulate a three-dimensional box.
4 |
5 | - This project is inspired by [Gabriel Gambetta's Computer Graphics from Scratch](https://www.gabrielgambetta.com/computer-graphics-from-scratch/scene-setup.html) online book
6 |
7 | - [`mithi.github.io/hello-tiny-box`](https://mithi.github.io/hello-tiny-box/)
8 |
9 | - See also: My [`quick and dirty Python script`](./prototype-script.py)
10 |
11 | 
12 |
13 | ## Contributing
14 |
15 | PRs welcome! Please read the [contributing guidelines](https://github.com/mithi/hexapod/blob/master/CONTRIBUTING.md) and the [commit style guide](https://github.com/mithi/hexapod/wiki/A-Commit-Style-Guide)!
16 |
17 | ## References
18 |
19 | - [x] [Scratch a Pixel 2.0: Finding the 2D pixel coordinates of a 3D Point Explained from Beginning to End](https://www.scratchapixel.com/lessons/3d-basic-rendering/computing-pixel-coordinates-of-3d-point/mathematics-computing-2d-coordinates-of-3d-points)
20 |
21 | - [x] [Gabriel Gambeta: Computer Graphics from scratch (Perspective Projection)](https://www.gabrielgambetta.com/computer-graphics-from-scratch/perspective-projection.html)
22 |
23 | - [x] [David J. Eck: Introduction to Computer Graphics (Projection and Viewing), Hobart and William Smith Colleges](http://math.hws.edu/graphicsbook/c3/s3.html)
24 |
25 | - [x] [Jeremiah: 3D Game Engine Programming (Understanding the View Matrix)](https://www.3dgep.com/understanding-the-view-matrix/)
26 |
27 | - [x] [Etay Meiri: OLDEV Model OpenGL Tutorial (Camera Space)](http://ogldev.org/www/tutorial13/tutorial13.html)
28 |
29 | - [x] [Plotly: 3D Camera Controls in Python](https://plotly.com/python/3d-camera-controls/)
30 |
--------------------------------------------------------------------------------
/src/templates/cameraViewParams.js:
--------------------------------------------------------------------------------
1 | const INIT_STATE = {
2 | rx: 0,
3 | ry: 0,
4 | rz: 0,
5 | tx: 0,
6 | ty: 0,
7 | tz: 0,
8 | zoom: 1,
9 | }
10 |
11 | /** THE PROPS REQUIRED FOR EACH INPUT FIELD **/
12 | const STATE_PROPS = {
13 | rx: {
14 | rangeParams: {
15 | minVal: -90,
16 | maxVal: 90,
17 | stepVal: 0.5,
18 | },
19 | label: "rotX",
20 | id: "camera-view-rot-x",
21 | },
22 | ry: {
23 | rangeParams: {
24 | minVal: -90,
25 | maxVal: 90,
26 | stepVal: 0.5,
27 | },
28 | label: "rotY",
29 | id: "camera-view-rot-z",
30 | },
31 | rz: {
32 | rangeParams: {
33 | minVal: -90,
34 | maxVal: 90,
35 | stepVal: 0.5,
36 | },
37 | label: "rotZ",
38 | id: "camera-view-rot-y",
39 | },
40 | tx: {
41 | rangeParams: {
42 | minVal: -10,
43 | maxVal: 10,
44 | stepVal: 0.1,
45 | },
46 | label: "t.X",
47 | id: "camera-view-trans-x",
48 | },
49 | ty: {
50 | rangeParams: {
51 | minVal: -10,
52 | maxVal: 10,
53 | stepVal: 0.1,
54 | },
55 | label: "t.Y",
56 | id: "camera-view-trans-y",
57 | },
58 | tz: {
59 | rangeParams: {
60 | minVal: -10,
61 | maxVal: 10,
62 | stepVal: 0.1,
63 | },
64 | label: "t.Z",
65 | id: "camera-view-trans-z",
66 | },
67 | zoom: {
68 | rangeParams: {
69 | minVal: 0,
70 | maxVal: 10,
71 | stepVal: 0.1,
72 | },
73 | label: "zoom",
74 | id: "camera-view-zoom",
75 | },
76 | }
77 |
78 | const ID_TO_KEY_MAP = {
79 | [STATE_PROPS.rx.id]: "rx",
80 | [STATE_PROPS.ry.id]: "ry",
81 | [STATE_PROPS.rz.id]: "rz",
82 | [STATE_PROPS.tx.id]: "tx",
83 | [STATE_PROPS.ty.id]: "ty",
84 | [STATE_PROPS.tz.id]: "tz",
85 | [STATE_PROPS.zoom.id]: "zoom",
86 | }
87 |
88 | export default { INIT_STATE, STATE_PROPS, ID_TO_KEY_MAP }
89 |
--------------------------------------------------------------------------------
/src/components/NumericInputField.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from "react"
2 | import TextField from "@material-ui/core/TextField"
3 |
4 | const cleanValue = (newValue, ref, { minVal, maxVal, stepVal }) => {
5 | const validity = ref.current.validity
6 | const isValid = false
7 |
8 | if (validity.badInput) {
9 | return { isValid, message: "NaN" }
10 | }
11 |
12 | if (validity.rangeOverflow) {
13 | return { isValid, message: `max=${maxVal}` }
14 | }
15 |
16 | if (validity.rangeUnderflow) {
17 | return { isValid, message: `min=${minVal}` }
18 | }
19 |
20 | if (validity.stepMismatch) {
21 | return { isValid, message: `step=${stepVal}` }
22 | }
23 |
24 | if (!ref.current.checkValidity()) {
25 | return { isValid, message: "Error" }
26 | }
27 |
28 | const numberValue = parseFloat(newValue)
29 |
30 | if (isNaN(numberValue)) {
31 | return { isValid, message: "NAN" }
32 | }
33 |
34 | return { isValid: true, message: "", value: numberValue }
35 | }
36 |
37 | const InputField = ({ id, label, value, setField, rangeParams }) => {
38 | const { minVal, maxVal, stepVal } = rangeParams
39 |
40 | const [message, setMessage] = useState("")
41 | const ref = useRef(null)
42 |
43 | const handleChange = newValue => {
44 | const { isValid, value: cleanedValue, message: newMessage } = cleanValue(
45 | newValue,
46 | ref,
47 | rangeParams
48 | )
49 | if (isValid) {
50 | setField(id, cleanedValue)
51 | }
52 |
53 | setMessage(newMessage)
54 | }
55 |
56 | return (
57 |
58 | handleChange(e.target.value)}
72 | helperText={message}
73 | />
74 |
75 | )
76 | }
77 |
78 | export default InputField
79 |
--------------------------------------------------------------------------------
/src/templates/boxModelParams.js:
--------------------------------------------------------------------------------
1 | const INIT_STATE = {
2 | rx: 0,
3 | ry: 0,
4 | rz: 0,
5 | tx: 0,
6 | ty: 0,
7 | tz: 0,
8 | sx: 1,
9 | sy: 1,
10 | sz: 1,
11 | color: 0,
12 | }
13 |
14 | /** THE PROPS REQUIRED FOR EACH INPUT FIELD **/
15 | const STATE_PROPS = {
16 | rx: {
17 | rangeParams: {
18 | minVal: -180,
19 | maxVal: 180,
20 | stepVal: 0.5,
21 | },
22 | label: "rotX",
23 | id: "cube-state-rot-x",
24 | },
25 | ry: {
26 | rangeParams: {
27 | minVal: -180,
28 | maxVal: 180,
29 | stepVal: 0.5,
30 | },
31 | label: "rotY",
32 | id: "cube-state-rot-z",
33 | },
34 | rz: {
35 | rangeParams: {
36 | minVal: -180,
37 | maxVal: 180,
38 | stepVal: 0.5,
39 | },
40 | label: "rotZ",
41 | id: "cube-state-rot-y",
42 | },
43 |
44 | // ---------------------------
45 |
46 | tx: {
47 | rangeParams: {
48 | minVal: -100,
49 | maxVal: 100,
50 | stepVal: 0.1,
51 | },
52 | label: "t.X",
53 | id: "cube-state-trans-x",
54 | },
55 | ty: {
56 | rangeParams: {
57 | minVal: -100,
58 | maxVal: 100,
59 | stepVal: 0.1,
60 | },
61 | label: "t.Y",
62 | id: "cube-state-trans-y",
63 | },
64 | tz: {
65 | rangeParams: {
66 | minVal: -100,
67 | maxVal: 100,
68 | stepVal: 0.1,
69 | },
70 | label: "t.Z",
71 | id: "cube-state-trans-z",
72 | },
73 |
74 | // ---------------------------
75 |
76 | sx: {
77 | rangeParams: {
78 | minVal: 0,
79 | maxVal: 10,
80 | stepVal: 0.1,
81 | },
82 | label: "s.X",
83 | id: "cube-state-scale-x",
84 | },
85 |
86 | sy: {
87 | rangeParams: {
88 | minVal: 0,
89 | maxVal: 10,
90 | stepVal: 0.1,
91 | },
92 | label: "s.Y",
93 | id: "cube-state-scale-y",
94 | },
95 | sz: {
96 | rangeParams: {
97 | minVal: 0,
98 | maxVal: 10,
99 | stepVal: 0.1,
100 | },
101 | label: "s.Z",
102 | id: "cube-state-scale-z",
103 | },
104 | // ---------------------------
105 |
106 | color: {
107 | label: "color",
108 | id: "cube-color",
109 | },
110 | }
111 |
112 | const ID_TO_KEY_MAP = {
113 | [STATE_PROPS.rx.id]: "rx",
114 | [STATE_PROPS.ry.id]: "ry",
115 | [STATE_PROPS.rz.id]: "rz",
116 |
117 | [STATE_PROPS.tx.id]: "tx",
118 | [STATE_PROPS.ty.id]: "ty",
119 | [STATE_PROPS.tz.id]: "tz",
120 |
121 | [STATE_PROPS.sx.id]: "sx",
122 | [STATE_PROPS.sy.id]: "sy",
123 | [STATE_PROPS.sz.id]: "sz",
124 |
125 | [STATE_PROPS.color.id]: "color",
126 | }
127 |
128 | export default { INIT_STATE, STATE_PROPS, ID_TO_KEY_MAP }
129 |
--------------------------------------------------------------------------------
/src/components/layouts.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { ThemeProvider } from "@material-ui/styles"
3 | import {
4 | Grid,
5 | CssBaseline,
6 | Typography,
7 | createMuiTheme,
8 | CardContent,
9 | Slider,
10 | Card,
11 | Radio,
12 | RadioGroup,
13 | FormControlLabel,
14 | Box,
15 | } from "@material-ui/core"
16 |
17 | const theme = createMuiTheme({
18 | palette: {
19 | type: "dark",
20 | },
21 | })
22 |
23 | const SliderInputField = ({ id, label, value, rangeParams, setField }) => {
24 | const handleChange = (_, newValue) => setField(id, newValue)
25 | return (
26 | <>
27 | {label}
28 |
38 | >
39 | )
40 | }
41 |
42 | const ControlCard = ({ title, children }) => (
43 |
44 |
45 |
46 |
47 | {title}
48 |
49 | {children}
50 |
51 |
52 |
53 | )
54 |
55 | const InputGroup3 = ({ children }) => {
56 | return (
57 |
58 | {children.map(child => (
59 |
60 | {child}
61 |
62 | ))}
63 |
64 | )
65 | }
66 |
67 | const ToggleRadioCard = ({ radioValue, onChange, option1Label, option2Label }) => (
68 |
69 | onChange(e.target.value)}>
70 | } label={option1Label} />
71 | } label={option2Label} />
72 |
73 |
74 | )
75 |
76 | class Layout extends React.Component {
77 | static Side = ({ children }) => (
78 |
79 | {children}
80 |
81 | )
82 | static Main = ({ children }) => (
83 |
84 |
92 | {children}
93 |
94 |
95 | )
96 |
97 | render() {
98 | return (
99 |
100 |
101 |
102 | {this.props.children}
103 |
104 |
105 | )
106 | }
107 | }
108 |
109 | export { Layout, ControlCard, SliderInputField, InputGroup3, ToggleRadioCard }
110 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react"
2 | import NumericInputField from "./components/NumericInputField"
3 | import {
4 | Layout,
5 | ControlCard,
6 | SliderInputField,
7 | InputGroup3,
8 | ToggleRadioCard,
9 | } from "./components/layouts"
10 | import CAM from "./templates/cameraViewParams"
11 | import BOX from "./templates/boxModelParams"
12 | import BareMinimum2d from "bare-minimum-2d"
13 | import { renderScene } from "./box"
14 | import { Button } from "@material-ui/core"
15 | import GitHubIcon from "@material-ui/icons/GitHub"
16 |
17 | // A helper to build the a set of required props... props that would be
18 | // be passed to components like SLIDER or INPUT TEXT FIELD
19 | const consolidateProp = (currentState, stateProps, setFunction) => {
20 | /**
21 | consolidatedProps = {
22 | rx: {
23 | id,
24 | label,
25 | rangeParams: {maxVal, minVal, stepVal},
26 | value,
27 | setField,
28 | },
29 | ry: { ... }
30 | ....
31 | }
32 | **/
33 | const consolidatedProps = Object.keys(stateProps).reduce(
34 | (props, key) => ({
35 | ...props,
36 | [key]: {
37 | ...stateProps[key],
38 | value: currentState[key],
39 | setField: setFunction,
40 | },
41 | }),
42 | {}
43 | )
44 | return consolidatedProps
45 | }
46 |
47 | const CameraControlView = ({ camProps }) => (
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | )
59 |
60 | const BoxModelControlView = ({ boxProps }) => (
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | {/*COLOR STATE IS NOT YET IMPLEMENTED FOR NOW */}
76 |
77 | )
78 |
79 | const App = () => {
80 | const [cameraViewState, setCameraViewState] = useState(CAM.INIT_STATE)
81 | const [boxModelState, setBoxModelState] = useState(BOX.INIT_STATE)
82 | const [isCameraView, setControlUi] = React.useState("true")
83 |
84 | const setCameraViewField = (id, newValue) => {
85 | setCameraViewState({ ...cameraViewState, [CAM.ID_TO_KEY_MAP[id]]: newValue })
86 | }
87 | const setBoxModelField = (id, newValue) => {
88 | setBoxModelState({ ...boxModelState, [BOX.ID_TO_KEY_MAP[id]]: newValue })
89 | }
90 |
91 | const showCamera = isCameraView === "true"
92 | const camProps = consolidateProp(cameraViewState, CAM.STATE_PROPS, setCameraViewField)
93 | const boxProps = consolidateProp(boxModelState, BOX.STATE_PROPS, setBoxModelField)
94 | const plotProps = renderScene(boxModelState, cameraViewState)
95 |
96 | return (
97 |
98 |
99 |
100 |
101 |
102 |
103 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
126 |
127 |
128 | )
129 | }
130 |
131 | export default App
132 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/prototype-script.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 |
4 | ____ __ __ ____
5 | /\ _`\ /'__`\ /\ \ /\ _`\
6 | \ \ \/\ \ _ __ __ __ __ __ __ /\_\L\ \ \_\ \ \ \ \L\ \ ___ __ _
7 | \ \ \ \ \/\`'__\/'__`\ /\ \/\ \/\ \ /'__`\ \/_/_\_<_ /'_` \ \ \ _ <' / __`\/\ \/'\
8 | \ \ \_\ \ \ \//\ \L\.\_\ \ \_/ \_/ \ /\ \L\.\_ /\ \L\ \/\ \L\ \ \ \ \L\ \/\ \L\ \/>
9 | \ \____/\ \_\\ \__/.\_\\ \___x___/' \ \__/.\_\ \ \____/\ \___,_\ \ \____/\ \____//\_/\_\
10 | \/___/ \/_/ \/__/\/_/ \/__//__/ \/__/\/_/ \/___/ \/__,_ / \/___/ \/___/ \//\/_/
11 |
12 | .+------+ +------+ +------+ +------+ +------+.
13 | .' | .'| /| /| | | |\ |\ |`. | `.
14 | +---+--+' | +-+----+ | +------+ | +----+-+ | `+--+---+
15 | | | | | | | | | | | | | | | | | | |
16 | | ,+--+---+ | +----+-+ +------+ +-+----+ | +---+--+ |
17 | |.' | .' |/ |/ | | \| \| `. | `. |
18 | +------+' +------+ +------+ +------+ `+------+
19 |
20 | +------+. +------+ +------+ +------+ .+------+
21 | |`. | `. |\ |\ | | /| /| .' | .'|
22 | | `+--+---+ | +----+-+ +------+ +-+----+ | +---+--+' |
23 | | | | | | | | | | | | | | | | | | |
24 | +---+--+. | +-+----+ | +------+ | +----+-+ | .+--+---+
25 | `. | `.| \| \| | | |/ |/ |.' | .'
26 | `+------+ +------+ +------+ +------+ +------+
27 | """
28 |
29 |
30 | import numpy as np
31 | import matplotlib.pyplot as plt
32 |
33 | def _return_sin_and_cos(theta):
34 | d = np.radians(theta)
35 | c = np.cos(d)
36 | s = np.sin(d)
37 | return c, s
38 |
39 |
40 | def rotx(theta):
41 | c, s = _return_sin_and_cos(theta)
42 | return np.array([
43 | [1, 0, 0],
44 | [0, c, -s],
45 | [0, s, c]]
46 | )
47 |
48 |
49 | def roty(theta):
50 | c, s = _return_sin_and_cos(theta)
51 | return np.array([
52 | [c, 0, s],
53 | [0, 1, 0],
54 | [-s, 0, c],
55 | ])
56 |
57 |
58 | def rotz(theta):
59 | c, s = _return_sin_and_cos(theta)
60 | return np.array([
61 | [c, -s, 0],
62 | [s, c, 0],
63 | [0, 0, 1],
64 | ])
65 |
66 |
67 | def rotxyz(euler_vec):
68 | rx = rotx(euler_vec.x)
69 | ry = roty(euler_vec.y)
70 | rz = rotz(euler_vec.z)
71 | rxy = np.matmul(rx, ry)
72 | rxyz = np.matmul(rxy, rz)
73 | return rxyz
74 |
75 |
76 | class Vector:
77 | def __init__(self, x, y, z, name=None, alias=None):
78 | self.x, self.y, self.z = x, y, z
79 | self.name = name
80 | self.alias = alias
81 |
82 | def get_transformed_point(self, transform_matrix):
83 | p = np.array([self.x, self.y, self.z, 1])
84 | p = np.matmul(transform_matrix, p)
85 | return Vector(p[0], p[1], p[2], self.name, self.alias)
86 |
87 | def __repr__(self):
88 | return f"Vector(x={self.x}, y={self.y}, z={self.z}, {self.name})"
89 |
90 | def t_matrix(translation):
91 | return [[1, 0, 0, translation.x],
92 | [0, 1, 0, translation.y],
93 | [0, 0, 1, translation.z],
94 | [0, 0, 0, 1]]
95 |
96 | def s_matrix(scale):
97 | return [[scale, 0, 0, 0],
98 | [ 0, scale, 0, 0],
99 | [ 0, 0, scale, 0],
100 | [ 0, 0, 0, 1]]
101 |
102 |
103 | """
104 | E------F y
105 | |`. | `. |
106 | | `A--+---B *----- x
107 | | | | | \
108 | G---+--H | \
109 | `. | `. | z
110 | `C------D
111 |
112 | """
113 | class NormalUnitCube:
114 | CENTER = Vector(0, 0, 0, "cube-center")
115 | POINTS = [
116 | Vector(-1, +1, +1, "front-top-left", "A"),
117 | Vector(+1, +1, +1, "front-top-right", "B"),
118 | Vector(-1, -1, +1, "front-bottom-left", "C"),
119 | Vector(+1, -1, +1, "front-bottom-right", "D"),
120 | Vector(-1, +1, -1, "back-top-left", "E"),
121 | Vector(+1, +1, -1, "back-top-right", "F"),
122 | Vector(-1, -1, -1, "back-bottom-left", "G"),
123 | Vector(+1, -1, -1, "back-bottom-right", "H"),
124 | ]
125 |
126 | def __init__(self):
127 | pass
128 |
129 | class Cube:
130 | UNIT_CUBE = NormalUnitCube()
131 | def __init__(
132 | self,
133 | euler_vec=Vector(0, 0, 0),
134 | scale=1,
135 | translate_vec=Vector(0, 0, 0)
136 | ):
137 | self.rotate_matrix = rotxyz(euler_vec)
138 | self.euler_vec = euler_vec
139 | self.scale = scale
140 | self.translate_vec = translate_vec
141 | r = self.rotate_matrix
142 | s = self.scale
143 | t = self.translate_vec
144 | r_matrix = [
145 | [r[0][0], r[0][1], r[0][2], 0],
146 | [r[1][0], r[1][1], r[1][2], 0],
147 | [r[2][0], r[2][1], r[2][2], 0],
148 | [0, 0, 0, 1],
149 | ]
150 | self.wrt_world_matrix = np.matmul(t_matrix(t), np.matmul(s_matrix(s), r_matrix))
151 | self.points = Cube.UNIT_CUBE.POINTS
152 |
153 | def get_world_wrt_camera_matrix(
154 | translate_vec=Vector(0, 0, 0),
155 | euler_vec=Vector(0, 0, 0)
156 | ):
157 | Rmatrix = rotxyz(euler_vec)
158 | t = translate_vec
159 | # Inverse of rotations matrix
160 | r = Rmatrix.T
161 |
162 | # matrix[row][column]
163 |
164 | # inverse_matrix = rotateCameraMatrixInverse * translateCameraMatrixInverse
165 | # world_to_camera_matrix
166 | return [
167 | [r[0][0], r[0][1], r[0][2], -t.x],
168 | [r[1][0], r[1][1], r[1][2], -t.y],
169 | [r[2][0], r[2][1], r[2][2], -t.z],
170 | [0, 0, 0, 1],
171 | ]
172 |
173 | def get_projected_point(point, projection_constant):
174 | return Vector(
175 | point.x / point.z * projection_constant,
176 | point.y / point.z * projection_constant,
177 | projection_constant,
178 | point.name,
179 | point.alias,
180 | )
181 |
182 | def plot_2d_points(points):
183 | p = points
184 | x1 = [p[0].x, p[1].x, p[3].x, p[2].x, p[0].x]
185 | y1 = [p[0].y, p[1].y, p[3].y, p[2].y, p[0].y]
186 |
187 | x2 = [p[4].x, p[5].x, p[7].x, p[6].x, p[4].x]
188 | y2 = [p[4].y, p[5].y, p[7].y, p[6].y, p[4].y]
189 |
190 | x3 = [p[0].x, p[4].x]
191 | y3 = [p[0].y, p[4].y]
192 |
193 | x4 = [p[1].x, p[5].x]
194 | y4 = [p[1].y, p[5].y]
195 |
196 | x5 = [p[2].x, p[6].x]
197 | y5 = [p[2].y, p[6].y]
198 |
199 | x6 = [p[3].x, p[7].x]
200 | y6 = [p[3].y, p[7].y]
201 |
202 |
203 | fig = plt.figure()
204 | ax = fig.add_subplot(111)
205 |
206 | lines = plt.plot(x1, y1)
207 | lines2 = plt.plot(x2, y2)
208 | lines3 = plt.plot(x3, y3)
209 | lines4 = plt.plot(x4, y4)
210 | lines5 = plt.plot(x5, y5)
211 | lines6 = plt.plot(x6, y6)
212 |
213 | plt.setp(lines, color='r', linewidth=5.0)
214 | plt.setp(lines2, color='b', linewidth=5.0)
215 | plt.setp(lines3, color='g', linewidth=5.0)
216 | plt.setp(lines4, color='g', linewidth=5.0)
217 | plt.setp(lines5, color='g', linewidth=5.0)
218 | plt.setp(lines6, color='g', linewidth=5.0)
219 | plt.xlim(-300, 300)
220 | plt.ylim(-300, 300)
221 |
222 | ax.set_aspect('equal', adjustable='box')
223 | plt.show()
224 |
225 | def render_cube(cube, cube_wrt_camera_matrix, projection_constant):
226 | projected_points = []
227 | for point in cube.points:
228 | transformed_point = point.get_transformed_point(cube_wrt_camera_matrix)
229 | projected_point = get_projected_point(transformed_point, projection_constant)
230 | projected_points.append(projected_point)
231 |
232 | print(projected_points)
233 | plot_2d_points(projected_points)
234 |
235 | # -----------------------------------
236 | # RENDER SCENE
237 | # -----------------------------------
238 |
239 | PROJECTION_CONSTANT = 1 * 600
240 | CAMERA_POSITION = Vector(0, 0, 5)
241 | CAMERA_ORIENTATION = Vector(0, 0, 45)
242 | world_wrt_camera_matrix = get_world_wrt_camera_matrix(CAMERA_POSITION, CAMERA_ORIENTATION)
243 |
244 | # euler angle vector (rot.x, rot.y, rot.z) in degrees
245 | r = Vector(-30, 0, 0)
246 | # translate vector
247 | t = Vector(0, 0, 0)
248 | # scale magnitude
249 | s = 1.25
250 | cube = Cube(r, s, t)
251 | cube_wrt_camera_matrix = np.matmul(world_wrt_camera_matrix, cube.wrt_world_matrix)
252 |
253 | render_cube(cube, cube_wrt_camera_matrix, PROJECTION_CONSTANT)
254 |
--------------------------------------------------------------------------------
/src/box.js:
--------------------------------------------------------------------------------
1 | const radians = thetaDegrees => (thetaDegrees * Math.PI) / 180
2 | const getSinCos = theta => [Math.sin(radians(theta)), Math.cos(radians(theta))]
3 | const dot = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z
4 |
5 | const vectorLength = v => Math.sqrt(dot(v, v))
6 | const vectorFromTo = (a, b) => new Vector(b.x - a.x, b.y - a.y, b.z - a.z)
7 | const scaleVector = (v, d) => new Vector(d * v.x, d * v.y, d * v.z)
8 |
9 | const cross = (a, b) => {
10 | const x = a.y * b.z - a.z * b.y
11 | const y = a.z * b.x - a.x * b.z
12 | const z = a.x * b.y - a.y * b.x
13 | return new Vector(x, y, z)
14 | }
15 |
16 | const getNormalofThreePoints = (a, b, c) => {
17 | const ba = vectorFromTo(b, a)
18 | const bc = vectorFromTo(b, c)
19 | const n = cross(ba, bc)
20 | const len_n = vectorLength(n)
21 | const unit_n = scaleVector(n, 1 / len_n)
22 |
23 | return unit_n
24 | }
25 |
26 | const uniformMatrix4x4 = d => {
27 | const dRow = [d, d, d, d]
28 | return [dRow.slice(), dRow.slice(), dRow.slice(), dRow.slice()]
29 | }
30 |
31 | const multiply4x4 = (matrixA, matrixB) => {
32 | let resultMatrix = uniformMatrix4x4(null)
33 |
34 | for (let i = 0; i < 4; i++) {
35 | for (let j = 0; j < 4; j++) {
36 | resultMatrix[i][j] =
37 | matrixA[i][0] * matrixB[0][j] +
38 | matrixA[i][1] * matrixB[1][j] +
39 | matrixA[i][2] * matrixB[2][j] +
40 | matrixA[i][3] * matrixB[3][j]
41 | }
42 | }
43 |
44 | return resultMatrix
45 | }
46 |
47 | function rotX(theta, tx = 0, ty = 0, tz = 0) {
48 | const [s, c] = getSinCos(theta)
49 |
50 | return [
51 | [1, 0, 0, tx],
52 | [0, c, -s, ty],
53 | [0, s, c, tz],
54 | [0, 0, 0, 1],
55 | ]
56 | }
57 |
58 | function rotY(theta) {
59 | const [s, c] = getSinCos(theta)
60 | return [
61 | [c, 0, s, 0],
62 | [0, 1, 0, 0],
63 | [-s, 0, c, 0],
64 | [0, 0, 0, 1],
65 | ]
66 | }
67 |
68 | function rotZ(theta) {
69 | const [s, c] = getSinCos(theta)
70 | return [
71 | [c, -s, 0, 0],
72 | [s, c, 0, 0],
73 | [0, 0, 1, 0],
74 | [0, 0, 0, 1],
75 | ]
76 | }
77 |
78 | const rotXYZ = eulerVec => {
79 | const rx = rotX(eulerVec.x)
80 | const ry = rotY(eulerVec.y)
81 | const rz = rotZ(eulerVec.z)
82 | const rxy = multiply4x4(rx, ry)
83 | const rxyz = multiply4x4(rxy, rz)
84 | return rxyz
85 | }
86 |
87 | class Vector {
88 | constructor(x, y, z, name) {
89 | this.x = x
90 | this.y = y
91 | this.z = z
92 | this.name = name
93 | }
94 |
95 | getTransformedPoint(transformMatrix) {
96 | const [r0, r1, r2] = transformMatrix.slice(0, 3)
97 | const [r00, r01, r02, tx] = r0
98 | const [r10, r11, r12, ty] = r1
99 | const [r20, r21, r22, tz] = r2
100 |
101 | const newX = this.x * r00 + this.y * r01 + this.z * r02 + tx
102 | const newY = this.x * r10 + this.y * r11 + this.z * r12 + ty
103 | const newZ = this.x * r20 + this.y * r21 + this.z * r22 + tz
104 | return new Vector(newX, newY, newZ, this.name)
105 | }
106 | }
107 |
108 | const tMatrix = translation => [
109 | [1, 0, 0, translation.x],
110 | [0, 1, 0, translation.y],
111 | [0, 0, 1, translation.z],
112 | [0, 0, 0, 1],
113 | ]
114 |
115 | const sMatrix = s => [
116 | [s.x, 0, 0, 0],
117 | [0, s.y, 0, 0],
118 | [0, 0, s.z, 0],
119 | [0, 0, 0, 1],
120 | ]
121 |
122 | /*
123 | E4------F5 y
124 | |`. | `. |
125 | | `A0-----B1 *----- x
126 | | | | | \
127 | G6--|--H7 | \
128 | `. | `. | z
129 | `C2-----D3
130 | */
131 | class NormalUnitCube {
132 | CENTER = new Vector(0, 0, 0, "cube-center") // cube-center
133 | POINTS = [
134 | new Vector(-1, +1, +1, "front-top-left"), // A0
135 | new Vector(+1, +1, +1, "front-top-right"), // B1
136 | new Vector(-1, -1, +1, "front-bottom-left"), // C2
137 | new Vector(+1, -1, +1, "front-bottom-right"), // D3
138 | new Vector(-1, +1, -1, "back-top-left"), // E4
139 | new Vector(+1, +1, -1, "back-top-right"), // F5
140 | new Vector(-1, -1, -1, "back-bottom-left"), // G6
141 | new Vector(+1, -1, -1, "back-bottom-right"), // H7
142 | ]
143 | }
144 |
145 | class Cube {
146 | UNIT_CUBE = new NormalUnitCube()
147 | constructor(
148 | eulerVec = new Vector(0, 0, 0),
149 | scale = new Vector(1, 1, 1),
150 | translateVec = new Vector(0, 0, 0)
151 | ) {
152 | const rMatrix = rotXYZ(eulerVec)
153 | const s = scale
154 | const t = translateVec
155 | this.wrtWorldMatrix = multiply4x4(tMatrix(t), multiply4x4(sMatrix(s), rMatrix))
156 | this.points = this.UNIT_CUBE.POINTS
157 | }
158 | }
159 |
160 | const getWorldWrtCameraMatrix = (
161 | translateVec = Vector(0, 0, 0),
162 | eulerVec = Vector(0, 0, 0)
163 | ) => {
164 | const r = rotXYZ(eulerVec)
165 | const t = translateVec
166 | // Inverse of rotations matrix
167 | // inverse_matrix = rotateCameraMatrixInverse * translateCameraMatrixInverse
168 | // world_to_camera_matrix
169 | return [
170 | [r[0][0], r[1][0], r[2][0], -t.x],
171 | [r[0][1], r[1][1], r[2][1], -t.y],
172 | [r[0][2], r[1][2], r[2][2], -t.z],
173 | [0, 0, 0, 1],
174 | ]
175 | }
176 | const getProjectedPoint = (point, projectionConstant) => {
177 | return new Vector(
178 | (point.x / point.z) * projectionConstant,
179 | (point.y / point.z) * projectionConstant,
180 | projectionConstant,
181 | point.name
182 | )
183 | }
184 |
185 | const renderCube = (cube, cubeWrtCameraMatrix, projectionConstant) => {
186 | let projectedPoints = []
187 | let pointsWrtCamera = []
188 | cube.points.forEach(point => {
189 | const pointWrtCamera = point.getTransformedPoint(cubeWrtCameraMatrix)
190 | const projectedPoint = getProjectedPoint(pointWrtCamera, projectionConstant)
191 | pointsWrtCamera.push(pointWrtCamera)
192 | projectedPoints.push(projectedPoint)
193 | })
194 |
195 | return [pointsWrtCamera, projectedPoints]
196 | }
197 |
198 | // RENDER SCENE
199 |
200 | const renderScene = (box, cam) => {
201 | const Z_TRANSLATE_OFFSET = 5
202 | const PROJECTION_CONSTANT = 300 * cam.zoom
203 | const CAMERA_POSITION = new Vector(cam.tx, cam.ty, cam.tz + Z_TRANSLATE_OFFSET)
204 | const CAMERA_ORIENTATION = new Vector(cam.rx, cam.ry, cam.rz)
205 | const worldWrtCameraMatrix = getWorldWrtCameraMatrix(
206 | CAMERA_POSITION,
207 | CAMERA_ORIENTATION
208 | )
209 | // euler orientation rotation
210 | const r = new Vector(box.rx, box.ry, box.rz)
211 | // translate vector
212 | const t = new Vector(box.tx, box.ty, box.tz)
213 | // scale magnitude
214 | const s = new Vector(box.sx, box.sy, box.sz)
215 |
216 | const cube = new Cube(r, s, t)
217 | const cubeWrtCameraMatrix = multiply4x4(worldWrtCameraMatrix, cube.wrtWorldMatrix)
218 | const [pointsWrtCamera, projectedPoints] = renderCube(
219 | cube,
220 | cubeWrtCameraMatrix,
221 | PROJECTION_CONSTANT
222 | )
223 |
224 | const isFrontFacing = whichPlanesFrontFacing(pointsWrtCamera)
225 | return drawBox(projectedPoints, isFrontFacing)
226 | }
227 |
228 | /*
229 | E4------F5 y
230 | |`. | `. |
231 | | `A0-----B1 *----- x
232 | | | | | \
233 | G6--|--H7 | \
234 | `. | `. | z
235 | `C2-----D3
236 |
237 | face 1 - A0, B1, D3 | C2 (front)
238 | face 2 - B1, F5, H7 | D3 (front right)
239 | face 3 - F5, E4, G6 | H7 (front left)
240 | face 4 - E4, A0, C2 | G6 (back)
241 | face 5 - E4, F5, B1 | A0 (top)
242 | face 6 - C2 , D3, H7 | G6 |(bottom)
243 |
244 | IMPORTANT!
245 | The second point (ie B1 of set [A0, B1, D3, C2]
246 | is the center of A0, B1, D3 which is where we will
247 | compute the normal of the plane
248 | */
249 |
250 | // use back face culling to figure out which
251 | // faces are in front
252 |
253 | const POINT_FACE_SET = [
254 | [0, 1, 3, 2],
255 | [1, 5, 7, 3],
256 | [5, 4, 6, 7],
257 | [4, 0, 2, 6],
258 | [4, 5, 1, 0],
259 | [2, 3, 7, 6],
260 | ]
261 | // returns an array of booleans with six elements
262 | // returns if the respective planes defined by the for each set of points (POINT_FACE_SET)
263 | // are front facing or not
264 | const whichPlanesFrontFacing = pointsWrtCamera => {
265 | const p = pointsWrtCamera
266 | return POINT_FACE_SET.map(pointIds => {
267 | const [a, b, c] = pointIds
268 |
269 | const n = getNormalofThreePoints(p[a], p[b], p[c])
270 | // v is vector from point p[b]
271 | // to cameraOriginPoint Vector(0, 0, 0)
272 | const v = new Vector(-p[b].x, -p[b].y, -p[b].z)
273 | const isFrontFacing = dot(n, v) > 0.0
274 |
275 | return isFrontFacing
276 | })
277 | }
278 |
279 | const drawBox = (projectedPoints, isFrontFacing) => {
280 | const p = projectedPoints
281 | const container = {
282 | color: "#333333",
283 | opacity: 1.0,
284 | xRange: 600,
285 | yRange: 600,
286 | }
287 |
288 | const COLORS = ["#32ff7e", "#e056fd", "#E91E63", "#fa8231", "#fff200", "#ff3838"]
289 | const OPACITY = [0.75, 0.75, 0.75, 0.75, 0.75, 0.75]
290 |
291 | let data = []
292 | isFrontFacing.forEach((isFront, index) => {
293 | const [a, b, c, d] = POINT_FACE_SET[index]
294 | const plane = {
295 | x: [p[a].x, p[b].x, p[c].x, p[d].x],
296 | y: [p[a].y, p[b].y, p[c].y, p[d].y],
297 | borderColor: "#0652DD",
298 | borderOpacity: 1.0,
299 | fillColor: COLORS[index],
300 | fillOpacity: OPACITY[index],
301 | borderSize: 8,
302 | type: "polygon",
303 | id: `plane-${index}`,
304 | }
305 | const points = {
306 | x: [p[a].x, p[b].x, p[c].x, p[d].x],
307 | y: [p[a].y, p[b].y, p[c].y, p[d].y],
308 | color: "#0652DD",
309 | opacity: 1.0,
310 | size: 15,
311 | type: "points",
312 | id: `points-${index}`,
313 | }
314 |
315 | data = isFront ? [...data, plane, points] : [plane, points, ...data]
316 | })
317 |
318 | return { data, container }
319 | }
320 |
321 | export { Cube, renderScene, drawBox }
322 |
--------------------------------------------------------------------------------