commit 84ebac763fb054af04cb30358847dc6c828b5135 Author: Michael Zhang Date: Tue Oct 1 19:16:16 2024 -0500 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/README.md b/README.md new file mode 100644 index 0000000..74872fd --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default tseslint.config({ + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` +- Optionally add `...tseslint.configs.stylisticTypeChecked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: + +```js +// eslint.config.js +import react from 'eslint-plugin-react' + +export default tseslint.config({ + // Set the react version + settings: { react: { version: '18.3' } }, + plugins: { + // Add the react plugin + react, + }, + rules: { + // other rules... + // Enable its recommended rules + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + }, +}) +``` diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..017fe2c --- /dev/null +++ b/biome.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..5d46ce1 Binary files /dev/null and b/bun.lockb differ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/index.html b/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..88a626c --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "cubeviz", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@react-three/drei": "^9.114.0", + "@react-three/fiber": "^8.17.8", + "@types/three": "^0.169.0", + "meshline": "^3.3.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-katex": "^3.0.1", + "sass": "^1.79.3", + "three": "^0.169.0", + "three-stdlib": "^2.33.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.2", + "@eslint/js": "^9.9.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/react-katex": "^3.0.4", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite": "^5.4.1" + }, + "trustedDependencies": [ + "@biomejs/biome" + ] +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..497920e --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,112 @@ +import { ReactNode, useCallback, useRef, useState } from "react"; +import { Canvas, extend, useFrame } from "@react-three/fiber"; +import { OrbitControls, OrthographicCamera } from "@react-three/drei"; +import { BoxGeometry, EdgesGeometry, LineBasicMaterial } from "three"; +import { InlineMath, BlockMath } from "react-katex"; +import { createPortal } from "react-dom"; + +import "./App.css"; +import styles from "./styles.module.scss"; +import "katex/dist/katex.min.css"; +import Point from "./components/Point"; +import Path from "./components/Path"; + +// https://threejs.org/manual/#en/align-html-elements-to-3d + +function createBox({ dimensions, ...props }) { + // This reference gives us direct access to the THREE.Mesh object + const ref = useRef(); + // Hold state for hovered and clicked events + const [hovered, hover] = useState(false); + const [clicked, click] = useState(false); + + const box = new BoxGeometry(1, 1, 1); + const edges = new EdgesGeometry(box); + + const label = ( +
+ helloge {JSON.stringify(clicked)} + +
+ ); + + // Return the view, these are regular Threejs elements expressed in JSX + const view = ( + click(!clicked)} + onPointerOver={(event) => hover(true)} + onPointerOut={(event) => hover(false)} + > + {/* */} + {/* */} + + + + ); + + return [view, label]; +} + +function App() { + const [labelContainerEl, setLabelContainerEl] = + useState(null); + const labelContainerRef = useCallback((el) => { + if (el) setLabelContainerEl(el); + }, []); + + let coords: [number, number, number][] = [ + [0, 0, 0], + [0, 0, 1], + [0, 1, 0], + [0, 1, 1], + [1, 0, 0], + [1, 0, 1], + [1, 1, 0], + [1, 1, 1], + ]; + + coords = coords.map((a) => [a[0] - 0.5, a[1] - 0.5, a[2] - 0.5]); + + const paths = coords + .flatMap((a) => coords.map((b) => [a, b])) + .filter( + ([[a1, a2, a3], [b1, b2, b3]]) => + [a1 === b1 ? 1 : 0, a2 === b2 ? 1 : 0, a3 === b3 ? 1 : 0].reduce( + (x, y) => x + y, + ) === 2 && + a1 <= b1 && + a2 <= b2 && + a3 <= b3, + ); + + return ( + <> + + + + + + + {coords.map((coord) => ( + + ))} + + {paths.map(([start, end]) => ( + + ))} + + + ); +} + +export default App; diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/EditBox.module.scss b/src/components/EditBox.module.scss new file mode 100644 index 0000000..d19aa3e --- /dev/null +++ b/src/components/EditBox.module.scss @@ -0,0 +1,7 @@ +.editBox { + padding: 4px; +} + +.empty { + color: rgba(0, 0, 0, 0.2) +} \ No newline at end of file diff --git a/src/components/EditBox.tsx b/src/components/EditBox.tsx new file mode 100644 index 0000000..ea5a7ff --- /dev/null +++ b/src/components/EditBox.tsx @@ -0,0 +1,41 @@ +import { useCallback, useState } from "react"; +import { InlineMath } from "react-katex"; + +import styles from "./EditBox.module.scss"; + +export interface EditBoxProps {} + +export default function EditBox({}: EditBoxProps) { + const [value, setValue] = useState(""); + const [isEditing, setIsEditing] = useState(false); + + const handleDblClick = useCallback(() => { + if (!isEditing) setIsEditing(true); + }, [isEditing]); + + const done = useCallback( + (evt) => { + evt.preventDefault(); + if (isEditing) setIsEditing(false); + }, + [isEditing], + ); + + return ( +
+ {isEditing ? ( +
+ setValue(evt.target.value)} + /> +
+ ) : value === "" ? ( + (empty) + ) : ( + + )} +
+ ); +} diff --git a/src/components/Path.tsx b/src/components/Path.tsx new file mode 100644 index 0000000..4af3a4c --- /dev/null +++ b/src/components/Path.tsx @@ -0,0 +1,30 @@ +import { extend, useFrame } from "@react-three/fiber"; +import { Html, Line } from "@react-three/drei"; +import { MeshLineGeometry, MeshLineMaterial, raycast } from "meshline"; +import EditBox from "./EditBox"; + +extend({ MeshLineGeometry, MeshLineMaterial }); + +export interface PointProps { + start: [number, number, number]; + end: [number, number, number]; +} + +export default function Path({ start, end }: PointProps) { + const midpoint: [number, number, number] = [ + (start[0] + end[0]) / 2.0, + (start[1] + end[1]) / 2.0, + (start[2] + end[2]) / 2.0, + ]; + + return ( + <> + + + + + + + + ); +} diff --git a/src/components/Point.tsx b/src/components/Point.tsx new file mode 100644 index 0000000..bb9115d --- /dev/null +++ b/src/components/Point.tsx @@ -0,0 +1,19 @@ +import { Html } from "@react-three/drei"; +import EditBox from "./EditBox"; + +export interface PointProps { + coord: [number, number, number]; +} + +export default function Point({ coord }: PointProps) { + return ( + + + + + + + + + ); +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..cdb8e50 --- /dev/null +++ b/src/index.css @@ -0,0 +1,17 @@ +body, html { + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +body { + display: flex; +} + +#root { + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..6f4ac9b --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/styles.module.scss b/src/styles.module.scss new file mode 100644 index 0000000..75d6a64 --- /dev/null +++ b/src/styles.module.scss @@ -0,0 +1,11 @@ +.canvas { + position: relative; + width: 100%; + height: 100%; +} + +.labels { + position: absolute; + left: 0; + top: 0; +} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..6564507 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +import type { Object3DNode, MaterialNode } from "@react-three/fiber"; + +declare module "@react-three/fiber" { + interface ThreeElements { + meshLineGeometry: Object3DNode; + meshLineMaterial: MaterialNode; + } +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..f0a2350 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..0d3d714 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5a33944 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +})