Skip to content

Commit

Permalink
Merge pull request #6 from clarkmcc/5-input-groups
Browse files Browse the repository at this point in the history
Node Input Groups
  • Loading branch information
clarkmcc authored Jan 16, 2024
2 parents 4fefd21 + bd71a7c commit 69edee8
Show file tree
Hide file tree
Showing 10 changed files with 968 additions and 27 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[![Banner-Dark](https://raw.githubusercontent.com/clarkmcc/ngraph/main/assets/ngraph-logo-dark.png#gh-dark-mode-only)](https://raw.githubusercontent.com/clarkmcc/ngraph/main/assets/ngraph-logo-dark.png#gh-dark-mode-only)
[![Banner-Dark](https://raw.githubusercontent.com/clarkmcc/ngraph/main/assets/ngraph-logo-light.png#gh-light-mode-only)](https://raw.githubusercontent.com/clarkmcc/ngraph/main/assets/ngraph-logo-light.png#gh-light-mode-only)
![](./assets/ngraph-banner.png)

A blender-style node editor built on [xyflow](https://github.com/xyflow/xyflow). Try it out in [Storybook](https://clarkmcc.github.io/ngraph/) or checkout the getting started [CodeSandbox](https://codesandbox.io/p/sandbox/ngraph-example-xst5gl?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clrcmtycc00063b6jzvptx4ag%2522%252C%2522sizes%2522%253A%255B100%252C0%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clrcmtycc00023b6j23ou3m6q%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clrcmtycc00033b6jjdpmq85q%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clrcmtycc00053b6jw9mwz96t%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B50%252C50%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clrcmtycc00023b6j23ou3m6q%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clrcmtycc00013b6ju1878nh7%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252Fpublic%252Findex.html%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522id%2522%253A%2522clrcorw2n00023b6jzpv9w2ok%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A20%252C%2522startColumn%2522%253A26%252C%2522endLineNumber%2522%253A20%252C%2522endColumn%2522%253A26%257D%255D%252C%2522filepath%2522%253A%2522%252Fsrc%252FApp.tsx%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522id%2522%253A%2522clrcmtycc00023b6j23ou3m6q%2522%252C%2522activeTabId%2522%253A%2522clrcorw2n00023b6jzpv9w2ok%2522%257D%252C%2522clrcmtycc00053b6jw9mwz96t%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clrcmtycc00043b6j23ig61o6%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A0%252C%2522path%2522%253A%2522%252F%2522%257D%255D%252C%2522id%2522%253A%2522clrcmtycc00053b6jw9mwz96t%2522%252C%2522activeTabId%2522%253A%2522clrcmtycc00043b6j23ig61o6%2522%257D%252C%2522clrcmtycc00033b6jjdpmq85q%2522%253A%257B%2522tabs%2522%253A%255B%255D%252C%2522id%2522%253A%2522clrcmtycc00033b6jjdpmq85q%2522%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Afalse%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D).
A Blender-style node editor built on [xyflow](https://github.com/xyflow/xyflow). Try it out in [Storybook](https://clarkmcc.github.io/ngraph/) or checkout the getting started [CodeSandbox](https://codesandbox.io/p/sandbox/ngraph-example-xst5gl?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clrcmtycc00063b6jzvptx4ag%2522%252C%2522sizes%2522%253A%255B100%252C0%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clrcmtycc00023b6j23ou3m6q%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clrcmtycc00033b6jjdpmq85q%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clrcmtycc00053b6jw9mwz96t%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B50%252C50%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clrcmtycc00023b6j23ou3m6q%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clrcmtycc00013b6ju1878nh7%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252Fpublic%252Findex.html%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522id%2522%253A%2522clrcorw2n00023b6jzpv9w2ok%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A20%252C%2522startColumn%2522%253A26%252C%2522endLineNumber%2522%253A20%252C%2522endColumn%2522%253A26%257D%255D%252C%2522filepath%2522%253A%2522%252Fsrc%252FApp.tsx%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522id%2522%253A%2522clrcmtycc00023b6j23ou3m6q%2522%252C%2522activeTabId%2522%253A%2522clrcorw2n00023b6jzpv9w2ok%2522%257D%252C%2522clrcmtycc00053b6jw9mwz96t%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clrcmtycc00043b6j23ig61o6%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A0%252C%2522path%2522%253A%2522%252F%2522%257D%255D%252C%2522id%2522%253A%2522clrcmtycc00053b6jw9mwz96t%2522%252C%2522activeTabId%2522%253A%2522clrcmtycc00043b6j23ig61o6%2522%257D%252C%2522clrcmtycc00033b6jjdpmq85q%2522%253A%257B%2522tabs%2522%253A%255B%255D%252C%2522id%2522%253A%2522clrcmtycc00033b6jjdpmq85q%2522%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Afalse%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D).

_Note: This project is still in development and the API is likely to change._

Expand Down
Binary file added assets/ngraph-banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
289 changes: 289 additions & 0 deletions lib/NodeGraphEditorInputGroups.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import { NodeGraphEditor } from './NodeGraphEditor'
import { Meta, StoryObj } from '@storybook/react'
import { GraphConfigProvider } from './context/GraphConfigContext'
import {
Background,
BackgroundVariant,
Edge,
Node,
ReactFlowProvider,
} from 'reactflow'
import { useBuildGraphConfig } from './hooks/config.ts'
import { InputProps } from './config.ts'
import { Wheel } from '@uiw/react-color'
import { useNodeFieldValue } from './hooks/node.ts'

const meta = {
title: 'Node Graph Editor',
component: ({ nodes, edges }) => {
function ColorPicker({ slots, ...config }: InputProps) {
const [hsva, setHsva] = useNodeFieldValue(config.id, '#f87171')
const Handle = slots?.Handle
return (
<div
style={{
position: 'relative',
display: 'flex',
flexDirection: 'column',
padding: '12px 12px',
}}
>
{Handle && <Handle />}
<Wheel
width={140}
height={140}
color={hsva}
onChange={(color) => setHsva(color.hex)}
onFocus={config.onFocus}
onBlur={config.onBlur}
/>
</div>
)
}

const config = useBuildGraphConfig(
{
valueTypes: {
string: {
name: 'String',
color: '#a1a1a1',
inputType: 'value',
defaultValue: '',
},
number: {
name: 'Number',
color: '#a1a1a1',
inputType: 'value',
defaultValue: '0.000',
},
boolean: {
name: 'Boolean',
color: '#a1a1a1',
inputType: 'checkbox',
defaultValue: true,
},
specularDistribution: {
name: 'Specular Distribution',
color: '#06b6d4',
inputType: 'options',
options: [
{ name: 'GGX', value: 'ggx' },
{ name: 'Beckmann', value: 'beckmann' },
{ name: 'Phong', value: 'phong' },
],
defaultValue: 'GET',
},
},
nodeGroups: {
default: {
name: 'Default',
color: '#CE4040',
},
inputs: {
name: 'Inputs',
color: '#83324A',
},
},
nodes: {
number: {
group: 'default',
name: 'Number',
inputs: [
{
name: 'Value',
id: 'value',
valueType: 'number',
isConstant: true,
},
],
outputs: [
{
name: 'Value',
id: 'value',
valueType: 'number',
},
],
},
color: {
group: 'inputs',
name: 'Color',
style: {
width: '100px',
},
inputs: [
{
name: 'Color',
id: 'color',
valueType: 'color',
isConstant: true,
},
],
outputs: [
{
name: 'Color',
id: 'color',
valueType: 'color',
},
],
},
bsdf: {
group: 'default',
name: 'Principled BSDF',
inputs: [
{
name: 'Metallic',
id: 'metallic',
valueType: 'number',
},
{
name: 'Roughness',
id: 'roughness',
valueType: 'number',
defaultValue: '0.550',
},
{
name: 'IOR',
id: 'ior',
valueType: 'number',
defaultValue: '1.450',
},
{
name: 'Alpha',
id: 'alpha',
valueType: 'number',
defaultValue: '1.000',
},
{
name: 'Distribution',
id: 'distribution',
group: 'Specular',
valueType: 'specularDistribution',
},
{
name: 'IOR Level',
id: 'iorLevel',
group: 'Specular',
valueType: 'number',
},
{
name: 'Tint',
id: 'tint',
group: 'Specular',
valueType: 'color',
},
{
name: 'Anisotropic',
id: 'anisotropic',
group: 'Specular',
valueType: 'number',
},
{
name: 'Anisotropic Rotation',
id: 'anisotropicRotation',
group: 'Specular',
valueType: 'number',
},
{
name: 'Strength',
id: 'strength',
group: 'Emission',
valueType: 'number',
},
],
},
},
},
(config) => {
config.registerInput('color', ColorPicker, {
name: 'Color',
color: '#C7C728',
})
},
)
return (
<GraphConfigProvider defaultConfig={config}>
<ReactFlowProvider>
<NodeGraphEditor defaultNodes={nodes} defaultEdges={edges}>
<Background color="#52525b" variant={BackgroundVariant.Dots} />
</NodeGraphEditor>
</ReactFlowProvider>
</GraphConfigProvider>
)
},
decorators: (Story) => (
<div style={{ width: '100vw', height: '100vh' }}>
<Story />
</div>
),
tags: ['autodocs'],
} satisfies Meta<{ nodes: Node[]; edges: Edge[] }>

export default meta

type Story = StoryObj<typeof meta>

export const InputGroups: Story = {
parameters: {
layout: 'fullscreen',
},
args: {
nodes: [
{
id: '1',
type: 'bsdf',
position: { x: 350, y: 100 },
data: {
__inputGroupsExpanded: ['Specular'],
},
},
{
id: '2',
type: 'number',
position: { x: 100, y: 100 },
data: {},
},
{
id: '3',
type: 'number',
position: { x: 100, y: 200 },
data: {},
},
{
id: '4',
type: 'color',
position: { x: 100, y: 300 },
data: {},
},
],
edges: [
{
id: 'e1',
source: '3',
sourceHandle: 'value',
target: '1',
targetHandle: 'strength',
},
{
id: 'e2',
source: '2',
sourceHandle: 'value',
target: '1',
targetHandle: 'anisotropicRotation',
},
{
id: 'e3',
source: '3',
sourceHandle: 'value',
target: '1',
targetHandle: 'anisotropic',
},
{
id: 'e4',
source: '4',
sourceHandle: 'value',
target: '1',
targetHandle: 'tint',
},
],
},
}
79 changes: 79 additions & 0 deletions lib/components/InputGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { CSSProperties, ReactNode, useMemo } from 'react'
import { GoTriangleDown } from 'react-icons/go'
import { useNodeInputGroupState } from '../hooks/node.ts'

type InputGroupProps = {
label: string
children: ReactNode
style?: CSSProperties
labelStyle?: CSSProperties
handles: ReactNode
}

export const InputGroup = ({
label,
children,
labelStyle,
handles,
}: InputGroupProps) => {
const [isOpen, setIsOpen] = useNodeInputGroupState(label)

const toggleAccordion = () => {
setIsOpen(!isOpen)
}

const defaultStyles: Record<string, CSSProperties> = useMemo(
() => ({
label: {
fontSize: '12px',
cursor: 'pointer',
padding: '3px 8px',
display: 'flex',
color: 'white',
textShadow: '0 1px rgba(0,0,0,0.4)',
transition: 'transform 0.1s ease',
},
icon: {
color: 'white',
opacity: 0.4,
fontSize: 18,
cursor: 'pointer',
transition: 'transform 0.1s ease',
transform: isOpen ? 'rotate(0deg)' : 'rotate(-90deg)',
},
content: {
background: isOpen ? '#1e1e1e' : undefined,
padding: '8px 0 8px',
display: 'flex',
flexDirection: 'column',
},
}),
[isOpen],
)

return (
<div style={{ position: 'relative' }}>
<div
style={{
margin: '2px 0',
padding: '0 12px',
opacity: 0 /* using display:none makes edges go crazy*/,
}}
>
{!isOpen && handles}
</div>
<div style={{ position: 'relative' }}>
<div
onClick={toggleAccordion}
style={{ ...defaultStyles.label, ...labelStyle }}
>
<div style={{ position: 'relative', display: 'flex' }}>
<GoTriangleDown style={defaultStyles.icon} />
<span style={{ marginLeft: 2, paddingTop: 2 }}>{label}</span>
</div>
</div>
{isOpen && <div style={defaultStyles.content}>{children}</div>}
</div>
</div>
)
}
Loading

0 comments on commit 69edee8

Please sign in to comment.