Add real-time icon scaling slider to fix oversized imported icons #112 (#142) Thanks to @F4tal1t

This commit is contained in:
Dibyendu Sahoo
2025-09-24 01:04:56 +05:30
committed by GitHub
parent 77231c97a2
commit 108b5e2f27
6 changed files with 72 additions and 11 deletions

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useRef, useState } from 'react';
import { Stack, Alert, IconButton as MUIIconButton, Box, Button, FormControlLabel, Checkbox, Typography } from '@mui/material';
import { Stack, Alert, IconButton as MUIIconButton, Box, Button, FormControlLabel, Checkbox, Typography, Slider } from '@mui/material';
import { ControlsContainer } from 'src/components/ItemControls/components/ControlsContainer';
import { useUiStateStore } from 'src/stores/uiStateStore';
import { useModelStore } from 'src/stores/modelStore';
@@ -27,6 +27,7 @@ export const IconSelectionControls = () => {
const { iconCategories } = useIconCategories();
const fileInputRef = useRef<HTMLInputElement>(null);
const [treatAsIsometric, setTreatAsIsometric] = useState(true);
const [iconScale, setIconScale] = useState(100);
const [showAlert, setShowAlert] = useState(() => {
// Check localStorage to see if user has dismissed the alert
return localStorage.getItem('fossflow-show-drag-hint') !== 'false';
@@ -111,10 +112,11 @@ export const IconSelectionControls = () => {
const TARGET_SIZE = 128; // Square size for consistency
// Calculate scaling to fit within square while maintaining aspect ratio
// Remove the upper limit (1) to allow upscaling of small images
const scale = Math.min(TARGET_SIZE / img.width, TARGET_SIZE / img.height);
const scaledWidth = img.width * scale;
const scaledHeight = img.height * scale;
const basScale = Math.min(TARGET_SIZE / img.width, TARGET_SIZE / img.height);
// Apply user's custom scaling
const finalScale = basScale * (iconScale / 100);
const scaledWidth = img.width * finalScale;
const scaledHeight = img.height * finalScale;
// Set canvas to square size
canvas.width = TARGET_SIZE;
@@ -170,7 +172,7 @@ export const IconSelectionControls = () => {
// Reset input
event.target.value = '';
}, [currentIcons, modelActions, iconCategoriesState, uiStateActions, treatAsIsometric]);
}, [currentIcons, modelActions, iconCategoriesState, uiStateActions, treatAsIsometric, iconScale]);
return (
<ControlsContainer

View File

@@ -1,8 +1,9 @@
import React from 'react';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { Slider, Box, TextField } from '@mui/material';
import { ModelItem, ViewItem } from 'src/types';
import { MarkdownEditor } from 'src/components/MarkdownEditor/MarkdownEditor';
import { useModelItem } from 'src/hooks/useModelItem';
import { useModelStore } from 'src/stores/modelStore';
import { DeleteButton } from '../../components/DeleteButton';
import { Section } from '../../components/Section';
@@ -25,6 +26,50 @@ export const NodeSettings = ({
onDeleted
}: Props) => {
const modelItem = useModelItem(node.id);
const modelActions = useModelStore((state) => state.actions);
const icons = useModelStore((state) => state.icons);
// Local state for smooth slider interaction
const currentIcon = icons.find(icon => icon.id === modelItem?.icon);
const [localScale, setLocalScale] = useState(currentIcon?.scale || 1);
const debounceRef = useRef<NodeJS.Timeout>();
// Update local scale when icon changes
useEffect(() => {
setLocalScale(currentIcon?.scale || 1);
}, [currentIcon?.scale]);
// Debounced update to store
const updateIconScale = useCallback((scale: number) => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
debounceRef.current = setTimeout(() => {
const updatedIcons = icons.map(icon =>
icon.id === modelItem?.icon
? { ...icon, scale }
: icon
);
modelActions.set({ icons: updatedIcons });
}, 100); // 100ms debounce
}, [icons, modelItem?.icon, modelActions]);
// Handle slider change with local state + debounced store update
const handleScaleChange = useCallback((e: Event, newScale: number | number[]) => {
const scale = newScale as number;
setLocalScale(scale); // Immediate UI update
updateIconScale(scale); // Debounced store update
}, [updateIconScale]);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, []);
if (!modelItem) {
return null;
@@ -65,6 +110,17 @@ export const NodeSettings = ({
/>
</Section>
)}
<Section title="Icon size">
<Slider
marks
step={0.1}
min={0.3}
max={2.5}
value={localScale}
onChange={handleScaleChange}
/>
</Section>
<Section>
<Box>
<DeleteButton onClick={onDeleted} />

View File

@@ -5,10 +5,11 @@ import { useResizeObserver } from 'src/hooks/useResizeObserver';
interface Props {
url: string;
scale?: number;
onImageLoaded?: () => void;
}
export const IsometricIcon = ({ url, onImageLoaded }: Props) => {
export const IsometricIcon = ({ url, scale = 1, onImageLoaded }: Props) => {
const ref = useRef();
const { size, observe, disconnect } = useResizeObserver();
@@ -28,7 +29,7 @@ export const IsometricIcon = ({ url, onImageLoaded }: Props) => {
src={url}
sx={{
position: 'absolute',
width: PROJECTED_TILE_SIZE.width * 0.8,
width: PROJECTED_TILE_SIZE.width * 0.8 * scale,
top: -size.height,
left: -size.width / 2,
pointerEvents: 'none'

View File

@@ -24,7 +24,7 @@ export const NonIsometricIcon = ({ icon }: Props) => {
component="img"
src={icon.url}
alt={`icon-${icon.id}`}
sx={{ width: PROJECTED_TILE_SIZE.width * 0.7 }}
sx={{ width: PROJECTED_TILE_SIZE.width * 0.7 * (icon.scale || 1) }}
/>
</Box>
</Box>

View File

@@ -31,6 +31,7 @@ export const useIcon = (id: string | undefined) => {
return (
<IsometricIcon
url={icon.url}
scale={icon.scale || 1}
onImageLoaded={() => {
setHasLoaded(true);
}}

View File

@@ -6,7 +6,8 @@ export const iconSchema = z.object({
name: constrainedStrings.name,
url: z.string(),
collection: constrainedStrings.name.optional(),
isIsometric: z.boolean().optional()
isIsometric: z.boolean().optional(),
scale: z.number().min(0.1).max(3).optional()
});
export const iconsSchema = z.array(iconSchema);