♻️ refactor!: massive refactor

This commit is contained in:
2026-05-31 03:04:30 +07:00
parent c1f12c7201
commit df3c944547
86 changed files with 3691 additions and 1129 deletions
+141 -64
View File
@@ -2,7 +2,16 @@
import React from "react";
import { useParams } from "next/navigation";
import { MousePointer2, Hand, ScanFace, PaintBucket, Brush, Type, ImagePlus } from "lucide-react";
import { getLayerRuntimeSource } from "@pien-studio/types";
import {
MousePointer2,
Hand,
ScanFace,
PaintBucket,
Brush,
Type,
ImagePlus,
} from "lucide-react";
import { useEditorStore } from "../../../store/editor-store";
import { useUiStore, isDarkTheme } from "../../../store/ui-store";
import { CanvasSizeModal } from "../../../components/canvas-size-modal";
@@ -27,7 +36,10 @@ import { useAssetCleanupJob } from "../../../hooks/use-asset-cleanup-job";
import { useTranslations } from "../../../hooks/use-translations";
import type { AspectRatio } from "@pien-studio/types";
const TOOL_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
const TOOL_ICONS: Record<
string,
React.ComponentType<{ className?: string }>
> = {
pointer: MousePointer2,
hand: Hand,
face: ScanFace,
@@ -89,7 +101,8 @@ export default function EditorPage() {
const { theme, hydrate } = useUiStore((s) => s);
const { t } = useTranslations();
const { headerLabels, contextMenuLabels, mobileLabels } = useEditorLabels(t);
const { contextMenu, openContextMenu, closeContextMenu } = useEditorContextMenu();
const { contextMenu, openContextMenu, closeContextMenu } =
useEditorContextMenu();
const [canvasModalOpen, setCanvasModalOpen] = React.useState(false);
const [faceMlErrorModalOpen, setFaceMlErrorModalOpen] = React.useState(false);
const [fillColor, setFillColor] = React.useState("#ff0000");
@@ -98,16 +111,21 @@ export default function EditorPage() {
const [brushSize, setBrushSize] = React.useState(20);
const [brushOpacity, setBrushOpacity] = React.useState(1);
const [brushHardness, setBrushHardness] = React.useState(0.8);
const previousFaceStatusRef = React.useRef<"idle" | "detecting" | "unsupported">("idle");
const previousFaceStatusRef = React.useRef<
"idle" | "detecting" | "unsupported"
>("idle");
const imageInputRef = React.useRef<HTMLInputElement | null>(null);
const selectedImageLayer = React.useMemo(() => {
if (!selectedLayer || selectedLayer.type !== "raster" || !selectedLayer.sourceUri) {
const sourceUri = selectedLayer
? getLayerRuntimeSource(selectedLayer)
: undefined;
if (!selectedLayer || selectedLayer.type !== "raster" || !sourceUri) {
return null;
}
return {
id: selectedLayer.id,
sourceUri: selectedLayer.sourceUri,
sourceUri,
width: selectedLayer.width,
height: selectedLayer.height,
};
@@ -118,11 +136,12 @@ export default function EditorPage() {
return state.selectedLayerId === layerId;
}, []);
const { faceDetections, faceDetectionsLayerId, facePreviews, faceStatus } = useFaceDetection({
tool,
selectedImageLayer,
activeLayerStillSelected: isLayerStillSelected,
});
const { faceDetections, faceDetectionsLayerId, facePreviews, faceStatus } =
useFaceDetection({
tool,
selectedImageLayer,
activeLayerStillSelected: isLayerStillSelected,
});
const {
blurMethod,
@@ -204,7 +223,10 @@ export default function EditorPage() {
});
React.useEffect(() => {
if (faceStatus === "unsupported" && previousFaceStatusRef.current !== "unsupported") {
if (
faceStatus === "unsupported" &&
previousFaceStatusRef.current !== "unsupported"
) {
setFaceMlErrorModalOpen(true);
}
previousFaceStatusRef.current = faceStatus;
@@ -217,41 +239,53 @@ export default function EditorPage() {
event.target.value = "";
}
function handleCanvasApply(width: number, height: number, aspect: AspectRatio) {
function handleCanvasApply(
width: number,
height: number,
aspect: AspectRatio,
) {
void aspect;
setCanvasSize(width, height);
}
const handleFillLayer = React.useCallback(async (layerId: string, x: number, y: number) => {
const layer = project.layers.find((l) => l.id === layerId);
if (!layer || layer.type !== "raster" || !layer.sourceUri) return;
const layerWidth = layer.width ?? Math.round(200 * layer.scale);
const layerHeight = layer.height ?? Math.round(150 * layer.scale);
// x/y are in layer CSS-pixel space; scale to image pixel space
const img = new Image();
const uri = layer.sourceUri;
const color = fillColor;
const tolerance = fillTolerance;
img.onload = () => {
const scaleX = img.naturalWidth / layerWidth;
const scaleY = img.naturalHeight / layerHeight;
const pixelX = x * scaleX;
const pixelY = y * scaleY;
floodFillDataUrl(uri, pixelX, pixelY, color, tolerance).then((nextUri) => {
if (nextUri !== uri) updateImageLayerSource(layerId, nextUri);
}).catch(() => {});
};
img.src = uri;
}, [project.layers, fillColor, fillTolerance, updateImageLayerSource]);
const handleFillLayer = React.useCallback(
async (layerId: string, x: number, y: number) => {
const layer = project.layers.find((l) => l.id === layerId);
const uri = layer ? getLayerRuntimeSource(layer) : undefined;
if (!layer || layer.type !== "raster" || !uri) return;
const layerWidth = layer.width;
const layerHeight = layer.height;
const img = new Image();
const color = fillColor;
const tolerance = fillTolerance;
img.onload = () => {
const scaleX = img.naturalWidth / layerWidth;
const scaleY = img.naturalHeight / layerHeight;
const pixelX = x * scaleX;
const pixelY = y * scaleY;
floodFillDataUrl(uri, pixelX, pixelY, color, tolerance)
.then((nextUri) => {
if (nextUri !== uri) updateImageLayerSource(layerId, nextUri);
})
.catch(() => {});
};
img.src = uri;
},
[project.layers, fillColor, fillTolerance, updateImageLayerSource],
);
const handleBrushCommit = React.useCallback(async (layerId: string, stroke: BrushStroke) => {
const layer = project.layers.find((l) => l.id === layerId);
if (!layer || layer.type !== "raster" || !layer.sourceUri) return;
try {
const nextUri = await commitStroke(layer.sourceUri, stroke);
updateImageLayerSource(layerId, nextUri);
} catch {}
}, [project.layers, updateImageLayerSource]);
const handleBrushCommit = React.useCallback(
async (layerId: string, stroke: BrushStroke) => {
const layer = project.layers.find((l) => l.id === layerId);
const sourceUri = layer ? getLayerRuntimeSource(layer) : undefined;
if (!layer || layer.type !== "raster" || !sourceUri) return;
try {
const nextUri = await commitStroke(sourceUri, stroke);
updateImageLayerSource(layerId, nextUri);
} catch {}
},
[project.layers, updateImageLayerSource],
);
const handleCreateFillLayer = React.useCallback(() => {
const canvas = document.createElement("canvas");
@@ -275,23 +309,42 @@ export default function EditorPage() {
imageInputRef.current?.click();
}, []);
const toolControllers = React.useMemo<EditorToolController[]>(() => [
{ kind: "mode", id: "pointer", label: t("editor.toolPointer") },
{ kind: "mode", id: "hand", label: t("editor.toolPan") },
{ kind: "mode", id: "face", label: t("editor.toolFace") },
{ kind: "mode", id: "fill", label: t("editor.toolFill") },
{ kind: "mode", id: "brush", label: t("editor.toolBrush") },
{ kind: "action", id: "add-text", label: t("editor.toolText"), run: () => addLayerByType("text") },
{ kind: "action", id: "import-image", label: t("editor.toolImage"), run: handleImportImageClick },
], [addLayerByType, handleImportImageClick, t]);
const toolControllers = React.useMemo<EditorToolController[]>(
() => [
{ kind: "mode", id: "pointer", label: t("editor.toolPointer") },
{ kind: "mode", id: "hand", label: t("editor.toolPan") },
{ kind: "mode", id: "face", label: t("editor.toolFace") },
{ kind: "mode", id: "fill", label: t("editor.toolFill") },
{ kind: "mode", id: "brush", label: t("editor.toolBrush") },
{
kind: "action",
id: "add-text",
label: t("editor.toolText"),
run: () => addLayerByType("text"),
},
{
kind: "action",
id: "import-image",
label: t("editor.toolImage"),
run: handleImportImageClick,
},
],
[addLayerByType, handleImportImageClick, t],
);
const canvasBindings = {
onMoveLayer: (_id: string, x: number, y: number) => setSelectedLayerPositionDraft(x, y),
onMoveLayerEnd: (_id: string, x: number, y: number) => setSelectedLayerPosition(x, y),
onResizeLayer: (_id: string, width: number, height: number) => setSelectedLayerSizeDraft(width, height),
onResizeLayerEnd: (_id: string, width: number, height: number) => setSelectedLayerSize(width, height),
onRotateLayer: (_id: string, rotation: number) => setSelectedLayerRotationDraft(rotation),
onRotateLayerEnd: (_id: string, rotation: number) => setSelectedLayerRotation(rotation),
onMoveLayer: (_id: string, x: number, y: number) =>
setSelectedLayerPositionDraft(x, y),
onMoveLayerEnd: (_id: string, x: number, y: number) =>
setSelectedLayerPosition(x, y),
onResizeLayer: (_id: string, width: number, height: number) =>
setSelectedLayerSizeDraft(width, height),
onResizeLayerEnd: (_id: string, width: number, height: number) =>
setSelectedLayerSize(width, height),
onRotateLayer: (_id: string, rotation: number) =>
setSelectedLayerRotationDraft(rotation),
onRotateLayerEnd: (_id: string, rotation: number) =>
setSelectedLayerRotation(rotation),
onInteractionStart: startTransaction,
onInteractionEnd: commitTransaction,
};
@@ -361,7 +414,12 @@ export default function EditorPage() {
onCut={cutSelectedLayer}
onPaste={pasteLayer}
onFillLayer={handleFillLayer}
brushOptions={{ color: brushColor, size: brushSize, opacity: brushOpacity, hardness: brushHardness }}
brushOptions={{
color: brushColor,
size: brushSize,
opacity: brushOpacity,
hardness: brushHardness,
}}
onBrushCommit={handleBrushCommit}
/>
@@ -397,7 +455,7 @@ export default function EditorPage() {
selectedFaceIndices={selectedFaceIndices}
onSelectLayer={selectLayer}
onSetLayerVisible={setLayerVisible}
onSetEffectEnabled={(layerId, kind, enabled) => setEffectEnabled(layerId, kind as import("@pien-studio/types").LayerEffect["kind"], enabled)}
onSetEffectEnabled={setEffectEnabled}
onMoveLayerOrder={moveSelectedLayerOrder}
onRemoveSelectedLayer={removeSelectedLayer}
onUndo={undo}
@@ -437,7 +495,13 @@ export default function EditorPage() {
onInteractionStart={canvasBindings.onInteractionStart}
onInteractionEnd={canvasBindings.onInteractionEnd}
/>
<input ref={imageInputRef} type="file" accept="image/*" className="hidden" onChange={handleImageImport} />
<input
ref={imageInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleImageImport}
/>
<CanvasSizeModal
isOpen={canvasModalOpen}
@@ -450,15 +514,24 @@ export default function EditorPage() {
/>
{faceMlErrorModalOpen ? (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={() => setFaceMlErrorModalOpen(false)}>
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
onClick={() => setFaceMlErrorModalOpen(false)}
>
<div
className={`w-full max-w-sm rounded-2xl border p-5 shadow-2xl ${
isDark ? "border-white/15 bg-[#2b2d31]" : "border-black/15 bg-white"
isDark
? "border-white/15 bg-[#2b2d31]"
: "border-black/15 bg-white"
}`}
onClick={(event) => event.stopPropagation()}
>
<div className="mb-3 flex items-center justify-between">
<h2 className={`text-base font-semibold ${isDark ? "text-[#f5f7fa]" : "text-[#1f2430]"}`}>{t("editor.faceTool")}</h2>
<h2
className={`text-base font-semibold ${isDark ? "text-[#f5f7fa]" : "text-[#1f2430]"}`}
>
{t("editor.faceTool")}
</h2>
<button
type="button"
onClick={() => setFaceMlErrorModalOpen(false)}
@@ -467,7 +540,11 @@ export default function EditorPage() {
</button>
</div>
<p className={`text-sm ${isDark ? "text-[#d7dae0]" : "text-[#374151]"}`}>{t("editor.faceMlFailed")}</p>
<p
className={`text-sm ${isDark ? "text-[#d7dae0]" : "text-[#374151]"}`}
>
{t("editor.faceMlFailed")}
</p>
<div className="mt-5 flex justify-end">
<button
type="button"