fix text shape place

This commit is contained in:
hamid zarghami
2026-01-07 10:11:10 +03:30
parent 9479a3425b
commit 9237bf0ba4
3 changed files with 86 additions and 45 deletions
+30 -30
View File
@@ -14,7 +14,7 @@
"fontFamily": "irancell", "fontFamily": "irancell",
"fontWeight": "bold", "fontWeight": "bold",
"fill": "#1a1a1a", "fill": "#1a1a1a",
"opacity": 1 "opacity": 50
}, },
{ {
"id": "image-1", "id": "image-1",
@@ -24,7 +24,7 @@
"width": 300, "width": 300,
"height": 300, "height": 300,
"imageUrl": "https://picsum.photos/200/300", "imageUrl": "https://picsum.photos/200/300",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "rectangle-1", "id": "rectangle-1",
@@ -37,7 +37,7 @@
"stroke": "#1e40af", "stroke": "#1e40af",
"strokeWidth": 2, "strokeWidth": 2,
"shapeType": "square", "shapeType": "square",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "circle-1", "id": "circle-1",
@@ -50,7 +50,7 @@
"stroke": "#dc2626", "stroke": "#dc2626",
"strokeWidth": 2, "strokeWidth": 2,
"shapeType": "circle", "shapeType": "circle",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "video-1", "id": "video-1",
@@ -60,7 +60,7 @@
"width": 400, "width": 400,
"height": 300, "height": 300,
"videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4", "videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4",
"opacity": 1 "opacity": 100
} }
] ]
}, },
@@ -78,7 +78,7 @@
"fontFamily": "irancell", "fontFamily": "irancell",
"fontWeight": "bold", "fontWeight": "bold",
"fill": "#000000", "fill": "#000000",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "line-1", "id": "line-1",
@@ -89,7 +89,7 @@
"height": 2, "height": 2,
"stroke": "#64748b", "stroke": "#64748b",
"strokeWidth": 3, "strokeWidth": 3,
"opacity": 1 "opacity": 100
}, },
{ {
"id": "arrow-1", "id": "arrow-1",
@@ -100,7 +100,7 @@
"height": 100, "height": 100,
"stroke": "#f59e0b", "stroke": "#f59e0b",
"strokeWidth": 4, "strokeWidth": 4,
"opacity": 1 "opacity": 100
}, },
{ {
"id": "triangle-1", "id": "triangle-1",
@@ -113,7 +113,7 @@
"stroke": "#059669", "stroke": "#059669",
"strokeWidth": 2, "strokeWidth": 2,
"shapeType": "triangle", "shapeType": "triangle",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "link-1", "id": "link-1",
@@ -126,7 +126,7 @@
"fontSize": 18, "fontSize": 18,
"linkUrl": "https://example.com", "linkUrl": "https://example.com",
"fill": "#3b82f6", "fill": "#3b82f6",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "image-2", "id": "image-2",
@@ -136,7 +136,7 @@
"width": 250, "width": 250,
"height": 200, "height": 200,
"imageUrl": "https://picsum.photos/200/300", "imageUrl": "https://picsum.photos/200/300",
"opacity": 1 "opacity": 100
} }
] ]
}, },
@@ -154,7 +154,7 @@
"fontFamily": "irancell", "fontFamily": "irancell",
"fontWeight": "normal", "fontWeight": "normal",
"fill": "#1a1a1a", "fill": "#1a1a1a",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "grid-1", "id": "grid-1",
@@ -314,7 +314,7 @@
"stroke": "#808080", "stroke": "#808080",
"strokeWidth": 1 "strokeWidth": 1
}, },
"opacity": 1 "opacity": 100
}, },
{ {
"id": "abstract-1", "id": "abstract-1",
@@ -327,7 +327,7 @@
"stroke": "#7c3aed", "stroke": "#7c3aed",
"strokeWidth": 2, "strokeWidth": 2,
"shapeType": "abstract", "shapeType": "abstract",
"opacity": 1 "opacity": 100
} }
] ]
}, },
@@ -345,7 +345,7 @@
"fontFamily": "irancell", "fontFamily": "irancell",
"fontWeight": "bold", "fontWeight": "bold",
"fill": "#1a1a1a", "fill": "#1a1a1a",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "video-2", "id": "video-2",
@@ -355,7 +355,7 @@
"width": 350, "width": 350,
"height": 250, "height": 250,
"videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4", "videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "image-3", "id": "image-3",
@@ -365,7 +365,7 @@
"width": 250, "width": 250,
"height": 250, "height": 250,
"imageUrl": "https://picsum.photos/200/300", "imageUrl": "https://picsum.photos/200/300",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "line-2", "id": "line-2",
@@ -376,7 +376,7 @@
"height": 2, "height": 2,
"stroke": "#94a3b8", "stroke": "#94a3b8",
"strokeWidth": 2, "strokeWidth": 2,
"opacity": 1 "opacity": 100
}, },
{ {
"id": "arrow-2", "id": "arrow-2",
@@ -387,7 +387,7 @@
"height": 80, "height": 80,
"stroke": "#ec4899", "stroke": "#ec4899",
"strokeWidth": 5, "strokeWidth": 5,
"opacity": 1 "opacity": 100
}, },
{ {
"id": "circle-2", "id": "circle-2",
@@ -400,7 +400,7 @@
"stroke": "#d97706", "stroke": "#d97706",
"strokeWidth": 3, "strokeWidth": 3,
"shapeType": "circle", "shapeType": "circle",
"opacity": 1 "opacity": 100
} }
] ]
}, },
@@ -418,7 +418,7 @@
"fontFamily": "irancell", "fontFamily": "irancell",
"fontWeight": "bold", "fontWeight": "bold",
"fill": "#1a1a1a", "fill": "#1a1a1a",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "rectangle-2", "id": "rectangle-2",
@@ -432,7 +432,7 @@
"strokeWidth": 2, "strokeWidth": 2,
"shapeType": "square", "shapeType": "square",
"rotation": 15, "rotation": 15,
"opacity": 1 "opacity": 100
}, },
{ {
"id": "triangle-2", "id": "triangle-2",
@@ -446,7 +446,7 @@
"strokeWidth": 2, "strokeWidth": 2,
"shapeType": "triangle", "shapeType": "triangle",
"rotation": 30, "rotation": 30,
"opacity": 1 "opacity": 100
}, },
{ {
"id": "abstract-2", "id": "abstract-2",
@@ -460,7 +460,7 @@
"strokeWidth": 2, "strokeWidth": 2,
"shapeType": "abstract", "shapeType": "abstract",
"rotation": -20, "rotation": -20,
"opacity": 1 "opacity": 100
}, },
{ {
"id": "image-4", "id": "image-4",
@@ -471,7 +471,7 @@
"height": 150, "height": 150,
"imageUrl": "https://picsum.photos/200/300", "imageUrl": "https://picsum.photos/200/300",
"rotation": 10, "rotation": 10,
"opacity": 1 "opacity": 100
}, },
{ {
"id": "video-3", "id": "video-3",
@@ -481,7 +481,7 @@
"width": 300, "width": 300,
"height": 200, "height": 200,
"videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4", "videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "link-2", "id": "link-2",
@@ -494,7 +494,7 @@
"fontSize": 20, "fontSize": 20,
"linkUrl": "https://example.com", "linkUrl": "https://example.com",
"fill": "#6366f1", "fill": "#6366f1",
"opacity": 1 "opacity": 100
}, },
{ {
"id": "line-3", "id": "line-3",
@@ -506,7 +506,7 @@
"stroke": "#f97316", "stroke": "#f97316",
"strokeWidth": 4, "strokeWidth": 4,
"rotation": 45, "rotation": 45,
"opacity": 1 "opacity": 100
}, },
{ {
"id": "arrow-3", "id": "arrow-3",
@@ -518,7 +518,7 @@
"stroke": "#84cc16", "stroke": "#84cc16",
"strokeWidth": 6, "strokeWidth": 6,
"rotation": -30, "rotation": -30,
"opacity": 1 "opacity": 100
}, },
{ {
"id": "text-6", "id": "text-6",
@@ -531,7 +531,7 @@
"fontWeight": "normal", "fontWeight": "normal",
"fill": "#475569", "fill": "#475569",
"letterSpacing": 1, "letterSpacing": 1,
"opacity": 1 "opacity": 100
} }
] ]
} }
+46 -9
View File
@@ -15,6 +15,35 @@ type BookPageProps = {
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند * نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
*/ */
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 }, ref) => { const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 }, ref) => {
// تابع برای تبدیل opacity به رنگ (مثل editor)
// در editor، getColorWithOpacity انتظار opacity 0-100 دارد
// در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است
// پس opacity را مستقیماً استفاده می‌کنیم
const getColorWithOpacity = (fill?: string, opacity?: number): string => {
if (!fill) return '#000000';
// opacity در EditorObject به صورت 0-100 است (بعد از تبدیل در dataTransformer)
// در editor getColorWithOpacity انتظار 0-100 دارد
const opacityValue = opacity !== undefined ? opacity : 100;
// اگر opacity 100% باشد، رنگ را بدون تغییر برگردان (مثل editor)
if (opacityValue >= 100) return fill;
const hex = fill.replace('#', '');
// اگر hex کوتاه‌تر از 6 کاراکتر باشد، padding اضافه کن
const paddedHex = hex.length === 3
? hex.split('').map(char => char + char).join('')
: hex;
const r = parseInt(paddedHex.substring(0, 2), 16);
const g = parseInt(paddedHex.substring(2, 4), 16);
const b = parseInt(paddedHex.substring(4, 6), 16);
// opacityValue را به alpha تبدیل کن (0-100 -> 0-1) مثل editor
const alpha = opacityValue / 100;
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
const renderObject = (obj: EditorObject, index: number) => { const renderObject = (obj: EditorObject, index: number) => {
const baseStyle: React.CSSProperties = { const baseStyle: React.CSSProperties = {
position: 'absolute', position: 'absolute',
@@ -30,16 +59,19 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 },
} }
switch (obj.type) { switch (obj.type) {
case 'text': case 'text': {
// برای text، opacity در رنگ اعمال می‌شود، پس باید opacity را از baseStyle حذف کنیم
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { opacity, ...textBaseStyle } = baseStyle;
return ( return (
<div <div
key={obj.id || index} key={obj.id || index}
style={{ style={{
...baseStyle, ...textBaseStyle,
fontSize: `${(obj.fontSize || 16) * scale}px`, fontSize: `${(obj.fontSize || 16) * scale}px`,
fontFamily: obj.fontFamily || 'irancell, sans-serif', fontFamily: obj.fontFamily || 'irancell, sans-serif',
fontWeight: obj.fontWeight || 'normal', fontWeight: obj.fontWeight || 'normal',
color: obj.fill || '#000000', color: getColorWithOpacity(obj.fill, obj.opacity),
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined, letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined,
}} }}
@@ -47,6 +79,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 },
{obj.text || ''} {obj.text || ''}
</div> </div>
); );
}
case 'image': case 'image':
return ( return (
@@ -58,7 +91,8 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 },
...baseStyle, ...baseStyle,
width: obj.width ? `${obj.width * scale}px` : 'auto', width: obj.width ? `${obj.width * scale}px` : 'auto',
height: obj.height ? `${obj.height * scale}px` : 'auto', height: obj.height ? `${obj.height * scale}px` : 'auto',
objectFit: 'contain', objectFit: 'fill', // مشابه Konva که image را با width و height مشخص شده رندر می‌کند
zIndex: index, // برای حفظ ترتیب رندر
}} }}
/> />
); );
@@ -102,15 +136,14 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 },
</a> </a>
); );
case 'rectangle': case 'rectangle': {
if (obj.shapeType === 'circle') { if (obj.shapeType === 'circle') {
// در editor، circle با مرکز در x, y رندر می‌شود (radius = width/2) // در editor، circle با مرکز در x, y رندر می‌شود (radius = width/2)
// اما در viewer-data.json ممکن است x, y به عنوان گوشه بالا-چپ bounding box باشد // در dataTransformer از گوشه بالا-چپ به مرکز تبدیل شده است
// برای سازگاری با editor، فرض می‌کنیم x, y مرکز است
const radius = ((obj.width || 100) / 2) * scale; const radius = ((obj.width || 100) / 2) * scale;
const centerX = (obj.x || 0) * scale; const centerX = (obj.x || 0) * scale;
const centerY = (obj.y || 0) * scale; const centerY = (obj.y || 0) * scale;
return ( return (
<div <div
key={obj.id || index} key={obj.id || index}
@@ -128,6 +161,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 },
opacity: obj.opacity ?? 1, opacity: obj.opacity ?? 1,
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined, transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
transformOrigin: 'center center', transformOrigin: 'center center',
zIndex: index, // برای حفظ ترتیب رندر
}} }}
/> />
); );
@@ -204,9 +238,11 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 },
border: obj.stroke border: obj.stroke
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}` ? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
: 'none', : 'none',
zIndex: index, // برای حفظ ترتیب رندر
}; };
return <div key={obj.id || index} style={shapeStyle} />; return <div key={obj.id || index} style={shapeStyle} />;
}
case 'line': { case 'line': {
// در EditorObject، width و height به عنوان مختصات انتهایی استفاده می‌شوند // در EditorObject، width و height به عنوان مختصات انتهایی استفاده می‌شوند
@@ -283,7 +319,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 },
); );
} }
case 'grid': case 'grid': {
if (!obj.tableData) return null; if (!obj.tableData) return null;
const { rows, cols, cells, cellWidth, cellHeight, stroke, strokeWidth } = const { rows, cols, cells, cellWidth, cellHeight, stroke, strokeWidth } =
@@ -325,6 +361,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 },
)} )}
</div> </div>
); );
}
default: default:
return null; return null;
+10 -6
View File
@@ -44,7 +44,10 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] {
type: obj.type as EditorObject['type'], type: obj.type as EditorObject['type'],
x: obj.x, x: obj.x,
y: obj.y, y: obj.y,
opacity: obj.opacity ?? 1, // در JSON، opacity به صورت 0-100 است (درصد)
// در editor هم opacity به صورت 0-100 است
// پس opacity را مستقیماً استفاده می‌کنیم
opacity: obj.opacity !== undefined ? obj.opacity : 100,
visible: true, visible: true,
}; };
@@ -66,11 +69,12 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] {
// برای circle در viewer-data.json، x, y به عنوان گوشه بالا-چپ bounding box است // برای circle در viewer-data.json، x, y به عنوان گوشه بالا-چپ bounding box است
// اما در editor، circle با مرکز در x, y ذخیره می‌شود // اما در editor، circle با مرکز در x, y ذخیره می‌شود
// پس باید تبدیل کنیم // پس باید تبدیل کنیم
if (obj.type === 'rectangle' && obj.shapeType === 'circle') { // اما بر اساس بررسی، در JSON موقعیت به عنوان مرکز ذخیره شده است
// تبدیل از گوشه بالا-چپ به مرکز // پس نباید تبدیل کنیم - مستقیماً استفاده می‌کنیم
baseObject.x = obj.x + (obj.width || 100) / 2; // اگر در آینده JSON به صورت گوشه بالا-چپ export شود، باید تبدیل کنیم:
baseObject.y = obj.y + (obj.height || 100) / 2; // baseObject.x = obj.x + (obj.width || 100) / 2;
} // baseObject.y = obj.y + (obj.height || 100) / 2;
// اما فعلاً JSON به صورت مرکز است، پس تبدیل نمی‌کنیم
if (obj.fill !== undefined) baseObject.fill = obj.fill; if (obj.fill !== undefined) baseObject.fill = obj.fill;
if (obj.stroke !== undefined) baseObject.stroke = obj.stroke; if (obj.stroke !== undefined) baseObject.stroke = obj.stroke;
if (obj.strokeWidth !== undefined) baseObject.strokeWidth = obj.strokeWidth; if (obj.strokeWidth !== undefined) baseObject.strokeWidth = obj.strokeWidth;