Skip to content

Commit

Permalink
Added linked inputs group (#1983)
Browse files Browse the repository at this point in the history
* Added linked inputs group

* WIP

* Final design

* It doesn't support nested groups
  • Loading branch information
RunDevelopment authored Jul 25, 2023
1 parent 0d2513d commit 081bf70
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 42 deletions.
13 changes: 12 additions & 1 deletion backend/src/nodes/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,17 @@ def optional_list_group(*inputs: BaseInput | NestedGroup):
can be used to create a similar effect.
See the Text Append node for an example.
```
"""
return group("optional-list")(*inputs)


def linked_inputs_group(*inputs: BaseInput):
"""
This group wraps around inputs of the same type. It ensures that all inputs have the same
value.
"The same type" here not only refers to the Navi type of those inputs. All possible values
from all inputs must also be valid values for all other inputs. This typically necessitates
that the inputs are of the same class and use the same parameters.
"""
return group("linked-inputs")(*inputs)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import cv2
import numpy as np

from nodes.groups import linked_inputs_group
from nodes.properties.inputs import ImageInput, SliderInput
from nodes.properties.outputs import ImageOutput

Expand Down Expand Up @@ -52,25 +53,27 @@ def get_kernel_2d(radius_x: float, radius_y) -> np.ndarray:
icon="MdBlurOn",
inputs=[
ImageInput(),
SliderInput(
"Radius X",
minimum=0,
maximum=1000,
default=1,
precision=1,
controls_step=1,
slider_step=0.1,
scale="log",
),
SliderInput(
"Radius Y",
minimum=0,
maximum=1000,
default=1,
precision=1,
controls_step=1,
slider_step=0.1,
scale="log",
linked_inputs_group(
SliderInput(
"Radius X",
minimum=0,
maximum=1000,
default=1,
precision=1,
controls_step=1,
slider_step=0.1,
scale="log",
),
SliderInput(
"Radius Y",
minimum=0,
maximum=1000,
default=1,
precision=1,
controls_step=1,
slider_step=0.1,
scale="log",
),
),
],
outputs=[ImageOutput(image_type="Input0")],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import numpy as np

from nodes.groups import linked_inputs_group
from nodes.impl.image_utils import fast_gaussian_blur
from nodes.properties.inputs import ImageInput, SliderInput
from nodes.properties.outputs import ImageOutput
Expand All @@ -16,25 +17,27 @@
icon="MdBlurOn",
inputs=[
ImageInput(),
SliderInput(
"Radius X",
minimum=0,
maximum=1000,
default=1,
precision=1,
controls_step=1,
slider_step=0.1,
scale="log",
),
SliderInput(
"Radius Y",
minimum=0,
maximum=1000,
default=1,
precision=1,
controls_step=1,
slider_step=0.1,
scale="log",
linked_inputs_group(
SliderInput(
"Radius X",
minimum=0,
maximum=1000,
default=1,
precision=1,
controls_step=1,
slider_step=0.1,
scale="log",
),
SliderInput(
"Radius Y",
minimum=0,
maximum=1000,
default=1,
precision=1,
controls_step=1,
slider_step=0.1,
scale="log",
),
),
],
outputs=[ImageOutput(image_type="Input0")],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import cv2
import numpy as np

from nodes.groups import linked_inputs_group
from nodes.properties.inputs import ImageInput, SliderInput
from nodes.properties.outputs import ImageOutput

Expand All @@ -16,8 +17,10 @@
icon="MdOutlineAutoFixHigh",
inputs=[
ImageInput(),
SliderInput("Size X", minimum=1, maximum=1024, default=10, scale="log"),
SliderInput("Size Y", minimum=1, maximum=1024, default=10, scale="log"),
linked_inputs_group(
SliderInput("Size X", minimum=1, maximum=1024, default=10, scale="log"),
SliderInput("Size Y", minimum=1, maximum=1024, default=10, scale="log"),
),
],
outputs=[ImageOutput(image_type="Input0")],
)
Expand Down
7 changes: 6 additions & 1 deletion src/common/common-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,19 @@ interface SeedGroup extends GroupBase {
readonly kind: 'seed';
readonly options: Readonly<Record<string, never>>;
}
interface LinkedInputsGroup extends GroupBase {
readonly kind: 'linked-inputs';
readonly options: Readonly<Record<string, never>>;
}
export type GroupKind = Group['kind'];
export type Group =
| NcnnFileInputGroup
| FromToDropdownsGroup
| OptionalListGroup
| ConditionalGroup
| RequiredGroup
| SeedGroup;
| SeedGroup
| LinkedInputsGroup;

export type OfKind<T extends { readonly kind: string }, Kind extends T['kind']> = T extends {
readonly kind: Kind;
Expand Down
15 changes: 15 additions & 0 deletions src/common/group-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
NodeSchema,
NumberInput,
OfKind,
SliderInput,
} from './common-types';
import { getChainnerScope } from './types/chainner-scope';
import { fromJson } from './types/json';
Expand All @@ -34,6 +35,7 @@ type DeclaredGroupInputs = InputGuarantees<{
'ncnn-file-inputs': readonly [FileInput, FileInput];
'optional-list': readonly [InputItem, ...InputItem[]];
seed: readonly [NumberInput];
'linked-inputs': readonly [SliderInput, SliderInput, ...SliderInput[]];
}>;

// A bit hacky, but this ensures that GroupInputs covers exactly all group types, no more and no less
Expand Down Expand Up @@ -143,6 +145,19 @@ const groupInputsChecks: {

if (input.kind !== 'number') return 'Expected the input to be a number input';
},
'linked-inputs': (inputs) => {
if (inputs.length < 2) return 'Expected at least 2 inputs';
if (!allInputsOfKind(inputs, 'slider')) return `Expected all inputs to be slider inputs`;

const [ref] = inputs;
for (const i of inputs) {
if (i.min !== ref.min) return 'Expected all inputs to have the same min value';
if (i.max !== ref.max) return 'Expected all inputs to have the same max value';
if (i.precision !== ref.precision)
return 'Expected all inputs to have the same precision value';
if (i.def !== ref.def) return 'Expected all inputs to have the same default value';
}
},
};

export const checkGroupInputs = (
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/components/groups/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { InputItem } from '../../../common/group-inputs';
import { NodeState } from '../../helpers/nodeState';
import { ConditionalGroup } from './ConditionalGroup';
import { FromToDropdownsGroup } from './FromToDropdownsGroup';
import { LinkedInputsGroup } from './LinkedInputsGroup';
import { NcnnFileInputsGroup } from './NcnnFileInputsGroup';
import { OptionalInputsGroup } from './OptionalInputsGroup';
import { GroupProps, InputItemRenderer } from './props';
Expand All @@ -19,6 +20,7 @@ const GroupComponents: {
'ncnn-file-inputs': NcnnFileInputsGroup,
'optional-list': OptionalInputsGroup,
seed: SeedGroup,
'linked-inputs': LinkedInputsGroup,
};

interface GroupElementProps {
Expand Down
167 changes: 167 additions & 0 deletions src/renderer/components/groups/LinkedInputsGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { Box, HStack, IconButton, Tooltip } from '@chakra-ui/react';
import { memo, useCallback, useState } from 'react';
import { IoMdLink } from 'react-icons/io';
import { IoUnlink } from 'react-icons/io5';
import { InputId, InputValue } from '../../../common/common-types';
import { joinEnglish } from '../../../common/util';
import { NodeState } from '../../helpers/nodeState';
import { useMemoObject } from '../../hooks/useMemo';
import { GroupProps } from './props';

export const LinkedInputsGroup = memo(
({ inputs, nodeState, ItemRenderer }: GroupProps<'linked-inputs'>) => {
const { inputData, setInputValue, isLocked } = nodeState;

const [linked, setLinked] = useState<boolean>(() => {
const allSameValue = inputs.every((input) => {
const value = inputData[input.id];
return value === inputData[inputs[0].id];
});
return allSameValue;
});

const [lastUsedInput, setLastUsedInput] = useState<InputId | null>(null);

const modifiedNodeState = useMemoObject<NodeState>({
...nodeState,
setInputValue: useCallback(
(inputId: InputId, value: InputValue): void => {
setInputValue(inputId, value);
setLastUsedInput(inputId);

if (linked && typeof value === 'number') {
for (const input of inputs) {
if (input.id !== inputId) {
setInputValue(input.id, value);
}
}
}
},
[linked, setInputValue, inputs]
),
});

const allConnected = inputs.every((input) => nodeState.connectedInputs.has(input.id));

const label = linked
? `The values of ${joinEnglish(
inputs.map((input) => input.label),
'and'
)} are currently linked to the same value. Click here to undo this link.`
: `Click here to link ${joinEnglish(
inputs.map((input) => input.label),
'and'
)} to the same value.`;

const linkButtonWidth = 1.4;
const linkButtonHeight = 1.6;

return (
<HStack
alignItems="normal"
pr={1}
spacing={0}
>
<Box w="full">
{inputs.map((item) => (
<ItemRenderer
item={item}
key={item.id}
nodeState={modifiedNodeState}
/>
))}
</Box>
<Box
alignItems="center"
display="flex"
position="relative"
>
<Box
borderColor="white white transparent transparent"
borderRadius="0 .5rem 0 0"
borderStyle="solid"
borderWidth="1px 2px 0 0"
bottom={`calc(50% + ${linkButtonHeight / 2}rem)`}
opacity={allConnected ? 0.25 : 0.5}
position="absolute"
right={`calc(${linkButtonWidth / 2}rem - 1px)`}
top=".5rem"
w={2}
/>
<Box
borderColor="transparent white white transparent"
borderRadius="0 0 .5rem 0"
borderStyle="solid"
borderWidth="0 2px 1px 0"
bottom=".5rem"
opacity={allConnected ? 0.25 : 0.5}
position="absolute"
right={`calc(${linkButtonWidth / 2}rem - 1px)`}
top={`calc(50% + ${linkButtonHeight / 2}rem)`}
w={2}
/>
<Tooltip
closeOnClick
closeOnPointerDown
hasArrow
borderRadius={8}
isDisabled={isLocked || allConnected}
label={label}
openDelay={2000}
>
<IconButton
aria-label={label}
className="nodrag"
h="2rem"
icon={
linked ? (
<IoMdLink style={{ transform: 'rotate(90deg)' }} />
) : (
<IoUnlink style={{ transform: 'rotate(90deg)' }} />
)
}
isDisabled={isLocked || allConnected}
minWidth={0}
ml="-0.25rem"
size="md"
variant="ghost"
w={`${linkButtonWidth}rem`}
onClick={() => {
if (linked) {
// just unlink
setLinked(false);
} else {
// link and set inputs to the same value
setLinked(true);

let value: InputValue;
if (
lastUsedInput !== null &&
!nodeState.connectedInputs.has(lastUsedInput)
) {
// use the value of the last used input (if not connected)
value = nodeState.inputData[lastUsedInput];
} else {
// use the value of the first unconnected input
const firstUnconnectedInput = inputs.find(
(input) => !nodeState.connectedInputs.has(input.id)
);
if (firstUnconnectedInput) {
value = nodeState.inputData[firstUnconnectedInput.id];
}
}

if (value !== undefined) {
for (const input of inputs) {
setInputValue(input.id, value);
}
}
}
}}
/>
</Tooltip>
</Box>
</HStack>
);
}
);

0 comments on commit 081bf70

Please sign in to comment.