Compare commits
1 commit
main
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
a81c1ae89f |
9 changed files with 104 additions and 156 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
150
src/App.tsx
150
src/App.tsx
|
@ -1,63 +1,92 @@
|
||||||
import { Canvas, useThree } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { OrbitControls } from "@react-three/drei";
|
import {
|
||||||
|
OrbitControls,
|
||||||
|
OrthographicCamera,
|
||||||
|
type OrbitControlsChangeEvent,
|
||||||
|
} from "@react-three/drei";
|
||||||
|
|
||||||
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";
|
import { SettingsBox } from "./SettingsContext";
|
||||||
import { useCallback } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { atom, useAtom, useSetAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { defaultValues, parseCoord, parsePath } from "./lib/points";
|
import { Euler, type Matrix4 } from "three";
|
||||||
|
|
||||||
// https://threejs.org/manual/#en/align-html-elements-to-3d
|
// https://threejs.org/manual/#en/align-html-elements-to-3d
|
||||||
|
|
||||||
function getInitialValue() {
|
|
||||||
try {
|
|
||||||
const h = location.hash;
|
|
||||||
if (h.length === 0) return defaultValues;
|
|
||||||
return JSON.parse(atob(h.replace("#", "")));
|
|
||||||
} catch (e) {
|
|
||||||
console.log("error", e);
|
|
||||||
return defaultValues;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const stateAtom = atom<{ [_: string]: string }>(getInitialValue());
|
|
||||||
|
|
||||||
export const updateStateAtom = atom(null, (_, set, newValue) => {
|
|
||||||
// @ts-ignore
|
|
||||||
set(stateAtom, newValue);
|
|
||||||
location.hash = btoa(JSON.stringify(newValue));
|
|
||||||
});
|
|
||||||
|
|
||||||
function AdjustCamera() {
|
|
||||||
useThree(({ camera }) => {
|
|
||||||
camera.position.z = 2;
|
|
||||||
});
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [state] = useAtom(stateAtom);
|
const [rotationMatrix, setRotationMatrix] = useState<Matrix4 | undefined>(
|
||||||
const updateStateFunc = useSetAtom(updateStateAtom);
|
undefined,
|
||||||
|
|
||||||
console.log("state", state);
|
|
||||||
|
|
||||||
const updateState = useCallback(
|
|
||||||
(z: string, value: string) => {
|
|
||||||
const newState = { ...state, [z]: value };
|
|
||||||
updateStateFunc(newState);
|
|
||||||
},
|
|
||||||
[state]
|
|
||||||
);
|
);
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
const orbitChange = useCallback((evt?: OrbitControlsChangeEvent) => {
|
||||||
|
// if (evt) setRotation(evt.target.object.rotation);
|
||||||
|
console.log(evt?.target.object.toJSON());
|
||||||
|
setRotationMatrix(evt?.target.object.matrix);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<SettingsBox />
|
<SettingsBox />
|
||||||
|
<div className={styles.coordsBox}>
|
||||||
|
<Canvas>
|
||||||
|
<OrthographicCamera
|
||||||
|
makeDefault
|
||||||
|
zoom={200}
|
||||||
|
top={200}
|
||||||
|
bottom={-200}
|
||||||
|
left={-200}
|
||||||
|
right={200}
|
||||||
|
near={-1.5}
|
||||||
|
far={1.5}
|
||||||
|
matrix={rotationMatrix}
|
||||||
|
position={[200, 0, 0]}
|
||||||
|
>
|
||||||
|
<axesHelper />
|
||||||
|
</OrthographicCamera>
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
<Canvas className={styles.canvas}>
|
<Canvas className={styles.canvas}>
|
||||||
<AdjustCamera />
|
<OrthographicCamera
|
||||||
<OrbitControls />
|
makeDefault
|
||||||
|
zoom={200}
|
||||||
|
top={200}
|
||||||
|
bottom={-200}
|
||||||
|
left={-200}
|
||||||
|
right={200}
|
||||||
|
near={-1.5}
|
||||||
|
far={1.5}
|
||||||
|
matrix={rotationMatrix}
|
||||||
|
position={[200, 0, 0]}
|
||||||
|
/>
|
||||||
|
<OrbitControls onChange={orbitChange} makeDefault />
|
||||||
|
|
||||||
<ambientLight intensity={Math.PI / 2} />
|
<ambientLight intensity={Math.PI / 2} />
|
||||||
<spotLight
|
<spotLight
|
||||||
position={[10, 10, 10]}
|
position={[10, 10, 10]}
|
||||||
|
@ -68,34 +97,17 @@ function App() {
|
||||||
/>
|
/>
|
||||||
<pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
|
<pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
|
||||||
|
|
||||||
{Object.entries(state).map(([thing, value]) => {
|
{coords.map((coord) => (
|
||||||
if (thing.length === 6) {
|
<Point key={JSON.stringify(coord)} coord={coord} />
|
||||||
const [start, end] = parsePath(thing);
|
))}
|
||||||
return (
|
|
||||||
<Path
|
{paths.map(([start, end]) => (
|
||||||
key={thing}
|
<Path key={JSON.stringify([start, end])} start={start} end={end} />
|
||||||
start={start}
|
))}
|
||||||
end={end}
|
|
||||||
onEdit={(newValue) => updateState(thing, newValue)}
|
|
||||||
value={value}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (thing.length === 3) {
|
|
||||||
const coord = parseCoord(thing);
|
|
||||||
return (
|
|
||||||
<Point
|
|
||||||
key={thing}
|
|
||||||
coord={coord}
|
|
||||||
onEdit={(newValue) => updateState(thing, newValue)}
|
|
||||||
value={value}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</Canvas>
|
</Canvas>
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
<a
|
<a
|
||||||
href="https://git.mzhang.io/michael/cubeviz"
|
href="https://git.sr.ht/~mzhang/cubeviz"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,25 +1,13 @@
|
||||||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import styles from "./styles.module.scss";
|
import styles from "./styles.module.scss";
|
||||||
import { stateAtom, updateStateAtom } from "./App";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
|
|
||||||
export const showEmptyAtom = atom(true);
|
export const showEmptyAtom = atom(true);
|
||||||
|
|
||||||
export function SettingsBox() {
|
export function SettingsBox() {
|
||||||
const state = useAtomValue(stateAtom);
|
|
||||||
const updateState = useSetAtom(updateStateAtom);
|
|
||||||
const [showEmpty, setShowEmpty] = useAtom(showEmptyAtom);
|
const [showEmpty, setShowEmpty] = useAtom(showEmptyAtom);
|
||||||
|
|
||||||
const doClear = useCallback(() => {
|
|
||||||
updateState(
|
|
||||||
Object.fromEntries(Object.entries(state).map(([a, _]) => [a, ""]))
|
|
||||||
);
|
|
||||||
}, [state]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<button onClick={doClear}>Clear</button>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="showEmptyCheckbox"
|
id="showEmptyCheckbox"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
.editBox {
|
.editBox {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
|
||||||
transform: translateX(-50%) translateY(-50%);
|
transform: translateX(-50%) translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,27 @@
|
||||||
import { type FormEvent, useCallback, useEffect, useState } from "react";
|
import { type FormEvent, useCallback, useState } from "react";
|
||||||
import { BlockMath } 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 { showEmptyAtom } from "../SettingsContext";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
|
|
||||||
export interface EditBoxProps {
|
// export interface EditBoxProps {}
|
||||||
value: string;
|
|
||||||
onUpdate?: (newValue: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EditBox({ onUpdate, value }: EditBoxProps) {
|
export default function EditBox() {
|
||||||
const [innerValue, setInnerValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const showEmpty = useAtomValue(showEmptyAtom);
|
const showEmpty = useAtomValue(showEmptyAtom);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setInnerValue(value);
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
const handleDblClick = useCallback(() => {
|
const handleDblClick = useCallback(() => {
|
||||||
if (!isEditing) setIsEditing(true);
|
if (!isEditing) setIsEditing(true);
|
||||||
}, [isEditing]);
|
}, [isEditing]);
|
||||||
|
|
||||||
const done = useCallback(
|
const done = useCallback(
|
||||||
(evt: FormEvent) => {
|
(evt: FormEvent) => {
|
||||||
onUpdate?.(innerValue);
|
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
if (isEditing) setIsEditing(false);
|
if (isEditing) setIsEditing(false);
|
||||||
},
|
},
|
||||||
[value, isEditing, innerValue]
|
[isEditing],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -40,8 +32,8 @@ export default function EditBox({ onUpdate, value }: EditBoxProps) {
|
||||||
// biome-ignore lint/a11y/noAutofocus: <explanation>
|
// biome-ignore lint/a11y/noAutofocus: <explanation>
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
onBlur={done}
|
onBlur={done}
|
||||||
value={innerValue}
|
value={value}
|
||||||
onChange={(evt) => setInnerValue(evt.target.value)}
|
onChange={(evt) => setValue(evt.target.value)}
|
||||||
placeholder="Type latex code..."
|
placeholder="Type latex code..."
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -8,11 +8,9 @@ extend({ MeshLineGeometry, MeshLineMaterial });
|
||||||
export interface PointProps {
|
export interface PointProps {
|
||||||
start: [number, number, number];
|
start: [number, number, number];
|
||||||
end: [number, number, number];
|
end: [number, number, number];
|
||||||
value: string;
|
|
||||||
onEdit?: (newValue: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Path({ start, end, onEdit, value }: PointProps) {
|
export default function Path({ start, end }: PointProps) {
|
||||||
const midpoint: [number, number, number] = [
|
const midpoint: [number, number, number] = [
|
||||||
(start[0] + end[0]) / 2.0,
|
(start[0] + end[0]) / 2.0,
|
||||||
(start[1] + end[1]) / 2.0,
|
(start[1] + end[1]) / 2.0,
|
||||||
|
@ -25,7 +23,7 @@ export default function Path({ start, end, onEdit, value }: PointProps) {
|
||||||
<meshBasicMaterial />
|
<meshBasicMaterial />
|
||||||
</Line>
|
</Line>
|
||||||
<Html position={midpoint}>
|
<Html position={midpoint}>
|
||||||
<EditBox value={value} onUpdate={onEdit} />
|
<EditBox />
|
||||||
</Html>
|
</Html>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,18 +3,16 @@ import EditBox from "./EditBox";
|
||||||
|
|
||||||
export interface PointProps {
|
export interface PointProps {
|
||||||
coord: [number, number, number];
|
coord: [number, number, number];
|
||||||
value: string;
|
|
||||||
onEdit?: (newValue: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Point({ coord, onEdit, value }: PointProps) {
|
export default function Point({ coord }: PointProps) {
|
||||||
return (
|
return (
|
||||||
<mesh position={coord}>
|
<mesh position={coord}>
|
||||||
<sphereGeometry args={[0.05]} />
|
<sphereGeometry args={[0.05]} />
|
||||||
<meshStandardMaterial color="gray" />
|
<meshStandardMaterial color="gray" />
|
||||||
|
|
||||||
<Html>
|
<Html>
|
||||||
<EditBox value={value} onUpdate={onEdit} />
|
<EditBox />
|
||||||
</Html>
|
</Html>
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
export type coord = [number, number, number];
|
|
||||||
const coords: coord[] = [
|
|
||||||
[0, 0, 0],
|
|
||||||
[0, 0, 1],
|
|
||||||
[0, 1, 0],
|
|
||||||
[0, 1, 1],
|
|
||||||
[1, 0, 0],
|
|
||||||
[1, 0, 1],
|
|
||||||
[1, 1, 0],
|
|
||||||
[1, 1, 1],
|
|
||||||
];
|
|
||||||
|
|
||||||
const ppCoord = (c: coord): string => c.map((n) => n.toString()).join("");
|
|
||||||
|
|
||||||
const offset = (a: coord): coord => [a[0] - 0.5, a[1] - 0.5, a[2] - 0.5];
|
|
||||||
|
|
||||||
const offsetCoords: [string, coord][] = coords.map((a) => [
|
|
||||||
ppCoord(a),
|
|
||||||
offset(a),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const paths: [string, [coord, coord]][] = offsetCoords
|
|
||||||
.flatMap((a) => offsetCoords.map((b) => [a, b]))
|
|
||||||
.filter(
|
|
||||||
([[_a, [a1, a2, a3]], [_b, [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
|
|
||||||
)
|
|
||||||
.map(([[aname, acoord], [bname, bcoord]]) => [
|
|
||||||
aname + bname,
|
|
||||||
[acoord, bcoord],
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const defaultValues: [string, string] = Object.fromEntries([
|
|
||||||
...offsetCoords.map(([a]) => [a, ""]),
|
|
||||||
...paths.map(([a]) => [a, ""]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function parseCoord(s: string): coord {
|
|
||||||
// @ts-ignore
|
|
||||||
const t: [number, number, number] = s.split("").map((n) => parseInt(n));
|
|
||||||
return offset(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parsePath(s: string): [coord, coord] {
|
|
||||||
return [parseCoord(s.substring(0, 3)), parseCoord(s.substring(3, 6))];
|
|
||||||
}
|
|
|
@ -6,6 +6,16 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.coordsBox {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
place-self: start end;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border: 1px solid gray;
|
||||||
|
z-index: 60;
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
.canvas {
|
.canvas {
|
||||||
grid-area: 1 / 1;
|
grid-area: 1 / 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
Loading…
Reference in a new issue