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