mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add shape fill/stroke transparency, RGBA color picker, and dash styles
Closes #193. Shapes now support no-fill and no-stroke toggles for drawing outlines or fill-only shapes. Adds RGBA color picker with opacity control, stroke dash styles (solid/dashed/dotted), and i18n for all shape labels.
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
// apps/web/src/components/editor/common/shape-color-picker.tsx
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { RgbaColorPicker } from "react-colorful";
|
||||
import { createPortal } from "react-dom";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const CHECKERBOARD =
|
||||
"repeating-conic-gradient(rgba(128,128,128,0.3) 0% 25%, transparent 0% 50%) 0 0 / 8px 8px";
|
||||
|
||||
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||
const r = Number.parseInt(hex.slice(1, 3), 16);
|
||||
const g = Number.parseInt(hex.slice(3, 5), 16);
|
||||
const b = Number.parseInt(hex.slice(5, 7), 16);
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
function rgbToHex(r: number, g: number, b: number): string {
|
||||
const toHex = (n: number) => n.toString(16).padStart(2, "0");
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
}
|
||||
|
||||
interface ShapeColorPickerProps {
|
||||
label: string;
|
||||
color: string | null;
|
||||
opacity: number;
|
||||
onColorChange: (color: string | null) => void;
|
||||
onOpacityChange: (opacity: number) => void;
|
||||
allowNone?: boolean;
|
||||
}
|
||||
|
||||
export function ShapeColorPicker({
|
||||
label,
|
||||
color,
|
||||
opacity,
|
||||
onColorChange,
|
||||
onOpacityChange,
|
||||
allowNone = true,
|
||||
}: ShapeColorPickerProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [hexInput, setHexInput] = useState(color ?? "#000000");
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const [popoverPos, setPopoverPos] = useState({ top: 0, left: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (color) setHexInput(color);
|
||||
}, [color]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
if (buttonRef.current) {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
setPopoverPos({ top: rect.bottom + 6, left: rect.left });
|
||||
}
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const target = e.target as Node;
|
||||
if (
|
||||
popoverRef.current &&
|
||||
!popoverRef.current.contains(target) &&
|
||||
buttonRef.current &&
|
||||
!buttonRef.current.contains(target)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setOpen(false);
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const rgbaValue = color ? { ...hexToRgb(color), a: opacity } : { r: 0, g: 0, b: 0, a: 0 };
|
||||
|
||||
const handleRgbaChange = useCallback(
|
||||
(rgba: { r: number; g: number; b: number; a: number }) => {
|
||||
const hex = rgbToHex(rgba.r, rgba.g, rgba.b);
|
||||
onColorChange(hex);
|
||||
onOpacityChange(rgba.a);
|
||||
setHexInput(hex);
|
||||
},
|
||||
[onColorChange, onOpacityChange],
|
||||
);
|
||||
|
||||
const handleHexCommit = useCallback(
|
||||
(value: string) => {
|
||||
let hex = value.trim();
|
||||
if (!hex.startsWith("#")) hex = `#${hex}`;
|
||||
if (/^#[0-9a-f]{6}$/i.test(hex)) {
|
||||
onColorChange(hex.toLowerCase());
|
||||
setHexInput(hex.toLowerCase());
|
||||
} else {
|
||||
setHexInput(color ?? "#000000");
|
||||
}
|
||||
},
|
||||
[color, onColorChange],
|
||||
);
|
||||
|
||||
const isNone = color === null;
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center gap-1.5">
|
||||
<span className="text-xs text-muted-foreground">{label}</span>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className={cn(
|
||||
"relative w-6 h-6 rounded border border-border cursor-pointer",
|
||||
"hover:ring-1 hover:ring-primary/50 transition-shadow shrink-0",
|
||||
)}
|
||||
style={{
|
||||
background: isNone ? undefined : CHECKERBOARD,
|
||||
}}
|
||||
aria-label={`${label} color picker`}
|
||||
>
|
||||
{isNone ? (
|
||||
<span className="absolute inset-0 rounded bg-white">
|
||||
<span
|
||||
className="absolute inset-0 rounded"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to top right, transparent calc(50% - 1px), #ef4444 calc(50% - 1px), #ef4444 calc(50% + 1px), transparent calc(50% + 1px))",
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="absolute inset-0 rounded"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
opacity,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={popoverRef}
|
||||
className={cn(
|
||||
"fixed z-[9999] p-3 rounded-lg shadow-xl",
|
||||
"bg-card border border-border w-[220px]",
|
||||
)}
|
||||
style={{ top: popoverPos.top, left: popoverPos.left }}
|
||||
>
|
||||
{allowNone && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isNone) {
|
||||
onColorChange("#000000");
|
||||
setHexInput("#000000");
|
||||
} else {
|
||||
onColorChange(null);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"w-full mb-2 px-2 py-1 text-xs rounded border transition-colors",
|
||||
isNone
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "bg-muted text-muted-foreground border-border hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{isNone ? `${label}: None` : `No ${label}`}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!isNone && (
|
||||
<>
|
||||
<div className="[&_.react-colorful]:!w-full [&_.react-colorful]:!h-[150px] rounded overflow-hidden">
|
||||
<RgbaColorPicker color={rgbaValue} onChange={handleRgbaChange} />
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center gap-1.5">
|
||||
<span className="text-[10px] text-muted-foreground font-medium">#</span>
|
||||
<input
|
||||
type="text"
|
||||
value={hexInput.replace("#", "")}
|
||||
onChange={(e) => setHexInput(`#${e.target.value}`)}
|
||||
onBlur={(e) => handleHexCommit(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleHexCommit(e.currentTarget.value);
|
||||
}}
|
||||
maxLength={6}
|
||||
className="flex-1 h-6 text-xs bg-muted border border-border rounded px-1.5 font-mono"
|
||||
/>
|
||||
<span className="text-[10px] text-muted-foreground font-medium ms-1">
|
||||
{Math.round(opacity * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -408,6 +408,7 @@ function CanvasObjectRenderer({
|
||||
tension={a.tension}
|
||||
lineCap={a.lineCap}
|
||||
lineJoin={a.lineJoin}
|
||||
dash={a.dash}
|
||||
opacity={a.opacity}
|
||||
globalCompositeOperation={
|
||||
a.globalCompositeOperation as "source-over" | "destination-out" | undefined
|
||||
@@ -433,6 +434,7 @@ function CanvasObjectRenderer({
|
||||
stroke={a.stroke}
|
||||
strokeWidth={a.strokeWidth}
|
||||
cornerRadius={a.cornerRadius}
|
||||
dash={a.dash}
|
||||
rotation={a.rotation}
|
||||
opacity={a.opacity}
|
||||
draggable={draggable}
|
||||
@@ -457,6 +459,7 @@ function CanvasObjectRenderer({
|
||||
fill={a.fill}
|
||||
stroke={a.stroke}
|
||||
strokeWidth={a.strokeWidth}
|
||||
dash={a.dash}
|
||||
rotation={a.rotation}
|
||||
opacity={a.opacity}
|
||||
draggable={draggable}
|
||||
@@ -510,6 +513,7 @@ function CanvasObjectRenderer({
|
||||
strokeWidth={a.strokeWidth}
|
||||
pointerLength={a.pointerLength}
|
||||
pointerWidth={a.pointerWidth}
|
||||
dash={a.dash}
|
||||
rotation={a.rotation}
|
||||
opacity={a.opacity}
|
||||
draggable={draggable}
|
||||
@@ -534,6 +538,7 @@ function CanvasObjectRenderer({
|
||||
fill={a.fill}
|
||||
stroke={a.stroke}
|
||||
strokeWidth={a.strokeWidth}
|
||||
dash={a.dash}
|
||||
rotation={a.rotation}
|
||||
opacity={a.opacity}
|
||||
draggable={draggable}
|
||||
@@ -559,6 +564,7 @@ function CanvasObjectRenderer({
|
||||
fill={a.fill}
|
||||
stroke={a.stroke}
|
||||
strokeWidth={a.strokeWidth}
|
||||
dash={a.dash}
|
||||
rotation={a.rotation}
|
||||
opacity={a.opacity}
|
||||
draggable={draggable}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// apps/web/src/components/editor/options/shape-options.tsx
|
||||
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useEditorStore } from "@/stores/editor-store";
|
||||
import type { ToolType } from "@/types/editor";
|
||||
import type { StrokeDashStyle, ToolType } from "@/types/editor";
|
||||
import { ShapeColorPicker } from "../common/shape-color-picker";
|
||||
|
||||
const SHAPE_TOOLS = new Set<ToolType>([
|
||||
"shape-rect",
|
||||
@@ -12,38 +14,49 @@ const SHAPE_TOOLS = new Set<ToolType>([
|
||||
"shape-star",
|
||||
]);
|
||||
|
||||
const SHAPE_TYPE_OPTIONS: { value: ToolType; label: string }[] = [
|
||||
{ value: "shape-rect", label: "Rectangle" },
|
||||
{ value: "shape-ellipse", label: "Ellipse" },
|
||||
{ value: "shape-line", label: "Line" },
|
||||
{ value: "shape-arrow", label: "Arrow" },
|
||||
{ value: "shape-polygon", label: "Polygon" },
|
||||
{ value: "shape-star", label: "Star" },
|
||||
];
|
||||
|
||||
export function ShapeOptions() {
|
||||
const activeTool = useEditorStore((s) => s.activeTool);
|
||||
const setTool = useEditorStore((s) => s.setTool);
|
||||
const shapeFill = useEditorStore((s) => s.shapeFill);
|
||||
const shapeStroke = useEditorStore((s) => s.shapeStroke);
|
||||
const shapeStrokeWidth = useEditorStore((s) => s.shapeStrokeWidth);
|
||||
const shapeCornerRadius = useEditorStore((s) => s.shapeCornerRadius);
|
||||
const shapePolygonSides = useEditorStore((s) => s.shapePolygonSides);
|
||||
const shapeStarPoints = useEditorStore((s) => s.shapeStarPoints);
|
||||
const setShapeFill = useEditorStore((s) => s.setShapeFill);
|
||||
const setShapeStroke = useEditorStore((s) => s.setShapeStroke);
|
||||
const setShapeStrokeWidth = useEditorStore((s) => s.setShapeStrokeWidth);
|
||||
const setShapeCornerRadius = useEditorStore((s) => s.setShapeCornerRadius);
|
||||
const setShapePolygonSides = useEditorStore((s) => s.setShapePolygonSides);
|
||||
const setShapeStarPoints = useEditorStore((s) => s.setShapeStarPoints);
|
||||
const { t } = useTranslation();
|
||||
const s = t.editor.shapes;
|
||||
|
||||
const activeTool = useEditorStore((st) => st.activeTool);
|
||||
const setTool = useEditorStore((st) => st.setTool);
|
||||
const shapeFill = useEditorStore((st) => st.shapeFill);
|
||||
const shapeFillOpacity = useEditorStore((st) => st.shapeFillOpacity);
|
||||
const shapeStroke = useEditorStore((st) => st.shapeStroke);
|
||||
const shapeStrokeOpacity = useEditorStore((st) => st.shapeStrokeOpacity);
|
||||
const shapeStrokeWidth = useEditorStore((st) => st.shapeStrokeWidth);
|
||||
const shapeStrokeDash = useEditorStore((st) => st.shapeStrokeDash);
|
||||
const shapeCornerRadius = useEditorStore((st) => st.shapeCornerRadius);
|
||||
const shapePolygonSides = useEditorStore((st) => st.shapePolygonSides);
|
||||
const shapeStarPoints = useEditorStore((st) => st.shapeStarPoints);
|
||||
const setShapeFill = useEditorStore((st) => st.setShapeFill);
|
||||
const setShapeFillOpacity = useEditorStore((st) => st.setShapeFillOpacity);
|
||||
const setShapeStroke = useEditorStore((st) => st.setShapeStroke);
|
||||
const setShapeStrokeOpacity = useEditorStore((st) => st.setShapeStrokeOpacity);
|
||||
const setShapeStrokeWidth = useEditorStore((st) => st.setShapeStrokeWidth);
|
||||
const setShapeStrokeDash = useEditorStore((st) => st.setShapeStrokeDash);
|
||||
const setShapeCornerRadius = useEditorStore((st) => st.setShapeCornerRadius);
|
||||
const setShapePolygonSides = useEditorStore((st) => st.setShapePolygonSides);
|
||||
const setShapeStarPoints = useEditorStore((st) => st.setShapeStarPoints);
|
||||
|
||||
if (!SHAPE_TOOLS.has(activeTool)) return null;
|
||||
|
||||
const SHAPE_TYPE_OPTIONS: { value: ToolType; label: string }[] = [
|
||||
{ value: "shape-rect", label: s.rectangle },
|
||||
{ value: "shape-ellipse", label: s.ellipse },
|
||||
{ value: "shape-line", label: s.line },
|
||||
{ value: "shape-arrow", label: s.arrow },
|
||||
{ value: "shape-polygon", label: s.polygon },
|
||||
{ value: "shape-star", label: s.star },
|
||||
];
|
||||
|
||||
const showFill = activeTool !== "shape-line";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Shape type selector */}
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
Shape
|
||||
{s.shape}
|
||||
<select
|
||||
value={activeTool}
|
||||
onChange={(e) => setTool(e.target.value as ToolType)}
|
||||
@@ -58,30 +71,28 @@ export function ShapeOptions() {
|
||||
</label>
|
||||
|
||||
{/* Fill color */}
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
Fill
|
||||
<input
|
||||
type="color"
|
||||
value={shapeFill}
|
||||
onChange={(e) => setShapeFill(e.target.value)}
|
||||
className="w-6 h-6 border border-border rounded cursor-pointer"
|
||||
{showFill && (
|
||||
<ShapeColorPicker
|
||||
label={s.fill}
|
||||
color={shapeFill}
|
||||
opacity={shapeFillOpacity}
|
||||
onColorChange={setShapeFill}
|
||||
onOpacityChange={setShapeFillOpacity}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* Stroke color */}
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
Stroke
|
||||
<input
|
||||
type="color"
|
||||
value={shapeStroke}
|
||||
onChange={(e) => setShapeStroke(e.target.value)}
|
||||
className="w-6 h-6 border border-border rounded cursor-pointer"
|
||||
/>
|
||||
</label>
|
||||
<ShapeColorPicker
|
||||
label={s.stroke}
|
||||
color={shapeStroke}
|
||||
opacity={shapeStrokeOpacity}
|
||||
onColorChange={setShapeStroke}
|
||||
onOpacityChange={setShapeStrokeOpacity}
|
||||
/>
|
||||
|
||||
{/* Stroke width */}
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
Width
|
||||
{s.strokeWidth}
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
@@ -100,10 +111,24 @@ export function ShapeOptions() {
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* Stroke dash style */}
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{s.dashStyle}
|
||||
<select
|
||||
value={shapeStrokeDash}
|
||||
onChange={(e) => setShapeStrokeDash(e.target.value as StrokeDashStyle)}
|
||||
className="h-6 text-xs bg-muted border border-border rounded px-1"
|
||||
>
|
||||
<option value="solid">{s.solid}</option>
|
||||
<option value="dashed">{s.dashed}</option>
|
||||
<option value="dotted">{s.dotted}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{/* Corner radius (only for rect) */}
|
||||
{activeTool === "shape-rect" && (
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
Radius
|
||||
{s.cornerRadius}
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
@@ -126,7 +151,7 @@ export function ShapeOptions() {
|
||||
{/* Polygon sides */}
|
||||
{activeTool === "shape-polygon" && (
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
Sides
|
||||
{s.sides}
|
||||
<input
|
||||
type="number"
|
||||
min={3}
|
||||
@@ -141,7 +166,7 @@ export function ShapeOptions() {
|
||||
{/* Star points */}
|
||||
{activeTool === "shape-star" && (
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
Points
|
||||
{s.points}
|
||||
<input
|
||||
type="number"
|
||||
min={3}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type Konva from "konva";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { generateId } from "@/lib/utils";
|
||||
import { useEditorStore } from "@/stores/editor-store";
|
||||
import { dashStyleToArray, hexToRgba, useEditorStore } from "@/stores/editor-store";
|
||||
import type { CanvasObject, ToolType } from "@/types/editor";
|
||||
|
||||
interface PendingShape {
|
||||
@@ -46,6 +46,22 @@ function constrainToDimension(
|
||||
return { w, h };
|
||||
}
|
||||
|
||||
function computeShapeColors(state: {
|
||||
shapeFill: string | null;
|
||||
shapeFillOpacity: number;
|
||||
shapeStroke: string | null;
|
||||
shapeStrokeOpacity: number;
|
||||
shapeStrokeDash: "solid" | "dashed" | "dotted";
|
||||
shapeStrokeWidth: number;
|
||||
}) {
|
||||
const fill = state.shapeFill ? hexToRgba(state.shapeFill, state.shapeFillOpacity) : undefined;
|
||||
const stroke = state.shapeStroke
|
||||
? hexToRgba(state.shapeStroke, state.shapeStrokeOpacity)
|
||||
: undefined;
|
||||
const dash = dashStyleToArray(state.shapeStrokeDash, state.shapeStrokeWidth);
|
||||
return { fill, stroke, dash };
|
||||
}
|
||||
|
||||
export function useShapeTool() {
|
||||
const dragRef = useRef<DragState | null>(null);
|
||||
const pendingRef = useRef<PendingShape | null>(null);
|
||||
@@ -59,8 +75,11 @@ export function useShapeTool() {
|
||||
const {
|
||||
activeTool,
|
||||
shapeFill,
|
||||
shapeFillOpacity,
|
||||
shapeStroke,
|
||||
shapeStrokeOpacity,
|
||||
shapeStrokeWidth,
|
||||
shapeStrokeDash,
|
||||
shapeCornerRadius,
|
||||
shapePolygonSides,
|
||||
shapeStarPoints,
|
||||
@@ -78,6 +97,14 @@ export function useShapeTool() {
|
||||
const y = (pointer.y - panOffset.y) / zoom;
|
||||
|
||||
const id = generateId();
|
||||
const { fill, stroke, dash } = computeShapeColors({
|
||||
shapeFill,
|
||||
shapeFillOpacity,
|
||||
shapeStroke,
|
||||
shapeStrokeOpacity,
|
||||
shapeStrokeDash,
|
||||
shapeStrokeWidth,
|
||||
});
|
||||
let obj: CanvasObject;
|
||||
|
||||
switch (activeTool) {
|
||||
@@ -91,10 +118,11 @@ export function useShapeTool() {
|
||||
y,
|
||||
width: 0,
|
||||
height: 0,
|
||||
fill: shapeFill,
|
||||
stroke: shapeStroke,
|
||||
fill,
|
||||
stroke,
|
||||
strokeWidth: shapeStrokeWidth,
|
||||
cornerRadius: shapeCornerRadius,
|
||||
dash,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
@@ -110,9 +138,10 @@ export function useShapeTool() {
|
||||
y,
|
||||
radiusX: 0,
|
||||
radiusY: 0,
|
||||
fill: shapeFill,
|
||||
stroke: shapeStroke,
|
||||
fill,
|
||||
stroke,
|
||||
strokeWidth: shapeStrokeWidth,
|
||||
dash,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
@@ -125,11 +154,12 @@ export function useShapeTool() {
|
||||
layerId: activeLayerId,
|
||||
attrs: {
|
||||
points: [x, y, x, y],
|
||||
stroke: shapeStroke,
|
||||
stroke,
|
||||
strokeWidth: shapeStrokeWidth,
|
||||
tension: 0,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
dash,
|
||||
opacity: 1,
|
||||
globalCompositeOperation: "source-over",
|
||||
},
|
||||
@@ -142,11 +172,12 @@ export function useShapeTool() {
|
||||
layerId: activeLayerId,
|
||||
attrs: {
|
||||
points: [x, y, x, y],
|
||||
fill: shapeFill,
|
||||
stroke: shapeStroke,
|
||||
fill,
|
||||
stroke,
|
||||
strokeWidth: shapeStrokeWidth,
|
||||
pointerLength: 15,
|
||||
pointerWidth: 12,
|
||||
dash,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
@@ -162,9 +193,10 @@ export function useShapeTool() {
|
||||
y,
|
||||
sides: shapePolygonSides,
|
||||
radius: 0,
|
||||
fill: shapeFill,
|
||||
stroke: shapeStroke,
|
||||
fill,
|
||||
stroke,
|
||||
strokeWidth: shapeStrokeWidth,
|
||||
dash,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
@@ -181,9 +213,10 @@ export function useShapeTool() {
|
||||
numPoints: shapeStarPoints,
|
||||
innerRadius: 0,
|
||||
outerRadius: 0,
|
||||
fill: shapeFill,
|
||||
stroke: shapeStroke,
|
||||
fill,
|
||||
stroke,
|
||||
strokeWidth: shapeStrokeWidth,
|
||||
dash,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
@@ -193,12 +226,10 @@ export function useShapeTool() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't add the object yet -- wait until the user drags past the threshold
|
||||
pendingRef.current = { startX: x, startY: y, toolType: activeTool, obj };
|
||||
}, []);
|
||||
|
||||
const handleMouseMove = useCallback((e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
// If we have a pending shape but haven't committed it yet, check threshold
|
||||
if (pendingRef.current && !dragRef.current) {
|
||||
const stage = e.target.getStage();
|
||||
if (!stage) return;
|
||||
@@ -214,7 +245,6 @@ export function useShapeTool() {
|
||||
|
||||
if (Math.abs(dx) < MIN_DRAG_THRESHOLD && Math.abs(dy) < MIN_DRAG_THRESHOLD) return;
|
||||
|
||||
// Threshold exceeded -- add the object to the store and promote to active drag
|
||||
const { obj, startX, startY, toolType } = pendingRef.current;
|
||||
useEditorStore.getState().addObject(obj);
|
||||
dragRef.current = { startX, startY, objectId: obj.id, toolType };
|
||||
@@ -313,7 +343,6 @@ export function useShapeTool() {
|
||||
}, []);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
// Click without drag -- pending shape was never added, just discard it
|
||||
if (pendingRef.current) {
|
||||
pendingRef.current = null;
|
||||
}
|
||||
@@ -325,7 +354,6 @@ export function useShapeTool() {
|
||||
const obj = objects.find((o) => o.id === objectId);
|
||||
|
||||
if (obj) {
|
||||
// Remove degenerate shapes that are still too small
|
||||
const attrs = obj.attrs;
|
||||
let isDegenerate = false;
|
||||
if ("width" in attrs && "height" in attrs) {
|
||||
|
||||
@@ -10,9 +10,31 @@ import type {
|
||||
EditorState,
|
||||
FilterConfig,
|
||||
SelectionMode,
|
||||
StrokeDashStyle,
|
||||
ToolType,
|
||||
} from "@/types/editor";
|
||||
|
||||
export function hexToRgba(hex: string, opacity: number): string {
|
||||
const r = Number.parseInt(hex.slice(1, 3), 16);
|
||||
const g = Number.parseInt(hex.slice(3, 5), 16);
|
||||
const b = Number.parseInt(hex.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
|
||||
export function dashStyleToArray(
|
||||
style: StrokeDashStyle,
|
||||
strokeWidth: number,
|
||||
): number[] | undefined {
|
||||
switch (style) {
|
||||
case "dashed":
|
||||
return [strokeWidth * 4, strokeWidth * 2];
|
||||
case "dotted":
|
||||
return [strokeWidth, strokeWidth * 2];
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers for objects that use points arrays (line, arrow) vs positioned objects
|
||||
function hasPointsArray(obj: CanvasObject): obj is CanvasObject & { attrs: { points: number[] } } {
|
||||
return "points" in obj.attrs && Array.isArray((obj.attrs as { points: number[] }).points);
|
||||
@@ -165,9 +187,12 @@ export const useEditorStore = create<EditorState & EditorStateExtensions>()(
|
||||
editingTextId: null,
|
||||
|
||||
// --- Shape settings ---
|
||||
shapeFill: "#3b82f6",
|
||||
shapeStroke: "#000000",
|
||||
shapeFill: "#3b82f6" as string | null,
|
||||
shapeFillOpacity: 1,
|
||||
shapeStroke: "#000000" as string | null,
|
||||
shapeStrokeOpacity: 1,
|
||||
shapeStrokeWidth: 2,
|
||||
shapeStrokeDash: "solid" as const,
|
||||
shapeCornerRadius: 0,
|
||||
shapePolygonSides: 6,
|
||||
shapeStarPoints: 5,
|
||||
@@ -1148,8 +1173,13 @@ export const useEditorStore = create<EditorState & EditorStateExtensions>()(
|
||||
|
||||
// Shape settings
|
||||
setShapeFill: (fill) => set({ shapeFill: fill }),
|
||||
setShapeFillOpacity: (opacity) =>
|
||||
set({ shapeFillOpacity: Math.max(0, Math.min(1, opacity)) }),
|
||||
setShapeStroke: (stroke) => set({ shapeStroke: stroke }),
|
||||
setShapeStrokeOpacity: (opacity) =>
|
||||
set({ shapeStrokeOpacity: Math.max(0, Math.min(1, opacity)) }),
|
||||
setShapeStrokeWidth: (width) => set({ shapeStrokeWidth: width }),
|
||||
setShapeStrokeDash: (dash) => set({ shapeStrokeDash: dash }),
|
||||
setShapeCornerRadius: (radius) => set({ shapeCornerRadius: radius }),
|
||||
setShapePolygonSides: (sides) => set({ shapePolygonSides: sides }),
|
||||
setShapeStarPoints: (points) => set({ shapeStarPoints: points }),
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
export type SelectionMode = "new" | "add" | "subtract";
|
||||
|
||||
export type StrokeDashStyle = "solid" | "dashed" | "dotted";
|
||||
|
||||
export type ToolType =
|
||||
| "move"
|
||||
| "marquee-rect"
|
||||
@@ -36,11 +38,12 @@ export type ToolType =
|
||||
|
||||
export interface LineAttrs {
|
||||
points: number[];
|
||||
stroke: string;
|
||||
stroke?: string;
|
||||
strokeWidth: number;
|
||||
tension: number;
|
||||
lineCap: "butt" | "round" | "square";
|
||||
lineJoin: "bevel" | "round" | "miter";
|
||||
dash?: number[];
|
||||
opacity: number;
|
||||
globalCompositeOperation: string;
|
||||
shadowBlur?: number;
|
||||
@@ -54,10 +57,11 @@ export interface RectAttrs {
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fill: string;
|
||||
stroke: string;
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
strokeWidth: number;
|
||||
cornerRadius: number;
|
||||
dash?: number[];
|
||||
rotation: number;
|
||||
opacity: number;
|
||||
}
|
||||
@@ -67,9 +71,10 @@ export interface EllipseAttrs {
|
||||
y: number;
|
||||
radiusX: number;
|
||||
radiusY: number;
|
||||
fill: string;
|
||||
stroke: string;
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
strokeWidth: number;
|
||||
dash?: number[];
|
||||
rotation: number;
|
||||
opacity: number;
|
||||
}
|
||||
@@ -106,11 +111,12 @@ export interface ImageAttrs {
|
||||
|
||||
export interface ArrowAttrs {
|
||||
points: number[];
|
||||
fill: string;
|
||||
stroke: string;
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
strokeWidth: number;
|
||||
pointerLength: number;
|
||||
pointerWidth: number;
|
||||
dash?: number[];
|
||||
rotation: number;
|
||||
opacity: number;
|
||||
}
|
||||
@@ -120,9 +126,10 @@ export interface PolygonAttrs {
|
||||
y: number;
|
||||
sides: number;
|
||||
radius: number;
|
||||
fill: string;
|
||||
stroke: string;
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
strokeWidth: number;
|
||||
dash?: number[];
|
||||
rotation: number;
|
||||
opacity: number;
|
||||
}
|
||||
@@ -133,9 +140,10 @@ export interface StarAttrs {
|
||||
numPoints: number;
|
||||
innerRadius: number;
|
||||
outerRadius: number;
|
||||
fill: string;
|
||||
stroke: string;
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
strokeWidth: number;
|
||||
dash?: number[];
|
||||
rotation: number;
|
||||
opacity: number;
|
||||
}
|
||||
@@ -305,9 +313,12 @@ export interface EditorState {
|
||||
editingTextId: string | null;
|
||||
|
||||
// Shape settings
|
||||
shapeFill: string;
|
||||
shapeStroke: string;
|
||||
shapeFill: string | null;
|
||||
shapeFillOpacity: number;
|
||||
shapeStroke: string | null;
|
||||
shapeStrokeOpacity: number;
|
||||
shapeStrokeWidth: number;
|
||||
shapeStrokeDash: StrokeDashStyle;
|
||||
shapeCornerRadius: number;
|
||||
shapePolygonSides: number;
|
||||
shapeStarPoints: number;
|
||||
@@ -455,9 +466,12 @@ export interface EditorState {
|
||||
setSpongeFlow: (flow: number) => void;
|
||||
|
||||
// Shape settings
|
||||
setShapeFill: (fill: string) => void;
|
||||
setShapeStroke: (stroke: string) => void;
|
||||
setShapeFill: (fill: string | null) => void;
|
||||
setShapeFillOpacity: (opacity: number) => void;
|
||||
setShapeStroke: (stroke: string | null) => void;
|
||||
setShapeStrokeOpacity: (opacity: number) => void;
|
||||
setShapeStrokeWidth: (width: number) => void;
|
||||
setShapeStrokeDash: (dash: StrokeDashStyle) => void;
|
||||
setShapeCornerRadius: (radius: number) => void;
|
||||
setShapePolygonSides: (sides: number) => void;
|
||||
setShapeStarPoints: (points: number) => void;
|
||||
|
||||
@@ -1365,6 +1365,27 @@ export const ar: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "فشل فك الترميز من الخادم",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "الإعدادات",
|
||||
|
||||
@@ -1379,6 +1379,27 @@ export const de: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Server-Dekodierung fehlgeschlagen",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Einstellungen",
|
||||
|
||||
@@ -1324,6 +1324,27 @@ export const en = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Server decode failed",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Settings",
|
||||
|
||||
@@ -1361,6 +1361,27 @@ export const es: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Error de decodificacion del servidor",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Configuracion",
|
||||
|
||||
@@ -1380,6 +1380,27 @@ export const fr: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Echec du decodage serveur",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Parametres",
|
||||
|
||||
@@ -1362,6 +1362,27 @@ export const hi: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "सर्वर डीकोड विफल",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "सेटिंग्स",
|
||||
|
||||
@@ -1374,6 +1374,27 @@ export const id: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Dekoding server gagal",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Pengaturan",
|
||||
|
||||
@@ -1373,6 +1373,27 @@ export const it: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Decodifica del server non riuscita",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Impostazioni",
|
||||
|
||||
@@ -1332,6 +1332,27 @@ export const ja: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "サーバーでのデコードに失敗しました",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "設定",
|
||||
|
||||
@@ -1317,6 +1317,27 @@ export const ko: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "서버 디코딩 실패",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "설정",
|
||||
|
||||
@@ -1376,6 +1376,27 @@ export const nl: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Server-decodering mislukt",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Instellingen",
|
||||
|
||||
@@ -1377,6 +1377,27 @@ export const pl: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Błąd dekodowania na serwerze",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Ustawienia",
|
||||
|
||||
@@ -1373,6 +1373,27 @@ export const ptBR: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Falha na decodificacao do servidor",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Configuracoes",
|
||||
|
||||
@@ -1375,6 +1375,27 @@ export const ru: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Ошибка декодирования на сервере",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Настройки",
|
||||
|
||||
@@ -1372,6 +1372,27 @@ export const sv: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Serveravkodning misslyckades",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Installningar",
|
||||
|
||||
@@ -1354,6 +1354,27 @@ export const th: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "การถอดรหัสจากเซิร์ฟเวอร์ล้มเหลว",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "ตั้งค่า",
|
||||
|
||||
@@ -1377,6 +1377,27 @@ export const tr: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Sunucu çözümleme hatası",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Ayarlar",
|
||||
|
||||
@@ -1375,6 +1375,27 @@ export const uk: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Помилка декодування на сервері",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Налаштування",
|
||||
|
||||
@@ -1374,6 +1374,27 @@ export const vi: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "Máy chủ giải mã thất bại",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "Cài đặt",
|
||||
|
||||
@@ -1306,6 +1306,27 @@ export const zhCN: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "服务器解码失败",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "设置",
|
||||
|
||||
@@ -1304,6 +1304,27 @@ export const zhTW: TranslationKeys = {
|
||||
errors: {
|
||||
serverDecodeFailed: "伺服器解碼失敗",
|
||||
},
|
||||
shapes: {
|
||||
shape: "Shape",
|
||||
fill: "Fill",
|
||||
stroke: "Stroke",
|
||||
strokeWidth: "Width",
|
||||
cornerRadius: "Radius",
|
||||
sides: "Sides",
|
||||
points: "Points",
|
||||
dashStyle: "Dash",
|
||||
solid: "Solid",
|
||||
dashed: "Dashed",
|
||||
dotted: "Dotted",
|
||||
none: "None",
|
||||
opacity: "Opacity",
|
||||
rectangle: "Rectangle",
|
||||
ellipse: "Ellipse",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
polygon: "Polygon",
|
||||
star: "Star",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
heading: "設定",
|
||||
|
||||
Reference in New Issue
Block a user