added shape color change
This commit is contained in:
1 parent
d44bd5edd9
commit
895159afbf
4 files changed
+245
-44
No files matched your search
@@ -306,7 +306,7 @@ export const PaintSurface = ({
|
||||
y: stagePoint.y,
|
||||
width: 1,
|
||||
height: 1,
|
||||
fill: "#e0e0e0",
|
||||
fill: store.shapeColor, // Use current shape color from store
|
||||
stroke: "#444",
|
||||
strokeWidth: 2,
|
||||
draggable: true,
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
faCircle,
|
||||
faSquare,
|
||||
faTriangle,
|
||||
faDroplet,
|
||||
} from "@fortawesome/pro-regular-svg-icons";
|
||||
import "../../App.css";
|
||||
import { HsvaColorPicker, HsvaColor } from "react-colorful";
|
||||
@@ -62,6 +63,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
className = "",
|
||||
}) => {
|
||||
/* ------------------------------------------------ state ---------- */
|
||||
const store = useSceneStore();
|
||||
const [open, setOpen] = useState<string | null>(null);
|
||||
|
||||
const [brushSize, setBrushSize] = useState(16);
|
||||
@@ -79,6 +81,52 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
a: 1,
|
||||
});
|
||||
|
||||
// Get selected nodes for shape color picker
|
||||
const selectedNodes =
|
||||
store.selectedNodeIds.length > 0
|
||||
? store.nodes.filter((node) => store.selectedNodeIds.includes(node.id))
|
||||
: [];
|
||||
|
||||
// Get first selected node for color preview (when multiple selected, show first one's color)
|
||||
const firstSelectedNode = selectedNodes.length > 0 ? selectedNodes[0] : null;
|
||||
|
||||
// State for shape color picker
|
||||
const [shapeHsva, setShapeHsva] = useState<HsvaColor>({
|
||||
h: 240, // Blue hue
|
||||
s: 100, // Full saturation
|
||||
v: 70, // Medium brightness blue
|
||||
a: 1,
|
||||
});
|
||||
|
||||
// Update shape color picker when selection changes or store's shapeColor changes
|
||||
React.useEffect(() => {
|
||||
// Use selected node's color if available, otherwise use store's default shapeColor
|
||||
const colorToUse = firstSelectedNode?.fill || store.shapeColor;
|
||||
|
||||
if (colorToUse && colorToUse.startsWith("#")) {
|
||||
// Convert hex to HSVA (simplified - might want to change to proper converter later)
|
||||
const hex = colorToUse;
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
|
||||
// Convert RGB to HSV (simplified)
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
const v = max / 255;
|
||||
const s = max === 0 ? 0 : (max - min) / max;
|
||||
|
||||
let h = 0;
|
||||
if (max !== min) {
|
||||
if (max === r) h = (60 * ((g - b) / (max - min)) + 360) % 360;
|
||||
else if (max === g) h = (60 * ((b - r) / (max - min)) + 120) % 360;
|
||||
else h = (60 * ((r - g) / (max - min)) + 240) % 360;
|
||||
}
|
||||
|
||||
setShapeHsva({ h, s: s * 100, v: v * 100, a: 1 });
|
||||
}
|
||||
}, [firstSelectedNode?.fill, store.shapeColor]);
|
||||
|
||||
/* debounced parent calls */
|
||||
const sendPaint = useDebounced<
|
||||
(hex: string, size: number, opacity: number) => void,
|
||||
@@ -90,6 +138,24 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
75,
|
||||
);
|
||||
|
||||
// Debounced shape color update - now updates ALL selected nodes AND store's shapeColor
|
||||
const sendShapeColor = useDebounced<(hex: string) => void, [string]>(
|
||||
(hex: string) => {
|
||||
// Update the store's default shape color for future shapes
|
||||
store.setShapeColor(hex);
|
||||
|
||||
// Update all selected nodes
|
||||
selectedNodes.forEach((node) => {
|
||||
store.updateNode(node.id, { fill: hex }, false); // Don't save state for each individual update
|
||||
});
|
||||
// Save state once after all updates
|
||||
if (selectedNodes.length > 0) {
|
||||
store.saveState();
|
||||
}
|
||||
},
|
||||
75,
|
||||
);
|
||||
|
||||
/* picker helper */
|
||||
const makePicker = (
|
||||
hsva: HsvaColor,
|
||||
@@ -138,9 +204,11 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
|
||||
const BgPopout = makePicker(bgHsva, setBgHsva, sendBg);
|
||||
|
||||
// Shape color popout
|
||||
const ShapePopout = makePicker(shapeHsva, setShapeHsva, sendShapeColor);
|
||||
|
||||
/* ------------------------------------------------ tools ---------- */
|
||||
|
||||
const store = useSceneStore(); // Use store directly
|
||||
const tools = [
|
||||
{
|
||||
id: "select",
|
||||
@@ -215,10 +283,19 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
id: "background",
|
||||
label: "Canvas Background",
|
||||
icon: (
|
||||
<span
|
||||
className="inline-block h-5 w-5 rounded-full border-2 border-white"
|
||||
style={{ backgroundColor: hsvaToHex(bgHsva) }}
|
||||
/>
|
||||
<div className="relative inline-flex h-5 w-5 items-center justify-center">
|
||||
<span
|
||||
className="absolute h-5 w-5 rounded-full border-2 border-white"
|
||||
style={{ backgroundColor: hsvaToHex(bgHsva) }}
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
icon={faDroplet}
|
||||
className="relative h-2 w-2 text-white drop-shadow-sm"
|
||||
style={{
|
||||
filter: "drop-shadow(0 0 1px rgba(0,0,0,0.8))",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
popout: BgPopout,
|
||||
},
|
||||
@@ -248,6 +325,32 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
<aside
|
||||
className={`glass ml-4 flex flex-col items-center gap-3 rounded-xl p-1.5 shadow-lg ${className}`}
|
||||
>
|
||||
{/* Shape Color Picker - appears above toolbar when shape is selected */}
|
||||
{selectedNodes.length > 0 && (
|
||||
<div className="glass absolute -top-14 left-1/2 -translate-x-1/2 rounded-xl border-2 border-primary/50 shadow-lg">
|
||||
<div className="relative">
|
||||
<button
|
||||
className="flex h-10 w-10 items-center justify-center rounded-lg border-2 border-transparent text-white transition-colors hover:bg-white/10"
|
||||
onMouseEnter={() => setOpen("shape-color")}
|
||||
>
|
||||
<span
|
||||
className="inline-block h-5 w-5 rounded-full border-2 border-white"
|
||||
style={{ backgroundColor: firstSelectedNode?.fill }}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{open === "shape-color" && (
|
||||
<div
|
||||
onMouseLeave={() => setOpen(null)}
|
||||
className="absolute left-14 top-1/2 -translate-y-1/2 rounded-xl border border-[#404040] bg-[#303030] transition-all duration-200 ease-in-out"
|
||||
>
|
||||
{ShapePopout}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tools.map((tool) => {
|
||||
if (tool.type === "separator") {
|
||||
return <div key={tool.id} className="my-1 h-px w-8 bg-white/15" />;
|
||||
@@ -291,37 +394,127 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
|
||||
return (
|
||||
<div key={id} className="relative">
|
||||
<Tooltip
|
||||
content={label}
|
||||
position="right"
|
||||
closeOnClick={true}
|
||||
className="ms-1 rounded-md px-3 py-1"
|
||||
delay={100}
|
||||
>
|
||||
{popout ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick?.();
|
||||
setOpen(open === id ? null : id);
|
||||
}}
|
||||
className={`${baseBtn} ${btnStyle}`}
|
||||
>
|
||||
{displayIcon}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick?.();
|
||||
}}
|
||||
className={`${baseBtn} ${btnStyle}`}
|
||||
>
|
||||
{displayIcon}
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
{/* Hide tooltip for brush tool when active and popover is open */}
|
||||
{!(id === "draw" && active && open === id) && (
|
||||
<Tooltip
|
||||
content={label}
|
||||
position="right"
|
||||
closeOnClick={true}
|
||||
className="ms-1 rounded-md px-3 py-1"
|
||||
delay={100}
|
||||
>
|
||||
{popout ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick?.();
|
||||
// For brush tool, don't toggle popout on click when active - only show on hover
|
||||
if (id !== "draw" || !active) {
|
||||
setOpen(open === id ? null : id);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
// Show popover on hover for active brush tool
|
||||
if (id === "draw" && active) {
|
||||
setOpen(id);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
// Don't immediately hide popover for active brush tool - let popover handle it
|
||||
if (id === "draw" && active) {
|
||||
// Add a small delay to allow moving to popover
|
||||
setTimeout(() => {
|
||||
// Only close if mouse is not over the popover
|
||||
const popoverElement = document.querySelector(
|
||||
`[data-popover="${id}"]`,
|
||||
);
|
||||
if (
|
||||
!popoverElement ||
|
||||
!popoverElement.matches(":hover")
|
||||
) {
|
||||
setOpen(null);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}}
|
||||
className={`${baseBtn} ${btnStyle}`}
|
||||
>
|
||||
{displayIcon}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick?.();
|
||||
}}
|
||||
className={`${baseBtn} ${btnStyle}`}
|
||||
>
|
||||
{displayIcon}
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Render button without tooltip when brush is active and popover is open */}
|
||||
{id === "draw" && active && open === id && (
|
||||
<>
|
||||
{popout ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick?.();
|
||||
// For brush tool, don't toggle popout on click when active - only show on hover
|
||||
if (id !== "draw" || !active) {
|
||||
setOpen(open === id ? null : id);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
// Show popover on hover for active brush tool
|
||||
if (id === "draw" && active) {
|
||||
setOpen(id);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
// Don't immediately hide popover for active brush tool - let popover handle it
|
||||
if (id === "draw" && active) {
|
||||
// Add a small delay to allow moving to popover
|
||||
setTimeout(() => {
|
||||
// Only close if mouse is not over the popover
|
||||
const popoverElement = document.querySelector(
|
||||
`[data-popover="${id}"]`,
|
||||
);
|
||||
if (
|
||||
!popoverElement ||
|
||||
!popoverElement.matches(":hover")
|
||||
) {
|
||||
setOpen(null);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
className={`${baseBtn} ${btnStyle}`}
|
||||
>
|
||||
{displayIcon}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick?.();
|
||||
}}
|
||||
className={`${baseBtn} ${btnStyle}`}
|
||||
>
|
||||
{displayIcon}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{open === id && popout && (
|
||||
<div
|
||||
data-popover={id}
|
||||
onMouseEnter={() => {
|
||||
// Keep popover open when mouse enters
|
||||
if (id === "draw" && active) {
|
||||
setOpen(id);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setOpen(null);
|
||||
}}
|
||||
|
||||
@@ -74,6 +74,8 @@ interface SceneState {
|
||||
fillColor: string;
|
||||
// Currently selected shape when shape tool is active
|
||||
currentShape: "rectangle" | "circle" | "triangle" | null;
|
||||
// Default color for new shapes
|
||||
shapeColor: string;
|
||||
|
||||
// Cursor state
|
||||
cursorPosition: { x: number; y: number } | null;
|
||||
@@ -175,6 +177,7 @@ interface SceneState {
|
||||
|
||||
// Shape tool actions
|
||||
setCurrentShape: (shape: "rectangle" | "circle" | "triangle") => void;
|
||||
setShapeColor: (color: string) => void;
|
||||
|
||||
// Cursor actions
|
||||
setCursorPosition: (position: { x: number; y: number } | null) => void;
|
||||
@@ -255,6 +258,8 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
fillColor: "white",
|
||||
// Shape tool initial state
|
||||
currentShape: null,
|
||||
// Default color for new shapes
|
||||
shapeColor: "#4d79b3",
|
||||
|
||||
// Cursor initial state
|
||||
cursorPosition: null,
|
||||
@@ -403,8 +408,9 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
y: number,
|
||||
width = 100,
|
||||
height = 100,
|
||||
fill = "#e0e0e0",
|
||||
fill?: string,
|
||||
) => {
|
||||
const finalFill = fill || get().shapeColor;
|
||||
const node = new Node({
|
||||
id: generateId(),
|
||||
type: "rectangle",
|
||||
@@ -412,7 +418,7 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
fill,
|
||||
fill: finalFill,
|
||||
stroke: "#444",
|
||||
strokeWidth: 2,
|
||||
draggable: true,
|
||||
@@ -421,7 +427,8 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
get().setActiveTool("select"); // Switch to select tool after creating
|
||||
},
|
||||
|
||||
createCircle: (x: number, y: number, radius = 50, fill = "#f0f0f0") => {
|
||||
createCircle: (x: number, y: number, radius = 50, fill?: string) => {
|
||||
const finalFill = fill || get().shapeColor;
|
||||
const node = new Node({
|
||||
id: generateId(),
|
||||
type: "circle",
|
||||
@@ -429,7 +436,7 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
y,
|
||||
width: radius * 2,
|
||||
height: radius * 2,
|
||||
fill,
|
||||
fill: finalFill,
|
||||
stroke: "#333",
|
||||
strokeWidth: 2,
|
||||
draggable: true,
|
||||
@@ -443,8 +450,9 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
y: number,
|
||||
width = 100,
|
||||
height = 100,
|
||||
fill = "#d0d0d0",
|
||||
fill?: string,
|
||||
) => {
|
||||
const finalFill = fill || get().shapeColor;
|
||||
const node = new Node({
|
||||
id: generateId(),
|
||||
type: "triangle",
|
||||
@@ -452,7 +460,7 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
fill,
|
||||
fill: finalFill,
|
||||
stroke: "#555",
|
||||
strokeWidth: 2,
|
||||
draggable: true,
|
||||
@@ -983,6 +991,7 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
// Shape tool actions
|
||||
setCurrentShape: (shape: "rectangle" | "circle" | "triangle") =>
|
||||
set({ currentShape: shape }),
|
||||
setShapeColor: (color: string) => set({ shapeColor: color }),
|
||||
|
||||
// Cursor actions
|
||||
setCursorPosition: (position: { x: number; y: number } | null) =>
|
||||
|
||||
@@ -332,9 +332,7 @@ export const PromptBox2D = ({
|
||||
const generateResponse = await EnqueueContextualEditImage({
|
||||
model: EnqueueContextualEditImageModel.GptImage1,
|
||||
scene_image_media_token: snapshotMediaToken.data!,
|
||||
image_media_tokens: referenceImages.map(
|
||||
(image) => image.mediaToken
|
||||
),
|
||||
image_media_tokens: referenceImages.map((image) => image.mediaToken),
|
||||
disable_system_prompt: !useSystemPrompt,
|
||||
prompt: prompt,
|
||||
image_count: 1,
|
||||
@@ -623,10 +621,11 @@ export const PromptBox2D = ({
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="h-9 w-9"
|
||||
className="h-9"
|
||||
onClick={onFitPressed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFrame} className="h-4 w-4" />
|
||||
Fit
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user