Compare commits

..

1 commit

Author SHA1 Message Date
a81c1ae89f experimenting with ortho 2024-10-02 20:56:14 -05:00
9 changed files with 104 additions and 156 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -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"
> >

View file

@ -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"

View file

@ -1,5 +1,6 @@
.editBox { .editBox {
padding: 4px; padding: 4px;
transform: translateX(-50%) translateY(-50%); transform: translateX(-50%) translateY(-50%);
} }

View file

@ -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>

View file

@ -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>
</> </>
); );

View file

@ -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>
); );

View file

@ -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))];
}

View file

@ -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;