update
This commit is contained in:
parent
84ebac763f
commit
069a35e959
11 changed files with 81 additions and 161 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -22,3 +22,5 @@ dist-ssr
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
51
README.md
51
README.md
|
@ -1,50 +1,3 @@
|
||||||
# React + TypeScript + Vite
|
# cubeviz
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
Cubical type theory visualizer.
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -13,6 +13,7 @@
|
||||||
"@react-three/drei": "^9.114.0",
|
"@react-three/drei": "^9.114.0",
|
||||||
"@react-three/fiber": "^8.17.8",
|
"@react-three/fiber": "^8.17.8",
|
||||||
"@types/three": "^0.169.0",
|
"@types/three": "^0.169.0",
|
||||||
|
"jotai": "^2.10.0",
|
||||||
"meshline": "^3.3.1",
|
"meshline": "^3.3.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
|
42
src/App.css
42
src/App.css
|
@ -1,42 +0,0 @@
|
||||||
#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;
|
|
||||||
}
|
|
68
src/App.tsx
68
src/App.tsx
|
@ -1,63 +1,15 @@
|
||||||
import { ReactNode, useCallback, useRef, useState } from "react";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { Canvas, extend, useFrame } from "@react-three/fiber";
|
import { OrbitControls } from "@react-three/drei";
|
||||||
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 styles from "./styles.module.scss";
|
||||||
import "katex/dist/katex.min.css";
|
import "katex/dist/katex.min.css";
|
||||||
import Point from "./components/Point";
|
import Point from "./components/Point";
|
||||||
import Path from "./components/Path";
|
import Path from "./components/Path";
|
||||||
|
import { SettingsBox } from "./SettingsContext";
|
||||||
|
|
||||||
// https://threejs.org/manual/#en/align-html-elements-to-3d
|
// 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 = (
|
|
||||||
<div>
|
|
||||||
helloge {JSON.stringify(clicked)}
|
|
||||||
<InlineMath math="f(x)" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Return the view, these are regular Threejs elements expressed in JSX
|
|
||||||
const view = (
|
|
||||||
<mesh
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
onClick={(event) => click(!clicked)}
|
|
||||||
onPointerOver={(event) => hover(true)}
|
|
||||||
onPointerOut={(event) => hover(false)}
|
|
||||||
>
|
|
||||||
{/* <boxGeometry args={[1, 1, 1]} /> */}
|
|
||||||
{/* <edgesGeometry args={[box]} /> */}
|
|
||||||
<lineSegments
|
|
||||||
args={[edges, new LineBasicMaterial({ color: "darkgray" })]}
|
|
||||||
/>
|
|
||||||
<meshStandardMaterial color={hovered ? "hotpink" : "orange"} />
|
|
||||||
</mesh>
|
|
||||||
);
|
|
||||||
|
|
||||||
return [view, label];
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [labelContainerEl, setLabelContainerEl] =
|
|
||||||
useState<HTMLDivElement | null>(null);
|
|
||||||
const labelContainerRef = useCallback((el) => {
|
|
||||||
if (el) setLabelContainerEl(el);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
let coords: [number, number, number][] = [
|
let coords: [number, number, number][] = [
|
||||||
[0, 0, 0],
|
[0, 0, 0],
|
||||||
[0, 0, 1],
|
[0, 0, 1],
|
||||||
|
@ -84,7 +36,8 @@ function App() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.container}>
|
||||||
|
<SettingsBox />
|
||||||
<Canvas className={styles.canvas}>
|
<Canvas className={styles.canvas}>
|
||||||
<OrbitControls />
|
<OrbitControls />
|
||||||
<ambientLight intensity={Math.PI / 2} />
|
<ambientLight intensity={Math.PI / 2} />
|
||||||
|
@ -105,7 +58,16 @@ function App() {
|
||||||
<Path key={JSON.stringify([start, end])} start={start} end={end} />
|
<Path key={JSON.stringify([start, end])} start={start} end={end} />
|
||||||
))}
|
))}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</>
|
<div className={styles.footer}>
|
||||||
|
<a
|
||||||
|
href="https://git.sr.ht/~mzhang/cubeviz"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
Source
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
src/SettingsContext.tsx
Normal file
20
src/SettingsContext.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import styles from "./styles.module.scss";
|
||||||
|
|
||||||
|
export const showEmptyAtom = atom(true);
|
||||||
|
|
||||||
|
export function SettingsBox() {
|
||||||
|
const [showEmpty, setShowEmpty] = useAtom(showEmptyAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.header}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="showEmptyCheckbox"
|
||||||
|
checked={showEmpty}
|
||||||
|
onChange={() => setShowEmpty((v) => !v)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="showEmptyCheckbox">Show empty?</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,20 +1,23 @@
|
||||||
import { useCallback, useState } from "react";
|
import { type FormEvent, useCallback, useState } from "react";
|
||||||
import { InlineMath } from "react-katex";
|
import { BlockMath } from "react-katex";
|
||||||
|
|
||||||
import styles from "./EditBox.module.scss";
|
import styles from "./EditBox.module.scss";
|
||||||
|
import { showEmptyAtom } from "../SettingsContext";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
|
||||||
export interface EditBoxProps {}
|
// export interface EditBoxProps {}
|
||||||
|
|
||||||
export default function EditBox({}: EditBoxProps) {
|
export default function EditBox() {
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const showEmpty = useAtomValue(showEmptyAtom);
|
||||||
|
|
||||||
const handleDblClick = useCallback(() => {
|
const handleDblClick = useCallback(() => {
|
||||||
if (!isEditing) setIsEditing(true);
|
if (!isEditing) setIsEditing(true);
|
||||||
}, [isEditing]);
|
}, [isEditing]);
|
||||||
|
|
||||||
const done = useCallback(
|
const done = useCallback(
|
||||||
(evt) => {
|
(evt: FormEvent) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
if (isEditing) setIsEditing(false);
|
if (isEditing) setIsEditing(false);
|
||||||
},
|
},
|
||||||
|
@ -26,15 +29,18 @@ export default function EditBox({}: EditBoxProps) {
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<form onSubmit={done}>
|
<form onSubmit={done}>
|
||||||
<input
|
<input
|
||||||
|
// biome-ignore lint/a11y/noAutofocus: <explanation>
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
onBlur={done}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(evt) => setValue(evt.target.value)}
|
onChange={(evt) => setValue(evt.target.value)}
|
||||||
|
placeholder="Type latex code..."
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
) : value === "" ? (
|
) : value === "" ? (
|
||||||
<span className={styles.empty}>(empty)</span>
|
<span className={styles.empty}>{showEmpty ? <>(empty)</> : ""}</span>
|
||||||
) : (
|
) : (
|
||||||
<InlineMath math={value} />
|
<BlockMath math={value} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { extend, useFrame } from "@react-three/fiber";
|
import { extend } from "@react-three/fiber";
|
||||||
import { Html, Line } from "@react-three/drei";
|
import { Html, Line } from "@react-three/drei";
|
||||||
import { MeshLineGeometry, MeshLineMaterial, raycast } from "meshline";
|
import { MeshLineGeometry, MeshLineMaterial } from "meshline";
|
||||||
import EditBox from "./EditBox";
|
import EditBox from "./EditBox";
|
||||||
|
|
||||||
extend({ MeshLineGeometry, MeshLineMaterial });
|
extend({ MeshLineGeometry, MeshLineMaterial });
|
||||||
|
|
|
@ -1,11 +1,28 @@
|
||||||
.canvas {
|
.container {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.labels {
|
.canvas {
|
||||||
position: absolute;
|
grid-area: 1 / 1;
|
||||||
left: 0;
|
position: relative;
|
||||||
top: 0;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
place-self: start center;
|
||||||
|
z-index: 50;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
place-self: end end;
|
||||||
|
z-index: 50;
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react'
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: "/cubeviz",
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
})
|
});
|
||||||
|
|
Loading…
Reference in a new issue