diff --git a/src/common/SaveFile.ts b/src/common/SaveFile.ts index a1ecd5e38..984e6b236 100644 --- a/src/common/SaveFile.ts +++ b/src/common/SaveFile.ts @@ -77,6 +77,7 @@ export class SaveFile { schemaId: n.data.schemaId, inputData: n.data.inputData, inputHeight: n.data.inputHeight, + outputHeight: n.data.outputHeight, nodeWidth: n.data.nodeWidth, id: n.data.id, iteratorSize: n.data.iteratorSize, diff --git a/src/common/common-types.ts b/src/common/common-types.ts index d2f7c6050..08b487ec1 100644 --- a/src/common/common-types.ts +++ b/src/common/common-types.ts @@ -242,6 +242,7 @@ export type NodeType = 'regularNode' | 'iterator' | 'iteratorHelper'; export type InputData = Readonly>; export type InputHeight = Readonly>; export type OutputData = Readonly>; +export type OutputHeight = Readonly>; export type OutputTypes = Readonly>>; export type GroupState = Readonly>; @@ -278,6 +279,7 @@ export interface NodeData { readonly inputData: InputData; readonly groupState?: GroupState; readonly inputHeight?: InputHeight; + readonly outputHeight?: OutputHeight; readonly nodeWidth?: number; readonly invalid?: boolean; readonly iteratorSize?: Readonly; diff --git a/src/renderer/components/NodeDocumentation/NodeExample.tsx b/src/renderer/components/NodeDocumentation/NodeExample.tsx index b9cec0c40..c016f90f3 100644 --- a/src/renderer/components/NodeDocumentation/NodeExample.tsx +++ b/src/renderer/components/NodeDocumentation/NodeExample.tsx @@ -10,6 +10,8 @@ import { InputValue, NodeData, NodeSchema, + OutputHeight, + OutputId, } from '../../../common/common-types'; import { checkNodeValidity } from '../../../common/nodes/checkNodeValidity'; import { DisabledStatus } from '../../../common/nodes/disabled'; @@ -90,6 +92,17 @@ export const NodeExample = memo(({ accentColor, selectedSchema }: NodeExamplePro [setNodeWidth] ); + const [outputHeight, setOutputHeight] = useStateForSchema( + selectedSchema, + EMPTY_OBJECT + ); + const setSingleOutputHeight = useCallback( + (outputId: OutputId, height: number): void => { + setOutputHeight((prev) => ({ ...prev, [outputId]: height })); + }, + [setOutputHeight] + ); + const nodeIdPrefix = 'FakeId '; const suffixLength = 36 - nodeIdPrefix.length; const nodeId = @@ -168,6 +181,8 @@ export const NodeExample = memo(({ accentColor, selectedSchema }: NodeExamplePro setInputValue, inputHeight, nodeWidth, + outputHeight, + setOutputHeight: setSingleOutputHeight, setWidth, setInputHeight: setSingleInputHeight, isLocked: false, diff --git a/src/renderer/components/node/NodeOutputs.tsx b/src/renderer/components/node/NodeOutputs.tsx index 3ef3a1718..df2521b89 100644 --- a/src/renderer/components/node/NodeOutputs.tsx +++ b/src/renderer/components/node/NodeOutputs.tsx @@ -1,7 +1,7 @@ import { NeverType, Type, evaluate } from '@chainner/navi'; import { memo, useCallback, useEffect } from 'react'; import { useContext, useContextSelector } from 'use-context-selector'; -import { OutputId, OutputKind } from '../../../common/common-types'; +import { OutputId, OutputKind, Size } from '../../../common/common-types'; import { log } from '../../../common/log'; import { getChainnerScope } from '../../../common/types/chainner-scope'; import { ExpressionJson, fromJson } from '../../../common/types/json'; @@ -48,7 +48,7 @@ interface NodeOutputProps { } export const NodeOutputs = memo(({ nodeState, animated }: NodeOutputProps) => { - const { id, schema, schemaId } = nodeState; + const { id, schema, schemaId, outputHeight, setOutputHeight, nodeWidth, setWidth } = nodeState; const { functionDefinitions } = useContext(BackendContext); const { setManualOutputType } = useContext(GlobalContext); @@ -90,6 +90,15 @@ export const NodeOutputs = memo(({ nodeState, animated }: NodeOutputProps) => { const definitionType = functions?.get(output.id) ?? NeverType.instance; const type = nodeState.type.instance?.outputs.get(output.id); + const size = + outputHeight?.[output.id] && nodeWidth + ? { height: outputHeight[output.id], width: nodeWidth } + : undefined; + const setSize = (newSize: Readonly) => { + setOutputHeight(output.id, newSize.height); + setWidth(newSize.width); + }; + const OutputType = OutputComponents[output.kind]; return ( { id={id} output={output} schema={nodeState.schema} + setSize={setSize} + size={size} type={type ?? NeverType.instance} useOutputData={useOutputData} /> diff --git a/src/renderer/components/outputs/LargeImageOutput.tsx b/src/renderer/components/outputs/LargeImageOutput.tsx index 7bd8d7b0b..ba9279b29 100644 --- a/src/renderer/components/outputs/LargeImageOutput.tsx +++ b/src/renderer/components/outputs/LargeImageOutput.tsx @@ -1,14 +1,18 @@ /* eslint-disable no-nested-ternary */ import { ViewOffIcon, WarningIcon } from '@chakra-ui/icons'; import { Box, Center, HStack, Image, Spinner, Text } from '@chakra-ui/react'; -import { memo } from 'react'; +import { Resizable, Size } from 're-resizable'; +import { memo, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useContextSelector } from 'use-context-selector'; +import { useContext, useContextSelector } from 'use-context-selector'; import { GlobalVolatileContext } from '../../contexts/GlobalNodeState'; +import { SettingsContext } from '../../contexts/SettingsContext'; import { useDevicePixelRatio } from '../../hooks/useDevicePixelRatio'; +import { useMemoArray } from '../../hooks/useMemo'; +import { DragHandleSVG } from '../CustomIcons'; import { OutputProps } from './props'; -const IMAGE_PREVIEW_SIZE = 200; +const IMAGE_PREVIEW_SIZE = 216; interface PreviewImage { size: number; @@ -30,120 +34,229 @@ const pickImage = (previews: PreviewImage[], realSize: number) => { return found ?? previews[previews.length - 1]; }; -export const LargeImageOutput = memo(({ output, useOutputData, animated }: OutputProps) => { - const { t } = useTranslation(); +export const LargeImageOutput = memo( + ({ output, useOutputData, animated, size, setSize }: OutputProps) => { + const { t } = useTranslation(); - const dpr = useDevicePixelRatio(); - const zoom = useContextSelector(GlobalVolatileContext, (c) => c.zoom); - const realSize = IMAGE_PREVIEW_SIZE * zoom * dpr; + const dpr = useDevicePixelRatio(); + const zoom = useContextSelector(GlobalVolatileContext, (c) => c.zoom); + const realSize = (size?.width ?? IMAGE_PREVIEW_SIZE) * zoom * dpr; - const { last, stale } = useOutputData(output.id); + const { last, stale } = useOutputData(output.id); - const imgBgColor = 'var(--node-image-preview-bg)'; - const fontColor = 'var(--node-image-preview-color)'; + const imgBgColor = 'var(--node-image-preview-bg)'; + const fontColor = 'var(--node-image-preview-color)'; - const pickedImage = last ? pickImage(last.previews, realSize) : null; + const previewImage = last ? pickImage(last.previews, realSize) : null; - return ( -
+ const { useSnapToGrid } = useContext(SettingsContext); + const [isSnapToGrid, , snapToGridAmount] = useSnapToGrid; + + const [resizeRef, setResizeRef] = useState(null); + + const firstRender = useRef(false); + useEffect(() => { + if (!firstRender.current) { + if (size) { + resizeRef?.updateSize({ + width: size.width, + height: size.height, + }); + firstRender.current = true; + } + } + }, [resizeRef, size]); + + const [maxSize, setMaxSize] = useState({ + width: IMAGE_PREVIEW_SIZE, + height: IMAGE_PREVIEW_SIZE, + }); + + useEffect(() => { + if (last?.previews) { + const biggestImage = last.previews[last.previews.length - 1]; + const img = new window.Image(); + img.src = biggestImage.url; + img.onload = () => { + setMaxSize({ + width: img.width, + height: img.height, + }); + }; + } + }, [last?.previews, previewImage, resizeRef]); + + return (
- ( + isSnapToGrid ? [snapToGridAmount, snapToGridAmount] : [1, 1] + )} + handleComponent={{ + bottomRight: ( +
+ +
+ ), + }} + maxHeight={1024} + maxWidth={1024} + minHeight={IMAGE_PREVIEW_SIZE} + minWidth={IMAGE_PREVIEW_SIZE} + ref={(r) => { + setResizeRef(r); + }} + scale={zoom} + onResizeStop={(e, direction, ref, d) => { + let baseWidth = size?.width ?? IMAGE_PREVIEW_SIZE; + let baseHeight = size?.height ?? IMAGE_PREVIEW_SIZE; + + if (baseWidth < IMAGE_PREVIEW_SIZE) baseWidth = IMAGE_PREVIEW_SIZE; + if (baseHeight < IMAGE_PREVIEW_SIZE) baseHeight = IMAGE_PREVIEW_SIZE; + + setSize({ + width: baseWidth + d.width, + height: baseHeight + d.height, + }); + }} > - - - - Outdated - - -
-
- {last && pickedImage ? ( + + + + Outdated + + +
- Image preview failed to load, probably unsupported file type. 1 && - realSize > IMAGE_PREVIEW_SIZE && - pickedImage.size < realSize - ? 'pixelated' - : 'auto', - }} - /> + {last && previewImage ? ( +
+ Image preview failed to load, probably unsupported file type. 1 && + realSize > IMAGE_PREVIEW_SIZE && + previewImage.size < realSize + ? 'pixelated' + : 'auto', + }} + w={`${maxSize.width}px`} + /> +
+ ) : animated ? ( + + ) : ( + + + + {t( + 'outputs.largeImage.imageNotAvailable', + 'Image not available.' + )} + + + )}
- ) : animated ? ( - - ) : ( - - - - {t('outputs.largeImage.imageNotAvailable', 'Image not available.')} - - - )} -
+
+
- - ); -}); + ); + } +); diff --git a/src/renderer/components/outputs/props.ts b/src/renderer/components/outputs/props.ts index ac459bea5..6c081bb91 100644 --- a/src/renderer/components/outputs/props.ts +++ b/src/renderer/components/outputs/props.ts @@ -1,5 +1,5 @@ import { Type } from '@chainner/navi'; -import { NodeSchema, Output, OutputId } from '../../../common/common-types'; +import { NodeSchema, Output, OutputId, Size } from '../../../common/common-types'; export interface UseOutputData { /** The current output data. Current here means most recent + up to date (= same input hash). */ @@ -18,4 +18,6 @@ export interface OutputProps { readonly type: Type; readonly useOutputData: (outputId: OutputId) => UseOutputData; readonly animated: boolean; + readonly size: Readonly | undefined; + readonly setSize: (size: Readonly) => void; } diff --git a/src/renderer/contexts/GlobalNodeState.tsx b/src/renderer/contexts/GlobalNodeState.tsx index f7bf6d763..a5bd7f832 100644 --- a/src/renderer/contexts/GlobalNodeState.tsx +++ b/src/renderer/contexts/GlobalNodeState.tsx @@ -130,6 +130,7 @@ interface Global { createConnection: (connection: Connection) => void; setNodeInputValue: (nodeId: string, inputId: InputId, value: T) => void; setNodeInputHeight: (nodeId: string, inputId: InputId, value: number) => void; + setNodeOutputHeight: (nodeId: string, outputId: OutputId, value: number) => void; setNodeWidth: (nodeId: string, value: number) => void; removeNodesById: (ids: readonly string[]) => void; removeEdgeById: (id: string) => void; @@ -984,6 +985,19 @@ export const GlobalProvider = memo( [modifyNode] ); + const setNodeOutputHeight = useCallback( + (nodeId: string, outputId: OutputId, height: number): void => { + modifyNode(nodeId, (old) => { + const newOutputHeight: Record = { + ...old.data.outputHeight, + [outputId]: height, + }; + return withNewData(old, 'outputHeight', newOutputHeight); + }); + }, + [modifyNode] + ); + const setNodeWidth = useCallback( (nodeId: string, width: number): void => { modifyNode(nodeId, (old) => { @@ -1388,6 +1402,7 @@ export const GlobalProvider = memo( createConnection, setNodeInputValue, setNodeInputHeight, + setNodeOutputHeight, setNodeWidth, toggleNodeLock, clearNodes, diff --git a/src/renderer/helpers/nodeState.ts b/src/renderer/helpers/nodeState.ts index 44bc64411..a55bd3f9d 100644 --- a/src/renderer/helpers/nodeState.ts +++ b/src/renderer/helpers/nodeState.ts @@ -8,6 +8,7 @@ import { InputValue, NodeData, NodeSchema, + OutputHeight, OutputId, SchemaId, } from '../../common/common-types'; @@ -65,6 +66,8 @@ export interface NodeState { readonly setInputValue: (inputId: InputId, value: InputValue) => void; readonly inputHeight: InputHeight | undefined; readonly setInputHeight: (inputId: InputId, height: number) => void; + readonly outputHeight: OutputHeight | undefined; + readonly setOutputHeight: (inputId: OutputId, size: number) => void; readonly nodeWidth: number | undefined; readonly setWidth: (width: number) => void; readonly isLocked: boolean; @@ -75,15 +78,21 @@ export interface NodeState { } export const useNodeStateFromData = (data: NodeData): NodeState => { - const { setNodeInputValue, setNodeInputHeight, setNodeWidth } = useContext(GlobalContext); + const { setNodeInputValue, setNodeInputHeight, setNodeOutputHeight, setNodeWidth } = + useContext(GlobalContext); - const { id, inputData, inputHeight, isLocked, schemaId, nodeWidth } = data; + const { id, inputData, inputHeight, outputHeight, isLocked, schemaId, nodeWidth } = data; const setInputValue = useMemo(() => setNodeInputValue.bind(null, id), [id, setNodeInputValue]); + const setInputHeight = useMemo( () => setNodeInputHeight.bind(null, id), [id, setNodeInputHeight] ); + const setOutputHeight = useMemo( + () => setNodeOutputHeight.bind(null, id), + [id, setNodeOutputHeight] + ); const setWidth = useMemo(() => setNodeWidth.bind(null, id), [id, setNodeWidth]); @@ -117,6 +126,8 @@ export const useNodeStateFromData = (data: NodeData): NodeState => { setInputValue, inputHeight, setInputHeight, + outputHeight, + setOutputHeight, nodeWidth, setWidth, isLocked: isLocked ?? false,