feat: initial design of parameter ui
This commit is contained in:
parent
154c9a8383
commit
71ebbfc348
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@ -7,6 +7,8 @@ yarn-error.log*
|
|||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
|
.svelte-kit/
|
||||||
|
|
||||||
vite.config.ts.timestamp*
|
vite.config.ts.timestamp*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
8
frontend/histoire.config.ts
Normal file
8
frontend/histoire.config.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'histoire'
|
||||||
|
import { HstSvelte } from '@histoire/plugin-svelte'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
HstSvelte(),
|
||||||
|
],
|
||||||
|
})
|
@ -7,8 +7,10 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
"tauri:dev": "tauri dev",
|
||||||
"tauri": "tauri"
|
"story:dev": "histoire dev",
|
||||||
|
"story:build": "histoire build",
|
||||||
|
"story:preview": "histoire preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/kit": "^2.5.0",
|
"@sveltejs/kit": "^2.5.0",
|
||||||
@ -21,10 +23,12 @@
|
|||||||
"three": "^0.159.0"
|
"three": "^0.159.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@histoire/plugin-svelte": "^0.17.9",
|
||||||
"@sveltejs/adapter-static": "^3.0.1",
|
"@sveltejs/adapter-static": "^3.0.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
||||||
"@tauri-apps/cli": "2.0.0-beta.3",
|
"@tauri-apps/cli": "2.0.0-beta.3",
|
||||||
"@tsconfig/svelte": "^5.0.2",
|
"@tsconfig/svelte": "^5.0.2",
|
||||||
|
"histoire": "^0.17.9",
|
||||||
"internal-ip": "^7.0.0",
|
"internal-ip": "^7.0.0",
|
||||||
"svelte": "^4.2.8",
|
"svelte": "^4.2.8",
|
||||||
"svelte-check": "^3.4.6",
|
"svelte-check": "^3.4.6",
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Canvas } from "@threlte/core";
|
|
||||||
import Scene from "./Scene.svelte";
|
|
||||||
import type { Graph } from "$lib/types";
|
|
||||||
|
|
||||||
const graph: Graph = {
|
|
||||||
edges: [],
|
|
||||||
nodes: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < 40; i++) {
|
|
||||||
const x = i % 20;
|
|
||||||
const y = Math.floor(i / 20);
|
|
||||||
|
|
||||||
graph.nodes.push({
|
|
||||||
id: `${i.toString()}`,
|
|
||||||
tmp: {
|
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
position: {
|
|
||||||
x: x * 7.5,
|
|
||||||
y: y * 5,
|
|
||||||
},
|
|
||||||
type: "test",
|
|
||||||
});
|
|
||||||
|
|
||||||
graph.edges.push({
|
|
||||||
from: i.toString(),
|
|
||||||
to: (i + 1).toString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Canvas shadows={false}>
|
|
||||||
<Scene {graph} />
|
|
||||||
</Canvas>
|
|
28
frontend/src/lib/components/Background.story.svelte
Normal file
28
frontend/src/lib/components/Background.story.svelte
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Hst } from "@histoire/plugin-svelte";
|
||||||
|
export let Hst: Hst;
|
||||||
|
import Background from "./Background.svelte";
|
||||||
|
import { Canvas } from "@threlte/core";
|
||||||
|
import Camera from "./Camera.svelte";
|
||||||
|
let width = globalThis.innerWidth || 100;
|
||||||
|
let height = globalThis.innerHeight || 100;
|
||||||
|
|
||||||
|
let cameraPosition: [number, number, number] = [0, 1, 0];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
|
||||||
|
|
||||||
|
<Hst.Story>
|
||||||
|
<Canvas shadows={false}>
|
||||||
|
<Camera bind:position={cameraPosition} />
|
||||||
|
|
||||||
|
<Background
|
||||||
|
cx={cameraPosition[0]}
|
||||||
|
cy={cameraPosition[1]}
|
||||||
|
cz={cameraPosition[2]}
|
||||||
|
{width}
|
||||||
|
{height}
|
||||||
|
/>
|
||||||
|
</Canvas>
|
||||||
|
</Hst.Story>
|
||||||
|
|
64
frontend/src/lib/components/Background.svelte
Normal file
64
frontend/src/lib/components/Background.svelte
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { T } from "@threlte/core";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
import BackgroundVert from "./Background.vert";
|
||||||
|
import BackgroundFrag from "./Background.frag";
|
||||||
|
|
||||||
|
export let minZoom = 4;
|
||||||
|
export let maxZoom = 150;
|
||||||
|
|
||||||
|
export let cx = 0;
|
||||||
|
export let cy = 0;
|
||||||
|
export let cz = 30;
|
||||||
|
|
||||||
|
export let width = globalThis?.innerWidth || 100;
|
||||||
|
export let height = globalThis?.innerHeight || 100;
|
||||||
|
|
||||||
|
let bw = 2;
|
||||||
|
let bh = 2;
|
||||||
|
|
||||||
|
$: if (width && height) {
|
||||||
|
bw = width / cz;
|
||||||
|
bh = height / cz;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<T.Group position.x={cx} position.z={cy} position.y={-1.0}>
|
||||||
|
<T.Mesh rotation.x={-Math.PI / 2} position.y={0.2} scale.x={bw} scale.y={bh}>
|
||||||
|
<T.PlaneGeometry args={[1, 1]} />
|
||||||
|
<T.ShaderMaterial
|
||||||
|
transparent
|
||||||
|
vertexShader={BackgroundVert}
|
||||||
|
fragmentShader={BackgroundFrag}
|
||||||
|
uniforms={{
|
||||||
|
cx: {
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
cy: {
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
cz: {
|
||||||
|
value: 30,
|
||||||
|
},
|
||||||
|
minz: {
|
||||||
|
value: minZoom,
|
||||||
|
},
|
||||||
|
maxz: {
|
||||||
|
value: maxZoom,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
uniforms.cx.value={cx}
|
||||||
|
uniforms.cy.value={cy}
|
||||||
|
uniforms.cz.value={cz}
|
||||||
|
uniforms.width.value={width}
|
||||||
|
uniforms.height.value={height}
|
||||||
|
/>
|
||||||
|
</T.Mesh>
|
||||||
|
</T.Group>
|
56
frontend/src/lib/components/Camera.svelte
Normal file
56
frontend/src/lib/components/Camera.svelte
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { T } from "@threlte/core";
|
||||||
|
import { OrbitControls } from "@threlte/extras";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { type OrthographicCamera } from "three";
|
||||||
|
|
||||||
|
export let camera: OrthographicCamera | undefined = undefined;
|
||||||
|
export let maxZoom = 150;
|
||||||
|
export let minZoom = 4;
|
||||||
|
|
||||||
|
let controls: OrbitControls | undefined = undefined;
|
||||||
|
|
||||||
|
export const position: [number, number, number] = [0, 1, 0];
|
||||||
|
|
||||||
|
function updateProps() {
|
||||||
|
if (!camera) return;
|
||||||
|
position[0] = camera.position.x;
|
||||||
|
position[1] = camera.position.z;
|
||||||
|
position[2] = camera.zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateProps();
|
||||||
|
controls?.addEventListener("change", updateProps);
|
||||||
|
return () => {
|
||||||
|
controls?.removeEventListener("change", updateProps);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<T.OrthographicCamera
|
||||||
|
bind:ref={camera}
|
||||||
|
makeDefault
|
||||||
|
position.y={1}
|
||||||
|
zoom={30}
|
||||||
|
on:create={({ ref, cleanup }) => {
|
||||||
|
ref.onBeforeRender = () => {
|
||||||
|
console.log(ref.position);
|
||||||
|
};
|
||||||
|
|
||||||
|
cleanup(() => {});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OrbitControls
|
||||||
|
bind:ref={controls}
|
||||||
|
enableZoom={true}
|
||||||
|
target.y={0}
|
||||||
|
rotateSpeed={0}
|
||||||
|
minPolarAngle={0}
|
||||||
|
maxPolarAngle={0}
|
||||||
|
enablePan={true}
|
||||||
|
zoomToCursor
|
||||||
|
{maxZoom}
|
||||||
|
{minZoom}
|
||||||
|
/>
|
||||||
|
</T.OrthographicCamera>
|
@ -9,8 +9,6 @@
|
|||||||
|
|
||||||
let samples = 20;
|
let samples = 20;
|
||||||
|
|
||||||
console.log("edge");
|
|
||||||
|
|
||||||
const curve = new CubicBezierCurve(
|
const curve = new CubicBezierCurve(
|
||||||
new Vector2(from.position.x + 20, from.position.y),
|
new Vector2(from.position.x + 20, from.position.y),
|
||||||
new Vector2(from.position.x + 2, from.position.y),
|
new Vector2(from.position.x + 2, from.position.y),
|
||||||
@ -33,10 +31,10 @@
|
|||||||
last_from_x = new_x;
|
last_from_x = new_x;
|
||||||
last_from_y = new_y;
|
last_from_y = new_y;
|
||||||
}
|
}
|
||||||
curve.v0.set(from.position.x + 5, from.position.y + 1.25);
|
curve.v0.set(from.position.x + 5, from.position.y + 5 / 8);
|
||||||
curve.v1.set(from.position.x + 6, from.position.y + 1.25);
|
curve.v1.set(from.position.x + 7, from.position.y + 5 / 8);
|
||||||
curve.v2.set(to.position.x - 1, to.position.y + 1.25);
|
curve.v2.set(to.position.x - 2, to.position.y + 2.55);
|
||||||
curve.v3.set(to.position.x, to.position.y + 1.25);
|
curve.v3.set(to.position.x + 0.2, to.position.y + 2.55);
|
||||||
points = curve.getPoints(samples).map((p) => new Vector3(p.x, 0, p.y));
|
points = curve.getPoints(samples).map((p) => new Vector3(p.x, 0, p.y));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
110
frontend/src/lib/components/Graph.svelte
Normal file
110
frontend/src/lib/components/Graph.svelte
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Edge from "./Edge.svelte";
|
||||||
|
|
||||||
|
import { HTML } from "@threlte/extras";
|
||||||
|
import Node from "./Node.svelte";
|
||||||
|
import type { GraphManager } from "$lib/graph-manager";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { snapToGrid } from "$lib/helpers";
|
||||||
|
|
||||||
|
export let graph: GraphManager;
|
||||||
|
|
||||||
|
let edges = graph?.getEdges() || [];
|
||||||
|
|
||||||
|
export let cameraPosition: [number, number, number] = [0, 1, 0];
|
||||||
|
|
||||||
|
export let width = globalThis?.innerWidth || 100;
|
||||||
|
export let height = globalThis?.innerHeight || 100;
|
||||||
|
|
||||||
|
let mouseX = 0;
|
||||||
|
let mouseY = 0;
|
||||||
|
|
||||||
|
let mouseDown = false;
|
||||||
|
let mouseDownX = 0;
|
||||||
|
let mouseDownY = 0;
|
||||||
|
|
||||||
|
let activeNodeId: string;
|
||||||
|
|
||||||
|
function handleMouseMove(event: MouseEvent) {
|
||||||
|
if (!mouseDown) return;
|
||||||
|
|
||||||
|
mouseX =
|
||||||
|
cameraPosition[0] + (event.clientX - width / 2) / cameraPosition[2];
|
||||||
|
mouseY =
|
||||||
|
cameraPosition[1] + (event.clientY - height / 2) / cameraPosition[2];
|
||||||
|
|
||||||
|
if (!activeNodeId) return;
|
||||||
|
|
||||||
|
const node = graph.getNode(activeNodeId);
|
||||||
|
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
let newX =
|
||||||
|
(node?.tmp?.downX || 0) +
|
||||||
|
(event.clientX - mouseDownX) / cameraPosition[2];
|
||||||
|
let newY =
|
||||||
|
(node?.tmp?.downY || 0) +
|
||||||
|
(event.clientY - mouseDownY) / cameraPosition[2];
|
||||||
|
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
newX = snapToGrid(newX, 2.5);
|
||||||
|
newY = snapToGrid(newY, 2.5);
|
||||||
|
}
|
||||||
|
node.position.x = newX;
|
||||||
|
node.position.y = newY;
|
||||||
|
node.position = node.position;
|
||||||
|
edges = [...edges];
|
||||||
|
graph.nodes = [...graph.nodes];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseDown(ev: MouseEvent) {
|
||||||
|
activeNodeId = (ev?.target as HTMLElement)?.getAttribute("data-node-id")!;
|
||||||
|
|
||||||
|
mouseDown = true;
|
||||||
|
mouseDownX = ev.clientX;
|
||||||
|
mouseDownY = ev.clientY;
|
||||||
|
const node = graph.nodes.find((node) => node.id === activeNodeId);
|
||||||
|
if (!node) return;
|
||||||
|
node.tmp = node.tmp || {};
|
||||||
|
node.tmp.downX = node.position.x;
|
||||||
|
node.tmp.downY = node.position.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp() {
|
||||||
|
mouseDown = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:mousemove={handleMouseMove} on:mouseup={handleMouseUp} />
|
||||||
|
|
||||||
|
{#each edges as edge}
|
||||||
|
<Edge from={edge[0]} to={edge[1]} />
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<HTML transform={false}>
|
||||||
|
<div
|
||||||
|
role="tree"
|
||||||
|
tabindex="0"
|
||||||
|
class="wrapper"
|
||||||
|
style={`--cz: ${cameraPosition[2]}`}
|
||||||
|
on:mousedown={handleMouseDown}
|
||||||
|
>
|
||||||
|
{#each graph.nodes as node}
|
||||||
|
<Node {node} {graph} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</HTML>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
transform: scale(calc(var(--cz) * 0.1));
|
||||||
|
}
|
||||||
|
</style>
|
46
frontend/src/lib/components/Node.svelte
Normal file
46
frontend/src/lib/components/Node.svelte
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { GraphManager } from "$lib/graph-manager";
|
||||||
|
import type { Node } from "$lib/types";
|
||||||
|
import NodeHeader from "./NodeHeader.svelte";
|
||||||
|
import NodeParameter from "./NodeParameter.svelte";
|
||||||
|
|
||||||
|
export let node: Node;
|
||||||
|
export let graph: GraphManager;
|
||||||
|
|
||||||
|
const type = graph.getNodeType(node.type);
|
||||||
|
|
||||||
|
const parameters = Object.entries(type?.inputs || {});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="node"
|
||||||
|
data-node-id={node.id}
|
||||||
|
style={`--nx:${node.position.x * 10}px;
|
||||||
|
--ny: ${node.position.y * 10}px`}
|
||||||
|
>
|
||||||
|
<NodeHeader {node} />
|
||||||
|
|
||||||
|
{#each parameters as [key, value], i}
|
||||||
|
<NodeParameter
|
||||||
|
value={node?.props?.[key]}
|
||||||
|
input={value}
|
||||||
|
label={key}
|
||||||
|
isLast={i == parameters.length - 1}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.node {
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
user-select: none !important;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 50px;
|
||||||
|
color: white;
|
||||||
|
transform: translate(var(--nx), var(--ny));
|
||||||
|
z-index: 1;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
97
frontend/src/lib/components/NodeHeader.svelte
Normal file
97
frontend/src/lib/components/NodeHeader.svelte
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Node } from "$lib/types";
|
||||||
|
|
||||||
|
export let node: Node;
|
||||||
|
|
||||||
|
function createPath({ depth = 8, height = 20, y = 50 } = {}) {
|
||||||
|
let corner = 10;
|
||||||
|
|
||||||
|
let right_bump = true;
|
||||||
|
|
||||||
|
return `M0,100
|
||||||
|
${
|
||||||
|
corner
|
||||||
|
? ` V${corner}
|
||||||
|
Q0,0 ${corner / 4},0
|
||||||
|
H${100 - corner / 4}
|
||||||
|
Q100,0 100,${corner}
|
||||||
|
`
|
||||||
|
: ` V0
|
||||||
|
H100
|
||||||
|
`
|
||||||
|
}
|
||||||
|
V${y - height / 2}
|
||||||
|
${
|
||||||
|
right_bump
|
||||||
|
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
|
||||||
|
: ` H100`
|
||||||
|
}
|
||||||
|
V100
|
||||||
|
Z`.replace(/\s+/g, " ");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper" data-node-id={node.id}>
|
||||||
|
<div class="content">
|
||||||
|
{node.type}
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
style={`
|
||||||
|
--path: path("${createPath({ depth: 3, height: 15 })}");
|
||||||
|
--hover-path: path("${createPath({ depth: 8, height: 24 })}");
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
vector-effect="non-scaling-stroke"
|
||||||
|
fill="none"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="0.1"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 12.5px;
|
||||||
|
}
|
||||||
|
.wrapper > * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: -1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% + 1px);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg path {
|
||||||
|
stroke-width: 0.2px;
|
||||||
|
transition: 0.2s;
|
||||||
|
fill: #060606;
|
||||||
|
d: var(--path);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 2px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg:hover path {
|
||||||
|
d: var(--hover-path) !important;
|
||||||
|
}
|
||||||
|
</style>
|
120
frontend/src/lib/components/NodeParameter.svelte
Normal file
120
frontend/src/lib/components/NodeParameter.svelte
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { NodeInput } from "$lib/types";
|
||||||
|
|
||||||
|
export let value: unknown;
|
||||||
|
export let input: NodeInput;
|
||||||
|
export let label: string;
|
||||||
|
|
||||||
|
export let isLast = false;
|
||||||
|
|
||||||
|
function createPath({ depth = 8, height = 20, y = 50 } = {}) {
|
||||||
|
let corner = isLast ? 2 : 0;
|
||||||
|
|
||||||
|
let right_bump = false;
|
||||||
|
let left_bump = true;
|
||||||
|
|
||||||
|
return `M0,0
|
||||||
|
H100
|
||||||
|
V${y - height / 2}
|
||||||
|
${
|
||||||
|
right_bump
|
||||||
|
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
|
||||||
|
: ` H100`
|
||||||
|
}
|
||||||
|
${
|
||||||
|
corner
|
||||||
|
? ` V${100 - corner}
|
||||||
|
Q100,100 ${100 - corner / 2},100
|
||||||
|
H${corner / 2}
|
||||||
|
Q0,100 0,${100 - corner}
|
||||||
|
`
|
||||||
|
: ` V100
|
||||||
|
H0
|
||||||
|
`
|
||||||
|
}
|
||||||
|
${
|
||||||
|
left_bump
|
||||||
|
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
|
||||||
|
: ` H0`
|
||||||
|
}
|
||||||
|
Z`.replace(/\s+/g, " ");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="content">
|
||||||
|
<label>{label}</label>
|
||||||
|
|
||||||
|
<input type="number" bind:value />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
style={`
|
||||||
|
--path: path("${createPath({ depth: 5, height: 15 })}");
|
||||||
|
--hover-path: path("${createPath({ depth: 8, height: 24 })}");
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
vector-effect="non-scaling-stroke"
|
||||||
|
fill="none"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="0.1"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
padding: 2px 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-around;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 0.5em;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
pointer-events: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg path {
|
||||||
|
stroke-width: 0.2px;
|
||||||
|
transition: 0.2s;
|
||||||
|
fill: #060606;
|
||||||
|
d: var(--path);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper:hover svg path {
|
||||||
|
d: var(--hover-path) !important;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,24 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Graph, Node } from "$lib/types";
|
|
||||||
import Edge from "./Edge.svelte";
|
|
||||||
import { T, useTask } from "@threlte/core";
|
|
||||||
import { settings } from "$lib/stores/settings";
|
|
||||||
import type { OrthographicCamera } from "three";
|
import type { OrthographicCamera } from "three";
|
||||||
import { HTML, OrbitControls } from "@threlte/extras";
|
import Camera from "./Camera.svelte";
|
||||||
import { onMount } from "svelte";
|
import Background from "./Background.svelte";
|
||||||
import BackgroundVert from "./Background.vert";
|
import type { GraphManager } from "$lib/graph-manager";
|
||||||
import BackgroundFrag from "./Background.frag";
|
import Graph from "./Graph.svelte";
|
||||||
import { max } from "three/examples/jsm/nodes/Nodes.js";
|
|
||||||
|
|
||||||
export let camera: OrthographicCamera;
|
export let graph: GraphManager;
|
||||||
|
|
||||||
export let graph: Graph;
|
const status = graph.status;
|
||||||
|
|
||||||
let cx = 0;
|
let camera: OrthographicCamera;
|
||||||
let cy = 0;
|
let cameraPosition: [number, number, number] = [0, 1, 0];
|
||||||
let cz = 30;
|
|
||||||
let bw = 2;
|
|
||||||
let bh = 2;
|
|
||||||
|
|
||||||
const minZoom = 4;
|
const minZoom = 4;
|
||||||
const maxZoom = 150;
|
const maxZoom = 150;
|
||||||
@ -26,230 +18,27 @@
|
|||||||
let width = globalThis?.innerWidth || 100;
|
let width = globalThis?.innerWidth || 100;
|
||||||
let height = globalThis?.innerHeight || 100;
|
let height = globalThis?.innerHeight || 100;
|
||||||
|
|
||||||
let mouseX = 0;
|
console.log({ graph });
|
||||||
let mouseY = 0;
|
|
||||||
|
|
||||||
let mouseDown = false;
|
|
||||||
let mouseDownX = 0;
|
|
||||||
let mouseDownY = 0;
|
|
||||||
|
|
||||||
let activeNodeId: string;
|
|
||||||
|
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
|
||||||
if (event.key === "a") {
|
|
||||||
$settings.useHtml = !$settings.useHtml;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function snapToGrid(value: number) {
|
|
||||||
return Math.round(value / 2.5) * 2.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseMove(event: MouseEvent) {
|
|
||||||
cx = camera.position.x || 0;
|
|
||||||
cy = camera.position.z || 0;
|
|
||||||
cz = camera.zoom || 30;
|
|
||||||
mouseX = cx + (event.clientX - width / 2) / cz;
|
|
||||||
mouseY = cy + (event.clientY - height / 2) / cz;
|
|
||||||
|
|
||||||
if (activeNodeId && mouseDown) {
|
|
||||||
graph.nodes = graph.nodes.map((node) => {
|
|
||||||
if (node.id === activeNodeId) {
|
|
||||||
node.position.x =
|
|
||||||
(node?.tmp?.downX || 0) + (event.clientX - mouseDownX) / cz;
|
|
||||||
node.position.y =
|
|
||||||
(node?.tmp?.downY || 0) + (event.clientY - mouseDownY) / cz;
|
|
||||||
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
node.position.x = snapToGrid(node.position.x);
|
|
||||||
node.position.y = snapToGrid(node.position.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
edges = [...edges];
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let edges: [Node, Node][] = [];
|
|
||||||
function calculateEdges() {
|
|
||||||
edges = graph.edges
|
|
||||||
.map((edge) => {
|
|
||||||
const from = graph.nodes.find((node) => node.id === edge.from);
|
|
||||||
const to = graph.nodes.find((node) => node.id === edge.to);
|
|
||||||
if (!from || !to) return;
|
|
||||||
return [from, to] as const;
|
|
||||||
})
|
|
||||||
.filter(Boolean) as unknown as [Node, Node][];
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateEdges();
|
|
||||||
|
|
||||||
function handleMouseDown(ev: MouseEvent) {
|
|
||||||
activeNodeId = ev?.target?.dataset?.nodeId;
|
|
||||||
mouseDown = true;
|
|
||||||
mouseDownX = ev.clientX;
|
|
||||||
mouseDownY = ev.clientY;
|
|
||||||
const node = graph.nodes.find((node) => node.id === activeNodeId);
|
|
||||||
if (!node) return;
|
|
||||||
node.tmp = node.tmp || {};
|
|
||||||
node.tmp.downX = node.position.x;
|
|
||||||
node.tmp.downY = node.position.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseUp() {
|
|
||||||
mouseDown = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCameraProps() {
|
|
||||||
cx = camera.position.x || 0;
|
|
||||||
cy = camera.position.z || 0;
|
|
||||||
cz = camera.zoom || 30;
|
|
||||||
width = window.innerWidth;
|
|
||||||
height = window.innerHeight;
|
|
||||||
bw = width / cz;
|
|
||||||
bh = height / cz;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
updateCameraProps();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window bind:innerHeight={height} bind:innerWidth={width} />
|
||||||
on:keydown={handleKeyDown}
|
|
||||||
on:mousemove={handleMouseMove}
|
|
||||||
on:mouseup={handleMouseUp}
|
|
||||||
on:wheel={updateCameraProps}
|
|
||||||
on:resize={updateCameraProps}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if true}
|
<Camera bind:camera {maxZoom} {minZoom} bind:position={cameraPosition} />
|
||||||
<T.Group position.x={mouseX} position.z={mouseY}>
|
|
||||||
<T.Mesh rotation.x={-Math.PI / 2} position.y={0.2}>
|
|
||||||
<T.CircleGeometry args={[0.5, 16]} />
|
|
||||||
<T.MeshBasicMaterial color="green" />
|
|
||||||
</T.Mesh>
|
|
||||||
</T.Group>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<T.Group position.x={cx} position.z={cy} position.y={-1.0}>
|
<Background
|
||||||
<T.Mesh rotation.x={-Math.PI / 2} position.y={0.2} scale.x={bw} scale.y={bh}>
|
cx={cameraPosition[0]}
|
||||||
<T.PlaneGeometry args={[1, 1]} />
|
cy={cameraPosition[1]}
|
||||||
<T.ShaderMaterial
|
cz={cameraPosition[2]}
|
||||||
transparent
|
|
||||||
vertexShader={BackgroundVert}
|
|
||||||
fragmentShader={BackgroundFrag}
|
|
||||||
uniforms={{
|
|
||||||
cx: {
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
cy: {
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
cz: {
|
|
||||||
value: 30,
|
|
||||||
},
|
|
||||||
minz: {
|
|
||||||
value: minZoom,
|
|
||||||
},
|
|
||||||
maxz: {
|
|
||||||
value: maxZoom,
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
value: 100,
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
value: 100,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
uniforms.cx.value={cx}
|
|
||||||
uniforms.cy.value={cy}
|
|
||||||
uniforms.cz.value={cz}
|
|
||||||
uniforms.width.value={width}
|
|
||||||
uniforms.height.value={height}
|
|
||||||
/>
|
|
||||||
</T.Mesh>
|
|
||||||
</T.Group>
|
|
||||||
|
|
||||||
<T.OrthographicCamera
|
|
||||||
bind:ref={camera}
|
|
||||||
makeDefault
|
|
||||||
position={[0, 1, 0]}
|
|
||||||
zoom={30}
|
|
||||||
>
|
|
||||||
<OrbitControls
|
|
||||||
enableZoom={true}
|
|
||||||
target.y={0}
|
|
||||||
rotateSpeed={0}
|
|
||||||
minPolarAngle={0}
|
|
||||||
maxPolarAngle={0}
|
|
||||||
enablePan={true}
|
|
||||||
zoomToCursor
|
|
||||||
{maxZoom}
|
{maxZoom}
|
||||||
{minZoom}
|
{minZoom}
|
||||||
|
{width}
|
||||||
|
{height}
|
||||||
/>
|
/>
|
||||||
</T.OrthographicCamera>
|
|
||||||
|
|
||||||
{#each edges as edge}
|
{#if $status === "idle"}
|
||||||
<Edge from={edge[0]} to={edge[1]} />
|
<Graph {graph} {cameraPosition} />
|
||||||
{/each}
|
{:else if $status === "loading"}
|
||||||
|
<a href="/graph">Loading...</a>
|
||||||
<HTML transform={false}>
|
{:else if $status === "error"}
|
||||||
<div class="wrapper" style={`--cz: ${cz}`} on:mousedown={handleMouseDown}>
|
<a href="/graph">Error</a>
|
||||||
{#each graph.nodes as node}
|
{/if}
|
||||||
<div
|
|
||||||
class="node"
|
|
||||||
data-node-id={node.id}
|
|
||||||
style={`--nx:${node.position.x * 10}px;
|
|
||||||
--ny: ${node.position.y * 10}px`}
|
|
||||||
>
|
|
||||||
{node.id}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</HTML>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:global(body) {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 100;
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
transform: scale(calc(var(--cz) * 0.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.node {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 1px;
|
|
||||||
user-select: none !important;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 50px;
|
|
||||||
height: 25px;
|
|
||||||
background: linear-gradient(-11.1grad, #000 0%, #0b0b0b 100%);
|
|
||||||
color: white;
|
|
||||||
transform: translate(var(--nx), var(--ny));
|
|
||||||
z-index: 1;
|
|
||||||
box-shadow: 0px 0px 0px calc(15px / var(--cz)) rgba(255, 255, 255, 0.3);
|
|
||||||
font-weight: 300;
|
|
||||||
font-size: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node::after {
|
|
||||||
/* content: ""; */
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: white;
|
|
||||||
border-radius: 2px;
|
|
||||||
transform: scale(1.01);
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
<script context="module" lang="ts">
|
|
||||||
export type Props = {
|
|
||||||
value: number;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export let props = { value: 0 };
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<p>output</p>
|
|
@ -1,11 +0,0 @@
|
|||||||
<script context="module" lang="ts">
|
|
||||||
export type Props = {
|
|
||||||
value: number;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export let props = { value: 0 };
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<input type="number" bind:value={props.value} />
|
|
@ -1,6 +0,0 @@
|
|||||||
import Test from './Test.svelte';
|
|
||||||
import Output from './Output.svelte';
|
|
||||||
export const nodes = {
|
|
||||||
test: Test,
|
|
||||||
output: Output,
|
|
||||||
} as const;
|
|
114
frontend/src/lib/graph-manager.ts
Normal file
114
frontend/src/lib/graph-manager.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
import type { Graph, NodeRegistry as INodeRegistry, NodeType, Node } from "./types";
|
||||||
|
import { snapToGrid } from "./helpers";
|
||||||
|
|
||||||
|
const nodeTypes: NodeType[] = [
|
||||||
|
{
|
||||||
|
id: "input/float",
|
||||||
|
inputs: {
|
||||||
|
"value": { type: "float" },
|
||||||
|
},
|
||||||
|
outputs: ["float"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "math",
|
||||||
|
inputs: {
|
||||||
|
"a": { type: "float" },
|
||||||
|
"b": { type: "float" },
|
||||||
|
},
|
||||||
|
outputs: ["float"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export class NodeRegistry implements INodeRegistry {
|
||||||
|
getNode(id: string): NodeType | undefined {
|
||||||
|
return nodeTypes.find((nodeType) => nodeType.id === id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class GraphManager {
|
||||||
|
|
||||||
|
status: Writable<"loading" | "idle" | "error"> = writable("loading");
|
||||||
|
|
||||||
|
nodes: Node[] = [];
|
||||||
|
edges: { from: string, to: string }[] = [];
|
||||||
|
|
||||||
|
private constructor(private graph: Graph, private nodeRegistry: NodeRegistry = new NodeRegistry()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
|
||||||
|
const nodes = this.graph.nodes;
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
const nodeType = this.getNodeType(node.type);
|
||||||
|
if (!nodeType) {
|
||||||
|
console.error(`Node type not found: ${node.type}`);
|
||||||
|
this.status.set("error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodes = this.graph.nodes;
|
||||||
|
this.edges = this.graph.edges;
|
||||||
|
this.status.set("idle");
|
||||||
|
}
|
||||||
|
|
||||||
|
getNode(id: string) {
|
||||||
|
return this.nodes.find((node) => node.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNodeType(id: string): NodeType {
|
||||||
|
return this.nodeRegistry.getNode(id)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEdges() {
|
||||||
|
return this.edges
|
||||||
|
.map((edge) => {
|
||||||
|
const from = this.nodes.find((node) => node.id === edge.from);
|
||||||
|
const to = this.nodes.find((node) => node.id === edge.to);
|
||||||
|
if (!from || !to) return;
|
||||||
|
return [from, to] as const;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as unknown as [Node, Node][];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static createEmptyGraph(): GraphManager {
|
||||||
|
|
||||||
|
const graph: Graph = {
|
||||||
|
edges: [],
|
||||||
|
nodes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < 40; i++) {
|
||||||
|
const x = i % 20;
|
||||||
|
const y = Math.floor(i / 20);
|
||||||
|
|
||||||
|
graph.nodes.push({
|
||||||
|
id: `${i.toString()}`,
|
||||||
|
tmp: {
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: x * 7.5,
|
||||||
|
y: y * 5,
|
||||||
|
},
|
||||||
|
props: i == 0 ? { value: 0 } : {},
|
||||||
|
type: i == 0 ? "input/float" : "math",
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.edges.push({
|
||||||
|
from: i.toString(),
|
||||||
|
to: (i + 1).toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GraphManager(graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
3
frontend/src/lib/helpers.ts
Normal file
3
frontend/src/lib/helpers.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function snapToGrid(value: number, gridSize: number = 10) {
|
||||||
|
return Math.round(value / gridSize) * gridSize;
|
||||||
|
}
|
@ -1,8 +1,7 @@
|
|||||||
import { nodes } from "$lib/components/nodes"
|
|
||||||
|
|
||||||
export type Node = {
|
export type Node = {
|
||||||
id: string;
|
id: string;
|
||||||
type: keyof typeof nodes;
|
type: string;
|
||||||
props?: Record<string, any>,
|
props?: Record<string, any>,
|
||||||
tmp?: {
|
tmp?: {
|
||||||
downX?: number;
|
downX?: number;
|
||||||
@ -19,6 +18,42 @@ export type Node = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NodeInputFloat = {
|
||||||
|
type: "float";
|
||||||
|
value?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeInputInteger = {
|
||||||
|
type: "integer";
|
||||||
|
value?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeInputSelect = {
|
||||||
|
type: "select";
|
||||||
|
value?: string;
|
||||||
|
options: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeInput = NodeInputFloat | NodeInputInteger | NodeInputSelect;
|
||||||
|
|
||||||
|
export type NodeType = {
|
||||||
|
id: string;
|
||||||
|
inputs?: Record<string, NodeInput>;
|
||||||
|
outputs?: string[];
|
||||||
|
meta?: {
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeRegistry {
|
||||||
|
getNode: (id: string) => NodeType | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type Edge = {
|
export type Edge = {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import App from "$lib/components/App.svelte";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
import { Canvas } from "@threlte/core";
|
||||||
|
import Scene from "$lib/components/Scene.svelte";
|
||||||
|
import { GraphManager } from "$lib/graph-manager";
|
||||||
|
|
||||||
|
const graph = GraphManager.createEmptyGraph();
|
||||||
|
graph.load();
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await invoke("greet", { name: "Dude" });
|
const res = await invoke("greet", { name: "Dude" });
|
||||||
@ -21,7 +27,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<App />
|
<Canvas shadows={false}>
|
||||||
|
<Scene {graph} />
|
||||||
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
958
pnpm-lock.yaml
958
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user