get feature highlighting working both ways
From the editor to the viewer and from the viewer to the editor
This commit is contained in:
@ -20,7 +20,8 @@
|
|||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"three": "^0.146.0",
|
"three": "^0.146.0",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^4.4.2",
|
||||||
"web-vitals": "^2.1.0"
|
"web-vitals": "^2.1.0",
|
||||||
|
"zustand": "^4.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
84
src/App.tsx
84
src/App.tsx
@ -1,16 +1,20 @@
|
|||||||
import React, { useRef, useState, useEffect } from "react";
|
import { useRef, useState, useEffect } from "react";
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { Allotment } from "allotment";
|
import { Allotment } from "allotment";
|
||||||
import { OrbitControls, OrthographicCamera } from "@react-three/drei";
|
import { OrbitControls, OrthographicCamera } from "@react-three/drei";
|
||||||
// import "allotment/dist/style.css";
|
|
||||||
import { lexer } from "./lang/tokeniser";
|
import { lexer } from "./lang/tokeniser";
|
||||||
import { abstractSyntaxTree } from "./lang/abstractSyntaxTree";
|
import { abstractSyntaxTree } from "./lang/abstractSyntaxTree";
|
||||||
import { executor } from "./lang/executor";
|
import { executor } from "./lang/executor";
|
||||||
import { BufferGeometry } from "three";
|
import { BufferGeometry } from "three";
|
||||||
import CodeMirror from '@uiw/react-codemirror';
|
import CodeMirror from "@uiw/react-codemirror";
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from "@codemirror/lang-javascript";
|
||||||
import { ViewUpdate } from '@codemirror/view'
|
import { ViewUpdate } from "@codemirror/view";
|
||||||
// import { Box } from "./lang/engine";
|
import {
|
||||||
|
lineHighlightField,
|
||||||
|
addLineHighlight,
|
||||||
|
} from "./editor/highlightextension";
|
||||||
|
import { useStore } from "./useStore";
|
||||||
|
import { isOverlapping } from "./lib/utils";
|
||||||
|
|
||||||
const _code = `sketch mySketch {
|
const _code = `sketch mySketch {
|
||||||
path myPath = lineTo(0,1)
|
path myPath = lineTo(0,1)
|
||||||
@ -25,10 +29,31 @@ const OrrthographicCamera = OrthographicCamera as any;
|
|||||||
function App() {
|
function App() {
|
||||||
const cam = useRef();
|
const cam = useRef();
|
||||||
const [code, setCode] = useState(_code);
|
const [code, setCode] = useState(_code);
|
||||||
const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
const { editorView, setEditorView, setSelectionRange, selectionRange } =
|
||||||
setCode(value)
|
useStore(
|
||||||
console.log('value:', value, viewUpdate);
|
({ editorView, setEditorView, setSelectionRange, selectionRange }) => ({
|
||||||
}, []);
|
editorView,
|
||||||
|
setEditorView,
|
||||||
|
setSelectionRange,
|
||||||
|
selectionRange,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||||
|
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||||
|
setCode(value);
|
||||||
|
if (editorView) {
|
||||||
|
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) });
|
||||||
|
}
|
||||||
|
}; //, []);
|
||||||
|
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||||
|
if (!editorView) {
|
||||||
|
setEditorView(viewUpdate.view);
|
||||||
|
}
|
||||||
|
const range = viewUpdate.state.selection.ranges[0];
|
||||||
|
const isNoChange = range.from === selectionRange[0] && range.to === selectionRange[1]
|
||||||
|
if (isNoChange) return
|
||||||
|
setSelectionRange([range.from, range.to]);
|
||||||
|
};
|
||||||
const [geoArray, setGeoArray] = useState<
|
const [geoArray, setGeoArray] = useState<
|
||||||
{ geo: BufferGeometry; sourceRange: [number, number] }[]
|
{ geo: BufferGeometry; sourceRange: [number, number] }[]
|
||||||
>([]);
|
>([]);
|
||||||
@ -50,7 +75,6 @@ function App() {
|
|||||||
)
|
)
|
||||||
.filter((a: any) => !!a.geo);
|
.filter((a: any) => !!a.geo);
|
||||||
setGeoArray(geos);
|
setGeoArray(geos);
|
||||||
console.log("length", geos.length, geos);
|
|
||||||
console.log(programMemory);
|
console.log(programMemory);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
@ -63,12 +87,15 @@ function App() {
|
|||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={_code}
|
value={_code}
|
||||||
height="200px"
|
height="200px"
|
||||||
extensions={[javascript({ jsx: true })]}
|
extensions={[javascript({ jsx: true }), lineHighlightField]}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
viewer
|
viewer
|
||||||
|
<div className="border h-full border-gray-300">
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
enableDamping={false}
|
enableDamping={false}
|
||||||
@ -93,11 +120,13 @@ function App() {
|
|||||||
sourceRange,
|
sourceRange,
|
||||||
}: { geo: BufferGeometry; sourceRange: [number, number] },
|
}: { geo: BufferGeometry; sourceRange: [number, number] },
|
||||||
index
|
index
|
||||||
) => <Line key={index} geo={geo} sourceRange={sourceRange} />
|
) => (
|
||||||
|
<Line key={index} geo={geo} sourceRange={sourceRange} />
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Allotment>
|
</Allotment>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -112,20 +141,37 @@ function Line({
|
|||||||
geo: BufferGeometry;
|
geo: BufferGeometry;
|
||||||
sourceRange: [number, number];
|
sourceRange: [number, number];
|
||||||
}) {
|
}) {
|
||||||
|
const { setHighlightRange, selectionRange } = useStore(
|
||||||
|
({ setHighlightRange, selectionRange }) => ({
|
||||||
|
setHighlightRange,
|
||||||
|
selectionRange,
|
||||||
|
})
|
||||||
|
);
|
||||||
// This reference will give us direct access to the mesh
|
// This reference will give us direct access to the mesh
|
||||||
// const ref = useRef<Mesh<BufferGeometry | Material | Material[]> | undefined>();
|
|
||||||
const ref = useRef<BufferGeometry | undefined>() as any;
|
const ref = useRef<BufferGeometry | undefined>() as any;
|
||||||
// Set up state for the hovered and active state
|
|
||||||
const [hovered, setHover] = useState(false);
|
const [hovered, setHover] = useState(false);
|
||||||
|
const [editorCursor, setEditorCursor] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldHighlight = isOverlapping(sourceRange, selectionRange);
|
||||||
|
setEditorCursor(shouldHighlight);
|
||||||
|
}, [selectionRange, sourceRange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onPointerOver={(event) => setHover(true)}
|
onPointerOver={(event) => {
|
||||||
onPointerOut={(event) => setHover(false)}
|
setHover(true);
|
||||||
|
setHighlightRange(sourceRange);
|
||||||
|
}}
|
||||||
|
onPointerOut={(event) => {
|
||||||
|
setHover(false);
|
||||||
|
setHighlightRange([0, 0]);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<primitive object={geo} />
|
<primitive object={geo} />
|
||||||
<meshStandardMaterial color={hovered ? "hotpink" : "orange"} />
|
<meshStandardMaterial
|
||||||
|
color={hovered ? "hotpink" : editorCursor ? "skyblue" : "orange"}
|
||||||
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
30
src/editor/highlightextension.ts
Normal file
30
src/editor/highlightextension.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { StateField, StateEffect } from "@codemirror/state";
|
||||||
|
import { EditorView, Decoration } from "@codemirror/view";
|
||||||
|
|
||||||
|
export { EditorView }
|
||||||
|
|
||||||
|
export const addLineHighlight = StateEffect.define<[number, number]>();
|
||||||
|
|
||||||
|
export const lineHighlightField = StateField.define({
|
||||||
|
create() {
|
||||||
|
return Decoration.none;
|
||||||
|
},
|
||||||
|
update(lines, tr) {
|
||||||
|
lines = lines.map(tr.changes);
|
||||||
|
const deco = []
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(addLineHighlight)) {
|
||||||
|
lines = Decoration.none;
|
||||||
|
const [from, to] = e.value
|
||||||
|
if (!(from === to && from === 0)) {
|
||||||
|
lines = lines.update({ add: [matchDeco.range(from, to)] });
|
||||||
|
deco.push(matchDeco.range(from, to))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
|
});
|
||||||
|
|
||||||
|
const matchDeco = Decoration.mark({class: "bg-yellow-200"})
|
19
src/lib/utils.test.ts
Normal file
19
src/lib/utils.test.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { isOverlapping } from "./utils";
|
||||||
|
import { Range } from "../useStore";
|
||||||
|
|
||||||
|
describe("testing isOverlapping", () => {
|
||||||
|
testBothOrders([0, 5], [3, 10]);
|
||||||
|
testBothOrders([0, 5], [3, 4]);
|
||||||
|
testBothOrders([0, 5], [5, 10]);
|
||||||
|
testBothOrders([0, 5], [6, 10], false);
|
||||||
|
testBothOrders([0, 5], [-1, 1]);
|
||||||
|
testBothOrders([0, 5], [-1, 0]);
|
||||||
|
testBothOrders([0, 5], [-2, -1], false);
|
||||||
|
});
|
||||||
|
|
||||||
|
function testBothOrders(a: Range, b: Range, result = true) {
|
||||||
|
it(`test is overlapping ${a} ${b}`, () => {
|
||||||
|
expect(isOverlapping(a, b)).toBe(result);
|
||||||
|
expect(isOverlapping(b, a)).toBe(result);
|
||||||
|
});
|
||||||
|
}
|
8
src/lib/utils.ts
Normal file
8
src/lib/utils.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
import { Range } from '../useStore'
|
||||||
|
|
||||||
|
export const isOverlapping = (a: Range, b: Range) => {
|
||||||
|
const startingRange = a[0] < b[0] ? a : b
|
||||||
|
const secondRange = a[0] < b[0] ? b : a
|
||||||
|
return startingRange[1] >= secondRange[0]
|
||||||
|
}
|
32
src/useStore.ts
Normal file
32
src/useStore.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import create from 'zustand'
|
||||||
|
import {addLineHighlight, EditorView} from './editor/highlightextension'
|
||||||
|
|
||||||
|
export type Range = [number, number]
|
||||||
|
|
||||||
|
interface StoreState {
|
||||||
|
editorView: EditorView | null,
|
||||||
|
setEditorView: (editorView: EditorView) => void,
|
||||||
|
highlightRange: [number, number],
|
||||||
|
setHighlightRange: (range: Range) => void,
|
||||||
|
selectionRange: [number, number],
|
||||||
|
setSelectionRange: (range: Range) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStore = create<StoreState>()((set, get) => ({
|
||||||
|
editorView: null,
|
||||||
|
setEditorView: (editorView) => {
|
||||||
|
set({editorView})
|
||||||
|
},
|
||||||
|
highlightRange: [0, 0],
|
||||||
|
setHighlightRange: (highlightRange) => {
|
||||||
|
set({ highlightRange })
|
||||||
|
const editorView = get().editorView
|
||||||
|
if (editorView) {
|
||||||
|
editorView.dispatch({ effects: addLineHighlight.of(highlightRange) });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectionRange: [0, 0],
|
||||||
|
setSelectionRange: (selectionRange) => {
|
||||||
|
set({ selectionRange })
|
||||||
|
},
|
||||||
|
}))
|
12
yarn.lock
12
yarn.lock
@ -9945,6 +9945,11 @@ use-resize-observer@^9.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@juggle/resize-observer" "^3.3.1"
|
"@juggle/resize-observer" "^3.3.1"
|
||||||
|
|
||||||
|
use-sync-external-store@1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
|
||||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
@ -10552,3 +10557,10 @@ zustand@^3.5.13, zustand@^3.7.1:
|
|||||||
version "3.7.2"
|
version "3.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d"
|
resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d"
|
||||||
integrity sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==
|
integrity sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==
|
||||||
|
|
||||||
|
zustand@^4.1.4:
|
||||||
|
version "4.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.1.4.tgz#b0286da4cc9edd35e91c96414fa54bfa4652a54d"
|
||||||
|
integrity sha512-k2jVOlWo8p4R83mQ+/uyB8ILPO2PCJOf+QVjcL+1PbMCk1w5OoPYpAIxy9zd93FSfmJqoH6lGdwzzjwqJIRU5A==
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store "1.2.0"
|
||||||
|
Reference in New Issue
Block a user