Format code
This commit is contained in:
@@ -17,7 +17,7 @@ const Container = styled.div<{ $cursor?: string }>`
|
|||||||
|
|
||||||
cursor: ${({ $cursor }) => $cursor || ""};
|
cursor: ${({ $cursor }) => $cursor || ""};
|
||||||
|
|
||||||
image-rendering: -webkit-optimize-contrast;
|
image-rendering: -webkit-optimize-contrast;
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
perspective: 1000;
|
perspective: 1000;
|
||||||
`;
|
`;
|
||||||
@@ -50,8 +50,10 @@ export default function A4Canvas({ handler }: CanvasHandlerProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
panRef.current.offsetX = (container.offsetWidth - (settings.landscape ? settings.paperHeight : settings.paperWidth) * settings.scale) / 2;
|
panRef.current.offsetX =
|
||||||
panRef.current.offsetY = (container.offsetHeight - (settings.landscape ? settings.paperWidth : settings.paperHeight) * settings.scale) / 2;
|
(container.offsetWidth - (settings.landscape ? settings.paperHeight : settings.paperWidth) * settings.scale) / 2;
|
||||||
|
panRef.current.offsetY =
|
||||||
|
(container.offsetHeight - (settings.landscape ? settings.paperWidth : settings.paperHeight) * settings.scale) / 2;
|
||||||
applyTransform();
|
applyTransform();
|
||||||
|
|
||||||
const unsub = handler.toolHandler.emitter.on("select", (tool) => {
|
const unsub = handler.toolHandler.emitter.on("select", (tool) => {
|
||||||
@@ -123,7 +125,7 @@ export default function A4Canvas({ handler }: CanvasHandlerProps) {
|
|||||||
break;
|
break;
|
||||||
case "Delete": {
|
case "Delete": {
|
||||||
const copy = [...handler.selected];
|
const copy = [...handler.selected];
|
||||||
if (copy.length && await handler.popup.confirm("Title", "Are you sure you want to delete")) {
|
if (copy.length && (await handler.popup.confirm("Title", "Are you sure you want to delete"))) {
|
||||||
for (const id of copy) {
|
for (const id of copy) {
|
||||||
handler.deleteImage(id);
|
handler.deleteImage(id);
|
||||||
}
|
}
|
||||||
@@ -135,7 +137,6 @@ export default function A4Canvas({ handler }: CanvasHandlerProps) {
|
|||||||
handler.selected.forEach((e) => {
|
handler.selected.forEach((e) => {
|
||||||
handler.move(e, x, y);
|
handler.move(e, x, y);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,7 +146,6 @@ export default function A4Canvas({ handler }: CanvasHandlerProps) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
if (!isDragging.current && handler.toolHandler.isToolSelected(TOOL_CANVAS_MOVE)) {
|
if (!isDragging.current && handler.toolHandler.isToolSelected(TOOL_CANVAS_MOVE)) {
|
||||||
setCursor("grabbing");
|
setCursor("grabbing");
|
||||||
@@ -174,16 +174,12 @@ export default function A4Canvas({ handler }: CanvasHandlerProps) {
|
|||||||
// }
|
// }
|
||||||
// const mx = e.clientX - panRef.current.offsetX;
|
// const mx = e.clientX - panRef.current.offsetX;
|
||||||
// const my = e.clientY - panRef.current.offsetY;
|
// const my = e.clientY - panRef.current.offsetY;
|
||||||
|
|
||||||
// const dx = mx - dragStart.current.x;
|
// const dx = mx - dragStart.current.x;
|
||||||
// const dy = my - dragStart.current.y;
|
// const dy = my - dragStart.current.y;
|
||||||
|
|
||||||
// const x = dx / settings.scale;
|
// const x = dx / settings.scale;
|
||||||
// const y = dy / settings.scale;
|
// const y = dy / settings.scale;
|
||||||
|
|
||||||
// dragStart.current.x = mx;
|
// dragStart.current.x = mx;
|
||||||
// dragStart.current.y = my;
|
// dragStart.current.y = my;
|
||||||
|
|
||||||
// handler.selected.forEach((e) => {
|
// handler.selected.forEach((e) => {
|
||||||
// handler.move(e, x, y);
|
// handler.move(e, x, y);
|
||||||
// });
|
// });
|
||||||
|
|||||||
@@ -26,79 +26,132 @@ const Column = styled.div`
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Box = styled.div<{ $width: number, $height: number }>`
|
const Box = styled.div<{ $width: number; $height: number }>`
|
||||||
width: ${({ $width }) => $width}px;
|
width: ${({ $width }) => $width}px;
|
||||||
height: ${({ $height }) => $height}px;
|
height: ${({ $height }) => $height}px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
export function ImportExportButtons({ handler, selected }: CanvasHandlerProps & { selected: ImageEditor[] }) {
|
export function ImportExportButtons({ handler, selected }: CanvasHandlerProps & { selected: ImageEditor[] }) {
|
||||||
const boxSize = 90;
|
const boxSize = 90;
|
||||||
return <>
|
return (
|
||||||
<Box $width={boxSize} $height={boxSize}>
|
<>
|
||||||
<Column>
|
<Box $width={boxSize} $height={boxSize}>
|
||||||
<Row>
|
<Column>
|
||||||
|
<Row>
|
||||||
<Btn onClick={() => { downloadZip(handler); }}>
|
<Btn
|
||||||
<FaFileZipper />ZIP
|
onClick={() => {
|
||||||
</Btn>
|
downloadZip(handler);
|
||||||
<Btn onClick={() => { createPdf(handler, true).save(`${handler.projectName}.pdf`); }}>
|
}}
|
||||||
<FaFilePdf />PDF
|
>
|
||||||
</Btn>
|
<FaFileZipper />
|
||||||
<Btn onClick={async () => { downloadBlob(await createSVGDoc(handler), `${handler.projectName}.svg`); }} >
|
ZIP
|
||||||
<FaDrawPolygon />SVG
|
</Btn>
|
||||||
</Btn>
|
<Btn
|
||||||
</Row>
|
onClick={() => {
|
||||||
<Row>
|
createPdf(handler, true).save(`${handler.projectName}.pdf`);
|
||||||
<Btn disabled={selected.length !== 1} onClick={() => {
|
}}
|
||||||
const v = handler.imagesRenders.find(e => e.id === selected[0].id);
|
>
|
||||||
if (v) {
|
<FaFilePdf />
|
||||||
const canvas = document.createElement("canvas");
|
PDF
|
||||||
canvas.width = v.image.naturalWidth * v.scale;
|
</Btn>
|
||||||
canvas.height = v.image.naturalHeight * v.scale;
|
<Btn
|
||||||
const ctx = canvas.getContext("2d")!;
|
onClick={async () => {
|
||||||
ctx.drawImage(v.image, 0, 0, canvas.width, canvas.height);
|
downloadBlob(await createSVGDoc(handler), `${handler.projectName}.svg`);
|
||||||
canvas.toBlob(blob => {
|
}}
|
||||||
if (blob) {
|
>
|
||||||
downloadBlob(blob, `${v.name}.png`);
|
<FaDrawPolygon />
|
||||||
|
SVG
|
||||||
|
</Btn>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Btn
|
||||||
|
disabled={selected.length !== 1}
|
||||||
|
onClick={() => {
|
||||||
|
const v = handler.imagesRenders.find((e) => e.id === selected[0].id);
|
||||||
|
if (v) {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = v.image.naturalWidth * v.scale;
|
||||||
|
canvas.height = v.image.naturalHeight * v.scale;
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
ctx.drawImage(v.image, 0, 0, canvas.width, canvas.height);
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) {
|
||||||
|
downloadBlob(blob, `${v.name}.png`);
|
||||||
|
} else {
|
||||||
|
handler.popup.alert("Error", "Cannot export image");
|
||||||
|
}
|
||||||
|
}, "image/png");
|
||||||
} else {
|
} else {
|
||||||
handler.popup.alert("Error", "Cannot export image");
|
handler.popup.alert("Error", "Image not found");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaImage />
|
||||||
|
Image
|
||||||
|
</Btn>
|
||||||
|
<Btn
|
||||||
|
disabled={selected.length !== 1}
|
||||||
|
onClick={() => {
|
||||||
|
const v = handler.imagesRenders.find((e) => e.id === selected[0].id);
|
||||||
|
if (v) {
|
||||||
|
if (v.svg.scale !== v.scale || v.svg.dirty) {
|
||||||
|
handler.redrawSvg(v.id);
|
||||||
|
}
|
||||||
|
downloadBlob(new Blob([v.svg.data], { type: "image/svg+xml" }), `${v.name}.svg`);
|
||||||
|
} else {
|
||||||
|
handler.popup.alert("Error", "Image not found");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<FaDrawPolygon />
|
||||||
|
SVG
|
||||||
|
</Btn>
|
||||||
|
</Row>
|
||||||
|
</Column>
|
||||||
|
</Box>
|
||||||
|
<Box $width={boxSize} $height={boxSize}>
|
||||||
|
<Column>
|
||||||
|
<Row>
|
||||||
|
<Btn
|
||||||
|
onClick={() => {
|
||||||
|
handler.exportProject();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaFileExport />
|
||||||
|
Export
|
||||||
|
</Btn>
|
||||||
|
<Btn
|
||||||
|
onClick={() => {
|
||||||
|
handler.importProject();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaFileImport />
|
||||||
|
Import
|
||||||
|
</Btn>
|
||||||
|
</Row>
|
||||||
|
<Btn
|
||||||
|
style={{ backgroundColor: "#ff000073" }}
|
||||||
|
disabled={selected.length !== 1}
|
||||||
|
onClick={async () => {
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
await handler.popup.confirm(
|
||||||
|
"Clear",
|
||||||
|
"Are you sure you want to clear project? Any unsaved changes will be discarded",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
handler.clear();
|
||||||
}
|
}
|
||||||
}, "image/png");
|
|
||||||
} else {
|
|
||||||
handler.popup.alert("Error", "Image not found");
|
|
||||||
}
|
|
||||||
}}><FaImage />Image</Btn>
|
|
||||||
<Btn disabled={selected.length !== 1} onClick={() => {
|
|
||||||
const v = handler.imagesRenders.find(e => e.id === selected[0].id);
|
|
||||||
if (v) {
|
|
||||||
if (v.svg.scale !== v.scale || v.svg.dirty) {
|
|
||||||
handler.redrawSvg(v.id);
|
|
||||||
}
|
}
|
||||||
downloadBlob(new Blob([v.svg.data], { type: "image/svg+xml" }), `${v.name}.svg`);
|
}}
|
||||||
} else {
|
>
|
||||||
handler.popup.alert("Error", "Image not found");
|
<FaBroom />
|
||||||
}
|
Clear
|
||||||
|
</Btn>
|
||||||
}}> <FaDrawPolygon />SVG</Btn>
|
</Column>
|
||||||
</Row>
|
</Box>
|
||||||
</Column>
|
</>
|
||||||
</Box>
|
);
|
||||||
<Box $width={boxSize} $height={boxSize}>
|
|
||||||
<Column>
|
|
||||||
<Row>
|
|
||||||
<Btn onClick={() => { handler.exportProject(); }}><FaFileExport />Export</Btn>
|
|
||||||
<Btn onClick={() => { handler.importProject(); }} ><FaFileImport />Import</Btn>
|
|
||||||
</Row>
|
|
||||||
<Btn style={{ backgroundColor: "#ff000073" }} disabled={selected.length !== 1} onClick={async () => {
|
|
||||||
{
|
|
||||||
if (await handler.popup.confirm("Clear", "Are you sure you want to clear project? Any unsaved changes will be discarded")) {
|
|
||||||
handler.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}} ><FaBroom />Clear</Btn>
|
|
||||||
</Column>
|
|
||||||
</Box>
|
|
||||||
</>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { FreeTransform } from "./selection-box";
|
|||||||
import type { GlobalSettings, GridType } from "../../interface";
|
import type { GlobalSettings, GridType } from "../../interface";
|
||||||
import { SvgRenderer } from "./svg-view";
|
import { SvgRenderer } from "./svg-view";
|
||||||
|
|
||||||
|
|
||||||
const CanvasEl = styled.div`
|
const CanvasEl = styled.div`
|
||||||
background: white;
|
background: white;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
@@ -17,10 +16,9 @@ const Img = styled.img<{ $selected: boolean }>`
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
opacity: ${({ $selected }) => $selected ? 0.5 : 1};
|
opacity: ${({ $selected }) => ($selected ? 0.5 : 1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
const Box = styled.div`
|
const Box = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -31,9 +29,9 @@ const Box = styled.div`
|
|||||||
function getGridLines(settings: GridType, width: number, height: number, strokeWidth: number, strokeWidthHalf: number) {
|
function getGridLines(settings: GridType, width: number, height: number, strokeWidth: number, strokeWidthHalf: number) {
|
||||||
if (settings === "none") return null;
|
if (settings === "none") return null;
|
||||||
const configs: Record<GridType, { vertical: number[]; horizontal: number[] }> = {
|
const configs: Record<GridType, { vertical: number[]; horizontal: number[] }> = {
|
||||||
"none": {
|
none: {
|
||||||
horizontal: [1],
|
horizontal: [1],
|
||||||
vertical: [1]
|
vertical: [1],
|
||||||
},
|
},
|
||||||
"2x2": {
|
"2x2": {
|
||||||
vertical: [1 / 2],
|
vertical: [1 / 2],
|
||||||
@@ -48,21 +46,25 @@ function getGridLines(settings: GridType, width: number, height: number, strokeW
|
|||||||
const config = configs[settings];
|
const config = configs[settings];
|
||||||
if (!config) return null;
|
if (!config) return null;
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
{config.vertical.map((fraction, i) => (
|
<>
|
||||||
<Box key={`v-${i}`} style={{ left: width * fraction - strokeWidthHalf, width: strokeWidth, height: "100%" }} />
|
{config.vertical.map((fraction, i) => (
|
||||||
))}
|
<Box key={`v-${i}`} style={{ left: width * fraction - strokeWidthHalf, width: strokeWidth, height: "100%" }} />
|
||||||
{config.horizontal.map((fraction, i) => (
|
))}
|
||||||
<Box key={`h-${i}`} style={{ top: height * fraction - strokeWidthHalf, height: strokeWidth, width: "100%" }} />
|
{config.horizontal.map((fraction, i) => (
|
||||||
))}
|
<Box key={`h-${i}`} style={{ top: height * fraction - strokeWidthHalf, height: strokeWidth, width: "100%" }} />
|
||||||
</>;
|
))}
|
||||||
};
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function PaperView({ handler, settings }: CanvasHandlerProps & { settings: GlobalSettings }) {
|
export function PaperView({ handler, settings }: CanvasHandlerProps & { settings: GlobalSettings }) {
|
||||||
const { images } = useImages(handler);
|
const { images } = useImages(handler);
|
||||||
const dpi = settings.DPI;
|
const dpi = settings.DPI;
|
||||||
const width = Math.round(((settings.landscape ? settings.paperHeight : settings.paperWidth) / MM_TO_INCH) * dpi) * settings.scale;
|
const width =
|
||||||
const height = Math.round(((settings.landscape ? settings.paperWidth : settings.paperHeight) / MM_TO_INCH) * dpi) * settings.scale;
|
Math.round(((settings.landscape ? settings.paperHeight : settings.paperWidth) / MM_TO_INCH) * dpi) * settings.scale;
|
||||||
|
const height =
|
||||||
|
Math.round(((settings.landscape ? settings.paperWidth : settings.paperHeight) / MM_TO_INCH) * dpi) * settings.scale;
|
||||||
|
|
||||||
const renderGuides = () => {
|
const renderGuides = () => {
|
||||||
const strokeWidth = 1;
|
const strokeWidth = 1;
|
||||||
@@ -70,37 +72,50 @@ export function PaperView({ handler, settings }: CanvasHandlerProps & { settings
|
|||||||
return getGridLines(settings.grid, width, height, strokeWidth, strokeWidthHalf);
|
return getGridLines(settings.grid, width, height, strokeWidth, strokeWidthHalf);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <CanvasEl style={{ width: `${width}px`, height: `${height}px` }} draggable={false}>
|
return (
|
||||||
{images.map(e => {
|
<CanvasEl style={{ width: `${width}px`, height: `${height}px` }} draggable={false}>
|
||||||
if (!e.visible) return;
|
{images.map((e) => {
|
||||||
const width = e.image.naturalWidth * e.scale * settings.scale;
|
if (!e.visible) return;
|
||||||
const height = e.image.naturalHeight * e.scale * settings.scale;
|
const width = e.image.naturalWidth * e.scale * settings.scale;
|
||||||
const x = e.x * settings.scale;
|
const height = e.image.naturalHeight * e.scale * settings.scale;
|
||||||
const y = e.y * settings.scale;
|
const x = e.x * settings.scale;
|
||||||
return <div key={e.id}>
|
const y = e.y * settings.scale;
|
||||||
<Img
|
return (
|
||||||
$selected={e.selected}
|
<div key={e.id}>
|
||||||
draggable="false"
|
<Img
|
||||||
src={e.url}
|
$selected={e.selected}
|
||||||
alt={e.id}
|
draggable="false"
|
||||||
style={{ left: `${x}px`, top: `${y}px` }}
|
src={e.url}
|
||||||
width={width}
|
alt={e.id}
|
||||||
height={height}
|
style={{ left: `${x}px`, top: `${y}px` }}
|
||||||
/>
|
width={width}
|
||||||
<SvgRenderer x={x} y={y} width={width} height={height} svg={e.svg} />
|
height={height}
|
||||||
{e.selected ?
|
/>
|
||||||
<FreeTransform scale={settings.scale} transform={{
|
<SvgRenderer x={x} y={y} width={width} height={height} svg={e.svg} />
|
||||||
x: x,
|
{e.selected ? (
|
||||||
y: y,
|
<FreeTransform
|
||||||
height,
|
scale={settings.scale}
|
||||||
width,
|
transform={{
|
||||||
rotation: 0
|
x: x,
|
||||||
}} onTransformChange={({ x, y, width }) => {
|
y: y,
|
||||||
const scale = Math.round(width / e.image.width / settings.scale * 100) / 100;
|
height,
|
||||||
e.onPropsChange({ x: Math.round(x / settings.scale), y: Math.round(y / settings.scale), scale });
|
width,
|
||||||
}} /> : null}
|
rotation: 0,
|
||||||
</div>;
|
}}
|
||||||
})}
|
onTransformChange={({ x, y, width }) => {
|
||||||
{renderGuides()}
|
const scale = Math.round((width / e.image.width / settings.scale) * 100) / 100;
|
||||||
</CanvasEl>;
|
e.onPropsChange({
|
||||||
|
x: Math.round(x / settings.scale),
|
||||||
|
y: Math.round(y / settings.scale),
|
||||||
|
scale,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{renderGuides()}
|
||||||
|
</CanvasEl>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,18 +105,14 @@ export type FreeTransformProps = {
|
|||||||
onTransformChange: (t: Transform) => void;
|
onTransformChange: (t: Transform) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FreeTransform({
|
export function FreeTransform({ transform, scale = 1, onTransformChange }: FreeTransformProps) {
|
||||||
transform,
|
|
||||||
scale = 1,
|
|
||||||
onTransformChange,
|
|
||||||
}: FreeTransformProps) {
|
|
||||||
const dragRef = useRef<DragState>({ type: "none" });
|
const dragRef = useRef<DragState>({ type: "none" });
|
||||||
|
|
||||||
const apply = useCallback(
|
const apply = useCallback(
|
||||||
(updater: (prev: Transform) => Transform) => {
|
(updater: (prev: Transform) => Transform) => {
|
||||||
onTransformChange(updater(transform));
|
onTransformChange(updater(transform));
|
||||||
},
|
},
|
||||||
[transform, onTransformChange]
|
[transform, onTransformChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMovePointerDown = useCallback(
|
const onMovePointerDown = useCallback(
|
||||||
@@ -131,7 +127,7 @@ export function FreeTransform({
|
|||||||
originY: transform.y,
|
originY: transform.y,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[transform.x, transform.y]
|
[transform.x, transform.y],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onResizePointerDown = useCallback(
|
const onResizePointerDown = useCallback(
|
||||||
@@ -146,7 +142,7 @@ export function FreeTransform({
|
|||||||
originTransform: { ...transform },
|
originTransform: { ...transform },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[transform]
|
[transform],
|
||||||
);
|
);
|
||||||
|
|
||||||
// const onRotatePointerDown = useCallback(
|
// const onRotatePointerDown = useCallback(
|
||||||
@@ -184,8 +180,7 @@ export function FreeTransform({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (drag.type === "rotate") {
|
if (drag.type === "rotate") {
|
||||||
const currentAngle =
|
const currentAngle = Math.atan2(e.clientY - drag.cy, e.clientX - drag.cx) * (180 / Math.PI);
|
||||||
Math.atan2(e.clientY - drag.cy, e.clientX - drag.cx) * (180 / Math.PI);
|
|
||||||
apply(() => ({
|
apply(() => ({
|
||||||
...transform,
|
...transform,
|
||||||
rotation: drag.originRotation + (currentAngle - drag.startAngle),
|
rotation: drag.originRotation + (currentAngle - drag.startAngle),
|
||||||
@@ -218,7 +213,7 @@ export function FreeTransform({
|
|||||||
apply(() => ({ ...transform, x, y, width, height }));
|
apply(() => ({ ...transform, x, y, width, height }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[transform, scale, apply]
|
[transform, scale, apply],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onPointerUp = useCallback(() => {
|
const onPointerUp = useCallback(() => {
|
||||||
@@ -235,7 +230,7 @@ export function FreeTransform({
|
|||||||
top: `${y}px`,
|
top: `${y}px`,
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
transform: `rotate(${rotation}deg)`
|
transform: `rotate(${rotation}deg)`,
|
||||||
}}
|
}}
|
||||||
onPointerDown={onMovePointerDown}
|
onPointerDown={onMovePointerDown}
|
||||||
onPointerMove={onPointerMove}
|
onPointerMove={onPointerMove}
|
||||||
@@ -266,4 +261,4 @@ export function FreeTransform({
|
|||||||
</TransformBox>
|
</TransformBox>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
@@ -6,12 +5,11 @@ const Wrapper = styled.div`
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
svg {
|
width: 100%;
|
||||||
width: 100%;
|
height: 100%;
|
||||||
height: 100%;
|
display: block;
|
||||||
display: block;
|
}
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -20,19 +18,20 @@ type Props = {
|
|||||||
y: number;
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export function SvgRenderer(props: Props) {
|
export function SvgRenderer(props: Props) {
|
||||||
const { svg, x, y, width, height } = props;
|
const { svg, x, y, width, height } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper style={{
|
<Wrapper
|
||||||
left: x,
|
style={{
|
||||||
top: y,
|
left: x,
|
||||||
width,
|
top: y,
|
||||||
height
|
width,
|
||||||
}} dangerouslySetInnerHTML={{ __html: svg }}
|
height,
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: svg }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function InfoBar({ handler }: CanvasHandlerProps) {
|
|||||||
const [projectName, setProjectName] = useState(handler.projectName);
|
const [projectName, setProjectName] = useState(handler.projectName);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return handler.emitter.on("name", name => {
|
return handler.emitter.on("name", (name) => {
|
||||||
setProjectName(name);
|
setProjectName(name);
|
||||||
});
|
});
|
||||||
}, [handler.projectName]);
|
}, [handler.projectName]);
|
||||||
@@ -81,9 +81,13 @@ export function InfoBar({ handler }: CanvasHandlerProps) {
|
|||||||
<FaPlus />
|
<FaPlus />
|
||||||
</Icon2525>
|
</Icon2525>
|
||||||
<ProjectName>
|
<ProjectName>
|
||||||
<span onClick={() => {
|
<span
|
||||||
handler.promptSetName();
|
onClick={() => {
|
||||||
}}> {projectName}
|
handler.promptSetName();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
{projectName}
|
||||||
</span>
|
</span>
|
||||||
</ProjectName>
|
</ProjectName>
|
||||||
<Icon2525 onClick={() => setDPI(false)}>
|
<Icon2525 onClick={() => setDPI(false)}>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const Icon = styled.span`
|
|||||||
export function LayerItem({ image, handler }: CanvasHandleImageEditorProps) {
|
export function LayerItem({ image, handler }: CanvasHandleImageEditorProps) {
|
||||||
const selectLayer = (image: ImageEditor, ctrl: boolean) => {
|
const selectLayer = (image: ImageEditor, ctrl: boolean) => {
|
||||||
if (!ctrl) {
|
if (!ctrl) {
|
||||||
const selected = handler.selected.filter(e => e !== image.id);
|
const selected = handler.selected.filter((e) => e !== image.id);
|
||||||
for (const sel of selected) {
|
for (const sel of selected) {
|
||||||
handler.setSelect(sel, false);
|
handler.setSelect(sel, false);
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ export function LayerItem({ image, handler }: CanvasHandleImageEditorProps) {
|
|||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
$selected={image.selected}
|
$selected={image.selected}
|
||||||
onClick={ev => {
|
onClick={(ev) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
selectLayer(image, ev.ctrlKey);
|
selectLayer(image, ev.ctrlKey);
|
||||||
@@ -49,7 +49,7 @@ export function LayerItem({ image, handler }: CanvasHandleImageEditorProps) {
|
|||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
style={{ opacity: image.visible ? 1 : 0.1 }}
|
style={{ opacity: image.visible ? 1 : 0.1 }}
|
||||||
onClick={ev => {
|
onClick={(ev) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
image.setVisible(!image.visible);
|
image.setVisible(!image.visible);
|
||||||
@@ -60,7 +60,7 @@ export function LayerItem({ image, handler }: CanvasHandleImageEditorProps) {
|
|||||||
|
|
||||||
<Img
|
<Img
|
||||||
draggable="false"
|
draggable="false"
|
||||||
onClick={ev => {
|
onClick={(ev) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
selectLayer(image, ev.ctrlKey);
|
selectLayer(image, ev.ctrlKey);
|
||||||
@@ -71,7 +71,7 @@ export function LayerItem({ image, handler }: CanvasHandleImageEditorProps) {
|
|||||||
<NamePlate image={image} />
|
<NamePlate image={image} />
|
||||||
<Icon
|
<Icon
|
||||||
style={{ opacity: image.locked ? 1 : 0.1 }}
|
style={{ opacity: image.locked ? 1 : 0.1 }}
|
||||||
onClick={ev => {
|
onClick={(ev) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
image.setLock(!image.locked);
|
image.setLock(!image.locked);
|
||||||
|
|||||||
@@ -44,13 +44,17 @@ export function NamePlate({ image }: ImageEditorProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span onDoubleClick={ev => {
|
<span
|
||||||
if (!image.locked) {
|
onDoubleClick={(ev) => {
|
||||||
ev.stopPropagation();
|
if (!image.locked) {
|
||||||
ev.preventDefault();
|
ev.stopPropagation();
|
||||||
setEditing(true);
|
ev.preventDefault();
|
||||||
}
|
setEditing(true);
|
||||||
}}>{image.name}</span>
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{image.name}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</NamePlateDiv>
|
</NamePlateDiv>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,15 +22,12 @@ const Version = styled.span`
|
|||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
const ToolBar = styled.div`
|
const ToolBar = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
border-top: 1px solid var(--secondary-color);
|
border-top: 1px solid var(--secondary-color);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
width: 200px;
|
width: 200px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
@@ -62,7 +59,7 @@ const Value = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Link = styled.a`
|
const Link = styled.a`
|
||||||
color: #4AF626;
|
color: #4af626;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -80,34 +77,37 @@ export function LayerPanel({ handler }: CanvasHandlerProps) {
|
|||||||
))}
|
))}
|
||||||
<Gap />
|
<Gap />
|
||||||
<ToolBar>
|
<ToolBar>
|
||||||
|
<Version
|
||||||
|
onDoubleClick={() => {
|
||||||
|
handler.popup.custom(
|
||||||
|
"About",
|
||||||
|
"",
|
||||||
|
<Wrapper>
|
||||||
|
<Title>Vectrace {handler.VERSION}v</Title>
|
||||||
|
|
||||||
<Version onDoubleClick={() => {
|
<Row>
|
||||||
handler.popup.custom("About", "", <Wrapper>
|
<Label>Author</Label>
|
||||||
<Title>Vectrace {handler.VERSION}v</Title>
|
<Value>{handler.AUTHOR.name}</Value>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
<Label>Author</Label>
|
<Label>Email</Label>
|
||||||
<Value>{handler.AUTHOR.name}</Value>
|
<Value>{handler.AUTHOR.email}</Value>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
<Row>
|
||||||
<Row>
|
<Label>URL</Label>
|
||||||
<Label>Email</Label>
|
<Value>
|
||||||
<Value>{handler.AUTHOR.email}</Value>
|
<Link href={handler.AUTHOR.url} target="_blank" rel="noreferrer">
|
||||||
</Row>
|
{handler.AUTHOR.url}
|
||||||
|
</Link>
|
||||||
<Row>
|
</Value>
|
||||||
<Label>URL</Label>
|
</Row>
|
||||||
<Value>
|
</Wrapper>,
|
||||||
<Link href={handler.AUTHOR.url} target="_blank" rel="noreferrer">
|
);
|
||||||
{handler.AUTHOR.url}
|
}}
|
||||||
</Link>
|
>
|
||||||
</Value>
|
|
||||||
</Row>
|
|
||||||
</Wrapper>);
|
|
||||||
}}>
|
|
||||||
{handler.VERSION}v
|
{handler.VERSION}v
|
||||||
|
|
||||||
</Version>
|
</Version>
|
||||||
<Gap />
|
<Gap />
|
||||||
<Icon2525
|
<Icon2525
|
||||||
|
|||||||
@@ -3,39 +3,22 @@ import styled from "styled-components";
|
|||||||
import { calculateMathExpression, canEvaluateMathExpression } from "../calc";
|
import { calculateMathExpression, canEvaluateMathExpression } from "../calc";
|
||||||
|
|
||||||
const StyledInput = styled.input<{ $isValid: boolean | null }>`
|
const StyledInput = styled.input<{ $isValid: boolean | null }>`
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
border: 1px solid
|
border: 1px solid ${({ $isValid }) => ($isValid === null ? "#888" : $isValid ? "#ffffff" : "#ef4444")};
|
||||||
${({ $isValid }) =>
|
outline: none;
|
||||||
$isValid === null
|
background-color: ${({ $isValid }) => ($isValid === null ? "transparent" : $isValid ? "#000000" : "#7e0000")};
|
||||||
? "#888"
|
color: ${({ $isValid }) => ($isValid === null ? "inherit" : $isValid ? "#15803d" : "#b91c1c")};
|
||||||
: $isValid
|
transition:
|
||||||
? "#ffffff"
|
border-color 0.2s ease,
|
||||||
: "#ef4444"};
|
background-color 0.2s ease;
|
||||||
outline: none;
|
|
||||||
background-color: ${({ $isValid }) =>
|
|
||||||
$isValid === null
|
|
||||||
? "transparent"
|
|
||||||
: $isValid
|
|
||||||
? "#000000"
|
|
||||||
: "#7e0000"};
|
|
||||||
color: ${({ $isValid }) =>
|
|
||||||
$isValid === null ? "inherit" : $isValid ? "#15803d" : "#b91c1c"};
|
|
||||||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: 0 0 0 3px
|
box-shadow: 0 0 0 3px ${({ $isValid }) => ($isValid === null ? "#88888844" : $isValid ? "#22c55e44" : "#ef444444")};
|
||||||
${({ $isValid }) =>
|
}
|
||||||
$isValid === null
|
|
||||||
? "#88888844"
|
|
||||||
: $isValid
|
|
||||||
? "#22c55e44"
|
|
||||||
: "#ef444444"};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
export interface NumberInputProps {
|
export interface NumberInputProps {
|
||||||
value?: number;
|
value?: number;
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -43,18 +26,9 @@ export interface NumberInputProps {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function NumberInput({ value, onChange, id, placeholder = "Enter a number..." }: NumberInputProps) {
|
||||||
export function NumberInput({
|
const [raw, setRaw] = useState<string>(() => (value !== undefined ? String(value) : ""));
|
||||||
value,
|
const [isValid, setIsValid] = useState<boolean | null>(value !== undefined ? true : null);
|
||||||
onChange,
|
|
||||||
id,
|
|
||||||
placeholder = "Enter a number...",
|
|
||||||
}: NumberInputProps) {
|
|
||||||
const [raw, setRaw] = useState<string>(() => value !== undefined ? String(value) : ""
|
|
||||||
);
|
|
||||||
const [isValid, setIsValid] = useState<boolean | null>(
|
|
||||||
value !== undefined ? true : null
|
|
||||||
);
|
|
||||||
|
|
||||||
const isValidNumber = (val: string): boolean => {
|
const isValidNumber = (val: string): boolean => {
|
||||||
if (val.trim() === "") return false;
|
if (val.trim() === "") return false;
|
||||||
@@ -68,7 +42,6 @@ export function NumberInput({
|
|||||||
setRaw(val);
|
setRaw(val);
|
||||||
|
|
||||||
if (val.trim() === "") {
|
if (val.trim() === "") {
|
||||||
|
|
||||||
setIsValid(null);
|
setIsValid(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -88,7 +61,6 @@ export function NumberInput({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const executeEvaluate = () => {
|
const executeEvaluate = () => {
|
||||||
if (canEvaluateMathExpression(raw)) {
|
if (canEvaluateMathExpression(raw)) {
|
||||||
const value = calculateMathExpression(raw);
|
const value = calculateMathExpression(raw);
|
||||||
@@ -116,7 +88,7 @@ export function NumberInput({
|
|||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
value={raw}
|
value={raw}
|
||||||
onBlur={executeEvaluate}
|
onBlur={executeEvaluate}
|
||||||
onKeyUp={ev => {
|
onKeyUp={(ev) => {
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case "Enter":
|
case "Enter":
|
||||||
executeEvaluate();
|
executeEvaluate();
|
||||||
@@ -139,5 +111,4 @@ export function NumberInput({
|
|||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,38 +5,23 @@ const OptionDiv = styled.div`
|
|||||||
margin: 2px;
|
margin: 2px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function NumericInputWithLabel({
|
export function NumericInputWithLabel({ value, onChange, placeholder, name, id }: NumberInputProps & { name: string }) {
|
||||||
value,
|
return (
|
||||||
onChange,
|
<OptionDiv>
|
||||||
placeholder,
|
|
||||||
name,
|
|
||||||
id
|
|
||||||
|
|
||||||
}: NumberInputProps & { name: string }) {
|
|
||||||
return <OptionDiv>
|
|
||||||
<label htmlFor={id}>{name}:</label>
|
|
||||||
<NumberInput id={id} placeholder={placeholder} value={value} onChange={onChange} />
|
|
||||||
</OptionDiv>;
|
|
||||||
}
|
|
||||||
export function NumericInputWithLabelTable({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
placeholder,
|
|
||||||
name,
|
|
||||||
id
|
|
||||||
|
|
||||||
}: NumberInputProps & { name: string }) {
|
|
||||||
return <tr>
|
|
||||||
<td>
|
|
||||||
<label htmlFor={id}>{name}:</label>
|
<label htmlFor={id}>{name}:</label>
|
||||||
</td>
|
<NumberInput id={id} placeholder={placeholder} value={value} onChange={onChange} />
|
||||||
<td>
|
</OptionDiv>
|
||||||
<NumberInput
|
);
|
||||||
id={id}
|
}
|
||||||
placeholder={placeholder}
|
export function NumericInputWithLabelTable({ value, onChange, placeholder, name, id }: NumberInputProps & { name: string }) {
|
||||||
value={value}
|
return (
|
||||||
onChange={onChange} />
|
<tr>
|
||||||
|
<td>
|
||||||
</td>
|
<label htmlFor={id}>{name}:</label>
|
||||||
</tr>;
|
</td>
|
||||||
|
<td>
|
||||||
|
<NumberInput id={id} placeholder={placeholder} value={value} onChange={onChange} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,20 @@ const Wrapper = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
export function PaperGuideSelector({ setSettings, settings }: UseSettings) {
|
export function PaperGuideSelector({ setSettings, settings }: UseSettings) {
|
||||||
return <Wrapper>
|
return (
|
||||||
{GRID.map((e, i) => <Button
|
<Wrapper>
|
||||||
$active={e === settings.grid}
|
{GRID.map((e, i) => (
|
||||||
key={i}
|
<Button
|
||||||
onClick={() => {
|
$active={e === settings.grid}
|
||||||
setSettings({ grid: e });
|
key={i}
|
||||||
}}
|
onClick={() => {
|
||||||
>{e}
|
setSettings({ grid: e });
|
||||||
</Button>)}
|
}}
|
||||||
</Wrapper>;
|
>
|
||||||
|
{e}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { RibbonButton } from "./buttons/button-ribbon";
|
|||||||
import type { UseSettings } from "../use/use-settings";
|
import type { UseSettings } from "../use/use-settings";
|
||||||
|
|
||||||
export function PaperOrientation({ setSettings, settings }: UseSettings) {
|
export function PaperOrientation({ setSettings, settings }: UseSettings) {
|
||||||
|
return (
|
||||||
return <RibbonButton
|
<RibbonButton
|
||||||
icon={() => <FaFile style={{ transform: `rotate(${settings.landscape ? "90" : "0"}deg)` }} />}
|
icon={() => <FaFile style={{ transform: `rotate(${settings.landscape ? "90" : "0"}deg)` }} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSettings({ landscape: !settings.landscape });
|
setSettings({ landscape: !settings.landscape });
|
||||||
}}
|
}}
|
||||||
name={settings.landscape ? "Landscape" : "Portrait"} />;
|
name={settings.landscape ? "Landscape" : "Portrait"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import {
|
import {
|
||||||
A0_HEIGHT, A0_WIDTH,
|
A0_HEIGHT,
|
||||||
A1_HEIGHT, A1_WIDTH,
|
A0_WIDTH,
|
||||||
A2_HEIGHT, A2_WIDTH,
|
A1_HEIGHT,
|
||||||
A3_HEIGHT, A3_WIDTH,
|
A1_WIDTH,
|
||||||
A4_HEIGHT, A4_WIDTH,
|
A2_HEIGHT,
|
||||||
A5_HEIGHT, A5_WIDTH,
|
A2_WIDTH,
|
||||||
A6_HEIGHT, A6_WIDTH,
|
A3_HEIGHT,
|
||||||
LETTER_HEIGHT, LETTER_WIDTH,
|
A3_WIDTH,
|
||||||
LEGAL_HEIGHT, LEGAL_WIDTH,
|
A4_HEIGHT,
|
||||||
TABLOID_HEIGHT, TABLOID_WIDTH,
|
A4_WIDTH,
|
||||||
LEDGER_HEIGHT, LEDGER_WIDTH,
|
A5_HEIGHT,
|
||||||
|
A5_WIDTH,
|
||||||
|
A6_HEIGHT,
|
||||||
|
A6_WIDTH,
|
||||||
|
LETTER_HEIGHT,
|
||||||
|
LETTER_WIDTH,
|
||||||
|
LEGAL_HEIGHT,
|
||||||
|
LEGAL_WIDTH,
|
||||||
|
TABLOID_HEIGHT,
|
||||||
|
TABLOID_WIDTH,
|
||||||
|
LEDGER_HEIGHT,
|
||||||
|
LEDGER_WIDTH,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { Button } from "../styles";
|
import { Button } from "../styles";
|
||||||
import type { UseSettings } from "../use/use-settings";
|
import type { UseSettings } from "../use/use-settings";
|
||||||
@@ -52,38 +62,44 @@ const papers = [
|
|||||||
createPaper("Ledger", LEDGER_WIDTH, LEDGER_HEIGHT),
|
createPaper("Ledger", LEDGER_WIDTH, LEDGER_HEIGHT),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
export function PaperSizeSelector({ setSettings, settings, popup }: UseSettings & { popup: Popup }) {
|
export function PaperSizeSelector({ setSettings, settings, popup }: UseSettings & { popup: Popup }) {
|
||||||
const custom = papers.find(e => settings.paperWidth === e.width && settings.paperHeight === e.height);
|
const custom = papers.find((e) => settings.paperWidth === e.width && settings.paperHeight === e.height);
|
||||||
return <Wrapper>
|
return (
|
||||||
{papers.map((e, i) => <Button
|
<Wrapper>
|
||||||
$active={settings.paperWidth === e.width && settings.paperHeight === e.height}
|
{papers.map((e, i) => (
|
||||||
key={i}
|
<Button
|
||||||
onClick={() => {
|
$active={settings.paperWidth === e.width && settings.paperHeight === e.height}
|
||||||
setSettings({ paperWidth: e.width, paperHeight: e.height });
|
key={i}
|
||||||
}}
|
onClick={() => {
|
||||||
>{e.name}
|
setSettings({ paperWidth: e.width, paperHeight: e.height });
|
||||||
</Button>)}
|
}}
|
||||||
<Button
|
>
|
||||||
$active={!custom}
|
{e.name}
|
||||||
onClick={async () => {
|
</Button>
|
||||||
const result = await popup.prompt(
|
))}
|
||||||
"Paper size",
|
<Button
|
||||||
`Enter paper size like <WIDTH>x<HEIGHT> (${A4_WIDTH}x${A4_HEIGHT})`,
|
$active={!custom}
|
||||||
`${settings.paperWidth}x${settings.paperHeight}`
|
onClick={async () => {
|
||||||
);
|
const result = await popup.prompt(
|
||||||
if (result) {
|
"Paper size",
|
||||||
const [width, height] = result.split("x").map(e => Math.max(parseInt(e, 10), 1));
|
`Enter paper size like <WIDTH>x<HEIGHT> (${A4_WIDTH}x${A4_HEIGHT})`,
|
||||||
if (isNaN(width) || isNaN(height)) {
|
`${settings.paperWidth}x${settings.paperHeight}`,
|
||||||
popup.alert("Paper size", "Invalid size");
|
);
|
||||||
} else {
|
if (result) {
|
||||||
setSettings({
|
const [width, height] = result.split("x").map((e) => Math.max(parseInt(e, 10), 1));
|
||||||
paperWidth: width,
|
if (isNaN(width) || isNaN(height)) {
|
||||||
paperHeight: height
|
popup.alert("Paper size", "Invalid size");
|
||||||
});
|
} else {
|
||||||
|
setSettings({
|
||||||
|
paperWidth: width,
|
||||||
|
paperHeight: height,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}
|
>
|
||||||
}>Custom</Button>
|
Custom
|
||||||
</Wrapper>;
|
</Button>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ const Table = styled.table`
|
|||||||
const WrapperColumn = styled.div`
|
const WrapperColumn = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const WrapperRow = styled.div`
|
const WrapperRow = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const LayerPanel = styled.div`
|
const LayerPanel = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -35,7 +35,7 @@ const LayerPanel = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const ToggleButton = styled.button<{ $enabled: boolean }>`
|
const ToggleButton = styled.button<{ $enabled: boolean }>`
|
||||||
background-color: ${({ $enabled }) => $enabled ? "var(--secondary-color)" : "var(--primary-color)"};
|
background-color: ${({ $enabled }) => ($enabled ? "var(--secondary-color)" : "var(--primary-color)")};
|
||||||
color: white;
|
color: white;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -44,19 +44,17 @@ const ToggleButton = styled.button<{ $enabled: boolean }>`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export function SvgTracerOptions({ selected, handler }: { selected: ImageEditor[]; handler: VectraceHandler }) {
|
||||||
|
|
||||||
export function SvgTracerOptions({ selected, handler }: { selected: ImageEditor[], handler: VectraceHandler }) {
|
|
||||||
const [options, setOptions] = useState(selected[0]?.tracerOptions ?? {});
|
const [options, setOptions] = useState(selected[0]?.tracerOptions ?? {});
|
||||||
const [updated, setUpdated] = useState(false);
|
const [updated, setUpdated] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOptions(selected[0]?.tracerOptions ?? {});
|
setOptions(selected[0]?.tracerOptions ?? {});
|
||||||
setUpdated(true);
|
setUpdated(true);
|
||||||
}, [selected.map(e => `${e.id}${JSON.stringify(e.svgData.options)}`).join("")]);
|
}, [selected.map((e) => `${e.id}${JSON.stringify(e.svgData.options)}`).join("")]);
|
||||||
|
|
||||||
const redraw = () => {
|
const redraw = () => {
|
||||||
selected.forEach(e => {
|
selected.forEach((e) => {
|
||||||
e.setTracerSvg({ ...options });
|
e.setTracerSvg({ ...options });
|
||||||
});
|
});
|
||||||
setUpdated(true);
|
setUpdated(true);
|
||||||
@@ -67,69 +65,83 @@ export function SvgTracerOptions({ selected, handler }: { selected: ImageEditor[
|
|||||||
setUpdated(false);
|
setUpdated(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <WrapperRow>
|
return (
|
||||||
<VerticalSlider
|
<WrapperRow>
|
||||||
max={255}
|
<VerticalSlider
|
||||||
min={0}
|
max={255}
|
||||||
value={options.transparencyThreshold ?? TRANSPARENCY_THRESHOLD_DEFAULT}
|
min={0}
|
||||||
onLabelClick={async () => {
|
value={options.transparencyThreshold ?? TRANSPARENCY_THRESHOLD_DEFAULT}
|
||||||
const value = await handler.popup.prompt(
|
onLabelClick={async () => {
|
||||||
"Transparency threshold",
|
const value = await handler.popup.prompt(
|
||||||
"Enter transparency threshold between 0-255",
|
"Transparency threshold",
|
||||||
(options.transparencyThreshold ?? TRANSPARENCY_THRESHOLD_DEFAULT).toString());
|
"Enter transparency threshold between 0-255",
|
||||||
if (value) {
|
(options.transparencyThreshold ?? TRANSPARENCY_THRESHOLD_DEFAULT).toString(),
|
||||||
const int = parseInt(value, 10);
|
);
|
||||||
if (!isNaN(int)) {
|
if (value) {
|
||||||
setValue("transparencyThreshold", clamp(int, 0, 255));
|
const int = parseInt(value, 10);
|
||||||
|
if (!isNaN(int)) {
|
||||||
|
setValue("transparencyThreshold", clamp(int, 0, 255));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}}
|
onChange={(value) => {
|
||||||
onChange={value => {
|
setValue("transparencyThreshold", value);
|
||||||
setValue("transparencyThreshold", value);
|
}}
|
||||||
}} />
|
/>
|
||||||
<WrapperColumn>
|
<WrapperColumn>
|
||||||
<Button $active={!updated} title="Draw" onClick={() => { redraw(); }}><FaBezierCurve /></Button>
|
<Button
|
||||||
<WrapperRow>
|
$active={!updated}
|
||||||
<Table>
|
title="Draw"
|
||||||
<tbody>
|
onClick={() => {
|
||||||
<NumericInputWithLabelTable
|
redraw();
|
||||||
name="Ltres"
|
}}
|
||||||
value={options.ltres}
|
>
|
||||||
onChange={value => setValue("ltres", value)}
|
<FaBezierCurve />
|
||||||
/>
|
</Button>
|
||||||
<NumericInputWithLabelTable
|
<WrapperRow>
|
||||||
name="Qtres"
|
<Table>
|
||||||
value={options.qtres}
|
<tbody>
|
||||||
onChange={value => setValue("qtres", value)}
|
<NumericInputWithLabelTable
|
||||||
/>
|
name="Ltres"
|
||||||
<NumericInputWithLabelTable
|
value={options.ltres}
|
||||||
name="Path omit"
|
onChange={(value) => setValue("ltres", value)}
|
||||||
value={options.pathomit}
|
/>
|
||||||
onChange={value => setValue("pathomit", value)}
|
<NumericInputWithLabelTable
|
||||||
/>
|
name="Qtres"
|
||||||
<NumericInputWithLabelTable
|
value={options.qtres}
|
||||||
name="Round corners"
|
onChange={(value) => setValue("qtres", value)}
|
||||||
value={options.roundcoords}
|
/>
|
||||||
onChange={value => setValue("roundcoords", value)}
|
<NumericInputWithLabelTable
|
||||||
/>
|
name="Path omit"
|
||||||
</tbody>
|
value={options.pathomit}
|
||||||
</Table >
|
onChange={(value) => setValue("pathomit", value)}
|
||||||
<LayerPanel>
|
/>
|
||||||
{(options.allowedPaths || []).map((e, i) => {
|
<NumericInputWithLabelTable
|
||||||
return <ToggleButton
|
name="Round corners"
|
||||||
key={i}
|
value={options.roundcoords}
|
||||||
$enabled={e}
|
onChange={(value) => setValue("roundcoords", value)}
|
||||||
onClick={() => {
|
/>
|
||||||
const array = options?.allowedPaths ? [...options.allowedPaths] : [];
|
</tbody>
|
||||||
array[i] = !array[i];
|
</Table>
|
||||||
setValue("allowedPaths", array);
|
<LayerPanel>
|
||||||
}}
|
{(options.allowedPaths || []).map((e, i) => {
|
||||||
>
|
return (
|
||||||
{`${i + 1} Layer`}
|
<ToggleButton
|
||||||
</ToggleButton>;
|
key={i}
|
||||||
})}
|
$enabled={e}
|
||||||
</LayerPanel>
|
onClick={() => {
|
||||||
|
const array = options?.allowedPaths ? [...options.allowedPaths] : [];
|
||||||
</WrapperRow>
|
array[i] = !array[i];
|
||||||
</WrapperColumn></WrapperRow>;
|
setValue("allowedPaths", array);
|
||||||
}
|
}}
|
||||||
|
>
|
||||||
|
{`${i + 1} Layer`}
|
||||||
|
</ToggleButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</LayerPanel>
|
||||||
|
</WrapperRow>
|
||||||
|
</WrapperColumn>
|
||||||
|
</WrapperRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
|
||||||
const SliderWrapper = styled.div`
|
const SliderWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface SliderProps {
|
interface SliderProps {
|
||||||
@@ -20,73 +19,73 @@ interface SliderProps {
|
|||||||
onChange?: (value: number) => void;
|
onChange?: (value: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface TrackTransientProps {
|
interface TrackTransientProps {
|
||||||
$fillPercent: number;
|
$fillPercent: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Track = styled.input.attrs({ type: "range" }) <TrackTransientProps>`
|
const Track = styled.input.attrs({ type: "range" })<TrackTransientProps>`
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
writing-mode: vertical-lr;
|
|
||||||
direction: rtl;
|
|
||||||
width: 6px;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(
|
|
||||||
to top,
|
|
||||||
white ${({ $fillPercent }) => $fillPercent}%,
|
|
||||||
var(--secondary-color) ${({ $fillPercent }) => $fillPercent}%
|
|
||||||
);
|
|
||||||
border-radius: 0;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s ease;
|
|
||||||
|
|
||||||
&::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 22px;
|
-webkit-appearance: none;
|
||||||
height: 22px;
|
writing-mode: vertical-lr;
|
||||||
|
direction: rtl;
|
||||||
|
width: 6px;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
white ${({ $fillPercent }) => $fillPercent}%,
|
||||||
|
var(--secondary-color) ${({ $fillPercent }) => $fillPercent}%
|
||||||
|
);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: #ffffff;
|
outline: none;
|
||||||
border: 3px solid var(--secondary-color);
|
cursor: pointer;
|
||||||
box-shadow: 0 0 6px rgba(255, 68, 68, 0.6);
|
transition: background 0.15s ease;
|
||||||
cursor: grab;
|
|
||||||
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
|
||||||
|
|
||||||
&:active {
|
&::-webkit-slider-thumb {
|
||||||
cursor: grabbing;
|
-webkit-appearance: none;
|
||||||
transform: scale(1.2);
|
appearance: none;
|
||||||
box-shadow: 0 0 12px rgba(255, 68, 68, 0.9);
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 3px solid var(--secondary-color);
|
||||||
|
box-shadow: 0 0 6px rgba(255, 68, 68, 0.6);
|
||||||
|
cursor: grab;
|
||||||
|
transition:
|
||||||
|
transform 0.1s ease,
|
||||||
|
box-shadow 0.1s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
transform: scale(1.2);
|
||||||
|
box-shadow: 0 0 12px rgba(255, 68, 68, 0.9);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&::-moz-range-thumb {
|
&::-moz-range-thumb {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 3px solid #ff4444;
|
border: 3px solid #ff4444;
|
||||||
box-shadow: 0 0 6px rgba(255, 68, 68, 0.6);
|
box-shadow: 0 0 6px rgba(255, 68, 68, 0.6);
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
transition: transform 0.1s ease;
|
transition: transform 0.1s ease;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
transform: scale(1.2);
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&::-moz-range-track {
|
&::-moz-range-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
const ValueLabel = styled.span`
|
const ValueLabel = styled.span`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: white;
|
color: white;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface SliderProps {
|
interface SliderProps {
|
||||||
@@ -98,17 +97,8 @@ interface SliderProps {
|
|||||||
onChange?: (value: number) => void;
|
onChange?: (value: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VerticalSlider({
|
export function VerticalSlider({ min = 0, max = 100, step = 1, onLabelClick, value: controlledValue, onChange }: SliderProps) {
|
||||||
min = 0,
|
const [internalValue, setInternalValue] = useState(controlledValue ?? Math.floor((max - min) / 2));
|
||||||
max = 100,
|
|
||||||
step = 1,
|
|
||||||
onLabelClick,
|
|
||||||
value: controlledValue,
|
|
||||||
onChange,
|
|
||||||
}: SliderProps) {
|
|
||||||
const [internalValue, setInternalValue] = useState(
|
|
||||||
controlledValue ?? Math.floor((max - min) / 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
const value = controlledValue ?? internalValue;
|
const value = controlledValue ?? internalValue;
|
||||||
const fillPercent = ((value - min) / (max - min)) * 100;
|
const fillPercent = ((value - min) / (max - min)) * 100;
|
||||||
@@ -121,17 +111,14 @@ export function VerticalSlider({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SliderWrapper>
|
<SliderWrapper>
|
||||||
<ValueLabel onDoubleClick={() => {
|
<ValueLabel
|
||||||
onLabelClick?.();
|
onDoubleClick={() => {
|
||||||
}}>{value}</ValueLabel>
|
onLabelClick?.();
|
||||||
<Track
|
}}
|
||||||
min={min}
|
>
|
||||||
max={max}
|
{value}
|
||||||
step={step}
|
</ValueLabel>
|
||||||
value={value}
|
<Track min={min} max={max} step={step} value={value} $fillPercent={fillPercent} onChange={handleChange} />
|
||||||
$fillPercent={fillPercent}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</SliderWrapper>
|
</SliderWrapper>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user