);
}
function measurementsString(line: Line, unitOfMeasure: string): string {
let a = -angleDeg(line);
if (a < 0) {
a += 360;
}
let label = a.toFixed(0);
if (label == "360") {
label = "0";
}
const d = distanceString(line, unitOfMeasure);
return `${d} ${label}°`;
}
function distanceString(line: Line, unitOfMeasure: string): string {
let d = dist(...line) / CSS_PIXELS_PER_INCH;
if (unitOfMeasure == CM) {
d *= 2.54;
}
const unit = unitOfMeasure == CM ? "cm" : '"';
return `${d.toFixed(2)}${unit}`;
}
================================================
FILE: app/_components/canvases/overlay-canvas.tsx
================================================
import React, { useEffect, useRef } from "react";
import { CanvasState, drawOverlays } from "@/_lib/drawing";
import { Point } from "@/_lib/point";
import { DisplaySettings, strokeColor } from "@/_lib/display-settings";
import {
RestoreTransforms,
getPerspectiveTransformFromPoints,
isFlipped,
} from "@/_lib/geometry";
import { useTransformContext } from "@/_hooks/use-transform-context";
import Matrix from "ml-matrix";
import { useTranslations } from "next-intl";
export default function OverlayCanvas({
className,
points,
width,
height,
unitOfMeasure,
displaySettings,
calibrationTransform,
zoomedOut,
magnifying,
restoreTransforms,
patternScale,
}: {
className: string | undefined;
points: Point[];
width: number;
height: number;
unitOfMeasure: string;
displaySettings: DisplaySettings;
calibrationTransform: Matrix;
zoomedOut: boolean;
magnifying: boolean;
restoreTransforms: RestoreTransforms | null;
patternScale: string;
}) {
const flipped = isFlipped(useTransformContext());
const canvasRef = useRef(null);
const t = useTranslations("OverlayCanvas");
useEffect(() => {
if (canvasRef !== null && canvasRef.current !== null) {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
if (ctx !== null && points.length == 4) {
const perspectiveMatrix = getPerspectiveTransformFromPoints(
points,
width,
height,
1.0,
false,
);
ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;
const cs = new CanvasState(
ctx,
{ x: 0, y: 0 },
points,
width,
height,
perspectiveMatrix,
false,
new Set(),
new Set(),
unitOfMeasure,
strokeColor(displaySettings.theme),
displaySettings,
flipped,
calibrationTransform,
zoomedOut,
magnifying,
restoreTransforms,
t,
patternScale,
);
drawOverlays(cs);
}
}
}, [
points,
width,
height,
unitOfMeasure,
displaySettings,
flipped,
calibrationTransform,
zoomedOut,
magnifying,
restoreTransforms,
t,
patternScale,
]);
return (
);
}
================================================
FILE: app/_components/draggable.tsx
================================================
import { Matrix } from "ml-matrix";
import {
ReactNode,
useState,
useEffect,
SetStateAction,
Dispatch,
} from "react";
import {
RestoreTransforms,
toMatrix3d,
transformPoint,
translate,
scale,
scaleAboutPoint,
transformPoints,
getBounds,
rectCorners,
} from "@/_lib/geometry";
import { Point } from "@/_lib/point";
import { CSS_PIXELS_PER_INCH } from "@/_lib/pixels-per-inch";
import { IN } from "@/_lib/unit";
import useProgArrowKeyToMatrix from "@/_hooks/use-prog-arrow-key-to-matrix";
import { visible } from "./theme/css-functions";
import {
useTransformContext,
useTransformerContext,
} from "@/_hooks/use-transform-context";
import { MenuStates } from "@/_lib/menu-states";
import { inverse } from "ml-matrix";
export default function Draggable({
children,
perspective,
isCalibrating,
unitOfMeasure,
calibrationTransform,
setCalibrationTransform,
setPerspective,
className,
magnifying,
setMagnifying,
restoreTransforms,
setRestoreTransforms,
zoomedOut,
setZoomedOut,
layoutWidth,
layoutHeight,
calibrationCenter,
menuStates,
file,
}: {
children: ReactNode;
perspective: Matrix;
isCalibrating: boolean;
unitOfMeasure: string;
calibrationTransform: Matrix;
setCalibrationTransform: Dispatch>;
setPerspective: Dispatch>;
className: string;
magnifying: boolean;
setMagnifying: Dispatch>;
setRestoreTransforms: Dispatch>;
restoreTransforms: RestoreTransforms | null;
zoomedOut: boolean;
setZoomedOut: Dispatch>;
layoutWidth: number;
layoutHeight: number;
calibrationCenter: Point;
menuStates: MenuStates;
file: File | null;
}) {
const [dragStart, setDragStart] = useState(null);
const [transformStart, setTransformStart] = useState(null);
const transform = useTransformContext();
const transformer = useTransformerContext();
const quarterInchPx = CSS_PIXELS_PER_INCH / 4;
const halfCmPx = CSS_PIXELS_PER_INCH / 2.54 / 2;
useProgArrowKeyToMatrix(
!isCalibrating,
unitOfMeasure === IN ? quarterInchPx : halfCmPx,
(matrix) => {
transformer.setLocalTransform(matrix.mmul(transform));
},
);
function handleOnEnd(): void {
setDragStart(null);
setTransformStart(null);
}
function handleMove(e: React.PointerEvent) {
const p = { x: e.clientX, y: e.clientY };
if (e.pointerType === "mouse") {
/* If we aren't currently dragging, ignore the mouse move event */
if (dragStart === null) {
return;
}
if (e.buttons === 0 && dragStart !== null) {
// If the mouse button is released, end the drag.
handleOnEnd();
return;
}
}
if (transformStart !== null && dragStart !== null) {
const dest = transformPoint(p, perspective);
const tx = dest.x - dragStart.x;
const ty = dest.y - dragStart.y;
const vec = { x: tx, y: ty };
transformer.setLocalTransform(translate(vec).mmul(transformStart));
}
}
function handleOnStart(e: React.PointerEvent): void {
const p = { x: e.clientX, y: e.clientY };
const pt = transformPoint(p, perspective);
if (magnifying) {
if (restoreTransforms === null) {
setRestoreTransforms({
localTransform: transform.clone(),
calibrationTransform: calibrationTransform.clone(),
});
transformer.magnify(5, pt);
setDragStart(pt);
setTransformStart(scaleAboutPoint(5, pt).mmul(transform));
} else {
setMagnifying(false);
}
} else if (restoreTransforms !== null) {
// zoomed out, so we need to zoom in
const oldLocal = restoreTransforms.localTransform;
const dest = transformPoint(transformPoint(p, perspective), oldLocal);
const newLocal = translate({
x: -dest.x + calibrationCenter.x,
y: -dest.y + calibrationCenter.y,
}).mmul(oldLocal);
setRestoreTransforms({
localTransform: newLocal,
calibrationTransform: restoreTransforms.calibrationTransform.clone(),
});
setZoomedOut(false);
} else {
setDragStart(pt);
setTransformStart(transform.clone());
}
}
useEffect(() => {
if (zoomedOut && restoreTransforms === null) {
setRestoreTransforms({
localTransform: transform.clone(),
calibrationTransform: calibrationTransform.clone(),
});
const layerMenuWidth = 190;
const stitchMenuHeight = 81;
const navHeight = 64;
let x = menuStates.layers ? layerMenuWidth : 0;
const y = menuStates.stitch ? navHeight + stitchMenuHeight : navHeight;
// get four corners layout width and height
// transform the four corners to the perspective
// find the min and max x and y
const [min, max] = getBounds(
transformPoints(rectCorners(layoutWidth, layoutHeight), transform),
);
const w = max.x - min.x;
const h = max.y - min.y;
// scale to fit below/next to the menu
const s = Math.min(
(window.innerWidth - x) / w,
(window.innerHeight - y) / h,
);
// center the layout
if (x + w * s < window.innerWidth) {
x = (window.innerWidth - w * s) / 2;
}
const zoomOut = translate({ x, y })
.mmul(scale(s))
.mmul(translate({ x: -min.x, y: -min.y }))
.mmul(transform);
setCalibrationTransform(zoomOut.clone());
setPerspective(inverse(zoomOut.clone()));
transformer.setLocalTransform(Matrix.identity(3));
}
}, [
zoomedOut,
setZoomedOut,
transform,
transformer,
layoutWidth,
layoutHeight,
restoreTransforms,
menuStates,
setCalibrationTransform,
setPerspective,
calibrationTransform,
setRestoreTransforms,
]);
useEffect(() => {
transformer.setLocalTransform(Matrix.identity(3));
}, [file]);
useEffect(() => {
if (!magnifying && !zoomedOut && restoreTransforms !== null) {
transformer.setLocalTransform(restoreTransforms.localTransform);
setCalibrationTransform(restoreTransforms.calibrationTransform);
setPerspective(inverse(restoreTransforms.calibrationTransform));
setRestoreTransforms(null);
}
}, [
magnifying,
zoomedOut,
restoreTransforms,
setRestoreTransforms,
setCalibrationTransform,
transformer,
setPerspective,
]);
let cursorMode = "cursor-grab";
if (zoomedOut || magnifying) {
cursorMode = "cursor-zoom-in";
}
if (magnifying && restoreTransforms !== null) {
cursorMode = "cursor-zoom-out";
}
if (dragStart !== null) {
cursorMode = "cursor-grabbing";
}
return (
{children}
);
}
================================================
FILE: app/_components/filters.tsx
================================================
export default function Filters() {
return (
);
}
================================================
FILE: app/_components/header.tsx
================================================
import { useTranslations } from "next-intl";
import {
ChangeEvent,
Dispatch,
SetStateAction,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { FullScreenHandle } from "react-full-screen";
import InlineInput from "@/_components/inline-input";
import InlineSelect from "@/_components/inline-select";
import DeleteIcon from "@/_icons/delete-icon";
import FlipHorizontalIcon from "@/_icons/flip-horizontal-icon";
import FlipVerticalIcon from "@/_icons/flip-vertical-icon";
import FlipCenterOnIcon from "@/_icons/flip-center-on-icon";
import GridOffIcon from "@/_icons/grid-off-icon";
import GridOnIcon from "@/_icons/grid-on-icon";
import OverlayBorderIcon from "@/_icons/overlay-border-icon";
import OverlayPaperIcon from "@/_icons/overlay-paper-icon";
import InfoIcon from "@/_icons/info-icon";
import InvertColorIcon from "@/_icons/invert-color-icon";
import InvertColorOffIcon from "@/_icons/invert-color-off-icon";
import PdfIcon from "@/_icons/pdf-icon";
import Rotate90DegreesCWIcon from "@/_icons/rotate-90-degrees-cw-icon";
import {
DisplaySettings,
isDarkTheme,
strokeColor,
themes,
} from "@/_lib/display-settings";
import { CM, IN } from "@/_lib/unit";
import RecenterIcon from "@/_icons/recenter-icon";
import { getCalibrationCenterPoint } from "@/_lib/geometry";
import { visible } from "@/_components/theme/css-functions";
import { IconButton } from "@/_components/buttons/icon-button";
import { DropdownCheckboxIconButton } from "@/_components/buttons/dropdown-checkbox-icon-button";
import Tooltip from "@/_components/tooltip/tooltip";
import ExpandLessIcon from "@/_icons/expand-less-icon";
import LineWeightIcon from "@/_icons/line-weight-icon";
import { useKeyDown } from "@/_hooks/use-key-down";
import { KeyCode } from "@/_lib/key-code";
import { MenuStates } from "@/_lib/menu-states";
import MoveIcon from "@/_icons/move-icon";
import {
getCalibrationContextUpdatedWithEvent,
getIsInvalidatedCalibrationContextWithPointerEvent,
} from "@/_lib/calibration-context";
import Modal from "./modal/modal";
import { ModalTitle } from "./modal/modal-title";
import { ModalText } from "./modal/modal-text";
import { ModalActions } from "./modal/modal-actions";
import { Button } from "./buttons/button";
import { useTransformerContext } from "@/_hooks/use-transform-context";
import { DropdownIconButton } from "./buttons/dropdown-icon-button";
import MarkAndMeasureIcon from "@/_icons/mark-and-measure-icon";
import FlippedPatternIcon from "@/_icons/flipped-pattern-icon";
import ZoomOutIcon from "@/_icons/zoom-out-icon";
import FullSceenExitIcon from "@/_icons/full-screen-exit-icon";
import FullScreenIcon from "@/_icons/full-screen-icon";
import LoadingSpinner from "@/_icons/loading-spinner";
import { LoadStatusEnum } from "@/_lib/load-status-enum";
import { ButtonStyle, getButtonStyleClasses } from "./theme/styles";
import { ButtonColor, getColorClasses } from "./theme/colors";
import MailIcon from "@/_icons/mail-icon";
import ZoomInIcon from "@/_icons/zoom-in-icon";
import { acceptedMimeTypes } from "@/_lib/is-valid-file";
import { toggleFullScreen } from "@/_lib/full-screen";
export default function Header({
isCalibrating,
setIsCalibrating,
widthInput,
heightInput,
width,
height,
handleHeightChange,
handleWidthChange,
handleResetCalibration,
handleFileChange,
fullScreenHandle,
unitOfMeasure,
setUnitOfMeasure,
displaySettings,
setDisplaySettings,
layoutWidth,
layoutHeight,
lineThickness,
setLineThickness,
measuring,
setMeasuring,
menuStates,
setMenuStates,
showingMovePad,
setShowingMovePad,
setCalibrationValidated,
fullScreenTooltipVisible,
magnifying,
setMagnifying,
zoomedOut,
setZoomedOut,
fileLoadStatus,
lineThicknessStatus,
buttonColor,
mailOpen,
setMailOpen,
invalidCalibration,
file,
}: {
isCalibrating: boolean;
setIsCalibrating: Dispatch>;
widthInput: string;
heightInput: string;
width: number;
height: number;
handleHeightChange: (e: ChangeEvent) => void;
handleWidthChange: (e: ChangeEvent) => void;
handleFileChange: (e: ChangeEvent) => void;
handleResetCalibration: () => void;
fullScreenHandle: FullScreenHandle;
unitOfMeasure: string;
setUnitOfMeasure: (newUnit: string) => void;
displaySettings: DisplaySettings;
setDisplaySettings: (newDisplaySettings: DisplaySettings) => void;
layoutWidth: number;
layoutHeight: number;
lineThickness: number;
setLineThickness: (newThickness: number) => void;
measuring: boolean;
setMeasuring: Dispatch>;
menuStates: MenuStates;
setMenuStates: Dispatch>;
showingMovePad: boolean;
setShowingMovePad: Dispatch>;
setCalibrationValidated: Dispatch>;
fullScreenTooltipVisible: boolean;
magnifying: boolean;
setMagnifying: Dispatch>;
zoomedOut: boolean;
setZoomedOut: Dispatch>;
fileLoadStatus: LoadStatusEnum;
lineThicknessStatus: LoadStatusEnum;
buttonColor: ButtonColor;
mailOpen: boolean;
setMailOpen: Dispatch>;
invalidCalibration: boolean;
file: File | null;
}) {
const [calibrationAlert, setCalibrationAlert] = useState("");
const fileInputRef = useRef(null);
const mailRead = useRef(true);
const transformer = useTransformerContext();
const t = useTranslations("Header");
const fileInputClassNames = useMemo(() => {
if (!isCalibrating && fileLoadStatus === LoadStatusEnum.LOADING) {
return "outline-gray-50 !text-gray-50 !bg-gray-500";
}
}, [isCalibrating, fileLoadStatus]);
function saveContextAndProject(e: React.MouseEvent) {
const current = getCalibrationContextUpdatedWithEvent(
e,
fullScreenHandle.active,
);
localStorage.setItem("calibrationContext", JSON.stringify(current));
setCalibrationValidated(true);
setIsCalibrating(false);
if (file === null && fileInputRef.current !== null) {
fileInputRef.current.click();
}
}
function handleCalibrateProjectButtonClick(
e: React.PointerEvent,
) {
if (isCalibrating) {
const expectedContext = localStorage.getItem("calibrationContext");
if (invalidCalibration) {
setCalibrationAlert(t("invalidCalibration"));
} else if (expectedContext) {
const expected = JSON.parse(expectedContext);
if (
getIsInvalidatedCalibrationContextWithPointerEvent(
expected,
e,
fullScreenHandle.active,
true,
)
) {
// Give user a chance to recalibrate or continue.
setCalibrationAlert(t("calibrationAlertContinue"));
} else {
saveContextAndProject(e);
}
} else {
saveContextAndProject(e);
}
} else {
// go to calibration.
setIsCalibrating(true);
}
}
const handleRotate90 = () => {
transformer.rotate(
getCalibrationCenterPoint(width, height, unitOfMeasure),
90,
);
};
const handleFlipHorizontal = () => {
transformer.flipHorizontal(
getCalibrationCenterPoint(width, height, unitOfMeasure),
);
};
const handleFlipVertical = () => {
transformer.flipVertical(
getCalibrationCenterPoint(width, height, unitOfMeasure),
);
};
const handleRecenter = () => {
transformer.recenter(
getCalibrationCenterPoint(width, height, unitOfMeasure),
layoutWidth,
layoutHeight,
);
};
const handleOpenMail = () => {
setMailOpen(true);
localStorage.setItem("mailRead", Date.now().toString());
};
const overlayOptions = {
disabled: {
icon: ,
text: t("overlayOptionDisabled"),
},
grid: {
icon: ,
text: t("overlayOptionGrid"),
},
border: {
icon: ,
text: t("overlayOptionBorder"),
},
paper: {
icon: ,
text: t("overlayOptionPaper"),
},
flipLines: {
icon: ,
text: t("overlayOptionFliplines"),
},
flippedPattern: {
icon: ,
text: t("overlayOptionFlippedPattern"),
},
};
const lineThicknessOptions = [
{
text: "0px",
value: 0,
},
{
text: "1px",
value: 1,
},
{
text: "2px",
value: 2,
},
{
text: "3px",
value: 3,
},
{
text: "4px",
value: 4,
},
{
text: "5px",
value: 5,
},
{
text: "6px",
value: 6,
},
{
text: "7px",
value: 7,
},
];
useEffect(() => {
const mailReadDate = localStorage.getItem("mailRead");
if (mailReadDate) {
const mailReadTime = parseInt(mailReadDate);
const lastReleaseDate = new Date("2025-04-22").getTime();
// If mail was read after the last release date, style mail as read.
if (mailReadTime > lastReleaseDate) {
mailRead.current = true;
} else {
mailRead.current = false;
}
} else {
mailRead.current = false;
}
}, [mailOpen]);
useKeyDown(() => {
handleFlipHorizontal();
}, [KeyCode.KeyH]);
useKeyDown(() => {
handleFlipVertical();
}, [KeyCode.KeyV]);
useKeyDown(() => {
handleRecenter();
}, [KeyCode.KeyC]);
useKeyDown(() => {
handleRotate90();
}, [KeyCode.KeyR]);
useKeyDown(() => {
setMeasuring(!measuring);
}, [KeyCode.KeyL]);
useKeyDown(() => {
setMagnifying(!magnifying);
}, [KeyCode.KeyM]);
useKeyDown(() => {
setZoomedOut(!zoomedOut);
}, [KeyCode.KeyZ]);
return (
<>
0}>
{t("calibrationAlertTitle")}{calibrationAlert}
{invalidCalibration ? (
) : (
<>
>
)}
>
);
}
================================================
FILE: app/_components/inline-input.tsx
================================================
import { ChangeEvent, ReactElement } from "react";
/**
* Controlled labelled text input
* @param handleChange - Function that handles change to input
* @param id - Global html attribute that must be unique to the document
* @param inputTestId - Input ID used for testing
* @param label - Input label visible to the user
* @param name - Name submitted with the form
* @param value - Input value
*/
export default function InlineInput({
className,
inputClassName,
handleChange,
id,
inputTestId,
label,
labelRight,
name,
value,
min,
type,
}: {
className?: string | undefined;
inputClassName?: string | undefined;
handleChange: (e: ChangeEvent) => void;
id?: string;
inputTestId?: string;
label?: string | ReactElement;
labelRight?: string | null;
name?: string;
value: string;
type?: string;
min?: string;
}) {
return (
) => handleChange(e)}
required
value={value}
/>
);
}
================================================
FILE: app/_components/inline-select.tsx
================================================
import { ChangeEvent } from "react";
import { SelectOption } from "@/_lib/interfaces/select-option";
/**
* Controlled labelled text input
* @param handleChange - Function that handles change to input
* @param id - Global html attribute that must be unique to the document
* @param inputTestId - Input ID used for testing
* @param label - Input label visible to the user
* @param name - Name submitted with the form
* @param value - Input value
*/
export default function InlineSelect({
className,
handleChange,
id,
name,
value,
options,
}: {
className?: string;
handleChange: (e: ChangeEvent) => void;
id: string;
inputTestId?: string;
name: string;
value: string;
options: SelectOption[];
}) {
return (
);
}
================================================
FILE: app/_components/troubleshooting-button.tsx
================================================
import { useTranslations } from "next-intl";
import Modal from "./modal/modal";
import { ModalTitle } from "./modal/modal-title";
import { ModalText } from "./modal/modal-text";
import { ModalActions } from "./modal/modal-actions";
import { Button } from "./buttons/button";
import ModalFigure from "./modal/modal-figure";
import { ModalSubtitle } from "./modal/modal-subtitle";
import { useState } from "react";
export default function TroubleshootingButton({
isDarkTheme,
}: {
isDarkTheme: boolean;
}) {
const t = useTranslations("Troubleshooting");
const [troubleshooting, setTroubleshooting] = useState(false);
return (
<>
{t("title")}{t("dragCorners.title")}
{t("dragCorners.description")}{t("inputMeasurement")}{t("offByOne.title")}{t("offByOne.description")}{t("unevenSurface.title")}{t("unevenSurface.description")}{t("dimensionsSwapped.title")}{t("dimensionsSwapped.description")}
>
);
}
================================================
FILE: app/_hooks/use-key-down.ts
================================================
import { useCallback, useEffect } from "react";
import { KeyCode } from "@/_lib/key-code";
export const useKeyDown = (
callback: (e: KeyboardEvent) => void,
keyCodes: KeyCode[],
) => {
const onKeyDown = useCallback(
(e: KeyboardEvent) => {
const keyDown = keyCodes.some((keyCode) => e.key === keyCode);
// Only intercept keydown events when no modifier keys are pressed to prevent issues like CTRL-V not working #369
// Removing shiftKey because it is used for snapping when using the line tool
if (keyDown && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
callback(e);
}
},
[keyCodes, callback],
);
useEffect(() => {
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [onKeyDown]);
};
================================================
FILE: app/_hooks/use-key-up.ts
================================================
import { useCallback, useEffect } from "react";
import { KeyCode } from "@/_lib/key-code";
export const useKeyUp = (callback: (T?: any) => void, keyCodes: KeyCode[]) => {
const onKeyUp = useCallback(
(e: KeyboardEvent) => {
const keyUp = keyCodes.some((keyCode) => e.key === keyCode);
if (keyUp) {
e.preventDefault();
callback();
}
},
[keyCodes, callback],
);
useEffect(() => {
document.addEventListener("keyup", onKeyUp);
return () => {
document.removeEventListener("keyup", onKeyUp);
};
}, [onKeyUp]);
};
================================================
FILE: app/_hooks/use-layers.ts
================================================
import { Layers } from "@/_lib/layers";
import layersReducer, { LayerAction } from "@/_reducers/layersReducer";
import { Dispatch, useCallback, useReducer } from "react";
/**
* Hook that stores visible layers per file name in local storage whenever there
* is a change in visibility, and loads those values whenever the "set-layers" action
* is dispatched.
*/
export default function useLayers(fileName: string) {
const [layers, dispatchLayersActionInternal] = useReducer(layersReducer, {});
const dispatchLayersAction: Dispatch = useCallback(
(action: LayerAction) => {
dispatchLayersActionInternal(action);
// We need to know the new layers value so we can synchronize with local storage
let newLayers = layersReducer(layers, action);
if (action.type === "set-layers") {
// Layers were freshly loaded, so let's check local storage for which layers should be visible
const visibleLayers = visibleLayersFromLocalStorage(fileName);
if (visibleLayers != null && visibleLayers.length > 0) {
const visibilityAction: LayerAction = {
type: "update-visibility",
visibleLayers: new Set(visibleLayers),
};
dispatchLayersActionInternal(visibilityAction);
// We also need to update the layers data we're about to write back to local storage -
// otherwise visible layers would reset to "everything visible"
newLayers = layersReducer(newLayers, visibilityAction);
}
}
if (newLayers == null || Object.keys(newLayers).length === 0) {
return;
}
writeToLocalStorage(fileName, newLayers);
},
[layers, fileName],
);
return { layers, dispatchLayersAction };
}
function visibleLayersFromLocalStorage(fileName: string): string[] | undefined {
return readFromLocalStorage(`visibleLayers:${fileName}`);
}
function writeToLocalStorage(fileName: string, layers: Layers) {
localStorage.setItem(
`visibleLayers:${fileName}`,
JSON.stringify(
Object.entries(layers)
.map(([key, layer]) => (layer.visible ? key : undefined))
.filter((x) => x != null),
),
);
}
function readFromLocalStorage(key: string): T | undefined {
const rawValue = localStorage.getItem(key);
if (rawValue == null) {
return undefined;
}
try {
return JSON.parse(rawValue);
} catch {
return undefined;
}
}
================================================
FILE: app/_hooks/use-on-click-outside.ts
================================================
import { RefObject, useEffect, useRef } from "react";
export default function useOnClickOutside(
containerRef: RefObject,
callback: () => void,
) {
// Store callback in a ref so that we get a stable reference for the click handler
const callbackRef = useRef<() => void>(callback);
useEffect(() => {
callbackRef.current = callback;
});
useEffect(() => {
const handleClickOutside = (event: PointerEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
callbackRef.current();
}
};
document.addEventListener("pointerdown", handleClickOutside);
return () => {
document.removeEventListener("pointerdown", handleClickOutside);
};
}, [containerRef]);
}
================================================
FILE: app/_hooks/use-prog-arrow-key-handler.ts
================================================
import { KeyCode } from "@/_lib/key-code";
import { useCallback, useEffect, useState } from "react";
const PIXEL_LIST = [1, 10, 20, 40];
export default function useProgArrowKeyHandler(
handler: (key: KeyCode, px: number, fullScreen: boolean) => void,
active: boolean,
pixelList: number[] = PIXEL_LIST,
fullScreen: boolean,
) {
const [pixelIdx, setPixelIdx] = useState(0);
const [timeoutFunc, setTimeoutFunc] = useState();
const keydownHandler = useCallback(
function (e: KeyboardEvent) {
if (e.target instanceof HTMLInputElement) return;
switch (e.code) {
case KeyCode.ArrowLeft:
case KeyCode.ArrowUp:
case KeyCode.ArrowRight:
case KeyCode.ArrowDown:
e.preventDefault();
if (!timeoutFunc && pixelIdx < pixelList.length - 1) {
setTimeoutFunc(
setTimeout(() => {
setPixelIdx(pixelIdx + 1);
setTimeoutFunc(null);
}, 600),
);
}
handler(e.code, pixelList[pixelIdx], fullScreen);
break;
default:
break;
}
},
[timeoutFunc, pixelIdx, handler, pixelList, fullScreen],
);
const keyupHandler = useCallback(
function () {
if (timeoutFunc) {
clearTimeout(timeoutFunc);
setTimeoutFunc(null);
}
setPixelIdx(0);
},
[timeoutFunc],
);
useEffect(() => {
if (active) {
document.addEventListener("keydown", keydownHandler);
document.addEventListener("keyup", keyupHandler);
return () => {
document.removeEventListener("keydown", keydownHandler);
document.removeEventListener("keyup", keyupHandler);
};
}
}, [keydownHandler, keyupHandler, active]);
}
================================================
FILE: app/_hooks/use-prog-arrow-key-points.ts
================================================
import useProgArrowKeyHandler from "@/_hooks/use-prog-arrow-key-handler";
import { getCalibrationContext } from "@/_lib/calibration-context";
import { KeyCode } from "@/_lib/key-code";
import { Point } from "@/_lib/point";
import { PointAction } from "@/_reducers/pointsReducer";
import { Dispatch } from "react";
export default function useProgArrowKeyPoints(
dispatch: Dispatch,
corners: Set,
active: boolean,
fullScreen: boolean,
) {
function getOffset(key: KeyCode, px: number): Point {
switch (key) {
case KeyCode.ArrowUp:
return { y: -px, x: 0 };
case KeyCode.ArrowDown:
return { y: px, x: 0 };
case KeyCode.ArrowLeft:
return { y: 0, x: -px };
case KeyCode.ArrowRight:
return { y: 0, x: px };
default:
return { x: 0, y: 0 };
}
}
function applyOffset(key: KeyCode, px: number, fullScreen: boolean) {
if (corners.size) {
dispatch({ type: "offset", offset: getOffset(key, px), corners });
localStorage.setItem(
"calibrationContext",
JSON.stringify(getCalibrationContext(fullScreen)),
);
}
}
useProgArrowKeyHandler(
applyOffset,
corners.size > 0 && active,
[1, 3, 5, 10],
fullScreen,
);
return null;
}
================================================
FILE: app/_hooks/use-prog-arrow-key-to-matrix.ts
================================================
import useProgArrowKeyHandler from "@/_hooks/use-prog-arrow-key-handler";
import { Point } from "@/_lib/point";
import { translate } from "@/_lib/geometry";
import { Matrix } from "ml-matrix";
export default function useProgArrowKeyToMatrix(
active: boolean,
scale: number,
applyChange: (matrix: Matrix) => void,
) {
const PIXEL_LIST = [1, 10, 20, 40];
function moveWithArrowKey(key: string, px: number) {
let newOffset: Point = { x: 0, y: 0 };
const dist = px * scale;
switch (key) {
case "ArrowUp":
newOffset = { y: -dist, x: 0 };
break;
case "ArrowDown":
newOffset = { y: dist, x: 0 };
break;
case "ArrowLeft":
newOffset = { y: 0, x: -dist };
break;
case "ArrowRight":
newOffset = { y: 0, x: dist };
break;
default:
break;
}
const m = translate(newOffset);
applyChange(m);
}
useProgArrowKeyHandler(moveWithArrowKey, active, PIXEL_LIST, false);
}
================================================
FILE: app/_hooks/use-render-context.ts
================================================
import { useContext } from "react";
import { createContext } from "react";
import { Layers } from "@/_lib/layers";
export interface RenderContextType {
erosions: number;
layers: Layers;
magnifying: boolean;
onPageRenderSuccess: () => void;
patternScale: number;
}
export const RenderContext = createContext({
erosions: 0,
layers: {},
magnifying: false,
onPageRenderSuccess: () => {},
patternScale: 1,
});
export default function useRenderContext() {
return useContext(RenderContext);
}
================================================
FILE: app/_hooks/use-transform-context.tsx
================================================
import debounce from "@/_lib/debounce";
import { Line } from "@/_lib/interfaces/line";
import { Point } from "@/_lib/point";
import localTransformReducer, {
LocalTransformAction,
} from "@/_reducers/localTransformReducer";
import Matrix from "ml-matrix";
import {
ReactNode,
useContext,
useMemo,
useReducer,
createContext,
Dispatch,
useCallback,
} from "react";
export interface TransformerContextType {
setLocalTransform: (localTransform: Matrix) => void;
rotateToHorizontal: (line: Line) => void;
flipAlong: (line: Line) => void;
flipVertical: (centerPoint: Point) => void;
flipHorizontal: (centerPoint: Point) => void;
rotate: (centerPoint: Point, degrees: number) => void;
recenter: (
centerPoint: Point,
layoutWidth: number,
layoutHeight: number,
) => void;
reset: () => void;
translate: (p: Point) => void;
align: (line: Line, to: Line) => void;
magnify: (scale: number, point: Point) => void;
}
const TransformerContext = createContext({
setLocalTransform: () => {},
rotateToHorizontal: () => {},
flipVertical: () => {},
flipHorizontal: () => {},
rotate: () => {},
recenter: () => {},
reset: () => {},
flipAlong: () => {},
translate: () => {},
align: () => {},
magnify: () => {},
});
const TransformContext = createContext(Matrix.eye(3));
export function useTransformContext() {
return useContext(TransformContext);
}
export function useTransformerContext() {
return useContext(TransformerContext);
}
export const Transformable = ({
children,
fileName,
}: {
children: ReactNode;
fileName: string;
}) => {
const [transform, dispatchInternal] = useReducer(
localTransformReducer,
Matrix.eye(3),
);
const dispatch: Dispatch = useCallback(
(action: LocalTransformAction) => {
dispatchInternal(action);
// Also store the new local transform in local storage so that
// we can restore it when the same file gets opened again later on
const newTransform = localTransformReducer(transform, action);
debouncedWriteToLocalStorage(fileName, newTransform);
},
[transform, fileName],
);
const api = useMemo(
() => ({
setLocalTransform: (localTransform: Matrix) =>
dispatch({ type: "set", localTransform }),
rotateToHorizontal: (line: Line) =>
dispatch({ type: "rotate_to_horizontal", line }),
flipAlong: (line: Line) => dispatch({ type: "flip_along", line }),
translate: (p: Point) => dispatch({ type: "translate", p }),
flipVertical: (centerPoint: Point) =>
dispatch({ type: "flip_vertical", centerPoint }),
flipHorizontal: (centerPoint: Point) =>
dispatch({ type: "flip_horizontal", centerPoint }),
rotate: (centerPoint: Point, degrees: number) =>
dispatch({ type: "rotate", centerPoint, degrees }),
recenter: (
centerPoint: Point,
layoutWidth: number,
layoutHeight: number,
) =>
dispatch({ type: "recenter", centerPoint, layoutWidth, layoutHeight }),
reset: () => dispatch({ type: "reset" }),
align: (line: Line, to: Line) => dispatch({ type: "align", line, to }),
magnify: (scale: number, point: Point) =>
dispatch({ type: "magnify", scale, point }),
}),
[dispatch],
);
return (
{children}
);
};
// Let's not write to local storage too often, but rather wait until the user is done.
const debouncedWriteToLocalStorage = debounce(writeToLocalStorage, 500);
function writeToLocalStorage(fileName: string, transform: Matrix) {
localStorage.setItem(`localTransform:${fileName}`, JSON.stringify(transform));
}
================================================
FILE: app/_icons/add-box-icon.tsx
================================================
export default function AddBoxIcon({
ariaLabel,
className,
}: {
ariaLabel: string;
className?: string;
}) {
return (
);
}
================================================
FILE: app/_icons/add-to-home-screen-icon.tsx
================================================
export default function AddToHomeScreenIcon({
ariaLabel,
className,
}: {
ariaLabel: string;
className?: string;
}) {
return (
);
}
================================================
FILE: app/_icons/check-icon.tsx
================================================
export default function CheckIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/close-icon.tsx
================================================
export default function CloseIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/cycle-icon.tsx
================================================
export default function CycleIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/delete-icon.tsx
================================================
export default function DeleteIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/download-icon.tsx
================================================
export default function DownloadIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/expand-less-icon.tsx
================================================
export default function ExpandLessIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/expand-more-icon.tsx
================================================
export default function ExpandMoreIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/flex-wrap-icon.tsx
================================================
export default function FlexWrapIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/flip-center-off-icon.tsx
================================================
export default function FlipCenterOffIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/flip-center-on-icon.tsx
================================================
export default function FlipCenterOnIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/flip-horizontal-icon.tsx
================================================
export default function FlipHorizontalIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/flip-vertical-icon.tsx
================================================
export default function FlipVerticalIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/flipped-pattern-icon.tsx
================================================
export default function FlippedPatternIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/full-screen-exit-icon.tsx
================================================
export default function FullSceenExitIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/full-screen-icon.tsx
================================================
export default function FullScreenIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/github-icon.tsx
================================================
export default function GithubIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/grid-off-icon.tsx
================================================
export default function GridOffIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/grid-on-icon.tsx
================================================
export default function GridOnIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/info-icon.tsx
================================================
export default function InfoIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/install-desktop-icon.tsx
================================================
export default function InstallDesktopIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/install-desktop.tsx
================================================
export default function InstallDesktopIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/invert-color-icon.tsx
================================================
export default function InvertColorIcon({
ariaLabel,
fill = "currentColor",
}: {
ariaLabel: string;
fill?: string;
}) {
return (
);
}
================================================
FILE: app/_icons/invert-color-off-icon.tsx
================================================
export default function InvertColorOffIcon({
ariaLabel,
fill,
}: {
ariaLabel: string;
fill?: string;
}) {
return (
);
}
================================================
FILE: app/_icons/ios-share-icon.tsx
================================================
export default function IosShareIcon({
ariaLabel,
className,
}: {
ariaLabel: string;
className: string;
}) {
return (
);
}
================================================
FILE: app/_icons/keyboard-arrow-down.tsx
================================================
export default function KeyboardArrowDownIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/keyboard-arrow-left.tsx
================================================
export default function KeyboardArrowLeftIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/keyboard-arrow-right.tsx
================================================
export default function KeyboardArrowRightIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/keyboard-arrow-up.tsx
================================================
export default function KeyboardArrowUpIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/language-icon.tsx
================================================
export default function LanguageIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/layers-icon.tsx
================================================
export default function LayersIcon({
fill = "currentColor",
ariaLabel,
}: {
fill?: string;
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/layers-off-icon.tsx
================================================
export default function LayersOffIcon({
fill = "currentColor",
ariaLabel,
}: {
fill?: string;
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/line-weight-icon.tsx
================================================
export default function LineWeightIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/loading-spinner.tsx
================================================
export default function LoadingSpinner({
height,
width,
className,
}: {
height?: number;
width?: number;
className?: string;
}) {
return (
);
}
================================================
FILE: app/_icons/mail-icon.tsx
================================================
export default function MailIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/mark-and-measure-icon.tsx
================================================
export default function MarkAndMeasureIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/more-vert-icon.tsx
================================================
export default function MoreVertIcon({
ariaLabel,
className,
}: {
ariaLabel: string;
className?: string;
}) {
return (
);
}
================================================
FILE: app/_icons/move-icon.tsx
================================================
export default function MoveIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/overlay-border-icon.tsx
================================================
export default function OverlayBorderIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/overlay-paper-icon.tsx
================================================
export default function OverlayPaperIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/pattern-projector-icon.tsx
================================================
export default function PatternProjectorIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/pdf-icon.tsx
================================================
export default function PdfIcon({
ariaLabel,
fill,
}: {
ariaLabel: string;
fill: string;
}) {
return (
);
}
================================================
FILE: app/_icons/recenter-icon.tsx
================================================
export default function RecenterIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/rotate-90-degrees-cw-icon.tsx
================================================
export default function Rotate90DegreesCWIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/rotate-to-horizontal.tsx
================================================
export default function RotateToHorizontalIcon({
ariaLabel,
}: {
ariaLabel: string;
}) {
return (
);
}
================================================
FILE: app/_icons/shift-icon.tsx
================================================
export default function ShiftIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/step-down-icon.tsx
================================================
export default function StepDownIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/step-up-icon.tsx
================================================
export default function StepUpIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/tune-icon.tsx
================================================
export default function TuneIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/warning-icon.tsx
================================================
export default function WarningIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/zoom-in-icon.tsx
================================================
export default function ZoomInIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_icons/zoom-out-icon.tsx
================================================
export default function ZoomOutIcon({ ariaLabel }: { ariaLabel: string }) {
return (
);
}
================================================
FILE: app/_lib/calibration-context.ts
================================================
export default interface CalibrationContext {
windowInnerWidth: number;
windowInnerHeight: number;
windowScreenTop: number;
windowScreenLeft: number;
devicePixelRatio: number;
clientScreenTop: number | null;
clientScreenLeft: number | null;
fullScreen: boolean;
}
export function logCalibrationContextDifferences(
context: CalibrationContext,
fullScreen: boolean,
): void {
const current = getCalibrationContext(fullScreen);
console.log("CalibrationContext differences:");
if (context.windowInnerWidth !== current.windowInnerWidth) {
console.log(
"windowInnerWidth:",
context.windowInnerWidth,
current.windowInnerWidth,
);
}
if (context.windowInnerHeight !== current.windowInnerHeight) {
console.log(
"windowInnerHeight:",
context.windowInnerHeight,
current.windowInnerHeight,
);
}
if (context.windowScreenTop !== current.windowScreenTop) {
console.log(
"windowScreenTop:",
context.windowScreenTop,
current.windowScreenTop,
);
}
if (context.windowScreenLeft !== current.windowScreenLeft) {
console.log(
"windowScreenLeft:",
context.windowScreenLeft,
current.windowScreenLeft,
);
}
if (context.devicePixelRatio !== current.devicePixelRatio) {
console.log(
"devicePixelRatio:",
context.devicePixelRatio,
current.devicePixelRatio,
);
}
if (context.fullScreen !== current.fullScreen) {
console.log("fullScreen:", context.fullScreen, current.fullScreen);
}
}
export function getCalibrationContext(fullScreen: boolean): CalibrationContext {
const top =
window.screenTop === undefined ? window.screenY : window.screenTop;
const left =
window.screenLeft === undefined ? window.screenX : window.screenLeft;
return {
windowInnerWidth: window.innerWidth,
windowInnerHeight: window.innerHeight,
windowScreenTop: top,
windowScreenLeft: left,
devicePixelRatio: window.devicePixelRatio,
clientScreenTop: null,
clientScreenLeft: null,
fullScreen,
};
}
export function getCalibrationContextUpdatedWithEvent(
e: React.PointerEvent | React.MouseEvent,
fullScreen: boolean,
): CalibrationContext {
return {
...getCalibrationContext(fullScreen),
clientScreenTop: e.screenY - e.clientY,
clientScreenLeft: e.screenX - e.clientX,
};
}
export function getIsInvalidatedCalibrationContext(
context: CalibrationContext,
fullScreen: boolean,
): boolean {
const current = getCalibrationContext(fullScreen);
return (
context.windowInnerWidth !== current.windowInnerWidth ||
context.windowInnerHeight !== current.windowInnerHeight ||
context.windowScreenTop !== current.windowScreenTop ||
context.windowScreenLeft !== current.windowScreenLeft ||
context.devicePixelRatio !== current.devicePixelRatio ||
context.fullScreen !== current.fullScreen
);
}
export function getIsInvalidatedCalibrationContextWithPointerEvent(
context: CalibrationContext,
e: React.PointerEvent,
fullScreen: boolean,
allowMissingClientScreen = false,
): boolean {
const current = getCalibrationContextUpdatedWithEvent(e, fullScreen);
if (getIsInvalidatedCalibrationContext(context, fullScreen)) {
return true;
}
if (allowMissingClientScreen) {
if (
context.clientScreenTop === null ||
context.clientScreenTop === undefined ||
current.clientScreenLeft === null ||
current.clientScreenLeft === undefined
) {
return false;
}
}
// check if the difference is greater than 3 since the values sometimes fluctuate without the viewport changing (Firefox on Desktop and Chrome on Android)
// considered no difference if any values are null
const topDiff =
context.clientScreenTop === null || current.clientScreenTop === null
? false
: Math.abs(context.clientScreenTop - current.clientScreenTop) > 3;
const leftDiff =
context.clientScreenLeft === null || current.clientScreenLeft === null
? false
: Math.abs(context.clientScreenLeft - current.clientScreenLeft) > 3;
if (context.clientScreenTop !== current.clientScreenTop) {
console.log(
"clientScreenTop:",
context.clientScreenTop,
current.clientScreenTop,
);
}
if (context.clientScreenLeft !== current.clientScreenLeft) {
console.log(
"clientScreenLeft:",
context.clientScreenLeft,
current.clientScreenLeft,
);
}
return (topDiff || leftDiff) && !context.fullScreen;
}
================================================
FILE: app/_lib/debounce.ts
================================================
export default function debounce(
fn: T,
waitMilliseconds: number,
) {
let timeoutId: ReturnType;
return function (this: any, ...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), waitMilliseconds);
};
}
================================================
FILE: app/_lib/direction.ts
================================================
export enum Direction {
Up = "Up",
Down = "Down",
Left = "Left",
Right = "Right",
}
================================================
FILE: app/_lib/display-settings.ts
================================================
export interface OverlaySettings {
disabled: boolean;
grid: boolean;
border: boolean;
paper: boolean;
flipLines: boolean;
flippedPattern: boolean;
}
export function getDefaultOverlaySettings() {
return {
disabled: false,
grid: false,
border: false,
paper: false,
flipLines: false,
flippedPattern: true,
};
}
export enum Theme {
Light = "Light",
Dark = "Dark",
Green = "Green",
}
export interface DisplaySettings {
theme: Theme;
overlay: OverlaySettings;
}
export function getDefaultDisplaySettings() {
return {
theme: Theme.Light,
overlay: getDefaultOverlaySettings(),
};
}
export function themes() {
return [Theme.Light, Theme.Green, Theme.Dark];
}
export function isDarkTheme(theme: Theme) {
return [Theme.Dark, Theme.Green].includes(theme);
}
export function themeFilter(theme: Theme): string {
switch (theme) {
case Theme.Dark:
return "invert(1)";
case Theme.Green:
return "invert(1) sepia(100%) saturate(300%) hue-rotate(80deg)";
case Theme.Light:
return "none";
}
}
export function strokeColor(theme: Theme) {
switch (theme) {
case Theme.Dark:
return "#fff";
case Theme.Green:
return "#32CD32";
case Theme.Light:
return "#000";
}
}
export function fillColor(theme: Theme) {
return isDarkTheme(theme) ? "#000" : "#fff";
}
================================================
FILE: app/_lib/drawing.ts
================================================
import { CM } from "@/_lib/unit";
import Matrix from "ml-matrix";
import { Point } from "@/_lib/point";
import {
DisplaySettings,
fillColor,
strokeColor,
} from "@/_lib/display-settings";
import {
RestoreTransforms,
checkIsConcave,
rectCorners,
transformLine,
transformPoint,
transformPoints,
translatePoints,
} from "@/_lib/geometry";
import { Line } from "./interfaces/line";
export class CanvasState {
isConcave: boolean = false;
majorLineWidth: number = 2;
minorLineWidth: number = 1;
constructor(
public ctx: CanvasRenderingContext2D,
public offset: Point = { x: 0, y: 0 },
public points: Point[],
public width: number,
public height: number,
public perspective: Matrix,
public isCalibrating: boolean,
public corners: Set,
public hoverCorners: Set,
public unitOfMeasure: string,
public errorFillPattern: CanvasFillStrokeStyles["fillStyle"],
public displaySettings: DisplaySettings,
public isFlipped: boolean,
public calibrationTransform: Matrix,
public zoomedOut: boolean,
public magnifying: boolean,
public restoreTransforms: RestoreTransforms | null,
public t: any,
public patternScale: string | null,
) {
this.isConcave = checkIsConcave(this.points);
}
}
export enum OverlayMode {
GRID,
BORDER,
PAPER,
NONE,
}
export function drawLine(ctx: CanvasRenderingContext2D, line: Line): void {
ctx.save();
ctx.beginPath();
ctx.moveTo(line[0].x, line[0].y);
ctx.lineTo(line[1].x, line[1].y);
ctx.stroke();
ctx.restore();
}
export function drawCircle(
ctx: CanvasRenderingContext2D,
p: Point,
radius: number,
) {
ctx.beginPath();
ctx.arc(p.x, p.y, radius, 0, 2 * Math.PI);
ctx.stroke();
}
export function drawArrow(ctx: CanvasRenderingContext2D, line: Line): void {
const dx = line[1].x - line[0].x;
const dy = line[1].y - line[0].y;
const angle = Math.atan2(dy, dx);
const length = Math.hypot(dy, dx);
const arrowLength = 16;
const arrowWidth = 8;
const whisker = 16;
ctx.save();
ctx.fillStyle = ctx.strokeStyle;
ctx.translate(line[1].x, line[1].y);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(-ctx.lineWidth / 2, 0);
ctx.lineTo(-arrowLength, arrowWidth);
ctx.lineTo(-arrowLength, -arrowWidth);
ctx.closePath();
ctx.fill();
ctx.moveTo(-arrowLength, 0);
ctx.lineTo(-length, 0);
ctx.moveTo(0, whisker);
ctx.lineTo(0, -whisker);
ctx.moveTo(-length, -whisker);
ctx.lineTo(-length, whisker);
ctx.stroke();
ctx.restore();
}
export function drawPolygon(
ctx: CanvasRenderingContext2D,
points: Point[],
): void {
ctx.beginPath();
for (const p of points) {
ctx.lineTo(p.x, p.y);
}
ctx.closePath();
}
export function drawOverlays(cs: CanvasState) {
const {
ctx,
displaySettings,
zoomedOut,
t,
magnifying,
restoreTransforms,
patternScale,
} = cs;
const { grid, border, paper, flipLines, flippedPattern, disabled } =
displaySettings.overlay;
const { theme } = displaySettings;
if (disabled) {
return;
}
ctx.strokeStyle = strokeColor(theme);
if (zoomedOut) {
drawMessage(cs, t("zoomedOut"));
drawViewportOutline(cs);
} else if (magnifying && restoreTransforms !== null) {
drawMessage(cs, t("magnifying"));
} else {
if (grid) {
drawGrid(cs, 8, [1]);
}
if (border) {
drawBorder(cs, strokeColor(theme), fillColor(theme));
}
if (paper) {
ctx.strokeStyle = "black";
drawPaperSheet(cs);
}
if (flipLines) {
drawCenterLines(cs);
}
if (flippedPattern && cs.isFlipped) {
drawFlippedPattern(cs);
}
if (patternScale && Number(patternScale) !== 1) {
drawMessage(
cs,
t.rich("scaled", {
scale: () => String(Number(patternScale).toFixed(2)),
}),
);
}
}
}
function drawMessage(cs: CanvasState, message: string) {
const { ctx, width, perspective } = cs;
ctx.save();
ctx.fillStyle = "#9333ea";
ctx.font = "48px sans-serif";
const text = message;
const textWidth = ctx.measureText(text).width;
const center = transformPoint(
{
x: width * 0.5,
y: 0,
},
perspective,
);
ctx.fillText(text, center.x - textWidth * 0.5, center.y);
ctx.restore();
}
function drawViewportOutline(cs: CanvasState) {
const { ctx, calibrationTransform, restoreTransforms } = cs;
ctx.save();
ctx.strokeStyle = "#9333ea";
ctx.lineWidth = 2;
let corners = transformPoints(
rectCorners(window.innerWidth, window.innerHeight),
calibrationTransform,
);
if (restoreTransforms !== null) {
const x = restoreTransforms.localTransform.get(0, 2);
const y = restoreTransforms.localTransform.get(1, 2);
const s = calibrationTransform.get(0, 0);
corners = translatePoints(corners, Math.abs(x) * s, Math.abs(y) * s);
drawPolygon(ctx, corners);
ctx.stroke();
}
ctx.restore();
}
export function drawCenterLines(cs: CanvasState) {
const { width, height, ctx, perspective } = cs;
ctx.save();
ctx.strokeStyle = "red";
function drawProjectedLine(p1: Point, p2: Point) {
const line = transformLine([p1, p2], perspective);
ctx.lineWidth = 2;
drawLine(ctx, line);
ctx.stroke();
}
// X-axis
drawProjectedLine({ x: 0, y: height * 0.5 }, { x: width, y: height * 0.5 });
drawProjectedLine({ x: width * 0.5, y: 0 }, { x: width * 0.5, y: height });
ctx.restore();
}
export function drawPaperSheet(cs: CanvasState) {
const { ctx, perspective, unitOfMeasure, width, height, displaySettings } =
cs;
const fontSize = 32;
ctx.save();
ctx.font = `${fontSize}px monospace`;
ctx.fillStyle = "white";
const [text, paperWidth, paperHeight] =
unitOfMeasure == CM ? ["A4", 29.7, 21] : ["11x8.5", 11, 8.5];
const cornersP = transformPoints(
translatePoints(
rectCorners(paperWidth, paperHeight),
(width - paperWidth) * 0.5,
(height - paperHeight) * 0.5,
),
perspective,
);
drawPolygon(ctx, cornersP);
ctx.setLineDash([4, 2]);
ctx.lineWidth = 4;
ctx.strokeStyle = strokeColor(displaySettings.theme);
ctx.stroke();
const labelWidth = ctx.measureText(text).width;
ctx.textBaseline = "middle";
ctx.fillStyle = strokeColor(displaySettings.theme);
const centerP = transformPoint(
{
x: width * 0.5,
y: height * 0.5,
},
perspective,
);
ctx.fillText(text, centerP.x - labelWidth * 0.5, centerP.y);
ctx.restore();
}
export function drawBorder(
cs: CanvasState,
lineColor: string,
dashColor: string,
) {
const ctx = cs.ctx;
ctx.save();
drawPolygon(ctx, cs.points);
ctx.strokeStyle = lineColor;
ctx.lineWidth = 5;
ctx.stroke();
ctx.lineDashOffset = 0;
ctx.setLineDash([4, 4]);
ctx.lineWidth = 1;
ctx.strokeStyle = dashColor;
ctx.stroke();
ctx.restore();
}
export function drawGrid(
cs: CanvasState,
outset: number,
lineDash?: number[],
): void {
const ctx = cs.ctx;
ctx.save();
if (lineDash === undefined) {
ctx.setLineDash([]);
} else {
ctx.setLineDash(lineDash);
}
const majorLine = 5;
/* Vertical lines */
for (let i = 1; i < cs.width; i++) {
let lineWidth = cs.minorLineWidth;
if (i % majorLine === 0 || i === cs.width) {
lineWidth = cs.majorLineWidth;
}
const line = transformLine(
[
{ x: i, y: -outset },
{ x: i, y: cs.height + outset },
],
cs.perspective,
);
ctx.lineWidth = lineWidth;
drawLine(ctx, line);
}
/* Horizontal lines */
for (let i = 1; i < cs.height; i++) {
let lineWidth = cs.minorLineWidth;
if (i % majorLine === 0 || i === cs.height) {
lineWidth = cs.majorLineWidth;
}
const y = cs.height - i;
const line = transformLine(
[
{ x: -outset, y: y },
{ x: cs.width + outset, y: y },
],
cs.perspective,
);
ctx.lineWidth = lineWidth;
drawLine(ctx, line);
}
if (cs.isCalibrating) {
ctx.fillStyle = strokeColor(cs.displaySettings.theme);
drawDimensionLabels(
ctx,
cs.width,
cs.height,
cs.perspective,
cs.unitOfMeasure,
);
}
ctx.restore();
}
export function drawDimensionLabels(
ctx: CanvasRenderingContext2D,
width: number,
height: number,
perspective: Matrix,
unitOfMeasure: string,
) {
const fontSize = 48;
const inset = 36;
ctx.font = `${fontSize}px monospace`;
const widthText = `${width}${unitOfMeasure.toLocaleLowerCase()}`;
const heightText = `${height}${unitOfMeasure.toLocaleLowerCase()}`;
const line = transformPoints(
[
{
x: width * 0.5,
y: height,
},
{
x: 0,
y: height * 0.5,
},
],
perspective,
);
const widthLabelWidth = ctx.measureText(widthText).width;
const heightLabelHeight = ctx.measureText(heightText).actualBoundingBoxAscent;
ctx.fillText(widthText, line[0].x - widthLabelWidth * 0.5, line[0].y - inset);
ctx.fillText(
heightText,
line[1].x + inset,
line[1].y + heightLabelHeight * 0.5,
);
}
function drawFlippedPattern(cs: CanvasState) {
const { ctx } = cs;
ctx.save();
ctx.fillStyle = "red";
// draw a grid of dots
const dotSize = 2;
const spacing = 72;
for (let y = 0; y < ctx.canvas.height; y += spacing) {
for (let x = 0; x < ctx.canvas.width; x += spacing) {
ctx.beginPath();
ctx.arc(x, y, dotSize, 0, 2 * Math.PI);
ctx.fill();
}
}
ctx.restore();
}
================================================
FILE: app/_lib/erode.ts
================================================
export function erosionFilter(erosions: number): string {
if (erosions <= 0) {
return "none";
}
const result = [];
while (erosions > 0) {
if (erosions >= 3) {
result.push("url(#erode-3)");
erosions -= 3;
} else if (erosions >= 2) {
result.push("url(#erode-2)");
erosions -= 2;
} else {
result.push("url(#erode-1)");
erosions -= 1;
}
}
return result.join(" ");
}
export function erodeImageData(imageData: ImageData, output: ImageData) {
const { width, height } = imageData;
const erodedData = output.data;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = (y * width + x) * 4;
for (let i = 0; i < 4; i++) {
erodedData[index + i] = erodeAtIndex(
imageData,
x,
y,
index + i,
width,
height,
);
}
}
}
}
function erodeAtIndex(
imageData: ImageData,
x: number,
y: number,
index: number,
width: number,
height: number,
): number {
const { data } = imageData;
let c = data[index];
if (x > 0) {
const n = data[index - 4];
if (n < c) {
c = n;
}
}
if (x < width - 1) {
const n = data[index + 4];
if (n < c) {
c = n;
}
}
if (y > 0) {
const n = data[index - width * 4];
if (n < c) {
c = n;
}
}
if (y < height - 1) {
const n = data[index + width * 4];
if (n < c) {
c = n;
}
}
return c;
}
================================================
FILE: app/_lib/full-screen.ts
================================================
import { FullScreenHandle } from "react-full-screen";
// Type error in react-full-screen so need to wrap in a try-catch
export function toggleFullScreen(fullScreenHandle: FullScreenHandle): void {
try {
if (fullScreenHandle.active) {
fullScreenHandle.exit();
} else {
fullScreenHandle.enter();
}
} catch (error) {
console.error("Error toggling full screen", error);
}
}
================================================
FILE: app/_lib/geometry.spec.ts
================================================
import { getPerspectiveTransform, toMatrix3d } from "@/_lib/geometry";
// opencv-python input and output
// >>> import cv2
// >>> import numpy as np
// >>> input_pts = np.float32([[33, 582], [151, 579], [145, 702], [25, 703]])
// >>> output_pts = np.float32([[0, 0], [300, 0], [300, 300], [0, 300]])
// >>> M = cv2.getPerspectiveTransform(input_pts,output_pts)
// >>> print(M)
// [[ 2.86405074e+00 1.89358727e-01 -2.04720453e+02]
// [ 7.10878995e-02 2.79612405e+00 -1.62969010e+03]
// [ 1.60734029e-04 1.73337133e-04 1.00000000e+00]]
test("gets perspective transform with the same inputs and outputs as opencv-python", () => {
const src = [
{ x: 33, y: 582 },
{ x: 151, y: 579 },
{ x: 145, y: 702 },
{ x: 25, y: 703 },
];
const dst = [
{ x: 0, y: 0 },
{ x: 300, y: 0 },
{ x: 300, y: 300 },
{ x: 0, y: 300 },
];
const openCVMatrix = [
2.86405074, 1.89358727e-1, -2.04720453e2, 7.10878995e-2, 2.79612405,
-1.6296901e3, 1.60734029e-4, 1.73337133e-4, 1.0,
];
const matrix = getPerspectiveTransform(src, dst);
for (let i = 0; i < matrix.length; i++) {
expect(matrix[i]).toBeCloseTo(openCVMatrix[i]);
}
});
test("coverts 3x3 matrix to 4x4 matrix", () => {
const src = [
{ x: 33, y: 582 },
{ x: 151, y: 579 },
{ x: 145, y: 702 },
{ x: 25, y: 703 },
];
const dst = [
{ x: 0, y: 0 },
{ x: 300, y: 0 },
{ x: 300, y: 300 },
{ x: 0, y: 300 },
];
const openCVMatrix = [
2.864050742318774, 0.07108789949297858, 0, 0.00016073402857508592,
0.1893587267624101, 2.7961240467325417, 0, 0.00017333713252165822, 0, 0, 1,
0, -204.72045347183263, -1629.6900958816486, 0, 1,
];
const matrix = toMatrix3d(src, dst);
for (let i = 0; i < matrix.length; i++) {
expect(matrix[i]).toBeCloseTo(openCVMatrix[i]);
}
});
================================================
FILE: app/_lib/geometry.ts
================================================
/*M///////////////////////////////////////////////////////////////////////////////////////
//
// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
//
// By downloading, copying, installing or using the software you agree to this license.
// If you do not agree to this license, do not download, install,
// copy or use the software.
//
//
// License Agreement
// For Open Source Computer Vision Library
//
// Copyright (C) 2000-2008, Intel Corporation, all rights reserved.
// Copyright (C) 2009, Willow Garage Inc., all rights reserved.
// Copyright (C) 2014-2015, Itseez Inc., all rights reserved.
// Third party copyrights are property of their respective owners.
//
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
// * Redistribution's of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistribution's in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// * The name of the copyright holders may not be used to endorse or promote products
// derived from this software without specific prior written permission.
//
// This software is provided by the copyright holders and contributors "as is" and
// any express or implied warranties, including, but not limited to, the implied
// warranties of merchantability and fitness for a particular purpose are disclaimed.
// In no event shall the Intel Corporation or contributors be liable for any direct,
// indirect, incidental, special, exemplary, or consequential damages
// (including, but not limited to, procurement of substitute goods or services;
// loss of use, data, or profits; or business interruption) however caused
// and on any theory of liability, whether in contract, strict liability,
// or tort (including negligence or otherwise) arising in any way out of
// the use of this software, even if advised of the possibility of such damage.
//
//M*/
import { Point, subtract } from "@/_lib/point";
import { AbstractMatrix, Matrix, solve } from "ml-matrix";
import { getPtDensity } from "./unit";
import { Line } from "./interfaces/line";
/** Calculates a perspective transform from four pairs of the corresponding points.
*
* A TypeScript implementation of OpenCV's getPerspectiveTransform(src, dst, solve) available here:
* https://github.com/opencv/opencv/blob/157b0e7760117a60de457a4ae874b0709edc4e53/modules/imgproc/src/imgwarp.cpp#L3432
*
* Calculates coefficients of perspective transformation
* which maps (xi,yi) to (ui,vi), (i=1,2,3,4):
*
* c00*xi + c01*yi + c02
* ui = ---------------------
* c20*xi + c21*yi + c22
*
* c10*xi + c11*yi + c12
* vi = ---------------------
* c20*xi + c21*yi + c22
*
* Coefficients are calculated by solving linear system:
* / x0 y0 1 0 0 0 -x0*u0 -y0*u0 \ /c00\ /u0\
* | x1 y1 1 0 0 0 -x1*u1 -y1*u1 | |c01| |u1|
* | x2 y2 1 0 0 0 -x2*u2 -y2*u2 | |c02| |u2|
* | x3 y3 1 0 0 0 -x3*u3 -y3*u3 |.|c10|=|u3|,
* | 0 0 0 x0 y0 1 -x0*v0 -y0*v0 | |c11| |v0|
* | 0 0 0 x1 y1 1 -x1*v1 -y1*v1 | |c12| |v1|
* | 0 0 0 x2 y2 1 -x2*v2 -y2*v2 | |c20| |v2|
* \ 0 0 0 x3 y3 1 -x3*v3 -y3*v3 / \c21/ \v3/
*
* where:
* cij - matrix coefficients, c22 = 1
*
* @param src - Coordinates of quadrangle vertices in the source image starting from top left clockwise.
* @param dst - Coordinates of the corresponding quadrangle vertices in the destination image starting from top left clockwise.
* @returns A 3x3 matrix of a perspective transform.
*/
export function getPerspectiveTransform(src: Point[], dst: Point[]): Matrix {
const a: number[][] = Array.from(Array(8), () => Array(8));
const b: number[] = new Array(8);
for (let i = 0; i < 4; i++) {
a[i][0] = src[i].x;
a[i + 4][3] = src[i].x;
a[i][1] = src[i].y;
a[i + 4][4] = src[i].y;
a[i][2] = 1;
a[i + 4][5] = 1;
a[i][3] = 0;
a[i][4] = 0;
a[i][5] = 0;
a[i + 4][0] = 0;
a[i + 4][1] = 0;
a[i + 4][2] = 0;
const srcX = src[i].x;
const dstX = dst[i].x;
const srcY = src[i].y;
const dstY = dst[i].y;
a[i][6] = -srcX * dstX;
a[i][7] = -srcY * dstX;
a[i + 4][6] = -srcX * dstY;
a[i + 4][7] = -srcY * dstY;
b[i] = dst[i].x;
b[i + 4] = dst[i].y;
}
const A = new Matrix(a);
const B = Matrix.columnVector(b);
const x = solve(A, B, true);
const X = x.getColumn(0);
X.push(1);
const s = Matrix.from1DArray(3, 3, X);
return s;
}
export function translatePoints(pts: Point[], dx: number, dy: number): Point[] {
return pts.map((p) => ({ x: p.x + dx, y: p.y + dy }));
}
export function rectCorners(width: number, height: number): Point[] {
return [
{ x: 0, y: 0 },
{ x: width, y: 0 },
{ x: width, y: height },
{ x: 0, y: height },
];
}
export function getBounds(pts: Point[]): Point[] {
const first = pts[0];
let minX = first.x;
let minY = first.y;
let maxX = first.x;
let maxY = first.y;
for (const { x, y } of pts) {
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
return [
{ x: minX, y: minY },
{ x: maxX, y: maxY },
];
}
function getDstVertices(
width: number,
height: number,
ptDensity: number,
): Point[] {
const dx = width * ptDensity;
const dy = height * ptDensity;
return rectCorners(dx, dy);
}
export function getCalibrationCenterPoint(
width: number,
height: number,
unitOfMeasure: string,
): Point {
return {
x: width * getPtDensity(unitOfMeasure) * 0.5,
y: height * getPtDensity(unitOfMeasure) * 0.5,
};
}
export function getPerspectiveTransformFromPoints(
points: Point[],
width: number,
height: number,
ptDensity: number,
reverse?: boolean,
): Matrix {
if (reverse) {
return getPerspectiveTransform(
points,
getDstVertices(width, height, ptDensity),
);
} else {
return getPerspectiveTransform(
getDstVertices(width, height, ptDensity),
points,
);
}
}
export function transformLine(line: Line, m: Matrix): Line {
return [transformPoint(line[0], m), transformPoint(line[1], m)];
}
export function transformPoints(points: Point[], m: Matrix): Point[] {
return points.map((p) => transformPoint(p, m));
}
export function transformPoint(p: Point, mm: Matrix): Point {
const m = mm.to1DArray();
let w = p.x * m[6] + p.y * m[7] + m[8];
w = 1 / w;
const ox = (p.x * m[0] + p.y * m[1] + m[2]) * w;
const oy = (p.x * m[3] + p.y * m[4] + m[5]) * w;
const result = { x: ox, y: oy };
return result;
}
export function translate(p: Point): Matrix {
return Matrix.from1DArray(3, 3, [1, 0, p.x, 0, 1, p.y, 0, 0, 1]);
}
export function scale(s: number, sy: null | number = null): Matrix {
sy = sy ?? s;
return Matrix.from1DArray(3, 3, [s, 0, 0, 0, sy, 0, 0, 0, 1]);
}
export function scaleAboutPoint(s: number, point: Point): Matrix {
return translate(point)
.mmul(scale(s))
.mmul(translate({ x: -point.x, y: -point.y }));
}
export function rotate(angle: number): Matrix {
const cosAngle = Math.cos(angle);
const sinAngle = Math.sin(angle);
return new Matrix([
[cosAngle, -sinAngle, 0],
[sinAngle, cosAngle, 0],
[0, 0, 1],
]);
}
export function align(line: Line, to: Line): Matrix {
const toOrigin = translate({ x: -line[0].x, y: -line[0].y });
const rotateTo = rotate(angle(to) - angle(line));
return translate(to[0]).mmul(rotateTo).mmul(toOrigin);
}
export function move(from: Point, to: Point): Matrix {
return translate(subtract(to, from));
}
export function transformAboutPoint(matrix: Matrix, point: Point): Matrix {
const translationToOrigin = translate({ x: -point.x, y: -point.y });
const translationBack = translate(point);
return translationBack.mmul(matrix).mmul(translationToOrigin);
}
export function rotateMatrixDeg(angleDegrees: number, origin: Point): Matrix {
const angleRadians = (angleDegrees * Math.PI) / 180;
const rotationMatrix = rotate(angleRadians);
return transformAboutPoint(rotationMatrix, origin);
}
export function flipVertical(origin: Point): Matrix {
return transformAboutPoint(scale(1, -1), origin);
}
export function flipHorizontal(origin: Point): Matrix {
return transformAboutPoint(scale(-1, 1), origin);
}
export function angleDeg(line: Line): number {
return angle(line) * (180 / Math.PI);
}
export function angle(line: Line): number {
const [p1, p2] = line;
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
return Math.atan2(dy, dx);
}
export function rotateToHorizontal(line: Line): Matrix {
return rotateMatrixDeg(-angleDeg(line), line[0]);
}
export function flipAlong(line: Line): Matrix {
const angle = angleDeg(line);
const a = rotateMatrixDeg(-angle, line[0]);
const b = translate({ x: 0, y: -line[0].y });
const c = scale(1, -1);
const d = translate({ x: 0, y: line[0].y });
const e = rotateMatrixDeg(angle, line[0]);
return e.mmul(d).mmul(c).mmul(b).mmul(a);
}
/**
* Converts 3x3 matrix returned from getPerspectiveTransform(src, dst) to a 4x4 matrix as per https://stackoverflow.com/a/4833408/3376039
* @param src - Coordinates of quadrangle vertices in the source image starting from top left clockwise.
* @param dst - Coordinates of the corresponding quadrangle vertices in the destination image starting from top left clockwise.
* @returns A css transform string
*/
export function toMatrix3d(m: Matrix): string {
let r = m.clone();
// Make the 3x3 into 4x4 by inserting a z component in the 3rd column.
r.addColumn(2, AbstractMatrix.zeros(1, 3));
r.addRow(2, AbstractMatrix.from1DArray(1, 4, [0, 0, 1, 0]));
// Transpose since CSS.matrix3d is column major.
r = r.transpose();
return `matrix3d(${r.to1DArray().toString()})`;
}
export function sqrDist(a: Point, b: Point): number {
const dx = a.x - b.x;
const dy = a.y - b.y;
return dx * dx + dy * dy;
}
export function dist(a: Point, b: Point): number {
return Math.sqrt(sqrDist(a, b));
}
export function minIndex(a: number[]): number {
let min = 0;
for (let i = 1; i < a.length; i++) {
if (a[i] < a[min]) {
min = i;
}
}
return min;
}
export function distToLine(line: Line, p: Point): number {
return Math.sqrt(sqrDistToLine(line, p));
}
export function sqrDistToLine(line: Line, p: Point): number {
const [a, b] = line;
const len2 = sqrDist(a, b);
if (len2 === 0) {
return sqrDist(p, a);
}
const rise = b.y - a.y;
const run = b.x - a.x;
let t = ((p.x - a.x) * run + (p.y - a.y) * rise) / len2;
// constrain t to the segment between a and b
t = Math.max(0, Math.min(1, t));
return sqrDist(p, { x: a.x + t * run, y: a.y + t * rise });
}
export function interp(from: Point, to: Point, portion: number): Point {
const rest = 1.0 - portion;
return {
x: to.x * portion + from.x * rest,
y: to.y * portion + from.y * rest,
};
}
/* Returns true if the list of points define a concave polygon, false otherwise */
export function checkIsConcave(points: Point[]): boolean {
const n = points.length;
if (n < 4) {
return false; // A polygon with less than 4 points is always convex
}
let prevOrientation = 0;
for (let i = 0; i < n; i++) {
const p1 = points[i];
const p2 = points[(i + 1) % n];
const p3 = points[(i + 2) % n];
const orientation = getOrientation(p1, p2, p3);
if (orientation !== 0) {
if (prevOrientation === 0) {
prevOrientation = orientation;
} else if (orientation !== prevOrientation) {
return true; // The polygon is concave
}
}
}
return false; // The polygon is convex
}
function getOrientation(p1: Point, p2: Point, p3: Point): number {
const crossProduct =
(p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x);
if (crossProduct < 0) {
return -1; // Clockwise orientation
} else if (crossProduct > 0) {
return 1; // Counterclockwise orientation
} else {
return 0; // Collinear points
}
}
export function constrained(p: Point, anchorPoint: Point) {
const dx = Math.abs(anchorPoint.x - p.x);
const dy = Math.abs(anchorPoint.y - p.y);
if (dx > 2 * dy) {
return { x: p.x, y: anchorPoint.y };
} else if (dy > 2 * dx) {
return { x: anchorPoint.x, y: p.y };
} else if (dx === 0 && dy === 0) {
return anchorPoint;
} else {
// snap to 45 degree angle
if (dx < dy) {
return { x: p.x, y: anchorPoint.y + ((p.y - anchorPoint.y) / dy) * dx };
} else {
return { x: anchorPoint.x + ((p.x - anchorPoint.x) / dx) * dy, y: p.y };
}
}
}
export function isFlipped(m: Matrix): boolean {
return m.get(1, 1) * m.get(0, 0) < 0;
}
export interface RestoreTransforms {
localTransform: Matrix;
calibrationTransform: Matrix;
}
================================================
FILE: app/_lib/get-page-numbers.ts
================================================
import { LineDirection } from "./interfaces/stitch-settings";
export function getPageNumbers(pageRange: string, pageCount: number): number[] {
const ranges = pageRange.split(",");
let pages = [];
for (const r of ranges) {
if (r.indexOf("-") < 0) {
pages.push(Math.min(pageCount, Number(r)));
} else {
const [start, end] = r.split("-");
const s = Number(start);
const e = end === "" ? pageCount : Number(end);
let a = Math.min(pageCount, Math.min(s, e));
const b = Math.min(Math.max(s, e), pageCount);
while (b >= a) {
pages.push(a);
a++;
}
}
}
if (pages.length === 0) {
pages = [...Array(pageCount).keys()].map((x) => ++x);
}
return pages;
}
export function getPageRange(pageNumbers: number[]): string {
let start = -1;
let end = -1;
const builder: string[] = [];
function range(start: number, end: number): string {
if (start < 0) {
return "";
}
return start == end ? `${start}` : `${start}-${end}`;
}
for (const page of pageNumbers) {
if (start < 1) {
// not valid
start = page;
end = page;
} else if (page == end + 1 && end >= start) {
end++;
} else if (page == end - 1 && end <= start) {
end--;
} else {
builder.push(range(start, end));
start = end = page;
}
}
builder.push(range(start, end));
return builder.join(",");
}
export function rotateRange(
pageRange: string,
pageCount: number,
increment: number,
): string {
const numbers = getPageNumbers(pageRange, pageCount);
if (increment > 0) {
// increment
if (numbers.length == 1) {
// single page
const a = numbers.shift();
if (a !== undefined) {
if (a == pageCount) {
// wrap to first
numbers.push(1);
} else {
numbers.push(a + 1); // move to next
}
}
} else {
// rotate range
const a = numbers.shift();
if (a !== undefined) {
numbers.push(a);
}
}
} else {
// decrement
if (numbers.length == 1) {
// single page move to previous
const a = numbers.pop();
if (a !== undefined) {
if (a <= 1) {
numbers.push(pageCount);
} else {
numbers.push(a - 1);
}
}
} else {
// rotate range
const a = numbers.pop();
if (a !== undefined) {
numbers.unshift(a);
}
}
}
return getPageRange(numbers);
}
export function getRowsColumns(
pages: number[],
lineCount: number,
lineDirection: LineDirection,
): [number, number] {
const itemCount = pages.length;
const a = Math.max(Math.min(lineCount, itemCount), 1);
const otherCount = Math.ceil((itemCount || 1) / a);
const isColumn = lineDirection == LineDirection.Column;
return isColumn ? [otherCount, a] : [a, otherCount];
}
================================================
FILE: app/_lib/interfaces/edge-insets.ts
================================================
export interface EdgeInsets {
horizontal: number;
vertical: number;
}
================================================
FILE: app/_lib/interfaces/layer.ts
================================================
export interface Layer {
name: string;
ids: string[];
visible: boolean;
}
================================================
FILE: app/_lib/interfaces/line.ts
================================================
import { Point } from "../point";
export type Line = [Point, Point];
================================================
FILE: app/_lib/interfaces/select-option.ts
================================================
export interface SelectOption {
label: string;
value: string;
}
================================================
FILE: app/_lib/interfaces/stitch-settings.ts
================================================
import { EdgeInsets } from "./edge-insets";
export enum LineDirection {
Row = "Row",
Column = "Column",
}
export interface StitchSettings {
key: string;
lineCount: number;
edgeInsets: EdgeInsets;
pageRange: string;
lineDirection: LineDirection;
}
================================================
FILE: app/_lib/is-valid-file.ts
================================================
export const acceptedMimeTypes = [
"image/svg+xml",
"application/pdf",
"application/xml", // Drive classifies Inkscape SVGs as XML text on iPad
];
export const acceptedExtensions = [".PDF", ".SVG"];
// Check if selected file is a PDF or accepted image type
export default function isValidFile(file: File): boolean {
for (const type of acceptedMimeTypes) {
if (file.type === type) {
return true;
}
}
if (file.type === "" && file.name) {
const fileName = file.name;
const lastDotIndex = fileName.lastIndexOf(".");
for (const extension of acceptedExtensions) {
if (
lastDotIndex === -1 ||
fileName.substring(lastDotIndex).toUpperCase() !== extension
) {
return false;
}
}
return true;
}
return false;
}
================================================
FILE: app/_lib/key-code.ts
================================================
export enum KeyCode {
ArrowUp = "ArrowUp",
ArrowDown = "ArrowDown",
ArrowLeft = "ArrowLeft",
ArrowRight = "ArrowRight",
KeyC = "c",
KeyH = "h",
KeyL = "l",
KeyM = "m",
KeyR = "r",
KeyV = "v",
KeyZ = "z",
Escape = "Escape",
Tab = "Tab",
Backspace = "Backspace",
Shift = "Shift",
ShiftLeft = "ShiftLeft",
ShiftRight = "ShiftRight",
}
================================================
FILE: app/_lib/layers.ts
================================================
import type { PDFDocumentProxy } from "pdfjs-dist";
import { Layer } from "./interfaces/layer";
export type Layers = { [key: string]: Layer };
export async function getLayersFromPdf(pdf: PDFDocumentProxy): Promise {
const layers: Layers = {};
const groups: { [key: string]: { name: string } } = (
await pdf.getOptionalContentConfig()
).getGroups();
if (groups == null) {
return layers;
}
Object.entries(groups).forEach(([key, group]) => {
const name = String(group.name) ?? key;
const existing = layers[name];
if (existing != null) {
existing.ids.push(key);
} else {
layers[name] = {
name,
ids: [key],
visible: true,
};
}
});
return layers;
}
================================================
FILE: app/_lib/load-status-enum.ts
================================================
export enum LoadStatusEnum {
DEFAULT,
LOADING,
FAILED,
SUCCESS,
}
================================================
FILE: app/_lib/menu-states.ts
================================================
import { Layers } from "./layers";
export interface MenuStates {
nav: boolean;
layers: boolean;
stitch: boolean;
scale: boolean;
}
export enum SideMenuType {
layers = "layers",
stitch = "stitch",
scale = "scale",
}
export function toggleSideMenuStates(
menuStates: MenuStates,
menu: SideMenuType,
) {
const visible = !menuStates[menu];
const newMenuStates = getDefaultMenuStates();
newMenuStates[menu] = visible;
return newMenuStates;
}
export function getDefaultMenuStates(): MenuStates {
return {
nav: true,
layers: false,
stitch: false,
scale: false,
};
}
export function getMenuStatesFromPageCount(
menuStates: MenuStates,
pageCount: number,
) {
if (pageCount > 1) {
return { nav: true, layers: false, scale: false, stitch: true };
} else {
return menuStates;
}
}
export function getMenuStatesFromLayers(
menuStates: MenuStates,
layers: Layers,
) {
if (menuStates.stitch) {
return menuStates;
} else {
return {
nav: true,
stitch: false,
scale: false,
layers: Object.keys(layers).length > 0,
};
}
}
export function sideMenuOpen(menuStates: MenuStates) {
for (const m in SideMenuType) {
if (menuStates[m as SideMenuType]) {
return true;
}
}
return false;
}
================================================
FILE: app/_lib/pdfstitcher.ts
================================================
import {
PDFDocument,
PDFName,
PDFRef,
PDFPageLeaf,
PDFOperator,
concatTransformationMatrix,
pushGraphicsState,
drawObject,
popGraphicsState,
PDFContentStream,
PDFDict,
PDFStream,
PDFArray,
PDFContext,
PDFRawStream,
decodePDFRawStream,
UnrecognizedStreamTypeError,
} from "@cantoo/pdf-lib";
import {
LineDirection,
StitchSettings,
} from "@/_lib/interfaces/stitch-settings";
import { getPageNumbers, getRowsColumns } from "./get-page-numbers";
import { Layers } from "./layers";
function trimmedPageSize(
inDoc: PDFDocument,
pages: number[],
settings: StitchSettings,
) {
/**
* Computes the size for each trimmed page.
* Chooses the largest page width and height from the user specified page range to match how the pdf viewer works.
*/
let width = 0;
let height = 0;
for (const page of pages) {
// Filter out blank pages specified by a 0
if (page > 0) {
const p = inDoc.getPage(page - 1);
const pageSize = p.getMediaBox();
width = Math.max(width, pageSize.width - settings.edgeInsets.horizontal);
height = Math.max(height, pageSize.height - settings.edgeInsets.vertical);
}
}
return { width, height };
}
function initDoc(doc: PDFDocument, pages: number[]): Map {
/**
* Creates a list of page numbers and references, then removes the pages from the document.
*/
const pageMap = new Map();
for (const p of pages.filter((p) => p > 0)) {
pageMap.set(p, doc.getPage(p - 1).ref);
}
// Remove all the pages
while (doc.getPageCount() > 0) {
doc.removePage(0);
}
return pageMap;
}
function mergeStreams(
streams: PDFArray | PDFStream,
context: PDFContext,
): PDFStream {
/**
* Content streams can be an array of streams or a single stream.
* This function merges them into a single stream, or just returns
* the stream if it was already a singleton.
*
* Note that the streams are first decoded, then joined with a newline,
* then re-encoded, as concatenating encoded streams led to broken pdfs.
* This results in an increase in file size, as the streams are copied.
* Removing the original streams sometimes led to broken pdfs, so we
* can't assume that they're never referenced elsewhere.
*
* Copied from the private function in pdf-lib here: https://github.com/cantoo-scribe/pdf-lib/blob/9593e75cbcf70f68dcf26bd541919e22514a5898/src/core/embedders/PDFPageEmbedder.ts#L118
*/
if (streams instanceof PDFStream) return streams;
else {
let totalLength = 0;
const decodedStreams: Uint8Array[] = [];
for (const ref of streams.asArray()) {
const stream = context.lookup(ref, PDFStream);
let content: Uint8Array;
if (stream instanceof PDFRawStream) {
content = decodePDFRawStream(stream).decode();
} else if (stream instanceof PDFContentStream) {
content = stream.getUnencodedContents();
} else {
throw new UnrecognizedStreamTypeError(stream);
}
totalLength += content.length + 1; // +1 for newline
decodedStreams.push(content);
}
const mergedStream = new Uint8Array(totalLength);
let offset = 0;
for (const content of decodedStreams) {
mergedStream.set(content, offset);
offset += content.length;
mergedStream[offset] = 0x0a; // newline
offset += 1;
}
return context.flateStream(mergedStream);
}
}
function getFormXObjectForPage(
context: PDFContext,
ref: PDFRef,
): PDFRef | undefined {
/**
* Create a form XObject from a page reference. Does not copy resources, just references them.
* Adapted from https://github.com/qpdf/qpdf/blob/2eefa580aa0ecf70ae3864d5c47e728480055c38/libqpdf/QPDFPageObjectHelper.cc#L705
*/
const page = context.lookup(ref) as PDFPageLeaf | undefined;
if (!page) return undefined;
// PDF treats pages differently from forms, so we need to extract
// the content stream and then add on the various attributes.
let xObject = page.Contents();
if (!xObject) return undefined;
xObject = mergeStreams(xObject, context);
xObject.dict.set(PDFName.of("Type"), PDFName.of("XObject"));
xObject.dict.set(PDFName.of("Subtype"), PDFName.of("Form"));
// Copy the contents, resources, and group info
const toCopy = ["Group", "Resources"].map((key) => PDFName.of(key));
for (const key of toCopy) {
const value = page.get(key);
if (value) xObject.dict.set(key, value);
}
// Bounding box is set by CropBox if it exists, otherwise MediaBox
const bbox =
page.get(PDFName.of("CropBox")) || page.get(PDFName.of("MediaBox"));
if (bbox) xObject.dict.set(PDFName.of("BBox"), bbox);
// register the new form XObject and return the reference
return context.register(xObject);
}
function getAsDict(name: string, dict: PDFDict): PDFDict | undefined {
/**
* Helper function to get a dictionary from a parent dictionary.
* If the object is a reference, find the actual dictionary.
*/
const obj = dict.get(PDFName.of(name));
if (obj instanceof PDFDict) return obj;
if (obj instanceof PDFRef) return dict.context.lookup(obj, PDFDict);
else return undefined;
}
function toggleLayers(doc: PDFDocument, layers: Layers) {
/**
* Toggle the default visibility of layers in the PDF based on user selections.
* Note that this does not actually remove content the way PDFStitcher does.
*/
const ocprops = getAsDict("OCProperties", doc.catalog);
if (!ocprops) return; // sometimes the document doesn't have layers
const D = getAsDict("D", ocprops) ?? doc.context.obj({});
ocprops.set(PDFName.of("D"), D);
const visible: PDFArray = doc.context.obj([]);
const hidden: PDFArray = doc.context.obj([]);
for (const layer of Object.values(layers)) {
const refs = layer.ids.map((id) => PDFRef.of(parseInt(id)));
refs.map((r) => (layer.visible ? visible.push(r) : hidden.push(r)));
}
D.set(PDFName.of("ON"), visible);
D.set(PDFName.of("OFF"), hidden);
}
async function tilePages(doc: PDFDocument, settings: StitchSettings) {
/**
* Do the stitching stuff and update the document. Converts a multi-page document
* into a single large page.
*/
const pages = getPageNumbers(settings.pageRange, doc.getPageCount());
const [rows, cols] = getRowsColumns(
pages,
settings.lineCount,
settings.lineDirection,
);
const trim = settings.edgeInsets;
// Compute the size of the output document
const pageSize = trimmedPageSize(doc, pages, settings);
const outWidth = pageSize.width * cols;
const outHeight = pageSize.height * rows;
// Modify the document to remove the pages but keep the objects
const pageMap = initDoc(doc, pages);
// Create a new page to hold the stitched pages
// Add at least a 1" margin because of weirdness.
const margin = Math.max(trim.horizontal, trim.vertical, 72);
const outPage = doc.addPage();
outPage.setMediaBox(
-margin,
-margin,
outWidth + margin * 2,
outHeight + margin * 2,
);
// Loop through the pages and copy them to the output document
let x = 0;
let y = outHeight - pageSize.height;
// define the commands to draw the page and the resources dictionary
const commands: PDFOperator[] = [];
const XObjectDict = doc.context.obj({});
for (const p of pages) {
const ref = pageMap.get(p);
if (ref) {
// create a new form XObject for the page
const xRef = await getFormXObjectForPage(doc.context, ref);
if (!xRef) {
throw new Error(`Failed to create form XObject for page ${p}`);
}
const pageName = `Page${p}`;
// Add commands to the content stream to draw the form
commands.push(pushGraphicsState());
commands.push(concatTransformationMatrix(1, 0, 0, 1, x, y));
commands.push(drawObject(pageName));
commands.push(popGraphicsState());
// Update the resources dictionary
XObjectDict.set(PDFName.of(pageName), xRef);
}
// Adjust the position for the next page
switch (settings.lineDirection) {
case LineDirection.Column:
x += pageSize.width;
if (x > outWidth - margin) {
x = 0;
y -= pageSize.height;
}
break;
case LineDirection.Row:
y -= pageSize.height;
if (y < -margin) {
y = outHeight - pageSize.height;
x += pageSize.width;
}
break;
}
}
// Write the commands to the content stream
const dict = doc.context.obj({});
const contentStream = PDFContentStream.of(dict, commands);
outPage.node.set(PDFName.Contents, doc.context.register(contentStream));
// Update the resources dictionary
const resources = outPage.node.get(PDFName.of("Resources")) as
| PDFDict
| undefined;
if (resources) {
resources.set(PDFName.of("XObject"), XObjectDict);
} else {
outPage.node.set(
PDFName.of("Resources"),
doc.context.obj({ XObject: XObjectDict }),
);
}
}
export async function savePDF(
file: File,
settings: StitchSettings,
layers: Layers,
password: string = "",
) {
// Grab the bytes from the file object and try to load the PDF
// Error handling is done in the calling function.
const pdfBytes = await file.arrayBuffer();
const doc = await PDFDocument.load(pdfBytes, {
ignoreEncryption: true,
password,
});
// Toggle the visibility of layers
toggleLayers(doc, layers);
// if it's a one-page document, we're done. Otherwise, stitch it together.
if (doc.getPageCount() > 1) {
await tilePages(doc, settings);
}
// Save the modified document and return the blob
return await doc.save();
}
================================================
FILE: app/_lib/pixels-per-inch.ts
================================================
export const CSS_PIXELS_PER_INCH = 96.0;
export const PDF_PIXELS_PER_INCH = 72.0;
export const PDF_TO_CSS_UNITS = CSS_PIXELS_PER_INCH / PDF_PIXELS_PER_INCH;
================================================
FILE: app/_lib/point.ts
================================================
/**
* The location of a pixel on the screen
* https://developer.mozilla.org/en-US/docs/Web/CSS/CSSOM_view/Coordinate_systems
*/
export interface Point {
/**
* Horizontal offset from the left of the screen
*/
readonly x: number;
/**
* Vertical offset from the top of the screen
*/
readonly y: number;
}
export function applyOffset(point: Point, offset: Point): Point {
return { x: point.x + offset.x, y: point.y + offset.y };
}
export function subtract(a: Point, b: Point): Point {
return { x: a.x - b.x, y: a.y - b.y };
}
================================================
FILE: app/_lib/remove-non-digits.spec.ts
================================================
import removeNonDigits from "@/_lib/remove-non-digits";
test("'abc' -> ''", () => {
expect(removeNonDigits("abc")).toEqual("");
});
test("'1a23bc' -> '123'", () => {
expect(removeNonDigits("1a23bc")).toEqual("123");
});
test("'123' -> '123'", () => {
expect(removeNonDigits("123")).toEqual("123");
});
test("'' -> ''", () => {
expect(removeNonDigits("")).toEqual("");
});
================================================
FILE: app/_lib/remove-non-digits.ts
================================================
export default function removeNonDigits(
newString: string,
oldString: string,
): string {
const num = newString.replace(/[^.\d]/g, "");
const decimalCount = (num.match(/\./g) || []).length;
if (num.localeCompare(".") === 0) {
return "0.";
}
if (decimalCount > 1) {
return oldString;
} else {
return num;
}
}
export function allowInteger(
s: string,
allowNegative: boolean = false,
): string {
const digitsOnly: string = s.replace(/\D/g, "");
if (!allowNegative) {
return digitsOnly;
}
return s.charAt(0) === "-" ? `-${digitsOnly}` : digitsOnly;
}
================================================
FILE: app/_lib/unit.ts
================================================
export const { CM, IN } = { IN: "IN", CM: "CM" };
export function getPtDensity(unitOfMeasure: string): number {
return unitOfMeasure === CM ? 96 / 2.54 : 96;
}
================================================
FILE: app/_reducers/layersReducer.ts
================================================
import { Layers } from "@/_lib/layers";
interface SetLayersAction {
type: "set-layers";
layers: Layers;
}
interface UpdateVisibilityAction {
type: "update-visibility";
visibleLayers: Set;
}
interface ToggleLayerAction {
type: "toggle-layer";
key: string;
}
interface HideAllAction {
type: "hide-all";
}
interface ShowAllAction {
type: "show-all";
}
export type LayerAction =
| SetLayersAction
| UpdateVisibilityAction
| ToggleLayerAction
| HideAllAction
| ShowAllAction;
export default function layersReducer(
layers: Layers,
action: LayerAction,
): Layers {
switch (action.type) {
case "set-layers":
return action.layers;
case "update-visibility":
return Object.fromEntries(
Object.entries(layers).map(([key, layer]) => [
key,
{ ...layer, visible: action.visibleLayers.has(key) },
]),
);
case "toggle-layer":
return {
...layers,
[action.key]: {
...layers[action.key],
visible: !layers[action.key]?.visible,
},
};
case "hide-all":
return Object.fromEntries(
Object.entries(layers).map(([key, layer]) => [
key,
{ ...layer, visible: false },
]),
);
case "show-all":
return Object.fromEntries(
Object.entries(layers).map(([key, layer]) => [
key,
{ ...layer, visible: true },
]),
);
}
}
================================================
FILE: app/_reducers/localTransformReducer.ts
================================================
import {
align,
move,
rotateToHorizontal,
flipAlong,
flipHorizontal,
flipVertical,
rotateMatrixDeg,
transformPoint,
translate,
scaleAboutPoint,
} from "@/_lib/geometry";
import { Line } from "@/_lib/interfaces/line";
import { Point } from "@/_lib/point";
import Matrix from "ml-matrix";
interface FlipAction {
type: "flip_vertical" | "flip_horizontal";
centerPoint: Point;
}
interface RotateAction {
type: "rotate";
centerPoint: Point;
degrees: number;
}
interface RecenterAction {
type: "recenter";
centerPoint: Point;
layoutWidth: number;
layoutHeight: number;
}
interface RotateToHorizontalAction {
type: "rotate_to_horizontal";
line: Line;
}
interface FlipAlongAction {
type: "flip_along";
line: Line;
}
interface TranslateAction {
type: "translate";
p: Point;
}
interface SetAction {
type: "set";
localTransform: Matrix;
}
interface ResetAction {
type: "reset";
}
interface AlignAction {
type: "align";
line: Line;
to: Line;
}
interface MagnifyAction {
type: "magnify";
scale: number;
point: Point;
}
export type LocalTransformAction =
| FlipAction
| RotateToHorizontalAction
| FlipAlongAction
| TranslateAction
| SetAction
| RotateAction
| RecenterAction
| ResetAction
| AlignAction
| MagnifyAction;
export default function localTransformReducer(
localTransform: Matrix,
action: LocalTransformAction,
) {
switch (action.type) {
case "set": {
return action.localTransform.clone();
}
case "rotate_to_horizontal": {
return rotateToHorizontal(action.line).mmul(localTransform);
}
case "flip_along": {
return flipAlong(action.line).mmul(localTransform);
}
case "translate": {
return translate(action.p).mmul(localTransform);
}
case "flip_vertical": {
return flipVertical(action.centerPoint).mmul(localTransform);
}
case "flip_horizontal": {
return flipHorizontal(action.centerPoint).mmul(localTransform);
}
case "rotate": {
return rotateMatrixDeg(90, action.centerPoint).mmul(localTransform);
}
case "recenter": {
const current = transformPoint(
{ x: action.layoutWidth * 0.5, y: action.layoutHeight * 0.5 },
localTransform,
);
return move(current, action.centerPoint).mmul(localTransform);
}
case "reset": {
return Matrix.identity(3);
}
case "align": {
return align(action.line, action.to).mmul(localTransform);
}
case "magnify": {
return scaleAboutPoint(action.scale, action.point).mmul(localTransform);
}
}
}
================================================
FILE: app/_reducers/patternScaleReducer.ts
================================================
interface DeltaAction {
type: "delta";
delta: number;
}
interface SetAction {
type: "set";
scale: string;
}
export type PatternScaleAction = DeltaAction | SetAction;
export default function PatternScaleReducer(
patternScale: string,
action: PatternScaleAction,
): string {
switch (action.type) {
case "set": {
return action.scale;
}
case "delta": {
const n = action.delta + Number(patternScale);
const hm = n > 0 ? String(n.toFixed(2)) : patternScale;
return hm;
}
}
}
================================================
FILE: app/_reducers/pointsReducer.ts
================================================
import { Point, applyOffset } from "@/_lib/point";
interface OffsetAction {
type: "offset";
offset: Point;
corners: Set;
}
interface SetAction {
type: "set";
points: Point[];
}
export type PointAction = OffsetAction | SetAction;
export default function pointsReducer(
points: Point[],
action: PointAction,
): Point[] {
const newPoints = reducePoints(points, action);
localStorage.setItem("points", JSON.stringify(newPoints));
return newPoints;
}
function reducePoints(points: Point[], action: PointAction) {
switch (action.type) {
case "set": {
return [...action.points];
}
case "offset": {
return points.map((p, i) => {
if (action.corners.has(i)) {
return applyOffset(p, action.offset);
} else {
return p;
}
});
}
}
}
================================================
FILE: app/_reducers/stitchSettingsReducer.ts
================================================
import { EdgeInsets } from "@/_lib/interfaces/edge-insets";
import { StitchSettings } from "@/_lib/interfaces/stitch-settings";
interface SetAction {
type: "set";
stitchSettings: StitchSettings;
}
interface SetPageRangeAction {
type: "set-page-range";
pageRange: string;
}
interface SetLineCountAction {
type: "set-line-count";
lineCount: number;
pageCount: number;
}
interface StepLineCountAction {
type: "step-line-count";
pageCount: number;
step: number;
}
interface StepHorizontalAction {
type: "step-horizontal";
step: number;
}
interface StepVerticalAction {
type: "step-vertical";
step: number;
}
interface SetEdgeInsetsAction {
type: "set-edge-insets";
edgeInsets: EdgeInsets;
}
export type StitchSettingsAction =
| SetAction
| SetPageRangeAction
| StepLineCountAction
| SetLineCountAction
| StepHorizontalAction
| StepVerticalAction
| SetEdgeInsetsAction;
export default function stitchSettingsReducer(
stitchSettings: StitchSettings,
action: StitchSettingsAction,
): StitchSettings {
const newStitchSettings = reduceStitchSettings(stitchSettings, action);
localStorage.setItem(
newStitchSettings.key,
JSON.stringify(newStitchSettings),
);
return newStitchSettings;
}
function reduceStitchSettings(
stitchSettings: StitchSettings,
action: StitchSettingsAction,
): StitchSettings {
switch (action.type) {
case "set":
return action.stitchSettings;
case "set-page-range":
return { ...stitchSettings, pageRange: action.pageRange };
case "set-line-count": {
const lineCount =
action.pageCount >= action.lineCount && action.lineCount >= 0
? action.lineCount
: stitchSettings.lineCount;
return { ...stitchSettings, lineCount };
}
case "step-line-count": {
const count = stitchSettings.lineCount + action.step;
const lineCount =
count <= action.pageCount && count >= 0
? count
: stitchSettings.lineCount;
return { ...stitchSettings, lineCount };
}
case "step-horizontal": {
const horizontal = Math.max(
0,
stitchSettings.edgeInsets.horizontal + action.step,
);
return {
...stitchSettings,
edgeInsets: { ...stitchSettings.edgeInsets, horizontal },
};
}
case "step-vertical": {
const vertical = Math.max(
0,
stitchSettings.edgeInsets.vertical + action.step,
);
return {
...stitchSettings,
edgeInsets: { ...stitchSettings.edgeInsets, vertical },
};
}
case "set-edge-insets": {
return { ...stitchSettings, edgeInsets: action.edgeInsets };
}
}
}
================================================
FILE: app/manifest.js
================================================
export default function manifest() {
return {
name: "Pattern Projector",
short_name: "PatternProjector",
start_url: "/calibrate",
icons: [
{
src: "144.png",
sizes: "144x144",
type: "image/png",
purpose: "any",
},
{
src: "512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
display: "fullscreen",
background_color: "#fff",
description:
"Calibrates projectors for projecting sewing patterns with accurate scaling and without perspective distortion",
theme_color: "#fff",
file_handlers: [
{
action: "/calibrate",
accept: {
"application/pdf": [".pdf"],
},
},
],
};
}
================================================
FILE: app/sw.ts
================================================
import { defaultCache } from "@serwist/next/browser";
import type { PrecacheEntry } from "@serwist/precaching";
import { installSerwist } from "@serwist/sw";
declare const self: ServiceWorkerGlobalScope & {
// Change this attribute's name to your `injectionPoint`.
// `injectionPoint` is an InjectManifest option.
// See https://serwist.pages.dev/docs/build/inject-manifest/configuring
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
};
installSerwist({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching: defaultCache,
});
================================================
FILE: cypress/fixtures/example.json
================================================
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
================================================
FILE: cypress/support/commands.ts
================================================
///
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable
// drag(subject: string, options?: Partial): Chainable
// dismiss(subject: string, options?: Partial): Chainable
// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
// }
// }
// }
================================================
FILE: cypress/support/component-index.html
================================================
Components App
================================================
FILE: cypress/support/component.ts
================================================
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
import { mount } from "cypress/react18";
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a at the top of your spec.
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount;
}
}
}
Cypress.Commands.add("mount", mount);
// Example use:
// cy.mount()
================================================
FILE: cypress.config.ts
================================================
import { defineConfig } from "cypress";
export default defineConfig({
component: {
devServer: {
framework: "next",
bundler: "webpack",
},
},
});
================================================
FILE: global.d.ts
================================================
// Use type safe message keys with `next-intl`
type Messages = typeof import("./messages/en.json");
declare interface IntlMessages extends Messages {}
================================================
FILE: i18n.ts
================================================
import { locales } from "middleware";
import { getRequestConfig } from "next-intl/server";
import { notFound } from "next/navigation";
import deepmerge from "deepmerge";
export default getRequestConfig(async ({ locale }) => {
// Validate that the incoming `locale` parameter is valid
if (!locales.includes(locale as any)) notFound();
const userMessages = (await import(`./messages/${locale}.json`)).default;
const defaultMessages = (await import(`./messages/en.json`)).default;
const messages = deepmerge(defaultMessages, userMessages);
return { messages };
});
================================================
FILE: jest.config.ts
================================================
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
import type { Config } from "jest";
import nextJest from "next/jest.js";
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./",
});
const config: Config = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/6f/c588475d4z59k9v6695sltdm0000gn/T/jest_dx",
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
roots: ["/app/_lib"],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "jsdom",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config);
================================================
FILE: messages/cs.json
================================================
{
"HomePage": {
"github": "Zobrazit zdrojový kód",
"calibrate": "Zahájit kalibraci",
"choose-language": "Jazyk",
"welcome": {
"title": "Vítejte v Pattern Projectoru!",
"description": "Pattern projector je zdarma a volně šířítelná webová aplikace, která umožňuje rychlou kalibraci projektorů pro šicí střihy. Má také nástroje pro slučování střihů na více stránkách, změnu tloušťky čar, prohození barev, otáčení střihů a další nástroje. Nejnovější updaty jsou popsány v changelogu."
},
"requirements": {
"title": "Co budete potřebovat",
"projector": "Projektor: doporučené rozlišení alespoň 720p",
"mat": "Řezací podložku s mřížkou (není nutná)",
"mount": "Stativ nebo držák na projektor na stěnu/polici/stůl",
"computer": "Počítač nebo tablet k připojení projektoru",
"pattern": "Střih na šití ve formátu PDF"
},
"setup": {
"title": "Nastavení",
"place": "Umístěte projektor nad řezací podložku, otočený směrem k řezací podložce. Pokud nemáte řezací podložku, můžete použít papír nebo lepicí pásku k vyznačení mřížky na stole nebo podlaze.",
"focus": "Upravujte zaostření projektoru dokud není obraz zcela zaostřený ve středu zobrazené plochy. Pokud nemůžete obraz zaostřit, přesvěčte se, že vzdálenost mezi projektorem a řezací podložkou je ve funkčních mezích stanovených výrobcem projektoru (minimální projekční vzdálenost).",
"connect": "Připojte váš počítač nebo tablet k projektoru a buď promítněte nebo rozšiřte plochu tak, aby se obraz z počítače nebo tabletu zobrazil pomocí projektoru.",
"keystone": "Pokud je váš projektor vybaven korekcí lichoběžníkového zkreslení, upravte ho tak, aby zobrazená plocha měla co nejvíce obdélníkový tvar a zaostření u krajů se zlepší."
},
"calibration": {
"title": "Kalibrace",
"start": "Klikněte na \"Kalibrovat\"",
"fullscreen": "Kliknutím přepněte na režim celé obrazovky ",
"size": "Není nutné provádět kalibraci za použití celé plochy vaší podložky, namísto toho použijte největší plochu, na kterou dokážete umístit kalibrační mřížku a zadejte do programu výšku a šířku vaší mřížky.",
"project": "Jakmile se promítnutá mřížka překryje s mřížkou na vaší podložce, klikněte na \"Promítat\".",
"drag": "Přetáhněte rohy mřížky tak, aby se překrývaly s rohy na vaší podložce. Sledujte podložku a upravte polohu rohů zobrazených na vašem počítaci nebo tabletu. Upravujte polohu rohů do té doby, dokud se zobrazená plocha neshoduje s tou na vaší podložce."
},
"youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr",
"project": {
"title": "Promítnout střih",
"open": "Pro otevření PDF stříhu klikněte na .",
"move": "Pro změnu polohy PDF plikněte na střih a přetáhněte PDF po obrazovce.",
"cut": "Vystřihněte promítnutý střih.",
"tools": "V módu Promítání je k dispozici mnoho užitěčných nástrojů. Některé mají klávesové zkratky, které jsou uvedeny v poznámkách.",
"fullscreen": {
"description": "Obecně je jednodušší používat tento program v režimu celé obrazovky.",
"title": "Celá obrazovka"
},
"showMenu": {
"title": "Zobrazit/ skrýt menu",
"description": "Zobrazit nebo skýt vrchní menu."
},
"moveTool": {
"title": "Zobrazit nebo skrýt nástroj \"přesunout\"",
"description": "Nástroj \"přesunout\" má 4 tlačítka se šipkami pro přesun rohů kalibrační mřížky. Také má ve středu také tlačítko \"další\" pro přepnutí na další roh."
},
"invert": {
"title": "Prohodit barvy",
"description": "Při promítání jsou obvykle lépe viditělné zelené nebo bílé čáry na černém pozadí. Klikněte jednou pro zelené čáry, dvakrát pro bílé a třikrát pro návrat k černým."
},
"overlayOptions": {
"title": "Zobrazit nebo skrýt možnosti překryvu",
"description": "Na PDF mohou být zobrazeny překryvy jako mřížka, hranice stránky, osa převrácení nebo druhá strana, pro snadnější kalibraci a stříhání. Pro více informací navštivte overlay options section."
},
"lineWeight": {
"title": "Tloušťka čáry",
"description": "Změnit tloušťku čar v PDF střihu."
},
"flip": {
"title": "Převrátit vertikálně (V) nebo Horizontálně (H)",
"description": "Hodí se pro zobrazení, kdy jsou zobrazeny poloviny střihových částí."
},
"rotate": {
"title": "Otočit (R)",
"description": "Otočit střih o 90 stupňů."
},
"recenter": {
"title": "Zacentrovat a resetovat (C)",
"description": "Posune střih na sřed a zruší otočení/ převrácení."
},
"layers": {
"description": "Zobrazí nebo skryje vrstvy v PDF.",
"title": "Zobrazit nebo skrýt vrstvy"
},
"stitch": {
"description": "Zobrazit nebo skrýt menu \"složit\" , které vám umožní složit dohromady několikastránkové PDF střihy.",
"title": "Zobrazit nebo skrýt menu \"složit\" (stitch)"
},
"measure": {
"title": "Nástroj čára (L)",
"description": "Označí čáry v PDF pro otočení, převrácení, přemístění nebo označení. Pro detailní popis tohoto nástroje otevřete Sekci nástroj čára."
},
"zoomOut": {
"title": "Oddálit (Z)",
"description": "Oddálit pro zobrazení celého PDF. Klikněte znovu pro přiblížení zpět k vybrané části."
},
"magnify": {
"title": "Přiblížit (M)",
"description": "Pro přiblížení klikněte na PDF. Klikněte znovu pro zrušení přiblížení."
},
"scale": {
"title": "Zobrazit nebo skrýt menu měřítko",
"description": "Změňte velikost střihu zadáním násobitele: mezi 0-1 střih zmenší a větší než 1 střih zvětší."
}
},
"tools": "Nástroje",
"lineTool": {
"title": "Nástroj čára",
"delete": {
"title": "Smazat čáru",
"description": "Smazat vybranou čáru."
},
"rotate": {
"title": "Zarovnat na střed",
"description": "Otočí PDF tak, že zvolená čára je zacentrovaná horizontálně na vaší podložce.",
"use": "Pro zarovnání směru střihu se směrem vláken vaší látky: nakreslete čáru na směrové značce střihu, poté klikněte na tlačítko zarovnat na střed."
},
"previousNext": {
"title": "Zarovnat předchozí/ následující čáru na střed",
"description": "Posune PDF tak, že předchozí/následující nakreslená čára je horizontálně zarovnána na střed.",
"use": "Pro pohyb mezi díly střihu během stříhání: nakreslete čáru na směrové značce každého dílu střihu, poté se pohybujte po střihu pomoci těchto tlačítek. Během pohybu po jednotlivých dílech střihu, mazejte jednotlivé čáry pro sledování toho, které díly ještě zbývá vystříhnout."
},
"flip": {
"title": "Převrátit podél osy",
"description": "Převrátí PDF podél osy.",
"use": "Pro rozložení dílů střihu: nakreslete čáru na skad látky na střihu, vyjměte díl až k skladu látky a klidněte na tlačítko převrátit podél osy, poté vystřihněte zbytek dílu."
},
"move": {
"title": "Posunout PDF o délku čáry",
"description": "Posune PDF o délku čáry.",
"use": "Pro prodloužení/ zkrácení dílů střihu: nakreslete čáru vodorovnou k čáře pro prodloužení/zkrácení dílu střihu, vyjměte tuto čáru a klidněte na tlačítko posunout o délku čáry a poté pokračujte ve stříhání dílu střihu. Směr nakreslené čáry bude záviset na tom, zda prodlužujete nebo zkracujete stříhový díl."
},
"description": "Nástroj čára může být použit pro měření vzdálenosti, označování čar, otáčení, převrácení a posunování PDF. Klikněte na tlačítko nástroje čára, poté klikněte na PDF a tahem nakreslete čáru. Jakmile je čára nakreslená, na spodku obrazovky se objeví menu s následujícími možnostmi:"
},
"overlayOptions": {
"title": "Možnosti překryvu",
"description": "Možnosti překryvu mohou být použity pro ověření kalibrace během promítání a pomoci při vystřihování střihů. K dispozici jsou následující možnosti:",
"border": {
"title": "Hranice",
"description": "Zobrazí hranice kalibrační mřížky."
},
"flipLines": {
"title": "Převrátit čáry",
"description": "Zobrazí čáry, které pomáhají s rozbalením střihu. Zarovnejte sklad střihu s osou převrácení a zvolte horizontální nebo vertikální převrácení pro zobrazení dílu."
},
"grid": {
"title": "Mřížka",
"description": "Zobrazí kalibrační mřížku."
},
"paper": {
"title": "List papíru",
"description": "Zobrazí obdélník o velikosti listu papíru Letter nebo A4 pro ověření kalibrace."
},
"flippedPattern": {
"title": "Opačná strana",
"description": "Zobrazí tečky, když je promítán otočený střih."
}
},
"faq": {
"title": "Často kladené dotazy (FAQ)",
"wrongSizePdf": {
"question": "Je vaše promítnuté PDF příliš malé nebo velké?",
"answer": "Kalibrační nástroj Pattern Projectoru nemá přiblížení, protože měřítko projekce je dáno velikostním nastavením PDF souboru. Střihy od návrhářů mají obvykle správné měřítko, talže změna měřítka může být přenastavena po otevření souboru v Affinity Designeru nebo Inkscapu."
},
"saveAsApp": {
"answer": "Patter Projektor je progresivní webová aplikace (Progressive Web App - PWA), takže může být uložen pouze jako applikace. Na plochu počítače ji můžete nainstalovat přes Chrome nebo Edge. Na tabletu otevřete menu Sdílení a klikněte na \"Přidat na Home Screen\"."
},
"annotationSupport": {
"question": "Je k dispozici podpora pro přidávání anotací?",
"answer": "Aktuálně ne, ale je v plánu pro další verzi."
},
"mobileSupport": {
"answer": "Ačkoliv je možné na smartphonu navštívit webovou stránku, je používání ztížené omezenou velikostí obrazovky.",
"question": "Podporuje aplikace mobilních telefony a tablety?"
}
},
"contribute": {
"develop": "Podílejte se na přidávání nových funkcí a opravě chyb na GitHub.",
"feedback": "Zpětná vazba a návrhy nových funcí jsou vítány!",
"title": "Přispějte do projektu",
"donation": "Pokud Vám Patter Projektor ušetřil peníze za tisk střihů nebo již nemusíte platit za specializované programy, zvažte prosím možnost Pozvat na kávu nebo příspěvku pomocí PayPalu!",
"translate": "Přeložte tento nástroj do dalších jayzků pomocí Weblate."
},
"resources": {
"links": {
"projectorsForSewing": {
"link": "https://www.facebook.com/groups/481078582801085",
"title": "Faceboková skupina Projectors for Sewing (v angličtině)"
},
"onePageGuide": {
"link": "https://bit.ly/onepageguidetoprojectorsewing",
"title": "Průvodce šitím s projektorem (v angličtině)"
}
},
"title": "Další zdroje informací"
},
"contact": "Kontakt"
},
"InstallButton": {
"title": "Naintalovat aplikaci",
"descriptionAndroid": "Pro nejsnadnější používání pro Android použijte Firefox. Aplikaci stáhněte kliknutím na tlačítko v menu prohlížeče a následně klikněte na Add to Home Screen.",
"ok": "OK",
"descriptionIOS": "Stáhněte aplikaci kliknutím na tlačítko v menu prohlížeče a poté klikněte na Add to Home Screen .",
"description": "Pro nejsnadnější používání nainstalujte Pattern Projektor pomocí Google Chrome. V Chromu stáhněte aplikaci kliknutím na v panelu Adresa."
},
"Header": {
"project": "Promítat",
"openPDF": "Otevřít PDF",
"height": "H:",
"width": "W:",
"calibrate": "Kalibrovat",
"delete": "Resetovat kalibraci",
"gridOn": "Zobrazit mřížku",
"gridOff": "Schovat mřížku",
"overlayOptions": "Možnosti překryvu",
"overlayOptionBorder": "Hranice",
"overlayOptionDisabled": "Deaktivováno",
"overlayOptionGrid": "Mřížka",
"overlayOptionPaper": "List papíru",
"overlayOptionFlippedPattern": "Opačná strana (líc)",
"invertColor": "Prohodit barvy",
"invertColorOff": "Vypnout prohození barev",
"flipVertical": "Převrátit vertikálně (V)",
"flipHorizontal": "Převrátit horizontálně (H)",
"flipCenterOff": "Vypnout převrácení podél středové osy",
"fourCornersOn": "Zvýraznit všechny rohy",
"fourCornersOff": "Zvýraznit pouze vybraný roh",
"rotate90": "Otočit o 90 stupňů po směru hodin (R)",
"arrowBack": "O jednu stranu zpět",
"arrowForward": "O jednu stranu vpřed",
"recenter": "Zarovnat na střed a resetovat (C)",
"fullscreen": "Přepnout na celou obrazovku",
"fullscreenExit": "Vypnout režim celé obrazovky",
"ok": "OK",
"calibrationAlertTitle": "Upozornění kalibrace",
"calibrationAlert": "Velikost okna se od začátku promítání změnila, zmáčkněte tlačítko Přepnout na celou obrazovku nebo zopakujte kalibraci.",
"calibrationAlertContinue": "Velikost okna se nesmí v průběhu kalibrace změnit. Pro promítání v režimu celé obrazovky proveďte kalibraci na celou obrazovku. Pokud chcete změnit velikost okna, proveďte kalibraci znovu.",
"checkCalibration": "Kontrola kalibrace",
"continue": "Uložit velikost okna a pokračovat",
"menuShow": "Zobrazit menu",
"menuHide": "Schovat menu",
"lineWeight": "Tloušťka čáry",
"stitchMenuDisabled": "Otevřít PDF pro použití spojovacího menu",
"hideMovement": "Shovat nástroj posun",
"magnify": "Přiblížit (M)",
"zoomOut": "Oddálit (Z)",
"overlayOptionFliplines": "Osy pro převrácení",
"flipCenterOn": "Převrátit podél středové osy",
"info": "Informace",
"stitchMenuShow": "Zobrazit spojovací menu",
"showMovement": "Zobrazit nástroj posun",
"stitchMenuHide": "Schovat spojovací menu",
"measure": "Nástroj čára (L)",
"mail": "Zpráva od Courtney"
},
"StitchMenu": {
"columnCount": "Sloupce",
"pageRange": "Spojit strany",
"horizontal": "Horizontální",
"vertical": "Vertikální",
"zeros": "Přidat 0 pro prázdné stránky, např. 1-3,0,0,4"
},
"LayerMenu": {
"layersOn": "Zobrazit menu vrstvy",
"layersOff": "Schovat menu vrstvy",
"title": "Vrstvy",
"showAll": "Zobrazit vše",
"noLayers": "PDF neobsahuje vrstvy",
"hideAll": "Schovat vše"
},
"MeasureCanvas": {
"flipAlong": "Převrátit podél osy",
"line": "čára",
"translate": "Posunout PDF o délku čáry",
"rotateToHorizontal": "Zarovnat na střed",
"rotateAndCenterPrevious": "Zarovnat předchozí čáru na střed",
"rotateAndCenterNext": "Zarovnat následující čáru na střed",
"deleteLine": "Vymazat čáru",
"lines": "čáry"
},
"SaveMenu": {
"save": "Exportovat PDF",
"saveTooltip": "Exportovat PDF se spojenými stranami a vybranými vrstvami",
"encryptedPDF": "Toto PDF používá přístupové heslo. Prosím, pro uložení spojeného PDF zadejte heslo."
},
"MovementPad": {
"up": "Nahoru",
"next": "Následující roh",
"down": "Dolu",
"left": "Doleva",
"right": "Doprava"
},
"OverlayCanvas": {
"zoomedOut": "Klidněte na PDF pro přiblížení",
"magnifying": "Klidněte na PDF pro zrušení přiblížení",
"scaled": "× měřítko"
},
"PdfViewer": {
"error": "Načítání PDF selhalo",
"noData": "Pro načtení PDF stiskněte tlačítko \"Otevřít PDF\""
},
"Troubleshooting": {
"notMatching": "Čáry neodpovídají čarám na vaší podložce?",
"title": "Odstranění problémů kalibrace",
"dragCorners": {
"title": "Jak kalibrovat",
"description": "Přetáhněte rohy mřížky na obdélní na vaší podložce velký alespoň 24x16 inchů (cca . 61 na 41 cm). Mřížka bz měla být co největší možná, ale nemusí pokrývat celou podložku.",
"caption": "Správně zkalibrovaná mřížka."
},
"offByOne": {
"title": "Čáry jsou rovné, ale nepřekrývají se s těmi na podložce?",
"caption": "Byly zadány chybné rozměry 23x17, místo 24x18. Na podložce jsou zobrazena čísla do 23 a 17, takže je snadné zmílit se v rozměrech. Použijte metr na přeměření!",
"description": "Pokud jsou promítané čáry rovné, ale neshodují se s těmi na vaší podložce, může se vaše šířka nebo výška lišit o 1 cm/inch. Znovu překontrolujte vaše rozměry pomocí metru. Rozměry podložky mohou být matoucí nebo chybné, takže je důležité je přeměřit metrem."
},
"inputMeasurement": "Použijte metr na změření šířky a výšky mřížky na vaší podložce. Tyto hodnoty zadejte do polí šířka a výška.",
"unevenSurface": {
"description": "Pokud jsou promítnuté čáry zakřivené, nemusí být vaše podložka nebo podklad rovné. Zkuste srovnat vaši podložku pomocí kartonu pod prohnutými částmi nebo zvolte jiný povrch pro řezání/ stříhání.",
"title": "Zakřivené čáry?",
"caption": "Tato podložka má pod sebou pravítko, což způsobuje zakřivení čar. Malé zakřivení je v pořádku, ale velké zakřivení způsobuje problémy."
},
"dimensionsSwapped": {
"title": "Obdélníky místo čtverců?",
"caption": "Šířka a výška jsou ve špatných polích. Rozměry by měly být Š:24 a V:18.",
"description": "Pokud promítaná mřížka vypadá jako obdélníky místo čtverců, možná jste prohodili výšku a šířku. Prohoď te zadané hodnoty."
},
"close": "Zavřít"
},
"ScaleMenu": {
"hide": "Skrýt menu měřítko",
"scale": "Měřítko střihu",
"show": "Zobrazit menu měřítko"
},
"General": {
"close": "Zavřít",
"error": "Chyba"
},
"Mail": {
"title": "Máte zprávu",
"donate": "Přispět"
}
}
================================================
FILE: messages/da.json
================================================
{
"HomePage": {
"beta": "beta",
"pdfstitcherHref": "https://www.pdfstitcher.org/",
"youTubeSrc": "https://www.youtube.com/embed/TGrLTFDrgmM?si=GORwjZyJ5CPIIymM",
"github": "Se kildekode",
"calibrate": "Påbegynd kalibrering",
"choose-language": "Sprog",
"welcome": {
"title": "Velkommen til Pattern Projector!",
"description": "Pattern Projector er en gratis og open source webapp, der hurtigt kalibrerer projektorer til brug med symønstre. PP har også værktøjer til at samle mønstre på mange sider, ændre linjetykkelse, vise farver omvendt, vende/rotere mønstre og meget andet. For at læse om de seneste opdateringer, se nærmere på changelog."
},
"requirements": {
"title": "Hvad du skal bruge",
"projector": "Projektor: mindst 720p anbefalet",
"mat": "Skæreunderlag, evt. med målelinjer",
"mount": "Tripod eller beslag/holder på væg, hylde eller bord til projektor",
"computer": "Computer eller tablet til at forbinde til projektoren",
"pattern": "Et PDF-mønster"
},
"setup": {
"title": "Montering/opsætning",
"place": "Placér projektoren direkte over skæreunderlaget, så den lyser ned på underlaget. Hvis du ikke har et skæreunderlag, kan du bruge papir eller malertape til at markere et gitter eller et rektangel på et bord eller gulvet.",
"connect": "Forbind din computer eller tablet til projektoren. Spejl eller udvid skærm, så billedet fra computer eller tavlet vises på projektoren.",
"focus": "Justér fokus på projektoren indtil teksten står skarpt i midten af projektorbilledet. Hvis du ikke kan få et tydeligt billede, skal du kontrollere, om afstanden mellem projektoren og skæreunderlaget er inden for producentens anbefalinger.",
"keystone": "Hvis din projektor har mulighed for at justere keystone, så justér den indtil projektorbilledet er tæt på rektangulær og fokus ude i kanten forbedres."
},
"calibration": {
"title": "Kalibrering",
"start": "Start kalibrering",
"input": "Indtast bredde og højde på dit skæreunderlag.",
"fullscreen": "Gå til fuld skærm ved at klikke eller tappe ",
"drag": "Træk i hjørnerne af gitteret, så ternene kommer til at passe med ternene på dit skæreunderlag. Justér hjørnerne på din tablet eller computer, mens du kigger på skæreunderlaget. Justér hjørnernes placering indtil gitteret i projektorbilledet passer med skæreunderlagets gitter.",
"size": "Du behøver ikke at bruge hele dit skæreunderlag, når du kalibrerer. Brug i stedet det størst mulige område, som du kan få kalibreringsgitteret til at passe i. Indtast målene (bredde og højde), så de stemmer overens med målene på kalibreringsgitteret.",
"project": "Når gitteret i projektorbilledet passer med din skæremåtte, klik eller tap \"Vis mønster\""
},
"project": {
"title": "Vis mønster",
"open": "Klik eller tap for at åbne PDF-dokumentet.",
"move": "Flyt PDF’en ved at klikke og trække det rundt på skærmen.",
"cut": "Skær ud efter mønsteret.",
"tools": "Under \"Vis mønster\" er der flere værktøjer tilgængelige. Nogle af dem har genvejstaster, som står i parentes.",
"showGrid": {
"title": "Vis/skjul gitter",
"description": "Når mønsteret vises, kan man vælge at se et svagt gitter for kunne kontrollere, at kalibreringen er korrekt."
},
"invert": {
"title": "Omvend farver",
"description": "Når mønsteret vises, er det som regel nemmere at se grønne eller hvide linjer på sort. Klik én gang for grønne linjer, to gange for hvide linjer og tre gange for at vende tilbage til sorte linjer."
},
"flip": {
"title": "Vend lodret/vertikalt (V) eller horisontalt/vandret (H)",
"description": "Nyttigt til at spejle halve mønsterdele."
},
"rotate": {
"title": "Rotér (R)",
"description": "Ændr mønsterets retning med 90 grader."
},
"recenter": {
"title": "Centrér og nulstil (C)",
"description": "Centrerer mønsteret på skæreunderlaget og nulstiller rotation/vending."
},
"fullscreen": {
"title": "Fuld skærm",
"description": "Det er som regel nemmere at bruge softwaren i fuld skærm."
},
"measure": {
"title": "Linjeværktøj (L)",
"description": "Lav linjer på PDF'en for at rotere, vende, flytte måle eller markere. For en detaljeret beskrivelse af linjeværktøjet, se nærmere her."
},
"stitch": {
"title": "Vis eller skjul samlemenu",
"description": "Vis eller skjul samlemenu, som giver mulighed for at samle mønstre på flere sider (A4-mønstre)."
},
"layers": {
"title": "Vis eller skjul lag",
"description": "Vis eller skjul lag i PDF'en."
},
"lineWeight": {
"title": "Linjetykkelse",
"description": "Ændr tykkelsen på linjerne i PDF-mønsteret."
},
"overlayOptions": {
"description": "Markeringer for gitter, kant, papirark, vende-linjer eller vrangside kan vises på PDF'en for at hjælpe med kalibrering og udskæring. Se nærmere i afsnittet om overlay-funktioner.",
"title": "Vis eller skjul overlay-funktioner"
},
"moveTool": {
"title": "Vis eller skjul flytte-værktøj",
"description": "Flytte-værktøjet har fire piletaster til at flytte kalibreringsgitterets hjørner/kanter. Det har også en \"næste\"-knap i midten for at skifte til næste hjørne/kant."
},
"showMenu": {
"title": "Vis/skjul menu",
"description": "Vis eller skjul topmenu."
}
},
"faq": {
"title": "FAQ / OSS",
"wrongSizePdf": {
"question": "Vises PDF’en for stort eller for småt?",
"answer": "Pattern Projector kalibreringsværktøj har ingen zoom, fordi skaleringen kommer fra info i PDF’en. Mønstre fra designere har som regel den rigtige skalering, så en ændring i skaleringen kan være sket efter at have haft mønsteret åbnet i Affinity Designer eller Inkscape."
},
"saveAsApp": {
"question": "Vil du gemme Pattern Projector som en app?",
"answer": "Pattern Projector er en Progressive Web App (PWA) og kan gemmes som en app. På en computer kan du installere den med Chrome eller Edge. På en tablet skal du åbne Del-menu og trykke på \"Tilføj til hjemmeskærm”."
},
"annotationSupport": {
"question": "Er der mulighed for at lave notater?",
"answer": "Ikke endnu, men det er planlagt i en senere udgave."
},
"chromecastSupport": {
"question": "Er det muligt at bruge Chromecast?",
"answer": "Det er muligt at bruge Chromecast til at vise denne webside, men forsinkelse i forbindelsen kan være frustrerende. Der er planer om bedre understøttelse af Chromecast i en senere udgave."
},
"mobileSupport": {
"question": "Er det muligt at bruge en mobiltelefon eller tablet?",
"answer": "Det er muligt at bruge hjemmesiden på en smartphone, men den begrænsede skærmstørrelse gør det svært at bruge. Der er planer om bedre funktionalitet på mobile enheder i en senere udgave."
}
},
"resources": {
"title": "Yderligere ressourcer",
"links": {
"projectorsForSewing": {
"title": "Projektorsyning - sy med projektor",
"link": "https://www.facebook.com/groups/syprojektor"
},
"onePageGuide": {
"title": "Énsidesguide til projektorsyning",
"link": "https://bit.ly/onepageguidetoprojectorsewing"
}
}
},
"contribute": {
"title": "Bidrag til projektet",
"donation": "Hvis du har lyst til at støtte udviklingen af dette værktøj, så overvej at købe mig en kop kaffe!",
"develop": "Hjælp med at implementere funktioner og fixe problemer på GitHub.",
"translate": "Oversæt dette værktøj til flere sprog med Weblate.",
"feedback": "Feedback og forslag til funktioner er velkomne!"
},
"contact": "Kontakt",
"lineTool": {
"title": "Linjeværktøj",
"rotate": {
"title": "Justér til center",
"description": "Roterer PDF'en, så den valgte linje bliver vandret og centreret på dit skæreunderlag.",
"use": "For at justere trådretningen på en mønsterdel, så den følger trådretning på stoffet: Tegn en linje på trådretningsstregen på mønsteret og tryk derefter på \"Justér til center\"-knappen."
},
"delete": {
"title": "Slet linje",
"description": "Sletter den valgte linje."
},
"flip": {
"title": "Vend langs linjen",
"description": "Vender PDF'en langs linjen.",
"use": "For at folde mønsterdele ud: Tegn en linje på til fold-linjen af en mønstedel, skær mønstedelen ud frem til til fold-linjen, klik på \"vend langs streg\"-knappen og skær resten af delen ud."
},
"move": {
"title": "Forskyd PDF'en med linjens længde",
"description": "Forskyd PDF'en med linjens længde",
"use": "For at forkorte/forlænge mønsterdele: Tegn en linje vinkelret på forkort/forlæng-linjen på mønsterdelen, skær ud frem til forkort/forlæng-linjen og klik så på \"forskyd PDF med linjens længde\"-knappen og fortsæt med at skære ud. Retningen på den tegnede linje afhænger af, om du vil forkorte eller forlænge mønsterdelen."
},
"previousNext": {
"title": "Bring forrige/næste linje til center",
"description": "Flytter PDF'en, så den forrige/næste linje er vandret og centreret på skæreunderlaget.",
"use": "For at flytte mellem mønstedele under udskæring: Tegn en linje på trådretningslinjen af hver mønsterdel og gå derefter frem gennem mønsteret ved hjælp af disse knapper. Slet linjerne efterhånden, som du kommer igennem for at holde styr på hvilke dele, der stadig mangler at blive skåret ud."
},
"description": "Linjeværktøjet kan bruges til at måle afstande, lave linjer, rotere, vende og flytte PDF'en. Klik på linjeværktøjsknappen, klik derefter på PDF'en og træk for at tegne en linje. Når der er tegnet en linje, vil der dukke en menu op i bunden af skærmen med følgende funktioner:"
},
"tools": "Værktøjer",
"overlayOptions": {
"title": "Overlay-funktioner",
"border": {
"title": "Kant",
"description": "Viser kanten på kalibreringsgitteret."
},
"grid": {
"title": "Gitter",
"description": "Viser kalibreringsgitteret."
},
"paper": {
"title": "Papirark",
"description": "Viser et rektangel på størrelse med et papirark (Letter eller A4) for at kontrollere kalibreringen."
},
"flipLines": {
"title": "Vende-linjer",
"description": "Viser linjer, der hjælper med at folde mønstre ud. Få til fold-linjen på mønsterdelen til at passe med vende-linjen og tryk på \"vend vandret\" eller \"vend lodret\" for at spejle mønsterdelen."
},
"flippedPattern": {
"title": "Vrangside/bagside",
"description": "Viser prikker, når mønsteret vrangside/bagside vises."
},
"description": "Overlay-funktioner kan bruges til at kontrollere kalibreringen under visning af mønster og hjælpe undervejs med udskæringen. Følgende funktioner er tilgængelige:"
}
},
"Header": {
"projecting": "Visning af mønster",
"calibrating": "Kalibrering",
"openPDF": "Åbn PDF",
"height": "H:",
"width": "B:",
"project": "Vis mønster",
"calibrate": "Kalibrér",
"delete": "Nulstil kalibrering",
"gridOn": "Vis gitter",
"gridOff": "Skjul gitter",
"invertColor": "Vis farver omvendt",
"invertColorOff": "Vis farver normalt",
"flipVertical": "Vend lodret (V)",
"flipVerticalOff": "Vend lodret til normal",
"flipHorizontal": "Vend vandret (H)",
"flipHorizontalOff": "Vend vandret til normal",
"fourCornersOn": "Fremhæv alle hjørner",
"fourCornersOff": "Fremhæv kun valgt hjørne",
"rotate90": "Rotér 90 grader med uret (R)",
"arrowBack": "Gå en side tilbage",
"arrowForward": "Gå en side frem",
"info": "Infoside",
"layersOn": "Vis lag-menu",
"layersOff": "Skjul lag-menu",
"recenter": "Centrér og nulstil PDF'en (C)",
"fullscreen": "Gå til fuld skærm",
"fullscreenExit": "Forlad fuld skærm",
"lineWeight": "Linjetykkelse",
"stitchMenuShow": "Vis samlemenu",
"stitchMenuHide": "Skjult samlemenu",
"overlayModeBorder": "Kant",
"overlayModeOff": "Skjul",
"overlayModeGrid": "Gitter",
"overlayMode": "Type af overlay",
"overlayModePaper": "A4-ark",
"measureOn": "Vis opmåling",
"measureOff": "Skjul opmåling",
"menuShow": "Vis menu",
"menuHide": "Skjul menu",
"measure": "Linjeværktøj (L)",
"showMovement": "Vis flytte-værktøj",
"overlayOptions": "Overlay-funktioner",
"overlayOptionBorder": "Kant",
"overlayOptionDisabled": "Deaktiveret",
"overlayOptionGrid": "Gitter",
"overlayOptionPaper": "Papirark",
"overlayOptionFliplines": "Vende-linjer",
"calibrationAlertContinue": "Størrelsen på vinduet kan ikke ændres efter kalibrering. For at vise mønster i fuld skærm skal du kalibrere i fuld skærm. Hvis du vil ændre vinduets størrelse, skal du kalibrere igen.",
"ok": "OK",
"hideMovement": "Skjul flytte-værktøj",
"checkCalibration": "Tjek kalibrering",
"overlayOptionFlippedPattern": "Vrangside/bagside",
"calibrationAlertTitle": "Kalibreringsadvarsel",
"calibrationAlert": "Størrelsen på vinduet har ændret sig siden mønstervisningen begyndte, tryk på \"fuld skærm\"-knappen for at rette problemet.",
"continue": "Gem størrelse på vindue og fortsæt",
"flipCenterOn": "Vend hen over midten",
"flipCenterOff": "Vend hen over midten slået fra",
"magnify": "Forstør (M)",
"zoomOut": "Zoom ud (Z)",
"stitchMenuDisabled": "Åbn PDF for at bruge samlemenu"
},
"StitchMenu": {
"columnCount": "Kolonner",
"pageRange": "Saml sider",
"horizontal": "Vandret",
"vertical": "Lodret"
},
"MeasureCanvas": {
"rotateToHorizontal": "Rotér til vandret",
"translate": "Forskyd PDF med linjens længde",
"deleteLine": "Slet linje",
"flipAlong": "Vend langs linjen",
"rotateAndCenterPrevious": "Bring forrige linje til center",
"rotateAndCenterNext": "Bring næste linje til center",
"line": "linje",
"lines": "linjer"
},
"LayerMenu": {
"layersOn": "Vis lagmenu",
"layersOff": "Skjul lagmenu",
"noLayers": "Ingen lag i PDF",
"showAll": "Vis alle",
"hideAll": "Skjul all",
"title": "Lag"
},
"MovementPad": {
"up": "Op",
"left": "Højre",
"right": "Venstre",
"next": "Næste hjørne",
"down": "Ned"
},
"InstallButton": {
"ok": "OK",
"title": "Installér app",
"description": "For bedste brugeroplevelse, installér Pattern Projector på Google Chrome.",
"descriptionIOS": "Download app'en ved at trykke på share-knappen i browsermenuen og derefter Add to Home Screen /Tilføj til hjemmeskærm.",
"descriptionAndroid": "For bedre brugeroplevelse på Android, brug Firefox. Download app'en ved at trykke på more-knappen i browsermenuen og derefter Add to Home Screen/Tilføj til hjemmeskærm."
},
"OverlayCanvas": {
"zoomedOut": "klik på PDF'en for at zoome ind",
"magnifying": "klik på PDF'en for at stoppe med forstørre"
},
"SaveMenu": {
"save": "Eksportér PDF",
"saveTooltip": "Eksportér PDF med samlede sider og valgte lag",
"encryptedPDF": "Denne PDF er krypteret. Indtast venligst password for at gemme den samlede PDF."
}
}
================================================
FILE: messages/de.json
================================================
{
"HomePage": {
"beta": "beta",
"pdfstitcherHref": "https://www.pdfstitcher.org/",
"youTubeSrc": "https://www.youtube.com/embed/m9RTLznEfRM?si=ELCJUrF4bx0OKJMf",
"github": "Siehe Quellcode",
"calibrate": "Starte Kalibrierung",
"choose-language": "Sprache",
"welcome": {
"title": "Willkommen bei Pattern Projector!",
"description": "Pattern Projector ist eine kostenlose und quelloffene Webanwendung, die Beamer für Schnittmuster schnell kalibriert. Sie bietet außerdem Werkzeuge zum Zusammenkleben von mehrseitigen Schnittmustern, zum Ändern der Linienstärke, zum Farbumkehren, zum Drehen und Spiegeln von Mustern und mehr. Die neuesten Updates finden Sie im Changelog."
},
"requirements": {
"title": "Was du benötigst",
"projector": "Beamer: mindestens 720p empfohlen",
"mat": "Eine Schneidematte mit Rasterlinien (optional)",
"mount": "Ständer oder Wand/Regal/Tischhalterung für den Beamer",
"computer": "Computer oder Tablet um es mit dem Beamer zu verbinden",
"pattern": "Ein PDF-Schnittmuster"
},
"setup": {
"title": "Setup",
"place": "Stellen/Hängen Sie den Beamer über der Schneidematte auf, so dass er direkt auf die Schneidematte zeigt. Wenn Sie keine Schneidematte haben, können Sie mit Papier oder Kreppband ein Raster/Rechteck auf einem Tisch oder dem Boden markieren.",
"connect": "Schließe deinen Computer oder Tablet an den Beamer an und dupliziere oder erweitere deine Anzeige.",
"focus": "Stelle den Focus bei deinem Beamer ein, bis der Text in der Mitte der Projektion scharf ist. Wenn du das Bild nicht scharf bekommst, versichere dich, dass die Entfernung zwischen Beamer und Schneidematte der Herstellerangabe entspricht.",
"keystone": "Wenn dein Beamer Keystones hat, dann versuche es so nah wie möglich am vorgesehenen Rechteck auszurichten."
},
"calibration": {
"title": "Kalibrierung",
"start": "Klicke auf “Starte Kalibrierung.”",
"input": "Füge die Weite und Höhe deiner Schneidematte ein.",
"fullscreen": "Gehe auf Vollbildmodus, indem du auf das Ikon klickst ",
"drag": "Ziehe die Ecken des Rasters auf die Ecken deiner Schneidematte. Mit Blick auf die Matte, justiere die Ecken am PC oder Tablet. Justiere es solange, bis die Ecken und Linien mit deiner Schneidematte übereinstimmen.",
"size": "Sie müssen nicht die gesamte Matte kalibrieren. Wählen Sie stattdessen den größten Bereich, in den das Kalibrierungsgitter passt, und geben Sie die Breite und Höhe entsprechend der Breite und Höhe des Gitters ein.",
"project": "Wenn das projizierte Gitter mit deiner Schneidematte übereinstimmt, klicke auf \"Beamen\"."
},
"project": {
"title": "Projiziere ein Schnittmuster",
"open": "Klicke auf um das PDF-Dokument zu öffnen.",
"move": "Bewege die PDF durch Klicken und Ziehen über den Bildschirm.",
"cut": "Schneide die Schnittteile an der Projektion aus.",
"tools": "Im Projektionsmodus sind verschiedene Tools enthalten. Manche funktionieren durch Tastatur-Kurzbefehl, diese sind in Klammern angegeben:",
"showGrid": {
"title": "Ein-/Ausblenden des Rasters",
"description": "Beim Beamen kann das abgeschwächte Raster ein-/ausgeblendet, um zu sehen, ob die Kalibrierung korrekt ist."
},
"invert": {
"title": "Farbumkehrung",
"description": "Bei der Projektion ist es normalerweise einfacher, grüne oder weiße Linien auf schwarzem Grund zu sehen. Klicken/Tippen Sie einmal für grüne Linien, zweimal für weiße Linien und dreimal, um zu schwarzen Linien zurückzukehren."
},
"flip": {
"title": "Vertikal (V) oder Horizontal (H) spiegeln",
"description": "Hilfreich um Stoffbruch oder gegengleiche Schnitteile zu spiegeln."
},
"rotate": {
"title": "Drehen (R)",
"description": "Dreht das Schnittmuster um 90 Grad."
},
"recenter": {
"title": "PDF zentrieren (C)",
"description": "Zentriert das Schnittmuster auf der Projektionsfläche und setzt Drehung/Spiegelung zurück."
},
"fullscreen": {
"title": "Vollbild",
"description": "Es ist generell einfacher, die Software im Vollbildmodus zu verwenden."
},
"measure": {
"description": "Markieren Sie Linien in der PDF-Datei zum Drehen, Spiegeln, Verschieben, Messen oder Markieren.",
"title": "Werkzeug zum Markieren, Messen und Transformieren starten"
},
"showMenu": {
"title": "Zeige/Verstecke Menü",
"description": "Das obere Menü ein- oder ausblenden."
},
"moveTool": {
"title": "Werkzeug zum Verschieben ein- oder ausblenden",
"description": "Das Verschiebewerkzeug verfügt über vier Pfeiltasten zum Verschieben der Ecken/Kanten des Kalibrierungsgitters. In der Mitte befindet sich außerdem eine Schaltfläche \"Weiter\", mit der Sie zur nächsten Ecke/Kante wechseln können."
},
"overlayOptions": {
"title": "Überlagerungsoptionen ein- oder ausblenden",
"description": "Die Overlays Raster, Rand, Papierbogen und Flip-Linien können ein- oder ausgeblendet werden. Das Raster entspricht dem Kalibrierungsraster, der Rand ist die Umrandung des Kalibrierungsrasters, das Papierblatt ist ein Rechteck in der Größe eines DIN-A4-Blattes, und die Umkehrlinien sind Linien, die bei der Spiegelung von Mustern helfen."
},
"lineWeight": {
"title": "Liniendicke",
"description": "Ändern Sie die Dicke der Linien im PDF-Muster."
},
"layers": {
"title": "Ebenen ein- oder ausblenden",
"description": "Ebenen in der PDF-Datei ein- oder ausblenden."
},
"stitch": {
"title": "Stichmenü anzeigen oder verbergen",
"description": "Blenden Sie das Stichmenü ein oder aus, mit dem Sie mehrseitige PDF-Designs zusammenheften können."
},
"magnify": {
"title": "Vergrößern (M)",
"description": "Klicke (oder tippe) auf das PDF, um es zu vergrößern. Klicke (oder tippe) erneut, um das Vergrößerung zu beenden."
},
"scale": {
"title": "Anzeigen oder Verstecken des Skalierungsmenüs"
},
"zoomOut": {
"description": "Herauszoomen, um das vollständige PDF anzuzeigen. Auf das PDF klicken, um zur gewählten Stelle zu Zoomen."
}
},
"faq": {
"title": "FAQ",
"wrongSizePdf": {
"question": "Wird die PDF zu groß oder zu klein projiziert?",
"answer": "Die Kalibrierung hat keinen Zoom, da der Maßstab der Projektion von den Größenangaben in der PDF-Datei stammt. Stellen Sie sicher, dass das Schnittmuster den richtigen Maßstab hat. Eine gute Möglichkeit um zu überprüfen, ob der Maßstab des Schnittmusters stimmt, ist die Projektion eines ein Zoll oder ein Zentimeter Rasters, um zu sehen, ob es mit der Schneidematte übereinstimmt."
},
"saveAsApp": {
"question": "Möchtest du Pattern Projector als App speichern?",
"answer": "Pattern Projector ist eine Progressive Web App (PWA), somit kann es als APP gespeichert werden. Auf dem PC kannst du es installieren, indem du Chrome oder Edge verwendet. Auf dem Tablet, öffne das Menü und füge diese Seite auf dem Hauptbildschirm hinzu."
},
"annotationSupport": {
"question": "Hast du einen Support für weitere Anmerkungen?",
"answer": "Noch nicht, aber es ist in einem der kommenden Updates geplant."
},
"chromecastSupport": {
"question": "Unterstützt du Chromecast?",
"answer": "Es ist möglich die Webseite mit Chrome zu benutzen, allerdings kann es zu Ausfällen in der Verbindung kommen. Dies kann frustierend sein, gerade beim Kalibrieren. Eine bessere Unterstützung für Chromecast ist für ein kommendes Update geplant."
},
"mobileSupport": {
"question": "Unterstützt du Tablets oder Handys?",
"answer": "Es ist möglich die Webseite auf dem Handy zu öffnen und zu benutzen, allerdings wird die begrenzte Bildschirmgröße die Benutzung erschweren. Besserer Support dafür ist geplant und wird in kommenden Updates umgesetzt."
}
},
"resources": {
"title": "Zusätzliche Informationen",
"links": {
"projectorsForSewing": {
"title": "Nähen mit Beamer",
"link": "https://www.facebook.com/groups/naehenmitbeamer/"
},
"onePageGuide": {
"title": "Einseitige Anleitung zum Nähen mit Beamer",
"link": "https://bit.ly/onepageguidetoprojectorsewing"
}
}
},
"contribute": {
"title": "Unterstütze dieses Projekt",
"donation": "Sofern du die Entwicklung dieses Projekts unterstützen möchtest, kannst du mich gerne hier kaufe mir einen Kaffee unterstützen oder mit PayPal!",
"develop": "Hilfe um neue Featuers zu implementieren oder Fehler zu beheben auf GitHub.",
"translate": "Übersetzung dieses Projekts in mehrere Sprachen mit Weblate.",
"feedback": "Feedback und Verbesserungsvorschläge sind herzlich Willkommen!"
},
"contact": "Kontakt",
"lineTool": {
"title": "Linien-Werkzeug",
"rotate": {
"description": "Dreht das PDF, so dass die ausgewählte Linie horizontal und mittig auf der Matte liegt.",
"title": "Zentrieren"
},
"delete": {
"title": "Linie löschen",
"description": "Ausgewählte Linie löschen."
},
"previousNext": {
"title": "Vorherige/nächste Linie zentrieren"
},
"move": {
"title": "Verschieben des PDF um die Länge der Linie",
"description": "Verschiebt das PDF um die Länge der Linie."
},
"flip": {
"description": "Spiegelt das PDF entlang der Linie.",
"title": "Spiegeln entlang der Linie"
}
},
"tools": "Werkzeuge"
},
"Header": {
"projecting": "Beamen",
"calibrating": "Kalibrierung",
"openPDF": "Öffne PDF",
"height": "H:",
"width": "B:",
"project": "Beamen",
"calibrate": "Kalibrieren",
"delete": "Kalibrierung zurücksetzen",
"gridOn": "Raster ein",
"gridOff": "Raster aus",
"invertColor": "Farbumkehrung ein",
"invertColorOff": "Farbumkehrung aus",
"flipVertical": "Spiegel vertikal (V)",
"flipVerticalOff": "Spiegel vertikal aus",
"flipHorizontal": "Spiegel horizontal (h)",
"flipHorizontalOff": "Spiegel horizontal aus",
"fourCornersOn": "Eckpunkte einblenden",
"fourCornersOff": "Eckpunkte ausblenden",
"rotate90": "Drehe 90 Grad im Uhrzeigersinn (r)",
"arrowBack": "Zurück 1 Seite",
"arrowForward": "Vorwärts 1 Seite",
"info": "Info Seite",
"layersOn": "Blende Ebenenmenü ein",
"layersOff": "Blende Ebenenmenü aus",
"recenter": "PDF zentrieren (c)",
"fullscreen": "Vollbild ein",
"fullscreenExit": "Vollbild aus",
"lineWeight": "Linienstärke",
"stitchMenuShow": "Zeige Linienmenü",
"stitchMenuHide": "Verstecke Linienmenü",
"overlayModeOff": "Aus",
"overlayModeGrid": "Raster",
"overlayModePaper": "Papierbogen",
"measureOff": "Messung beenden",
"overlayMode": "Overlay-Modus",
"overlayModeBorder": "Umrandung",
"menuShow": "Zeige Menü",
"menuHide": "Verstecke Menü",
"measureOn": "Messung beginnen",
"overlayOptions": "Overlay Optionen",
"overlayOptionBorder": "Ränder",
"overlayOptionDisabled": "Deaktiviert",
"overlayOptionGrid": "Raster",
"overlayOptionPaper": "Papierseite",
"overlayOptionFliplines": "Drehe Linien",
"flipCenterOn": "Über die Mitte drehen",
"measure": "Markieren, messen und transformieren (m)",
"ok": "Ok",
"flipCenterOff": "Nicht über die Mitte drehen",
"calibrationAlertTitle": "Kalibrierung Warnung",
"calibrationAlert": "Die Fenstergröße hat sich seit Beginn der Projektion geändert. Drücken Sie die Vollbildtaste oder kalibrieren Sie neu, um dieses Problem zu beheben.",
"calibrationAlertContinue": "Die Fenstergröße kann nach der Kalibrierung nicht mehr geändert werden. Um im Vollbildmodus zu projizieren, kalibrieren Sie im Vollbildmodus. Wenn Sie die Fenstergröße ändern möchten, kalibrieren Sie erneut.",
"continue": "Fenstergröße speichern und fortfahren",
"checkCalibration": "Kalibrierung prüfen",
"showMovement": "Werkzeug zum Verschieben anzeigen",
"hideMovement": "Werkzeug zum Verschieben ausblenden"
},
"StitchMenu": {
"columnCount": "Spalten",
"pageRange": "Stichseiten",
"horizontal": "Horizontal",
"vertical": "Vertikal"
},
"MeasureCanvas": {
"rotateToHorizontal": "Rotiere horizontal",
"deleteLine": "Lösche Linie",
"flipAlong": "Drehe an der Linie entlang",
"translate": "PDF nach Zeilenlänge verschieben"
},
"LayerMenu": {
"layersOn": "Zeige Ebenenmenü",
"layersOff": "Verstecke Ebenenmenü",
"noLayers": "Keine Ebenen in der PDF",
"title": "Ebenen",
"showAll": "Zeige alle",
"hideAll": "Verstecke alle"
},
"MovementPad": {
"left": "Links",
"right": "Rechts",
"down": "Runter",
"next": "Nächste Ecke",
"up": "Hoch"
}
}
================================================
FILE: messages/en.json
================================================
{
"HomePage": {
"youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr",
"github": "See source code",
"calibrate": "Start Calibrating",
"choose-language": "Language",
"welcome": {
"title": "Welcome to Pattern Projector!",
"description": "Pattern projector is a free and open source web app that quickly calibrates projectors for sewing patterns. It also has tools for stitching together multiple page patterns, changing line thickness, inverting colors, flipping/rotating patterns, and more. To read about the latest updates, check out the changelog."
},
"requirements": {
"title": "What You’ll Need",
"projector": "Projector: at least 720p recommended",
"mat": "A cutting mat with grid lines (optional)",
"mount": "Tripod or wall/shelf/table mount for projector",
"computer": "Computer or tablet to connect to the projector",
"pattern": "A PDF sewing pattern"
},
"setup": {
"title": "Setup",
"place": "Place the projector above the cutting mat, pointing directly at the cutting mat. If you do not have a cutting mat, you can use paper or masking tape to mark out a grid/rectangle on a table or floor.",
"connect": "Connect your computer or tablet to the projector and either mirror or extend the display so that image from computer or tablet is shown on the projector.",
"focus": "Adjust the focus on the projector, until text is crisp in the centre of the projection. If you cannot get a clear image, ensure the distance between the projector and cutting mat is within the functional range recommended by the manufacturer.",
"keystone": "If your projector has a keystone, adjust it so that projection is close to rectangular and focus near the edges improves."
},
"calibration": {
"title": "Calibration",
"start": "Click (or tap) “Start Calibrating.”",
"fullscreen": "Enter full screen mode by clicking (or tapping) ",
"drag": "Drag the corners of the grid to align with your mat. With your eyes on the mat, adjust the corners on your computer or tablet. Adjust the placement of the corners until the projected grid matches your mat's grid.",
"size": "You don't have to calibrate using your entire mat, instead choose the largest area you can fit the calibration grid in and input the width and height to match the width and height of the grid.",
"project": "When the projected grid is aligned with your mat, click (or tap) “Project.”"
},
"project": {
"title": "Projecting a Pattern",
"open": "Click (or tap) to open a pattern.",
"move": "Move the PDF by clicking and dragging it around the screen.",
"cut": "Cut along the projected design.",
"tools": "In projection mode there are several tools provided. Some have keyboard shortcuts, which are denoted in parentheses.",
"fullscreen": {
"title": "Full screen",
"description": "It's generally easier to use the software in full screen mode."
},
"showMenu": {
"title": "Show/hide menu",
"description": "Show or hide the top menu."
},
"invert": {
"title": "Invert Colors",
"description": "When projecting, it's usually easier to see green or white lines on black. Click/tap once for green lines, twice for white lines, and three times to return to black lines."
},
"moveTool": {
"title": "Show or hide move tool",
"description": "The move tool has four arrow buttons to move the calibration grid corners/edges. It also has a next button in the middle for switching to the next corner/edge."
},
"overlayOptions": {
"title": "Show or hide overlay options",
"description": "Grid, border, paper sheet, flip lines, or wrong side overlays can be shown on the PDF to help with calibration and cutting. For more information, see the overlay options section."
},
"lineWeight": {
"title": "Line weight",
"description": "Change the thickness of the lines in the PDF pattern."
},
"flip": {
"title": "Flip Vertical (V) or Horizontal (H)",
"description": "Helpful when mirroring patterns in half."
},
"rotate": {
"title": "Rotate (R)",
"description": "Change the orientation of the pattern by 90 degrees."
},
"recenter": {
"title": "Center and Reset (C)",
"description": "Centers the pattern on the cutting mat and resets the rotation/flipping."
},
"magnify": {
"title": "Magnify (M)",
"description": "Click (or tap) on the PDF to magnify it. Click (or tap) again to stop magnifying."
},
"zoomOut": {
"title": "Zoom out (Z)",
"description": "Zoom out to see the whole PDF. Click (or tap) on the PDF to zoom back in at selected location."
},
"layers": {
"title": "Show or hide layers",
"description": "Show or hide layers in the PDF."
},
"stitch": {
"title": "Show or hide stitch menu",
"description": "Show or hide the stitch menu, which allows you to stitch together multiple page PDF patterns."
},
"scale": {
"title": "Show or hide the scale menu",
"description": "Change size of the pattern by entering a multiplier: between 0-1 will make the pattern smaller and greater than 1 will make the pattern larger."
},
"measure": {
"title": "Line tool (L)",
"description": "Mark lines on the PDF for rotating, flipping, moving, measuring, or marking. For a detailed description of the line tool, see the line tool section."
}
},
"tools": "Tools",
"lineTool": {
"title": "Line Tool",
"description": "The line tool can be used to measure distances, mark lines on, rotate, flip, and move the PDF. Click (or tap) the line tool button, then click (or tap) on the PDF and drag to draw a line. Once a line is drawn, a menu will appear on the bottom of the screen with the following options:",
"delete": {
"title": "Delete line",
"description": "Deletes the selected line."
},
"rotate": {
"title": "Align to center",
"description": "Rotates the PDF so the selected line is horizontal and centered on your mat.",
"use": "For aligning the grain of a pattern piece with the grain of your fabric: draw a line on the grain line of the pattern piece, then click (or tap) the align to center button."
},
"previousNext": {
"title": "Bring previous/next line to center",
"description": "Moves the PDF so that the previous/next line drawn is horizontal and centered on your mat.",
"use": "For moving between pattern pieces when cutting: draw a line on the grain line of each pattern piece, then move through the pattern pieces using these buttons. Delete lines as you move through the pattern pieces to keep track of which pieces are left to be cut."
},
"flip": {
"title": "Flip along line",
"description": "Flips the PDF along the line.",
"use": "For unfolding pattern pieces: draw a line on the fold line of a pattern piece, cut the piece up to the fold line, click (or tap) the flip along line button, and then cut the remainder of the piece."
},
"move": {
"title": "Move PDF by line length",
"description": "Moves the PDF by the length of the line.",
"use": "For lengthening/shortening pattern pieces: draw a line perpendicular the lengthen/shorten line of a pattern piece, cut to the lengthen/shorten line, then click (or tap) the move by line length button, and then continue cutting the piece. The direction of the line drawn will depend on whether you are lengthening or shortening the pattern piece."
}
},
"overlayOptions": {
"title": "Overlay Options",
"description": "The overlay options can be used to verify calibration while projecting and help with cutting patterns. The following options are available:",
"border": {
"title": "Border",
"description": "Shows the border of the calibration grid."
},
"grid": {
"title": "Grid",
"description": "Shows the calibration grid."
},
"paper": {
"title": "Paper Sheet",
"description": "Shows a rectangle the size of a Letter or A4 piece of paper to verify calibration."
},
"flipLines": {
"title": "Flip Lines",
"description": "Shows lines that help with unfolding patterns. Line the fold line of the pattern up with the flip line and press flip horizontally or vertically to mirror the piece."
},
"flippedPattern": {
"title": "Wrong side",
"description": "Shows dots when the wrong side of the pattern is projected."
}
},
"faq": {
"title": "FAQ",
"wrongSizePdf": {
"question": "Is your PDF projecting too small or large?",
"answer": "The Pattern Projector calibration tool has no zoom because the scale of the projection comes from the size information in the PDF. Patterns from designers usually have the correct scale, so a change in scale could have been introduced when opening the pattern in Affinity Designer or Inkscape."
},
"saveAsApp": {
"answer": "Pattern Projector is a Progressive Web App (PWA) so it can be saved as an app. On a Desktop, you can install it using Chrome or Edge. On a tablet, open the Share menu and tap Add to Home Screen."
},
"mobileSupport": {
"question": "Do you have support mobile phones and tablets?",
"answer": "While possible to visit and use the webpage on a smart phone, the limited screen size makes it difficult to use."
}
},
"resources": {
"title": "Additional Resources",
"links": {
"projectorsForSewing": {
"title": "Projectors for Sewing Facebook Group",
"link": "https://www.facebook.com/groups/481078582801085"
},
"onePageGuide": {
"title": "One Page Guide to Projector Sewing",
"link": "https://bit.ly/onepageguidetoprojectorsewing"
}
}
},
"contribute": {
"title": "Contribute to the project",
"donation": "If Pattern Projector has saved you money at the copyshop or freed you from a PDF viewer subscription, please consider buying me a coffee or supporting via PayPal!",
"develop": "Help implement features and fix issues on GitHub.",
"translate": "Translate this tool into more languages using Weblate.",
"feedback": "Feedback and feature requests are welcome!"
},
"contact": "Contact"
},
"InstallButton": {
"title": "Install App",
"description": "For the best experience, install Pattern Projector using Google Chrome. In Chrome, download the app by tapping in the address bar.",
"descriptionAndroid": "For the best experience on Android, use Firefox. Download the app by tapping the more button in the browser menu and then Add to Home Screen.",
"descriptionIOS": "Download the app by tapping the share button in the browser menu and then Add to Home Screen .",
"ok": "OK"
},
"Mail": {
"title": "You've got mail",
"donate": "Donate"
},
"Troubleshooting": {
"notMatching": "Lines not matching your mat?",
"title": "Calibration Troubleshooting",
"dragCorners": {
"title": "How to calibrate",
"description": "Drag the corners of the grid to at least a 24x16 inch rectangle on your mat. The grid should be as large as possible but does not need to cover the entire mat.",
"caption": "A correctly calibrated grid."
},
"inputMeasurement": "Use a measuring tape to measure the width and height of the grid on your mat. Input these measurements into the width and height fields.",
"offByOne": {
"title": "Lines are straight but don’t align?",
"description": "If the projected lines are straight but don’t align with your mat, your width or height might be off by 1 inch/centimeter. Double-check your measurements with a measuring tape. Mat measurements can be confusing or inaccurate, so checking with a measuring tape is important.",
"caption": "The wrong measurements are input (23 x 17) instead of (24 x 18). The mat only shows numbers up to 23 and 17 so it is easy to confuse the measurements. Make sure you measure with a measuring tape!"
},
"unevenSurface": {
"title": "Curved lines?",
"description": "If the projected lines look curved, your mat or surface below the mat may not be flat. Try to flatten the mat but putting card stock under the low points or move to a different cutting surface.",
"caption": "This mat has a ruler under it causing the lines to curve in the center. A slight curve is ok but a large curve will cause issues."
},
"dimensionsSwapped": {
"title": "Rectangles instead of squares?",
"description": "If the projected grid looks like rectangles instead of squares, the width and height may be swapped. Simply swap the values in the fields.",
"caption": "The width and height are in the wrong boxes. The dimension should read 'W: 24' and 'H: 18'."
},
"close": "Close"
},
"General": {
"close": "Close",
"error": "Error"
},
"Header": {
"openPDF": "Open",
"height": "H:",
"width": "W:",
"project": "Project",
"calibrate": "Calibrate",
"delete": "Reset calibration",
"gridOn": "Grid on",
"gridOff": "Grid off",
"overlayOptions": "Overlay Options",
"overlayOptionBorder": "Border",
"overlayOptionDisabled": "Disabled",
"overlayOptionGrid": "Grid",
"overlayOptionPaper": "Paper Sheet",
"overlayOptionFliplines": "Flip Lines",
"overlayOptionFlippedPattern": "Wrong side",
"invertColor": "Invert colors",
"invertColorOff": "Invert colors off",
"flipVertical": "Flip vertically (V)",
"flipHorizontal": "Flip horizontally (H)",
"flipCenterOn": "Flip across center",
"flipCenterOff": "Flip across center off",
"fourCornersOn": "Highlight all corners",
"fourCornersOff": "Only highlight selected corner",
"rotate90": "Rotate 90 degrees clockwise (R)",
"arrowBack": "Back 1 page",
"arrowForward": "Forward 1 page",
"info": "Info page",
"recenter": "Center and Reset (C)",
"fullscreen": "Enter full screen",
"fullscreenExit": "Exit full screen",
"ok": "OK",
"calibrationAlertTitle": "Calibration Warning",
"calibrationAlert": "The window size has changed since projecting started, press the full screen button or recalibrate to fix this issue.",
"calibrationAlertContinue": "The window size can't change after calibration. To project in full screen, calibrate in full screen. If you want to change the window size, recalibrate.",
"continue": "Save window size and continue",
"checkCalibration": "Check calibration",
"menuShow": "Show menu",
"menuHide": "Hide menu",
"lineWeight": "Line weight",
"stitchMenuShow": "Show stitch menu",
"stitchMenuHide": "Hide stitch menu",
"stitchMenuDisabled": "Open PDF to use stitch menu",
"measure": "Line tool (L)",
"showMovement": "Show move tool",
"hideMovement": "Hide move tool",
"magnify": "Magnify (M)",
"zoomOut": "Zoom out (Z)",
"mail": "Mail from Courtney",
"invalidCalibration": "The calibration grid is not valid. Press 'Reset calibration' to try again."
},
"ScaleMenu": {
"scale": "Scale Pattern",
"hide": "Hide scale menu",
"show": "Show scale menu"
},
"StitchMenu": {
"columnCount": "Columns",
"rowCount": "Rows",
"pageRange": "Stitch Pages",
"horizontal": "Horizontal Overlap",
"vertical": "Vertical Overlap",
"zeros": "Add 0s for blank pages, e.g. 1-3,0,0,4"
},
"SaveMenu": {
"save": "Export PDF",
"saveTooltip": "Export PDF with stitched pages and selected layers",
"encryptedPDF": "This PDF is encrypted. Please enter the password to save the stitched PDF."
},
"LayerMenu": {
"title": "Layers",
"showAll": "Show all",
"hideAll": "Hide all",
"layersOn": "Show layer menu",
"layersOff": "Hide layer menu",
"noLayers": "No layers in pattern"
},
"MovementPad": {
"up": "Up",
"down": "Down",
"left": "Left",
"right": "Right",
"next": "Next corner"
},
"MeasureCanvas": {
"rotateToHorizontal": "Align to center",
"rotateAndCenterPrevious": "Bring previous line to center",
"rotateAndCenterNext": "Bring next line to center",
"deleteLine": "Delete line",
"flipAlong": "Flip along line",
"translate": "Move pattern by line length",
"line": "line",
"lines": "lines"
},
"OverlayCanvas": {
"zoomedOut": "click pattern to zoom in",
"magnifying": "click pattern to stop magnifying",
"scaled": "× scale"
},
"PdfViewer": {
"error": "Failed to load pattern",
"noData": "Press the \"Open\" button to load a pattern"
}
}
================================================
FILE: messages/es.json
================================================
{
"HomePage": {
"beta": "beta",
"pdfstitcherHref": "https://www.pdfstitcher.org/",
"youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr",
"github": "Ver código fuente",
"calibrate": "Empezar calibración",
"choose-language": "Idioma",
"welcome": {
"title": "¡Bienvenido a Pattern Projector!",
"description": "Pattern Projector es una app web libre y de código abierto que ayuda a los usuarios a calibrar rápidamente su proyectores para costura. También cuenta con una herramienta para montar patrones que estén cortados en varias páginas, y que permite cambiar el grosor de las líneas, invertir colores, girar o voltear patrones y más. Para leer acerca de las últimas actualizaciones puedes consultar el registro de cambios."
},
"requirements": {
"title": "Qué necesitas",
"projector": "Proyector: se recomienda uno de 720p mínimo",
"mat": "Una base de corte con cuadrícula (opcional)",
"mount": "Trípode o soporte de pared/estante/mesa para el proyector",
"computer": "Ordenador o tableta conectada al proyector",
"pattern": "Un patrón de costura en PDF"
},
"setup": {
"title": "Configuración",
"place": "Coloca el proyector sobre la base de corte apuntándola directamente. Si no tienes base de corte puedes utilizar papel o cinta para marcar un rectángulo o cuadrícula en tu mesa o en el suelo.",
"connect": "Conecta el ordenador o la tableta al proyector y duplica o extiende la pantalla para que la imagen del ordenador o la tableta se muestre en el proyector.",
"focus": "Ajusta el enfoque en el proyector hasta que el texto quede nítido en el centro de la proyección. Si no consigues obtener una imagen clara, asegúrate de que la distancia entre el proyector y el tapete de corte esté dentro del rango funcional recomendado por el fabricante.",
"keystone": "Si tu proyector tiene opción para corregir la distorsión trapezoidal (keystone), ajústala para que la proyección sea lo más rectangular posible y mejore el enfoque cerca de los bordes."
},
"calibration": {
"title": "Calibración",
"start": "Selecciona “Empezar calibración.”",
"input": "Introduzca el alto y el largo de su tapete de corte.",
"fullscreen": "Activa el modo de pantalla completa pinchando en este icono: ",
"drag": "Arrastra las esquinas de la cuadrícula para alinearlas con la base de corte. Mirando a la base de corte, ajusta las esquinas desde el ordenador o tableta. Ajusta la ubicación de las esquinas hasta que la cuadrícula proyectada coincida con la cuadrícula de la base de corte.",
"size": "No es necesario calibrar usando toda la base de corte; en su lugar, elige el área más grande en la que puedas colocar la cuadrícula de calibración y asegúrate de que los valores de alto y largo coincidan con el alto y el largo de la cuadrícula.",
"project": "Cuando la cuadricula proyectada esté alineada con la base de corte selecciona “Proyectar”."
},
"project": {
"title": "Proyectando un patrón",
"open": "Selecciona para abrir el patrón.",
"move": "Mueve el documento pinchando y arrastrándolo por la pantalla.",
"cut": "Corta el patrón proyectado.",
"tools": "En el modo de proyección se proporcionan varias herramientas. Algunos tienen atajos de teclado, que se indican entre paréntesis.",
"showGrid": {
"title": "Mostrar/Ocultar la cuadrícula",
"description": "Al proyectar, muestra/oculta una cuadrícula tenue para verificar que su calibración sea correcta."
},
"invert": {
"title": "Invertir colores",
"description": "Al proyectar, suele ser más fácil ver líneas verdes o blancas sobre fondo negro. Pincha una vez para obtener líneas verdes, dos veces para líneas blancas y tres veces para volver a líneas negras."
},
"flip": {
"title": "Girar vertical (V) u horizontalmente (H)",
"description": "Útil para reflejar patrones que sólo muestran una mitad."
},
"rotate": {
"title": "Girar (R)",
"description": "Gira la orientación del patrón 90 grados a la derecha."
},
"recenter": {
"title": "Centrar y restablecer (C)",
"description": "Centra el patrón en la base de corte y restablece la rotación/inversión."
},
"fullscreen": {
"title": "Pantalla completa",
"description": "Generalmente es más fácil usar el programa en modo de pantalla completa."
},
"stitch": {
"description": "Muestra u oculta el menú de unión de páginas, que permite montar/fusionar patrones en PDF que estén formados por varias páginas.",
"title": "Mostrar u ocultar menú de unión de páginas"
},
"layers": {
"description": "Muestra u oculta capas del patrón PDF.",
"title": "Mostrar u ocultar capas"
},
"measure": {
"title": "Herramienta de línea (L)",
"description": "Marca las líneas en el PDF para girar, voltear, mover, medir o marcar. Para obtener una descripción detallada de la herramienta de línea, consulte la sección herramienta de línea."
},
"lineWeight": {
"title": "Grosor de línea",
"description": "Cambia el grosor de las líneas del patrón en PDF."
},
"moveTool": {
"description": "La herramienta para mover tiene 4 botones en forma de flecha para mover las esquinas o bordes de la cuadrícula a pequeños toques. También tiene un botón en el centro para cambiar a la siguiente esquina o borde.",
"title": "Mostrar u ocultar herramienta de movimiento"
},
"showMenu": {
"title": "Mostrar/ocultar menú superior",
"description": "Muestra u oculta el menú superior."
},
"overlayOptions": {
"description": "Las superposiciones de la cuadrícula, borde, hoja de papel, líneas de giro o el lado incorrecto se pueden mostrar en el PDF para ayudar con la calibración y el corte. Para obtener más información, consulta la sección opciones de superposición.",
"title": "Mostrar u ocultar líneas de referencia"
},
"magnify": {
"title": "Aumentar (M)",
"description": "Haz clic (o toca) en el PDF para ampliarlo. Haz clic (o toca) de nuevo para detener la ampliación."
},
"zoomOut": {
"title": "Alejar (Z)",
"description": "Aleja el zoom para ver todo el PDF. Haz clic (o toca) en el PDF para volver a acercarlo a la ubicación seleccionada."
},
"scale": {
"title": "Mostrar u ocultar el menú de escala",
"description": "Cambiar el tamaño del patrón introduciendo un multiplicador: entre 0-1 hará que el patrón sea más pequeño y mayor de 1 hará que el patrón sea más grande."
}
},
"faq": {
"title": "FAQ",
"wrongSizePdf": {
"question": "¿El fichero PDF se proyecta demasiado pequeño o demasiado grande?",
"answer": "La herramienta de calibración de Pattern Projector no tiene zoom porque la escala de la proyección proviene de la información de tamaño del PDF. Los patrones de los diseñadores suelen tener la escala correcta, por lo que se podría haber introducido un cambio en la escala al abrir el patrón en Affinity Designer o Inkscape."
},
"saveAsApp": {
"question": "¿Quieres guardar Pattern Projector como una aplicación (APP)?",
"answer": "Pattern Projector es una aplicación web progresiva (PWA), por lo que se puede guardar como una aplicación. Puedes instalarlo en el escritorio usando Chrome o Edge.En una tableta, abrir el menú Compartir y seleccionar Agregar a la pantalla de inicio."
},
"annotationSupport": {
"question": "¿Existe forma de agregar anotaciones?",
"answer": "Todavía no, pero está previsto para próximas versiones."
},
"chromecastSupport": {
"question": "¿Es compatible con Chromecast?",
"answer": "Si bien es posible transmitir esta página web en Chrome, el retraso en la conexión puede resultar frustrante, especialmente durante la calibración. Está previsto un mejor soporte para Chromecast en próximas versiones."
},
"mobileSupport": {
"question": "¿Tiene soporte para teléfonos móviles y tabletas?",
"answer": "Aunque es posible visitar y utilizar la página web en un teléfono inteligente, el tamaño limitado de la pantalla dificulta el uso."
}
},
"resources": {
"title": "Recursos adicionales",
"links": {
"projectorsForSewing": {
"title": "Grupo de Facebook \"Projectors for Sewing\"",
"link": "https://www.facebook.com/groups/481078582801085"
},
"onePageGuide": {
"title": "Guía resumen de una página (en inglés)",
"link": "https://bit.ly/onepageguidetoprojectorsewing"
}
}
},
"contribute": {
"title": "Contribuye al proyecto",
"donation": "¡Si Pattern Projector le ha ahorrado dinero en la copistería o le ha liberado de la suscripción a un visor de PDF, por favor considera comprarme un café o apoyarme a través de PayPal!",
"develop": "Ayuda a implementar funciones y solucionar problemas en GitHub.",
"translate": "Traduzca esta herramienta a más idiomas usando Weblate.",
"feedback": "¡Los comentarios y solicitud de funciones son bien recibidos!"
},
"contact": "Contactar",
"tools": "Herramientas",
"lineTool": {
"title": "Herramienta línea",
"delete": {
"title": "Borrar línea",
"description": "Elimina la línea seleccionada."
},
"rotate": {
"title": "Centrar",
"use": "Para alinear la veta de una pieza de patrón con la veta de su tejido: dibuje una línea en la línea de veta de la pieza de patrón y, a continuación, haga clic (o toque) en el botón centrar.",
"description": "Gira el PDF para que la línea seleccionada quede horizontal y centrada en el tapete."
},
"flip": {
"title": "Girar a lo largo de la línea",
"description": "Girar el PDF a lo largo de la línea.",
"use": "Para desplegar las piezas del patrón: dibuje una línea en la línea de pliegue de una pieza de patrón, corte la pieza hasta la línea de pliegue, haga clic (o toque) en el botón girar a lo largo de la línea y, a continuación, corte el resto de la pieza."
},
"move": {
"title": "Mover PDF por la longitud de la línea",
"description": "Mover el PDF a lo largo de la línea.",
"use": "Para alargar/acortar las piezas del patrón: dibuje una línea perpendicular a la línea de alargar/acortar de una pieza de patrón, corte hasta la línea de alargar/acortar, luego haga clic (o toque) el botón de mover por longitud de línea, y luego continúe cortando la pieza. La dirección de la línea dibujada dependerá de si está alargando o acortando la pieza del patrón."
},
"previousNext": {
"title": "Traer la línea anterior/siguiente al centro",
"description": "Mueve el PDF para que la línea anterior/siguiente dibujada sea horizontal y esté centrada en el tapete.",
"use": "Para moverse entre las piezas del patrón al cortar: dibuje una línea en la línea de veta de cada pieza del patrón, luego muévase a través de las piezas del patrón usando estos botones. Elimine líneas a medida que avanza por las piezas del patrón para realizar un seguimiento de qué piezas quedan por cortar."
},
"description": "La herramienta línea se puede utilizar para medir distancias, marcar líneas, rotar, voltear y mover el PDF. Haga clic (o toque) el botón de la herramienta línea, luego haga clic (o toque) en el PDF y arrastre para dibujar una línea. Una vez dibujada una línea, aparecerá un menú en la parte inferior de la pantalla con las siguientes opciones:"
},
"overlayOptions": {
"title": "Líneas de referencia",
"description": "Las opciones de superposición se pueden utilizar para verificar la calibración mientras se proyecta y ayudar con los patrones de corte. Las siguientes opciones están disponibles:",
"border": {
"title": "Borde",
"description": "Muestra el borde de la cuadrícula de calibración."
},
"grid": {
"title": "Cuadrícula",
"description": "Muestra la cuadrícula de calibración."
},
"paper": {
"title": "Hoja de papel",
"description": "Muestra un rectángulo del tamaño de una carta o A4 para verificar la calibración."
},
"flipLines": {
"title": "Líneas invertidas",
"description": "Muestra líneas que ayudan a desarrollar patrones. Alinee la línea de plegado del patrón con la línea de giro y pulse girar horizontal o verticalmente para reflejar la pieza."
},
"flippedPattern": {
"title": "Lado erróneo",
"description": "Muestra puntos cuando se proyecta el lado equivocado del patrón."
}
}
},
"Header": {
"projecting": "Proyectando",
"calibrating": "Calibrando",
"openPDF": "Abrir",
"height": "A:",
"width": "L:",
"project": "Proyectar",
"calibrate": "Calibrar",
"delete": "Restablecer calibración",
"gridOn": "Cuadrícula activada",
"gridOff": "Cuadrícula desactivada",
"overlayOptions": "Líneas de referencia",
"overlayOptionBorder": "Borde",
"overlayOptionDisabled": "Deshabilitado",
"overlayOptionGrid": "Cuadrícula",
"overlayOptionPaper": "Hoja de papel",
"overlayOptionFliplines": "Líneas de volteo",
"invertColor": "Invertir colores",
"invertColorOff": "Invertir colores desactivado",
"flipVertical": "Girar verticalmente (V)",
"flipVerticalOff": "Voltear verticalmente desactivado",
"flipHorizontal": "Girar horizontalmente (H)",
"flipHorizontalOff": "Voltear horizontalmente desactivado",
"flipCenterOn": "Voltear centro",
"flipCenterOff": "Voltear centro desactivado",
"fourCornersOn": "Resaltar todas las esquinas",
"fourCornersOff": "Resaltar sólo la esquina seleccionada",
"rotate90": "Girar 90 grados en el sentido de las agujas del reloj (R)",
"arrowBack": "Retroceder 1 página",
"arrowForward": "Avanzar 1 página",
"info": "Página de inicio",
"layersOn": "Mostrar menú de capas",
"layersOff": "Ocultar menú de capas",
"recenter": "Centrar y reiniciar (C)",
"fullscreen": "Activar pantalla completa",
"fullscreenExit": "Salir de pantalla completa",
"menuShow": "Mostrar menú superior",
"menuHide": "Ocultar menú superior",
"lineWeight": "Grosor de línea",
"stitchMenuShow": "Mostrar menú de unión de páginas",
"stitchMenuHide": "Ocultar menú de unión de páginas",
"measureOn": "Empezar medición",
"measureOff": "Terminar medición",
"showMovement": "Mostrar herramienta mover",
"hideMovement": "Ocultar herramienta mover",
"fullScreenChange": "Por favor, asegúrese de que la calibración es correcta. Debe calibrar en modo de pantalla completa si desea proyectar en pantalla completa.",
"calibrationAlertContinue": "El tamaño de la ventana no puede cambiar después de la calibración. Para proyectar en pantalla completa, calibre en pantalla completa. Si desea cambiar el tamaño de la ventana, recalibre.",
"ok": "DE ACUERDO",
"calibrationAlertTitle": "Aviso de calibración",
"continue": "Guardar el tamaño de la ventana y continuar",
"checkCalibration": "Comprobar la calibración",
"calibrationAlert": "El tamaño de la ventana ha cambiado desde que comenzó la proyección, pulse el botón de pantalla completa o recalibre para solucionar este problema.",
"measure": "Herramienta de línea (L)",
"overlayOptionFlippedPattern": "Lado erróneo",
"stitchMenuDisabled": "Abra PDF para usar el menú de unión",
"magnify": "Aumentar (M)",
"zoomOut": "Alejar (Z)",
"mail": "Correo de Courtney",
"invalidCalibration": "La cuadrícula de calibración no es válida. Presiona ‘reiniciar calibración’ para intentar de nuevo."
},
"StitchMenu": {
"columnCount": "Columnas",
"pageRange": "Páginas de puntos",
"horizontal": "Solapamiento horizontal",
"vertical": "Solapamiento Vertical",
"zeros": "Añadir 0 para las páginas en blanco, por ejemplo 1-3,0,0,4",
"rowCount": "Filas"
},
"MovementPad": {
"left": "Izquierda",
"right": "Derecha",
"next": "Siguiente esquina",
"up": "Subir",
"down": "Bajar"
},
"LayerMenu": {
"title": "Capas",
"showAll": "Mostrar todo",
"hideAll": "Ocultar todo",
"layersOff": "Ocultar el menú de la capa",
"layersOn": "Mostrar menú de la capa",
"noLayers": "Sin capas en el patrón"
},
"MeasureCanvas": {
"rotateToHorizontal": "Centrar",
"deleteLine": "Borrar linea",
"flipAlong": "Girar a lo largo de la línea",
"translate": "Mover el patrón por la longitud de la línea",
"rotateAndCenterNext": "Centrar la siguiente línea",
"lines": "líneas",
"rotateAndCenterPrevious": "Centrar la línea anterior",
"line": "línea"
},
"InstallButton": {
"title": "Instalar aplicación",
"description": "Para la mejor experiencia, instalar Pattern Projector usando Google Chrome. En Chrome, descargar la aplicación pulsando en la barra de direcciones.",
"descriptionAndroid": "Para disfrutar de la mejor experiencia en Android, utiliza Firefox. Descarga la aplicación pulsando el botón más en el menú del navegador y luego Añadir a la pantalla de inicio.",
"descriptionIOS": "Descarga la aplicación tocando el botón compartir en el menú del navegador y luego Agregar a la pantalla de inicio .",
"ok": "De acuerdo"
},
"SaveMenu": {
"save": "Exportar el pdf",
"saveTooltip": "Exportar PDF con páginas unidas y capas seleccionadas",
"encryptedPDF": "Este PDF está cifrado. Ingresa la contraseña para guardar el PDF unido."
},
"OverlayCanvas": {
"zoomedOut": "Haz clic en el patrón para ampliar",
"magnifying": "Haz clic en el patrón para detener la ampliación",
"scaled": "× escala"
},
"PdfViewer": {
"error": "Error al cargar el patrón",
"noData": "Pulsa el botón \"Abrir\" para cargar un patrón"
},
"Troubleshooting": {
"title": "Solución de problemas de calibración",
"inputMeasurement": "Utilice una cinta métrica para medir el ancho y la altura de la cuadrícula de su tapete. Ingrese estas medidas en los campos de ancho y altura.",
"offByOne": {
"title": "¿Las líneas son rectas pero no se alinean?",
"description": "Si las líneas proyectadas son rectas pero no se alinean con el tapete, es posible que el ancho o la altura tengan una diferencia de 1 pulgada/centímetro. Verifique nuevamente las medidas con una cinta métrica. Las medidas del tapete pueden ser confusas o imprecisas, por lo que es importante verificarlas con una cinta métrica.",
"caption": "Se han introducido medidas incorrectas (23 x 17) en lugar de (24 x 18). El tapete solo muestra los números hasta el 23 y el 17, por lo que es fácil confundir las medidas. ¡Asegúrate de medir con una cinta métrica!"
},
"dragCorners": {
"title": "¿Cómo calibrar?",
"description": "Arrastre las esquinas de la cuadrícula hasta formar un rectángulo de al menos 24 x 16 pulgadas en el tapete. La cuadrícula debe ser lo más grande posible, pero no es necesario que cubra todo el tapete.",
"caption": "Una cuadrícula correctamente calibrada."
},
"unevenSurface": {
"title": "¿Tramos curvos?",
"description": "Si las líneas proyectadas parecen curvas, es posible que su alfombrilla o la superficie debajo de la alfombrilla no estén planas. Intente aplanar la alfombrilla colocando cartulina debajo de los puntos bajos o cambie a otra superficie de corte.",
"caption": "Esta alfombrilla tiene una regla debajo que hace que las líneas se curven en el centro. Una ligera curva está bien, pero una curva grande causará problemas."
},
"dimensionsSwapped": {
"title": "¿Rectángulos en lugar de cuadrados?",
"description": "Si la cuadrícula proyectada tiene forma de rectángulo en lugar de cuadrado, puede intercambiar la anchura y la altura. Basta con intercambiar los valores de los campos.",
"caption": "La anchura y la altura están en las casillas equivocadas. La dimensión debe ser \"An: 24\" y \"Al: 18\"."
},
"close": "Cerrar",
"notMatching": "¿Las líneas no coinciden con tu tapete?"
},
"Mail": {
"title": "¿Tienes un nuevo correo?",
"donate": "Donar"
},
"General": {
"close": "Cerrar",
"error": "Error"
},
"ScaleMenu": {
"scale": "Patrón de escala",
"hide": "Ocultar menú de escala",
"show": "Mostrar menú de escala"
}
}
================================================
FILE: messages/fr.json
================================================
{
"HomePage": {
"beta": "bêta",
"pdfstitcherHref": "https://www.pdfstitcher.org/",
"github": "Voir le code source",
"calibrate": "Démarrer la calibration",
"welcome": {
"title": "Bienvenue dans Pattern Projector !",
"description": "Pattern projector est une application web libre et open source qui aide les utilisateurs à calibrer rapidement les projecteurs pour les patrons de couture. Ce projet est actuellement en bêta donc attendez-vous à de grands changements et de nouvelles fonctionnalités tant que nous itérons. Pour en savoir plus sur les dernières mise à jour, allez voir le changelog."
},
"choose-language": "Langue",
"requirements": {
"computer": "Ordinateur ou tablette pour connecter au projecteur",
"pattern": "Un patron de couture PDF",
"projector": "Projecteur : au moins 720p sont recommandés",
"title": "Ce dont vous aurez besoin",
"mat": "Un tapis de découpe avec un quadrillage (optionnel)",
"mount": "Trépied ou support mur/étagère/table pour le projecteur"
},
"setup": {
"title": "Installation",
"connect": "Connectez votre ordinateur ou tablette au projecteur et soit dupliquez soit étendez votre écran pour que l'image de l'ordinateur ou de la tablette s'affiche sur le projecteur.",
"place": "Placez le projecteur au-dessus du tapis de découpe, en l'orientant directement vers le tapis de découpe. Si vous n'avez pas de tapis de découpe, vous pouvez utiliser du papier ou du masking tape pour marquer un quadrillage/rectangle sur la table ou le sol.",
"keystone": "Si votre projecteur a une correction trapézoïdale (keystone), ajustez-la pour que la projection soit autant rectangulaire que possible et que le focus proche des côtés s'améliore.",
"focus": "Ajustez le focus sur le projecteur jusqu'à ce que le texte soit net au centre de la projection. Si vous n'arrivez pas à obtenir une image clair, assurez-vous que la distance entre le projecteur et le tapis de découpe est dans l'intervalle de fonctionnement recommandé par le fabricant."
},
"calibration": {
"input": "Saisissez la largeur et la hauteur de votre tapis de découpe.",
"title": "Calibration",
"start": "Cliquez (ou appuyez) sur \"Démarrer la calibration.\"",
"fullscreen": "Passez en mode plein écran en cliquant (ou en appuyant) sur ",
"drag": "Déplacez les coins du quadrillage pour les aligner avec votre tapis. Avec vos yeux sur le tapis, ajustez les coins sur votre ordinateur ou tablette. Ajustez le placement des coins jusqu'à ce que le quadrillage projeté corresponde au quadrillage de votre tapis.",
"size": "Vous n'avez pas besoin de calibrer en utilisant tout votre tapis. Choisissez plutôt l'espace le plus grand où vous pouvez faire rentrer le quadrillage de calibration puis saisissez la largeur et la hauteur pour qu'elles correspondent à la largeur et la hauteur du quadrillage.",
"project": "Quand le quadrillage projeté est aligné avec votre tapis, cliquez (ou appuyer) sur \"Projeter.\""
},
"project": {
"title": "Projection d'un patron",
"flip": {
"title": "Renverser verticalement/horizontalement",
"description": "Utile lors du miroitement des patrons en 2."
},
"fullscreen": {
"title": "Plein écran",
"description": "Il est généralement plus facile d'utiliser le logiciel en mode plein écran."
},
"cut": "Découpez sur le dessin projeté.",
"tools": "En mode projection, il y a plusieurs outils proposés :",
"showGrid": {
"title": "Montrer/cacher le quadrillage",
"description": "Lors de la projection, cela montre un quadrillage atténué pour vérifier que la calibration est correcte."
},
"invert": {
"title": "Inverser les Couleurs",
"description": "Lors de la projection, il est généralement plus facile de voir des lignes vertes ou blanches sur du noir. Cliquez/appuyez une fois pour des lignes vertes, deux fois pour des lignes blanches et trois fois pour revenir à des lignes noires."
},
"rotate": {
"description": "Change l'orientation du patron de 90 degrés.",
"title": "Tourner"
},
"recenter": {
"title": "Recentrer",
"description": "Centre le patron sur le tapis de découpe."
},
"move": "Déplacez le PDF en cliquant et en le faisant glisser sur l'écran.",
"open": "Cliquez (ou appuyer) sur pour ouvrir le document PDF.",
"showMenu": {
"title": "Afficher/masquer le menu",
"description": "Affiche ou masque le menu supérieur."
},
"moveTool": {
"title": "Afficher/masquer l'outil déplacement",
"description": "L'outil déplacement a quatre boutons en forme de flèche pour déplacer les coins/côtés de la grille de calibration. Il a aussi un bouton suivant au milieu pour basculer au prochain coin/côté."
},
"overlayOptions": {
"title": "Afficher/masquer les options de recouvrement (overlay)"
},
"lineWeight": {
"title": "Épaisseur de ligne",
"description": "Change l'épaisseur des lignes du PDF."
},
"layers": {
"title": "Afficher/masquer les calques",
"description": "Affiche ou masque les calques du PDF."
},
"stitch": {
"title": "Afficher/masquer le menu d'assemblage",
"description": "Affiche ou masque le menu d'assemblage, qui permet d'assembler les patrons PDF multi-pages."
},
"measure": {
"title": "Démarrer/arrêter une mesure",
"description": "Mesure la distance entre deux points du PDF."
}
},
"faq": {
"title": "FAQ",
"annotationSupport": {
"answer": "Pas encore, mais c'est prévu pour une prochaine release.",
"question": "Avez-vous du support pour ajouter des annotations ?"
},
"wrongSizePdf": {
"question": "Est-ce que votre PDF est projeté trop petit ou trop large ?",
"answer": "L'outil de calibration Pattern Projector n'a pas de zoom car l'échelle de la projection provient des informations de taille du PDF. Les patrons des designers ont généralement une échelle correcte donc un changement d'échelle peut avoir été introduit lors de l'ouverture du patron dans Affinity Designer ou Inkscape."
},
"saveAsApp": {
"question": "Voulez-vous enregistrer Pattern Projector en tant qu'application ?",
"answer": "Pattern Projector est une Progressive Web App (PWA) donc il peut être enregistrer comme une application. Sur un ordinateur, vous pouvez l'installer en utilisant Chrome ou Edge. Sur une tablette, ouvrir le menu Partager et appuyer sur Ajouter à l'écran d'accueil."
},
"chromecastSupport": {
"question": "Supportez-vous Chromecast ?",
"answer": "Bien qu'il soit possible de caster cette page web avec Chrome, la latence de connexion peut être frustrante, en particulier lors de la calibration. Un meilleur support de Chromecast est prévu pour une prochaine release."
},
"mobileSupport": {
"answer": "Bien qu'il soit possible de visiter et d'utiliser la page web depuis un smartphone, la taille d'écran limitée la rend difficile à utiliser. Un meilleur support pour mobile est prévu pour une prochaine release.",
"question": "Supportez-vous les téléphones mobiles et les tablettes ?"
}
},
"resources": {
"title": "Ressources additionnelles",
"links": {
"projectorsForSewing": {
"title": "Groupe Facebook Projection et Patrons (de couture)",
"link": "https://www.facebook.com/groups/612018956087067/"
},
"onePageGuide": {
"title": "Guide d'une page sur la projection pour la couture"
}
}
},
"contribute": {
"title": "Participer au projet",
"translate": "Traduisez cet outil dans plus de langues.",
"feedback": "Les retours d'expérience et demandes de fonctionnalités sont les bienvenus !",
"develop": "Aidez à implémenter des fonctionnalités et fixer des problèmes sur GitHub.",
"donation": "Si vous voulez soutenir le développement de cet outil, merci d'envisager de m'offrir un café"
},
"contact": "Contact",
"youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr"
},
"Header": {
"rotate90": "Tourner de 90 degrés dans le sens horaire",
"projecting": "Projection",
"calibrating": "Calibration",
"openPDF": "Ouvrir un PDF",
"height": "h :",
"width": "l :",
"calibrate": "Calibrer",
"delete": "Réinitialiser la calibration",
"overlayModeGrid": "Quadrillage",
"invertColor": "Inverser les couleurs",
"flipVertical": "Renverser verticalement",
"layersOn": "Afficher le menu des calques",
"layersOff": "Masquer le menu des calques",
"recenter": "Recentrer le PDF",
"fullscreen": "Passer en plein écran",
"fullscreenExit": "Quitter le plein écran",
"lineWeight": "Épaisseur des lignes",
"menuShow": "Afficher le menu",
"menuHide": "Masquer le menu",
"measureOn": "Commencer à mesurer",
"measureOff": "Arrêter de mesurer",
"project": "Projeter",
"gridOn": "Quadrillage activé",
"gridOff": "Quadrillage désactivé",
"invertColorOff": "Inversion de couleurs désactivée",
"flipVerticalOff": "Renversement vertical désactivé",
"flipHorizontalOff": "Renversement horizontal désactivé",
"flipHorizontal": "Renverser horizontalement",
"info": "Page d'infos",
"overlayOptions": "Options de recouvrement (overlay)",
"overlayOptionBorder": "Bordure",
"overlayOptionDisabled": "Désactivé",
"overlayOptionGrid": "Quadrillage",
"fourCornersOn": "Surligner tous les coins",
"fourCornersOff": "Surligner uniquement le coin sélectionné",
"arrowBack": "Reculer d'1 page",
"arrowForward": "Avancer d'1 page",
"stitchMenuHide": "Masquer le menu d'assemblage",
"stitchMenuShow": "Afficher le menu d'assemblage",
"overlayOptionPaper": "Feuille de papier",
"overlayOptionFliplines": "Lignes de renversement / symétrie",
"hideMovement": "Masquer l'outil déplacement",
"showMovement": "Afficher le menu des déplacements"
},
"StitchMenu": {
"columnCount": "Colonnes",
"pageRange": "Pages",
"horizontal": "Horizontal",
"vertical": "Vertical"
},
"MovementPad": {
"up": "Haut",
"down": "Bas",
"left": "Gauche",
"right": "Droite",
"next": "Coin suivant"
}
}
================================================
FILE: messages/hu.json
================================================
{
"HomePage": {
"youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr",
"github": "Lásd a forráskódot",
"calibrate": "Kezdje el a kalibrálást",
"choose-language": "Nyelv",
"welcome": {
"title": "Üdvözöljük a Pattern Projector-ban!",
"description": "Pattern Projector (Szabásminta kivetítő) egy ingyenes és nyílt forráskódú internetes applikáció, ami gyorsan kalibrál bármilyen projektort a szabásminták kivetítéséhez. Ezen kívül olyan eszközöket is tartalmaz, melyek segítségével össze lehet fűzni a több oldalból álló szabásmintákat, meg lehet változtatni a vonalak vastagságát, meg lehet cserélni a színeket, tükrözni és forgatni lehet a szabásmintákat, stb. A legújabb frissítésekről a frissítésekmenüpontban lehet többet megtudni."
},
"requirements": {
"title": "Amire szükség lesz",
"projector": "Projektor: legalább 720p felbontású ajánlott",
"mat": "Rácsos vágólap (opcionális)",
"mount": "Állvány, vagy falra/polcra/asztalra szerelhető projektor konzol",
"computer": "Számítógép vagy táblagép amivel a projektorhoz lehet csatlakozni",
"pattern": "PDF szabásminta"
},
"setup": {
"title": "Beállítás",
"place": "Helyezze a projektort közvetlenül a vágólap fölé úgy, hogy az közvetlenül a vágólapra vetítsen. Hogyha nincs vágólapja, akkor papírral vagy maszkolószalaggal (fedőszalag) is kijelölhet egy rácsot/téglalapot az asztalon vagy a földön.",
"connect": "Csatlakoztassa a számítógépét vagy táblagépét a projektorhoz, és vagy kettőzze meg vagy terjessze ki a kijelzőt úgy, hogy a számítógépen vagy táblagépen található képet vetítse ki a projektor.",
"focus": "Állítsa be a fókuszt a projektoron addig, amíg nem lesz teljesen éles a kivetített kép közepén található szöveg. Amennyiben sehogyan sem lesz éles a kép, ellenőrizze, hogy a projektor és a vágólap közötti távolság a gyártó által ajánlott értékek között van-e."
}
}
}
================================================
FILE: messages/it.json
================================================
{
"HomePage": {
"project": {
"moveTool": {
"description": "Lo strumento spostamento ha quattro pulsanti freccia per spostare gli angoli della griglia di calibrazione e i bordi. Ha anche un pulsante \"successivo\" al centro per passare all'angolo/bordo successivo.",
"title": "Mostra o nascondi lo strumento sposta"
},
"open": "Fai clic (o tocca) per aprire il cartamodello.",
"cut": "Taglia il cartamodello proiettato.",
"tools": "In modalità proiezione ci sono diversi strumenti a disposizione. Alcuni hanno scorciatoie da tastiera, che sono indicate tra parentesi.",
"fullscreen": {
"title": "Schermo intero",
"description": "Generalmente è più facile usare il software in modalità a schermo intero."
},
"showMenu": {
"description": "Mostra o nasconde il menu in alto.",
"title": "Menu mostra/nascondi"
},
"invert": {
"description": "Quando si proietta, di solito è più facile vedere linee verdi o bianche su nero. Fai clic/tocca una volta per linee verdi, due volte per linee bianche, e tre volte per tornare alle linee nere.",
"title": "Inverti colori"
},
"overlayOptions": {
"title": "Mostra o nascondi le opzioni di sovrapposizione",
"description": "Griglia, bordo, foglio di carta, linee di riflessione o sovrapposizioni del rovescio del modello possono essere mostrati sul PDF per aiutare con la calibrazione e il taglio. Per ulteriori informazioni, vedere la sezione opzioni di sovrapposizione."
},
"title": "Proiezione di un cartamodello",
"move": "Sposta il PDF facendo clic e trascinandolo sullo schermo.",
"lineWeight": {
"title": "Spessore della linea",
"description": "Cambia lo spessore delle linee nel modello PDF."
},
"flip": {
"title": "Rifletti verticale (V) o orizzontale (H)",
"description": "Utile quando si riflette metà di un cartamodello."
},
"rotate": {
"title": "Ruota (R)",
"description": "Modifica l'orientamento del modello di 90 gradi."
},
"recenter": {
"title": "Centra e ripristina (C)",
"description": "Centra il modello sul tappetino di taglio e ripristina la rotazione/riflessione."
},
"layers": {
"title": "Mostra o nascondi i livelli",
"description": "Mostra o nasconde i livelli nel PDF."
},
"stitch": {
"title": "Mostra o nasconde il menu \"cuci\"",
"description": "Mostra o nasconde il menu \"cuci\", che consente di unire insieme più pagine di un cartamodello PDF."
},
"measure": {
"title": "Strumento Linea (L)",
"description": "Crea delle linee nel PDF per ruotare, specchiare, spostare, misurare o contrassegnare. Per una descrizione dettagliata dello strumento linea, visita la sezione strumento linea."
},
"magnify": {
"title": "Ingrandisci (M)",
"description": "Fai click (o tocca) sul PDF per ingrandirlo. Fai click (o tocca) di nuovo sul PDF per smettere di ingrandirlo."
},
"zoomOut": {
"title": "Zoom indietro (Z)",
"description": "Zooma indietro per vedere tutto il PDF. Clicca (o tocca) sul PDF per zoomare avanti sul punto desiderato."
},
"scale": {
"title": "Mostra o nascondi il menu \"scala\"",
"description": "Cambia la dimensione del cartamodello inserendo un moltiplicatore: tra 0 e 1 renderà il cartamodello più piccolo, più grande di 1 renderà il cartamodello più grande."
}
},
"calibration": {
"size": "Non è necessario calibrare utilizzando l'intero tappetino, invece scegli l'area più grande in cui è possibile far entrare la griglia di calibrazione e inserisci larghezza e altezza per farle combaciare con la larghezza e l'altezza della griglia.",
"project": "Quando la griglia proiettata è allineata con il tappetino, fai clic (o tocca) “Proietta.”",
"title": "Calibrazione",
"fullscreen": "Entra in modalità a schermo intero cliccando (o toccando) ",
"start": "Fari clic (o tocca) su “Avvia calibrazione.”",
"drag": "Trascina gli angoli della griglia per allinearli con il tappetino. Mantenendo gli occhi sul tappetino, regola gli angoli sul computer o tablet. Sistema il posizionamento degli angoli fino a quando la griglia proiettata corrisponde alla griglia del tappetino."
},
"choose-language": "Lingua",
"youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr",
"requirements": {
"pattern": "Un cartamodello in PDF",
"title": "Di cosa hai bisogno",
"computer": "Computer o tablet da connettere al proiettore",
"projector": "Proiettore: raccomandato almeno 720p",
"mat": "Un tappetino da taglio con linee guida (opzionale)",
"mount": "Treppiede o aggancio per muro/mensola/tavola per il proiettore"
},
"setup": {
"keystone": "Se il proiettore ha il keystone (correzione trapezoidale), regolalo in modo che la proiezione sia più rettangolare possibile e la messa a fuoco migliori vicino ai bordi.",
"title": "Allestimento",
"place": "Posiziona il proiettore sopra il tappetino di taglio, puntando direttamente sul tappetino di taglio. Se non disponi di un tappetino da taglio, è possibile utilizzare carta o nastro adesivo per realizzare una griglia / rettangolo su un tavolo o sul pavimento.",
"connect": "Collega il computer o il tablet al proiettore e fai il mirroring o estendi lo schermo in modo che l'immagine da computer o tablet sia mostrata sul proiettore.",
"focus": "Regola la messa a fuoco sul proiettore, fino a quando il testo al centro della proiezione è nitido. Se non riesci ad ottenere un'immagine chiara, assicurati che la distanza tra il proiettore e il tappetino di taglio rientri nell'intervallo funzionale consigliato dal produttore."
},
"github": "Vedi codice sorgente",
"calibrate": "Inizia a calibrare",
"welcome": {
"title": "Benvenuto in Pattern Projector!",
"description": "Pattern projector è un'app web open source gratuita che calibra rapidamente i proiettori per i cartamodelli. Ha anche strumenti per unire cartamodelli a più pagine, cambiare lo spessore delle linee, invertire i colori, riflettere / ruotare i cartamodelli e altro ancora. Per leggere gli ultimi aggiornamenti, consulta il changelog."
},
"tools": "Strumenti",
"lineTool": {
"title": "Strumento linea",
"delete": {
"title": "Cancella linea",
"description": "Cancella la linea selezionata."
},
"rotate": {
"title": "Allinea al centro",
"description": "Ruota il PDF in modo che la linea selezionata sia orizzontale e centrata sul tappetino.",
"use": "Per allineare il drittofilo di un cartamodello con il drittofilo del tessuto: disegna una linea sulla drittofilo del pezzo modello, quindi fai clic (o tocca) il pulsante \"allinea al centro\"."
},
"previousNext": {
"title": "Porta la linea precedente/successiva al centro",
"description": "Sposta il PDF in modo che la linea precedente/successiva disegnata sia orizzontale e centrata sul tappetino.",
"use": "Per spostarsi tra i pezzi del cartamodello quando si taglia: disegna una linea sul drittofilo di ogni pezzo modello, quindi spostati attraverso i pezzi del cartamodello utilizzando questi pulsanti. Elimina le linee mentre passi attraverso i pezzi del cartamodello per tenere traccia di quali pezzi sono rimasti da tagliare."
},
"flip": {
"title": "Rifletti lungo la linea",
"description": "Riflette il PDF lungo la linea.",
"use": "Per dispiegare i pezzi del cartamodello: disegna una linea sulla linea di piega di un pezzo di modello, taglia il pezzo fino alla linea di piega, fai clic (o tocca) sul pulsante rifletti lungo la linea, e quindi taglia il resto del pezzo."
},
"move": {
"title": "Sposta il PDF della stessa lunghezza linea",
"description": "Sposta il PDF dalla lunghezza della linea.",
"use": "Per allungare/accorciare i pezzi del cartamodello: disegna una linea perpendicolare alla linea di allungamento di un pezzo del cartamodello, taglia fino alla linea di allungamento, quindi fai clic (o tocca) il pulsante \"sposta per lunghezza linea\" e continua a tagliare il pezzo. La direzione della linea disegnata dipenderà se stai allungando o accorciando il pezzo."
},
"description": "Lo strumento linea può essere utilizzato per misurare le distanze, contrassegnare le linee, ruotare, riflettere e spostare il PDF. Fai clic (o tocca) sul pulsante linea, quindi fai clic (o tocca) sul PDF e trascina per disegnare una linea. Una volta tracciata una riga, un menu apparirà nella parte bassa dello schermo con le seguenti opzioni:"
},
"overlayOptions": {
"border": {
"title": "Bordi",
"description": "Mostra il bordo della griglia di calibrazione."
},
"grid": {
"title": "Griglia",
"description": "Mostra la griglia di calibrazione."
},
"flipLines": {
"title": "Linee di riflessione",
"description": "Mostra delle linee che aiutano a dispiegare il cartamodello. Fai coincidere la linea di piega del cartamodello con la linea di riflessione e premi rifletti orizzontalmente o rifletti verticalmente per riflettere il pezzo."
},
"paper": {
"title": "Foglio di carta",
"description": "Mostra un rettangolo della dimensione di foglio formato Letter o A4 per verificare la calibrazione."
},
"title": "Opzioni di sovrapposizione",
"description": "Le opzioni di sovrapposizione possono essere utilizzate per verificare la calibrazione durante la proiezione e come aiuto per tagliare i cartamodelli. Sono disponibili le seguenti opzioni:",
"flippedPattern": {
"title": "Rovescio del cartamodello",
"description": "Mostra dei punti quando viene proiettato il rovescio del modello."
}
},
"faq": {
"title": "FAQ",
"wrongSizePdf": {
"question": "La proiezione del PDF è troppo grande o troppo piccola?",
"answer": "Lo strumento di calibrazione di Pattern Projector non ha zoom perché la scala della proiezione proviene dalle informazioni sulle dimensioni del PDF. I cartamodelli creati da designer di solito hanno la scala corretta, un cambiamento di scala potrebbe essere stato introdotto aprendo il modello in Affinity Designer o Inkscape."
},
"annotationSupport": {
"question": "C'è il supporto per aggiungere annotazioni?",
"answer": "Non ancora, ma è previsto un prossimo rilascio."
},
"mobileSupport": {
"question": "Telefoni cellulari e tablet sono supportati?",
"answer": "Anche se è possibile visitare e utilizzare la pagina web su uno smartphone , la dimensione limitata dello schermo la rende difficile da usare."
},
"saveAsApp": {
"answer": "Pattern Projector è un Progressive Web App (PWA) quindi può essere salvato come app. È possibile installarlo su un computer desktop utilizzando Chrome o Edge. Su un tablet, apri il menu Condividi e tocca Aggiungi alla schermata iniziale."
}
},
"resources": {
"title": "Risorse aggiuntive",
"links": {
"onePageGuide": {
"title": "Guida di una pagina per Projector Sewing",
"link": "https://bit.ly/onepageguidetoprojectorsewing"
},
"projectorsForSewing": {
"link": "https://www.facebook.com/groups/481078582801085",
"title": "Gruppo Facebook Projector for Sewing"
}
}
},
"contribute": {
"develop": "Aiuta a implementare funzionalità e risolvere problemi su GitHub.",
"feedback": "Feedback e richieste di funzionalità sono i benvenuti!",
"donation": "Se Pattern Projector ti ha fatto risparmiare soldi in copisteria o ti ha liberato dall'abbonamento ad un'app per visualizzare PDF, per favore, prendi in considerazione l'idea di offrirmi un caffè o di supportarmi tramite PayPal!",
"translate": "Traduci questo strumento in più lingue utilizzando Weblate.",
"title": "Contribuisci al progetto"
},
"contact": "Contattami"
},
"InstallButton": {
"title": "Installa l'app",
"ok": "OK",
"descriptionAndroid": "Per una migliore esperienza su Android, utilizza Firefox. Scarica l'applicazione toccando il pulsante altro nel menu del browser e poi aggiungi a schermata principale.",
"descriptionIOS": "Scarica l'applicazione toccando il pulsante di condivisione nel menu del browser e poi Aggiungi alla schermata iniziale .",
"description": "Per una migliore esperienza, installa Pattern Projector utilizzando Google Chrome. In Chrome, scarica l'app toccando nella barra degli indirizzi."
},
"Header": {
"width": "L:",
"height": "A:",
"project": "Proietta",
"delete": "Ripristina la calibrazione",
"overlayOptionBorder": "Bordi",
"overlayOptionFlippedPattern": "Rovescio del cartamodello",
"invertColor": "Inverti colori",
"invertColorOff": "Inverti colori off",
"overlayOptionPaper": "Foglio di carta",
"openPDF": "Apri",
"calibrate": "Calibra",
"overlayOptionDisabled": "Disabilitato",
"overlayOptionGrid": "Griglia",
"overlayOptions": "Opzioni di sovrapposizione",
"gridOn": "Griglia accesa",
"gridOff": "Griglia spenta",
"flipVertical": "Rifletti verticalmente (V)",
"flipHorizontal": "Rifletti orizzontalmente (H)",
"flipCenterOn": "Rifletti attraverso il centro",
"flipCenterOff": "Rifletti attraverso il centro off",
"rotate90": "Ruota di 90 gradi in senso orario (R)",
"arrowBack": "Indietro di 1 pagina",
"arrowForward": "Avanti di 1 pagina",
"info": "Informazioni sulla pagina",
"recenter": "Centra e ripristina (C)",
"ok": "OK",
"calibrationAlertTitle": "Avviso calibrazione",
"calibrationAlertContinue": "La dimensione della finestra non può cambiare dopo la calibrazione. Per proiettare a schermo intero, calibra a schermo intero. Se desideri modificare la dimensione della finestra, ricalibra.",
"checkCalibration": "Verifica la calibrazione",
"menuShow": "Mostra menu",
"menuHide": "Nascondi menu",
"lineWeight": "Spessore della linea",
"measure": "Strumento Linea (L)",
"calibrationAlert": "La dimensione della finestra è cambiata da quando è iniziata la proiezione, premi il pulsante schermo intero o ricalibra per risolvere questo problema.",
"continue": "Salva la dimensione della finestra e continua",
"overlayOptionFliplines": "Linee di riflessione",
"fourCornersOn": "Evidenzia tutti gli angoli",
"fourCornersOff": "Evidenzia solo l'angolo selezionato",
"fullscreen": "Entra in modalità schermo pieno",
"fullscreenExit": "Esci dalla modalità schermo pieno",
"stitchMenuShow": "Mostra il menu cuci",
"stitchMenuHide": "Nascondi il menu cuci",
"stitchMenuDisabled": "Apre il PDF per usare il menu cuci",
"showMovement": "Mostra lo strumento muovi",
"hideMovement": "Nasconde lo strumento muovi",
"magnify": "Ingrandisci (M)",
"zoomOut": "Zoom indietro (Z)",
"mail": "Messaggio da Courtney",
"invalidCalibration": "La griglia di calibrazione non è corretta. Premi \"resetta calibrazione\" per riprovare."
},
"MeasureCanvas": {
"flipAlong": "Rifletti lungo la linea",
"rotateToHorizontal": "Allinea al centro",
"rotateAndCenterPrevious": "Porta la linea precedente al centro",
"rotateAndCenterNext": "Porta la linea successiva al centro",
"deleteLine": "Cancella la linea",
"translate": "Sposta il cartamodello della lunghezza della linea",
"line": "linea",
"lines": "linee"
},
"StitchMenu": {
"columnCount": "Colonne",
"pageRange": "Cuci pagine",
"horizontal": "Sovrapposizione Orizzontale",
"vertical": "Sovrapposizione Verticale",
"zeros": "Aggiungi degli 0 per le pagine vuote, es. 1-3,0,0,4",
"rowCount": "Righe"
},
"SaveMenu": {
"save": "Esporta PDF",
"saveTooltip": "Esporta PDF con le pagine cucite e i livelli selezionati",
"encryptedPDF": "Questo PDF è criptato. Per favore inserisci la password per salvare il PDF cucito."
},
"LayerMenu": {
"title": "Livelli",
"showAll": "Mostra tutto",
"hideAll": "Nascondi tutto",
"layersOn": "Mostra il menu livelli",
"layersOff": "Nascondi il menu livelli",
"noLayers": "Nessun livello nel PDF"
},
"MovementPad": {
"up": "Su",
"down": "Giù",
"left": "Sinistra",
"right": "Destra",
"next": "Angolo successivo"
},
"OverlayCanvas": {
"magnifying": "clicca il cartamodelli per smettere di ingrandire",
"zoomedOut": "clicca il cartamodello per zoomare in avanti",
"scaled": "× scala"
},
"Troubleshooting": {
"notMatching": "Le linee non coincidono col tuo tappetino?",
"offByOne": {
"title": "Le linee sono dritte ma non coincidono?",
"caption": "Sono inserite le misure sbagliate (23 x 17) anziché (24 x 18). Il tappetino mostra i numeri fino a 23 e 17, per questo è facile sbagliare le misure. Assicurati di misurare con un metro!",
"description": "Se le linee proiettate sono dritte ma non coincidono con quelle del tappetino, l'altezza o la larghezza potrebbero essere sbagliate di 1 pollice/ centimetro. Ricontrolla le misure con un metro. Le misure del tappetino possono essere imprecise, quindi è importante controllare con un metro."
},
"dragCorners": {
"description": "Trascina gli angoli della griglia almeno fino al rettangolo 24x16 sul tuo tappetino. La griglia dovrebbe essere più grande possibile ma non è necessario che copra l'intero tappetino.",
"title": "Come calibrare",
"caption": "Una griglia correttamente calibrata."
},
"title": "Risoluzione dei problemi di calibrazione",
"inputMeasurement": "Usa un metro per misurare altezza e larghezza del tuo tappetino. Scrivi queste misure nei campi altezza e larghezza.",
"unevenSurface": {
"caption": "Questo tappetino ha un righello sotto di esso e rende le linee incurvate al centro. Una lieve curvatura va bene ma una curvatura grande può causare problemi.",
"title": "Linee incurvate?",
"description": "Se le linee proiettate sono incurvate, il tappetino o la superfice sotto di esso possono non essere piatti. Cerca di appiattire il tappetino mettendo del cartone sotto i punti più bassi o spostandolo su una diversa superficie di taglio."
},
"dimensionsSwapped": {
"title": "Rettangoli anziché quadrati?",
"description": "Se la griglia proiettata sembra composta da rettangoli anziché quadrati, larghezza e altezza potrebbero essere invertite. Scambia i valori nei campi.",
"caption": "Larghezza e altezza sono nelle caselle sbagliate. Le dimensioni dovrebbero aprire come \"L: 24\" e \"A: 18\"."
},
"close": "Chiudi"
},
"General": {
"close": "Chiudi",
"error": "Errore"
},
"Mail": {
"title": "Hai ricevuto un messaggio",
"donate": "Dona"
},
"PdfViewer": {
"error": "Impossibile caricare il cartamodello",
"noData": "Premi il pulsante \"Apri\" per caricare un cartamodello"
},
"ScaleMenu": {
"scale": "Scala cartamodello",
"show": "Mostra menu \"scala\"",
"hide": "Nascondi menu \"scala\""
}
}
================================================
FILE: messages/nb-NO.json
================================================
{
"HomePage": {
"welcome": {
"description": "Pattern projector er en gratis åpen kildekode web applikasjon som raskt kan kalibrere projektorer for symønstre. Det finnes også verktøy for å kombinere mønster som er spredt over flere sider, støtte for å endre linjetykkelse, invertere farger, flippe/rotere mønstre og mer. For å lese om de seneste oppdateringene, se endringsloggen.",
"title": "Velkommen til Pattern Projector!"
},
"setup": {
"place": "Plasser projektoren et sted direkte over skjærematten. Om du ikke har en skjærematte kan du bruke papir eller maskeringsteip for å markere et rutenett på et bord eller på gulvet.",
"focus": "Juster fokus på projektoren inntil teksten er skarp og klar i midten av projeksjonen. Om du ikke kan få et klart bilde, sjekk at distansen mellom projektoren og skjærematten er innenfor produsentens anbefalinger.",
"connect": "Koble en datamaskin eller nettbrett til projektoren. Speil eller utvid skjermen slik at bildet fra datamaskinen eller nettbrettet blir vist på projektoren.",
"title": "Oppsett",
"keystone": "Om din projektor har mulighet for å justere keystone, juster disse verdiene til projeksjonen er så rektangulært som mulig, og fokus i kantene av projeksjonen forbedres."
},
"youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr",
"requirements": {
"mat": "En skjærematte med rutemønster (valgfritt)",
"computer": "Datamaskin eller nettbrett for å koble til projektor",
"title": "Hva du vil trenge",
"projector": "Projektor: minst 720p anbefalt",
"mount": "Tripod eller vegg/hylle/bordmontering til projektor",
"pattern": "Et PDF symønster"
},
"github": "Se kildekode",
"calibrate": "Start kalibrering",
"choose-language": "Språk",
"calibration": {
"drag": "Dra i hjørnene på rutenettet på skjermen for å tilpasse størrelsen til rutenettet på skjæreunderlaget. Juster hjørnenes plassering til rutenettet som projiseres matcher rutenettet på skjærematten.",
"title": "Kalibrering",
"start": "Klikk eller tapp på \"Start kalibrering\"",
"fullscreen": "Gå til fullskjermmodus ved å klikke eller tappe. "
}
}
}
================================================
FILE: messages/nl.json
================================================
{
"HomePage": {
"beta": "beta",
"pdfstitcherHref": "https://www.pdfstitcher.org/",
"youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr",
"github": "Bekijk de broncode",
"calibrate": "Start kalibreren",
"choose-language": "Taal",
"welcome": {
"title": "Welkom bij Pattern Projector!",
"description": "Pattern projector is een gratis en open source web app die gebruikers helpt om snel en makkelijk projectors voor naaipatronen te kalibreren. Dit project is in een testfase ('beta'), je kan regelmatig veranderingen en nieuwe functionaliteit verwachten terwijl we er aan werken. Om te weten te komen over de laatste updates, kijk in de changelog."
},
"requirements": {
"title": "Wat je nodig hebt",
"projector": "Projector: ten minste 720p aanbevolen",
"mat": "Een snijmat met rasterlijnen (optioneel)",
"mount": "Een standaard, ophanging of bevestiging aan de muur, plafond of aan een tafel",
"computer": "Computer of tablet om verbinding te maken met de projector",
"pattern": "Een naaipatroon in PDF formaat"
},
"setup": {
"title": "Installatie",
"place": "Installeer de projector boven de snijmat, en laat hem naar de mat toe schijnen. Als je geen snijhebt hebt, kan je papier of schilderstape gebruiken om een raster of rechthoek te maken op de vloer of een tafel.",
"connect": "Maak met je computer of tablet verbinding met de projector en zet het scherm in 'spiegel' ('mirror') of 'uitbreiden' ('extend') modus (afhankelijk van hoe deze instelling op jouw device heet) zodat het beeld van de computer identiek op de projector weergegeven wordt.",
"focus": "Stel de focus van de projector zo in, dat tekst scherp wordt weergegeven, en in het midden van het projectiebeeld. Als je geen helder beeld kan krijgen, zorg er dan voor dat de afstand tussen de projector en de snijmat zich binnen de door de fabrikant aanbevolen afstanden bevindt.",
"keystone": "Als je projector trapeziumcorrectie ('keystone correction') heeft, stel die dan zo in dat de projectie zo rechthoekig mogelijk is, en het beeld aan de randen zo scherp mogelijk is."
},
"calibration": {
"title": "Calibratie",
"start": "Klik (of druk) op 'Start kalibreren.'",
"input": "Geef de breedte en hoogte van je snijmat in.",
"fullscreen": "Ga naar 'Volledig scherm' modus door te klikken (of drukken) op ",
"drag": "Sleep de hoeken van het raster tot ze overeenkomen met je snijmat. Stel de hoeken op je computer of tablet bij terwijl je naar de projectie kijkt.",
"size": "Je hoeft niet te kalibreren met je hele mat. Kies het grootste gebied op je mat waar het kalibratierooster in past en zorg ervoor dat je breedte- en hoogteinstellingen overeenkomen met het aantal vakjes van je raster.",
"project": "Als het geprojecteerde grid overeen komt met de snijmat, klik (of druk) dan op 'Projecteer'"
},
"project": {
"title": "Een patroon projecteren",
"open": "Klik (of druk) op om het patroon te openen.",
"move": "Leg je stof op je snijmat. Verplaats het geprojecteerde beeld door op de PDF te klikken en hem over het scherm te slepen. Zet zo het juiste deel van je patroon op de stof (en snijmat onder de stof).",
"cut": "Snij, knip of teken langs het geprojecteerde beeld.",
"tools": "In projectiemodus zijn er verschillende gereedschappen beschikbaar. Sommige hebben toetsenbord sneltoetsen, aangeduid tussen haakjes.",
"showGrid": {
"title": "Toon/verberg raster",
"description": "Toont of verbergt een licht raster tijdens het projecteren. Handig om te controleren dat je kalibratie correct is."
},
"invert": {
"title": "Draai kleuren om",
"description": "Tijdens het projecteren is het meestal het makkelijkst om groene of witte lijnen op een zwarte achtergrond te zien. Maar met dit gereedschap kan je dat desgewensd omdraaien. Klik of druk één keer voor groene lijnen, twee keer voor witte lijnen, en drie keer om terug te gaan naar zwarte lijnen."
},
"flip": {
"title": "Spiegel vertikaal (V) of horizontaal (H)",
"description": "Handig om patronen op de vouw te spiegelen."
},
"rotate": {
"title": "Draai (R)",
"description": "Draai het patroon 90 graden."
},
"recenter": {
"title": "Centreer PDF opnieuw en reset (C)",
"description": "Plaatst het patroon in het midden van de snijmat en reset draaien/spiegelen."
},
"fullscreen": {
"title": "Volledig scherm",
"description": "Het is meestal het makkelijkst om de software in 'Volledig scherm' modus te gebruiken."
},
"moveTool": {
"description": "Het verplaatsgereedschap heeft vier pijlen om de hoeken en zijkanten van het calibratierooster te verplaatsen. Het heeft ook een knop in het midden om naar de volgende hoek of zijkant te gaan.",
"title": "Toon of verberg verplaats-gereedschap"
},
"overlayOptions": {
"title": "Toon of verberg overlay opties",
"description": "De overlays raster, rondom, blad papier en spiegellijnen kunnen getoond of verborgen worden. Voor meer informatie, zie de overlay opties sectie."
},
"measure": {
"description": "Markeer lijnen op de PDF voor draaien, spiegelen, verplaatsen, meten of markeren. Voor een gedetailleerde beschrijving, zie de lijn gereedschap sectie.",
"title": "Lijn gereedschap (L)"
},
"lineWeight": {
"title": "Lijndikte",
"description": "Verandert de dikte van de lijnen in het PDF patroon."
},
"layers": {
"title": "Toon of verberg lagen",
"description": "Toont of verbergt de lagen in de PDF."
},
"stitch": {
"title": "Toon of verberg samenvoeg menu",
"description": "Toont of verbergt het samenvoeg menu, waarmee je meerder paginas van een PDF patroon samen kan voegen."
},
"showMenu": {
"description": "Toon of verberg het menu bovenaan.",
"title": "Toon/verberg menu"
},
"magnify": {
"title": "Vergroot (M)",
"description": "Klik op de PDF om deze te vergroten. Klik opnieuw om te vergroten te stoppen."
},
"zoomOut": {
"title": "Uitzoomen (Z)",
"description": "Zoom uit om de hele PDF te zien. Klik op de PDF om de geselecteerde locatie te vergroten."
},
"scale": {
"title": "Schaalmenu weergeven of verbergen",
"description": "Verander de grootte van het patroon door een vergrotingsfactor in te geven: tussen 0 en 1 maakt het patroon kleiner, groter dan 1 maakt het patroon groter."
}
},
"faq": {
"title": "FAQ",
"wrongSizePdf": {
"question": "Is je geprojecteerde PDF te klein of te groot?",
"answer": "Het Pattern Projector kalibratie gereedschap heeft geen 'zoom' functie omdat de schaal van de projectie uit de informatie in de PDF gehaald wordt. Patronen van ontwerpers hebben meestal de juiste schaal. Het zou kunnen dat er een foute schaal ingeslopen is door het patroon te openen in Affinity Designer of Inkscape."
},
"saveAsApp": {
"question": "Wil je Pattern Projector als een app opslaan?",
"answer": "Pattern Projector is een 'Progressive Web App' (PWA), en kan dus als app opgeslagen worden. Op een computer kan je het installeren met Chrome of Edge. Op een tablet open je het 'Delen' menu en drukt op 'Voeg to aan startscherm'."
},
"annotationSupport": {
"question": "Ondersteunen jullie het toevoegen van notities?",
"answer": "Nog niet, maar dit is gepland voor een toekomstige versie."
},
"chromecastSupport": {
"question": "Ondersteunen jullie Chromecast?",
"answer": "Hoewel het mogelijk is om deze pagina te 'casten' in Chrome, kan de vertraging die hierdoor optreedt frustrerend zijn, vooral tijdens het kalibreren. Betere ondersteuning voor Chromecast is gepland voor een toekomstige versie."
},
"mobileSupport": {
"question": "Ondersteunen jullie mobiele telefoons en tablets?",
"answer": "Hoewel het mogelijk is om de webpagina te bezoeken en gebruiken met een smartphone, is het moeilijk te gebruiken door het kleine schermformaat."
}
},
"resources": {
"title": "Verdere informatie",
"links": {
"projectorsForSewing": {
"title": "Naaien met een projector/beamer",
"link": "https://www.facebook.com/groups/492119878090767"
},
"onePageGuide": {
"title": "Naaien met een projector uitgelegd op één pagina",
"link": "https://bit.ly/onepageguidetoprojectorsewing"
}
}
},
"contribute": {
"title": "Help dit project",
"donation": "Als je de ontwikkeling van deze software wil ondersteunen, overweeg dan om me een koffie te kopen of gebruik PayPal!",
"develop": "Help verbeteringen toe te voegen en problemen op te lossen op GitHub.",
"translate": "Vertaal deze software in meer talen met Weblate.",
"feedback": "Commentaar en verzoeken om nieuwe functionaliteit zijn welkom (in het Engels)!"
},
"contact": "Contact",
"lineTool": {
"delete": {
"title": "Verwijder lijn",
"description": "Verwijdert de geselecteerde lijn."
},
"title": "Lijn gereedschap",
"description": "Het lijn gereedschap kan gebruikt worden om afstanden te meten, lijnen te markeren, en om de PDF te draaien, spiegelen of verplaatsen. Klik (of tik) op de lijn gereedschap knop, en klik (of tik) dan op de PDF om een lijn te tekenen. Zodra de lijn getekend is, verschijnt er een menu op het scherm met de volgende opties:",
"rotate": {
"title": "Lijn uit op het midden",
"description": "Draait de PDF zodat de geselecteerde lijn horizontaal en uitgelijnd op het midden van de mat is.",
"use": "Om de draadrichting op een patroondeel uit te lijnen met de draadrichting van je stof: teken een lijn op de draadrichtinglijn van je patroondeel. Klik (of tik) dan op de 'Lijn uit op het midden' knop."
},
"previousNext": {
"title": "Breng de vorige of volgende lijn naar het midden",
"description": "Verplaatst de PDF zodat de vorige of volgende getekende lijn horizontaal en uitgelijnd op het midden van de mat is.",
"use": "Om tussen patroondelen te wisselen terwijl je aan het snijden bent: teken een lijn op de draadrichtinglijn van elke patroondeel. Navigeer tussen de patroondelen met deze knoppen. Verwijder lijnen tijdens het verwerken van alle delen, om bij te houden welke delen nog gesneden moeten worden."
},
"flip": {
"title": "Spiegel langs lijn",
"description": "Spiegelt de PDF langs de lijn.",
"use": "Om patroondelen uit te vouwen: teken een lijn op de vouwlijn van een patroondeel. Snij het deel tot aan de vouwlijn. Klik (of tik) op de 'spiegel langs lijn' knop, en snij dan de rest van het patroondeel."
},
"move": {
"title": "Verplaats PDF lijnlengte",
"description": "Verplaatst de PDF de lengte van de lijn.",
"use": "Om patroondelen langer of korter te maken: teken een lijn loodrecht op de te verlengen lijn van een patroondeel. Snij tot deze lijn. Klik (of tik) dan op de 'verplaats lijn lengte' knop, en ga dan door met het snijden van het patroondeel. De richting van de getekende lijn hangt af van of je het patroondeel wil verlengen of verkorten."
}
},
"tools": "Gereedschappen",
"overlayOptions": {
"title": "Overlay opties",
"description": "De overlay opties kunnen gebruikt worden om kalibratie te controleren terwijl je aan het projekteren bent, en om te helpen bij het snijden van patroondelen. De volgende opties zijn beschikbaar:",
"border": {
"title": "Rand",
"description": "Toont de rand van het kalibratie raster."
},
"grid": {
"title": "Raster",
"description": "Toont het kalibratie raster."
},
"paper": {
"title": "Blad papier",
"description": "Toont een rechthoek met de maat van een Letter of A4 stuk papier, om de kalibratie te controleren."
},
"flipLines": {
"title": "Spiegel lijnen",
"description": "Toont lijnen om te helpen met het uitvouwen van patroondelen. Zet de vouwlijn van een patroondeel op de spiegellijn en klik op spiegel horizontaal of vertikaal om het patroon te spiegelen."
},
"flippedPattern": {
"title": "Verkeerde zijde",
"description": "Toont puntjes als de foute zijde van het patroon wordt geprojecteerd."
}
}
},
"Header": {
"projecting": "Projekteren",
"calibrating": "Kalibreren",
"openPDF": "Open",
"height": "H:",
"width": "W:",
"project": "Projecteer",
"calibrate": "Kalibreer",
"delete": "Reset kalibratie",
"gridOn": "Raster aan",
"gridOff": "Raster uit",
"invertColor": "Draai kleuren om",
"invertColorOff": "Kleuren omdraaien uit",
"flipVertical": "Spiegel vertikaal (V)",
"flipVerticalOff": "Spiegel vertikaal uit",
"flipHorizontal": "Spiegel horizontaal (H)",
"flipHorizontalOff": "Spiegel horizontaal uit",
"rotate90": "Draai 90 graden met de klok mee (R)",
"fourCornersOn": "Alle hoeken markeren",
"fourCornersOff": "Alleen geselecteerde hoek markeren",
"arrowBack": "1 pagina terug",
"arrowForward": "1 pagina vooruit",
"info": "Informatie pagina",
"layersOn": "Toon lagen menu",
"layersOff": "Verberg lagen menu",
"recenter": "Centreer PDF opnieuw (C)",
"fullscreen": "Start 'volledig scherm'",
"fullscreenExit": "Verlaat 'volledig scherm'",
"lineWeight": "Lijndikte",
"stitchMenuShow": "Toon het samenvoegen menu",
"stitchMenuHide": "Verberg het samenvoegen menu",
"menuShow": "Toon menu",
"menuHide": "Verberg menu",
"overlayMode": "Extra laag",
"overlayModeBorder": "Rand",
"overlayModeOff": "Geen",
"overlayModeGrid": "Raster",
"overlayModePaper": "Blad papier",
"measureOn": "Begin met meten",
"measureOff": "Stop met meten",
"overlayOptions": "Overlay instellingen",
"overlayOptionBorder": "Rand",
"overlayOptionDisabled": "Geen",
"overlayOptionGrid": "Raster",
"overlayOptionPaper": "Blad papier",
"overlayOptionFliplines": "Spiegel lijnen",
"flipCenterOn": "Spiegel rond midden aan",
"flipCenterOff": "Spiegel rond midden uit",
"showMovement": "Toon het verplaatsgereedschap",
"hideMovement": "Verberg het verplaatsgereedschap",
"ok": "OK",
"calibrationAlertTitle": "Kalibratie waarschuwing",
"calibrationAlert": "De venstergrootte is veranderd sinds het projecteren begonnen is, verander de \"schermvullend\" instelling of kalibreer opnieuw om dit op te lossen.",
"calibrationAlertContinue": "De venstergrootte kan niet meer veranderd worden na de kalibratie. Om in volledig venster te projekteren, kalibreer in volledig venster. Als je de venstergrootte wil veranderen, kalibreer dan opnieuw.",
"continue": "Sla venstergrootte op en ga door",
"checkCalibration": "Kalibratie nakijken",
"measure": "Lijn gereedschap (L)",
"stitchMenuDisabled": "Open PDF om het samenvoegen menu te gebruiken",
"overlayOptionFlippedPattern": "Verkeerde zijde",
"magnify": "Vergroot (M)",
"zoomOut": "Uitzoomen (Z)",
"invalidCalibration": "Het kalibratieraster is niet geldig. Klik op 'Kalibratie resetten' om opnieuw te proberen.",
"mail": "Email van Courtney"
},
"StitchMenu": {
"columnCount": "Kolommen",
"pageRange": "Paginas aan elkaar plakken",
"horizontal": "Horizontale overlap",
"vertical": "Verticale overlap",
"zeros": "Voeg 0en toe voor lege paginas, bv 1-3,0,0,4",
"rowCount": "Rijen"
},
"MovementPad": {
"up": "Omhoog",
"down": "Naar beneden",
"left": "Links",
"right": "Rechts",
"next": "Volgende hoek"
},
"MeasureCanvas": {
"rotateToHorizontal": "Lijn uit op midden",
"translate": "Verplaats het patroon met de lengte van de lijn",
"deleteLine": "Verwijder lijn",
"flipAlong": "Spiegel langs lijn",
"rotateAndCenterPrevious": "Breng de vorige lijn naar het midden",
"rotateAndCenterNext": "Breng de volgende lijn naar het midden",
"line": "lijn",
"lines": "lijnen"
},
"LayerMenu": {
"title": "Lagen",
"showAll": "Toon alles",
"hideAll": "Verberg alles",
"layersOn": "Toon lagen menu",
"layersOff": "Verberg lagen menu",
"noLayers": "Geen lagen in het patroon"
},
"InstallButton": {
"title": "Installeer App",
"ok": "OK",
"description": "Installeer Pattern Projector met Google Chrome voor de beste ervaring. In Chrome, download de app door op in de adresbalk te klikken.",
"descriptionAndroid": "Gebruik Firefox voor de beste ervaring op Android. Download de app met de 'meer' knop in het browsermenu en dan ‘Zet in beginscherm’ (Add to home screen).",
"descriptionIOS": "Dowload de app met de 'delen' knop in het browsermenu, en dan met de ‘Zet in beginscherm’ (Add to home screen) knop."
},
"SaveMenu": {
"encryptedPDF": "Deze PDF is beveiligd. Voer het wachtwoord in om de samengevoegde PDF op te slaan.",
"save": "Exporteer PDF",
"saveTooltip": "Exporteer PDF met samengevoegde paginas en geselecteerde lagen"
},
"OverlayCanvas": {
"zoomedOut": "klik op patroon om in te zoomen",
"magnifying": "klik op patroon om te stoppen met vergroten",
"scaled": "× schaal"
},
"PdfViewer": {
"error": "Niet gelukt om patroon te laden",
"noData": "Druk op de \"Open\" knop om een patroon te laden"
},
"ScaleMenu": {
"show": "Toon schaal menu",
"hide": "Verberg schaal menu",
"scale": "Schaal patroon"
},
"Troubleshooting": {
"notMatching": "Komen de lijnen niet overeen met je mat?",
"dragCorners": {
"caption": "Een juist gekalibreerd raster.",
"description": "Sleep de hoeken van het raster naar een rechthoek van minstens 80x60cm op je mat. Het raster is best zo groot mogelijk, maar hoeft niet de hele mat te beslaan.",
"title": "Hoe te kalibreren"
},
"offByOne": {
"caption": "De foute maten zijn ingevoerd (23x17) in plaats van (24x18). Op de mat staan slechts nummers tot 23 en 17 dus het is makkelijk om de maten door elkaar te halen. Zorg dat je meet met een meetlint!",
"title": "Lijnen recht maar komen niet overeen?",
"description": "Als de geprojecteerde lijnen recht zijn maar niet overeen komen met je mat, is je breedte of hoogte misschien 1 cm verkeerd. Controleer je maten met een meetlint. Maten van matten kunnen verwarrend of niet erg nauwkeurig zijn, dus het is belangrijk om ze met een meetlint te controleren."
},
"unevenSurface": {
"title": "Gebogen lijnen?",
"caption": "Deze mat heeft een meetlat eronder liggen waardoor de lijnen in het midden krom zijn. Een beetje kromming is niet erg, maar grote krommingen zullen problemen veroorzaken.",
"description": "Als de geprojecteerde lijnen er gebogen uit zien, is de mat of het oppervlak eronder misschien niet vlak. Probeer de mat vlak te krijgen door karton onder de te lage punten te leggen, of verplaats naar een ander snijoppervlak."
},
"dimensionsSwapped": {
"title": "Rechthoeken in plaats van vierkanten?",
"description": "Als het geprojecteerde raster op rechthoeken lijkt in plaats van vierkanten, dan zijn de breedte en hoogte misschien omgedraaid. Wissel de waarden om in de invoervelden.",
"caption": "De breedte en hoogte staan in de verkeerde invoervelden. De afmetingen zouden moeten zijn: 'W: 24' en 'H:18'."
},
"close": "Sluiten",
"title": "Problemen met de kalibratie oplossen",
"inputMeasurement": "Gebruik een meetlint om de breedte en hoogte van het raster op je mat te meten. Geef deze waarden in in de breedte en hoogte velden."
},
"General": {
"error": "Fout",
"close": "Sluiten"
},
"Mail": {
"donate": "Doneer",
"title": "Email ontvangen"
}
}
================================================
FILE: messages/sl.json
================================================
{
"HomePage": {
"calibration": {
"start": "Kliknite na \"Začni kalibrirati\"",
"drag": "Povlecite oglišča mreže in jih poravnajte z vašo rezalno podlogo. Glejte na rezalno podlogo ter nastavite oglišča na vašem računalniku/tablici. Korigirajte pozicijo oglišč dokler se projicirana mreža ne prekriva s mrežo na vaši rezalni podlagi.",
"title": "Kalibracija",
"fullscreen": "Aktiviraj celozaslonski prikaz s klikom na ikonco. ",
"size": "Ni vam potrebno kalibrirat z uporabo vaše celotne rezalne površine. Izberite največjo površino, ki jo lahko prekrijte z kalibracijsko mrežo ter vnesite ustrezno višino ter širino uporabljene mreže.",
"project": "Ko je projicirana mreža poravnana s rezalno podlogo kliknite na \"Projiciraj\""
},
"project": {
"moveTool": {
"description": "Orodje za premikanje ima štiri gumbe s puščicami za premikanje oglišč kalibracijske mreže. Na sredini ima tudi gumb naslednji za preklop na naslednje oglišče.",
"title": "Skrij ali prikaži orodje za premikanje"
},
"invert": {
"description": "Med projiciranjem je pogosto lažje gledat zelene ali bele črte na črnem ozadju. Kliknite enkrat za zelene, dvakrat za bele črte. Ter trikrat, da se vrnete nazaj na črne črte.",
"title": "Inverzija barv"
},
"fullscreen": {
"description": "Program uporabljat v celozaslonskem načinu, je pogosto najlažje.",
"title": "Celoten zaslon"
},
"tools": "V načini projiciranja je na voljo več orodij. Nekatere imajo bližnjice na tipkovnici, le te so podatke v oklepajih.",
"overlayOptions": {
"description": "Mreža, okvir, list papirja, zrcalne črte, napačna stran so orodja za šablone. Lahko so prikazana čez PDF, da pomagajo pri kalibraciji ter rezanju. Za več informacij, si oglejte odsek orodij za šablone.",
"title": "Prikaži ali skrij pripomočke za šablone"
},
"scale": {
"description": "Spremeni velikost šablone z vnosom mnogokratnika: Število med 0 in 1 bo šablono naredilo manjšo. Število večje od 1 pa bo šablono naredilo večjo.",
"title": "Prikaži ali skrij meni povečave"
},
"measure": {
"description": "Označi črtne na PDF-u za rotiranje, zrcaljenje, premikanje merjenje ali označevanje. Za podroben opis tega orodja si oglej Odsek orodja za izbor.",
"title": "Orodje za črto (L)"
},
"rotate": {
"description": "Zavrti šablono za 90 stopinj.",
"title": "Rotiraj (R)"
},
"zoomOut": {
"title": "Pomanjšaj (Z)",
"description": "Pomanjšaj, da vidiš celotni PDF. Klikno na PDF, da povečaš na izbrano mesto."
},
"title": "Projiciranje šablone",
"open": "Kliknite na da odprete šablono.",
"move": "Premikajte PDF s klikni ter povleci po površini zaslona.",
"cut": "Režite kose po projiciranih črtah.",
"showMenu": {
"title": "Prikaži/Skrij meni",
"description": "Skrij ali prikaži zgornji meni."
},
"lineWeight": {
"title": "Debelina črt",
"description": "Spremenite debelino črt na PDF šabloni."
},
"flip": {
"title": "Zrcali navpično (H) ali vodoravno (V)",
"description": "Uporabno pri izdelavi zrcalnih kosov."
},
"recenter": {
"title": "Centriraj ter Ponastavi (C)",
"description": "Centriraj šablono na rezalno podlago ter ponastavi rotacijo/zrcaljenje."
},
"magnify": {
"title": "Približaj (M)",
"description": "Klikni na PDF, da ga približaš. Klikni ponovno, da zapustiš približanje."
},
"layers": {
"title": "Prikaži ali skrij plasti",
"description": "Prikaži ali skrij plasti v PDF datoteki."
},
"stitch": {
"title": "Prikaži ali skrij meni spajanja",
"description": "Prikaže ali skrije meni spajanja, kateri vam omogoča spajanje šablon razrezanih na več strani."
}
},
"overlayOptions": {
"paper": {
"title": "List papirja",
"description": "Prikaže kvadrat velikosti papirja Letter ali A4, za kontrolo kalibracije."
},
"flipLines": {
"title": "Zrcalne črte",
"description": "Prikaže črte, ki pomagajo pri polovičnih/zrcalnih šablonah. Poravnaj delilno črto polovične/zrcalnega kosa na šabloni z zrcalno črto ter klikni na zrcali vodoravno ali navpično, da zrcališ kos."
},
"grid": {
"title": "Mreža",
"description": "Prikaže kalibracijsko mrežo."
},
"border": {
"title": "Rob",
"description": "Prikaže rob kalibracijske mreže."
},
"title": "Pripomočki za šablone",
"description": "Pripomočki za šablone se lahko uporabijo za kontrolo kalibracije med projiciranjem ter kot pomoč pri rezanju šablone. Sledeče možnosti so na voljo:",
"flippedPattern": {
"title": "Spodnja stran",
"description": "Prikaže pike, ko je projicirana spodnja stran šablone."
}
},
"contact": "Kontakt",
"lineTool": {
"description": "Orodje za črto se lahko uporabi za merjenje razdalj, označevanje črt na katerih se naj rotira, zrcali, premika PDF šablona. Kliknite na orodje za črto, nato kliknite na PDF ter povlecite črto. Ko je črta narisana, se bo pojavil meni s sledečimi možnostmi:",
"rotate": {
"title": "Poravnaj na sredino",
"description": "Rotira PDF, tako da je izbrana črta vodoravno ter na sredini vaše rezalne površine.",
"use": "Za poravnavo smeri kroja na šabloni s smerjo kroja na vašem blagu: Narišite črto na označbi poteka kroja na šabloni, nato kliknite na gumb poravnaj na sredino."
},
"previousNext": {
"use": "Za premik med kosi ko jih režeš: Nariši črto smeri kroja na vsakem kosu, nato se premikaj med kosi z uporabo teh gumbov. izbriši črto, ki izrežeš kos, da imaš pregled katere kose še rabiš izrezati.",
"title": "Postavi v center prejšnjo/naslednjo črto",
"description": "Premakne PDF tako, da je prejšnja/naslednja narisana črta vodoravna ter v sredini vaše rezalne površine."
},
"flip": {
"use": "Za polovične/zrcalne šablone: Nariši črto na zrcalni črti šablone, izreži/preriši kot do te črte, klikni gumb zrcali čez črto ter nato izreži/preriši preostanek kosa.",
"title": "Zrcali čez črto",
"description": "Zrcali PDF čez izbrano črto."
},
"move": {
"use": "Za podaljševanje/krajšanje dela šablone: Narišite črto vzporedno na črto dela šablone, ki jo želite podaljšat/krajšat. Izrežite/prerišite črto nato kliknite gumb premakni za dolžino črte ter nadaljujte z rezanjem/prerisovanjem. Smer risanja črte bo odvisna ali podaljšujete ali krajšate šablono.",
"title": "Premakni PDF za dolžino črte",
"description": "Premakne PDF za dolžino izbrane črte."
},
"title": "Orodje za črto",
"delete": {
"title": "Izbris črte",
"description": "Izbriše izbrano črto."
}
},
"faq": {
"mobileSupport": {
"answer": "Sicer je možno odpret in uporabljat to spletno aplikacijo na telefonu, a je zaradi majhnega zaslona naporna za uporabo.",
"question": "Se lahko uporablja tudi na mobilnem telefonu ali tablici?"
},
"wrongSizePdf": {
"answer": "Pattern Projector korak kalibracije nima povečave, ker mere projekcije prihajajo iz podatki o velikosti v PDF datoteki. Šablone od oblikovalcev imajo ponavadi nastavljeno pravilno merilo. Sprememba merila bi se lahko nehote zgodila ob odpiranju šablone v drugem programu kot na primer Affinity Designer ali Inkscape.",
"question": "Je vaš PDF projiciran premajhno ali preveliko?"
},
"saveAsApp": {
"answer": "Pattern Projector je Progressive Web App (PWA) tako si ga lahko shranite kot aplikacijo. Na vaše namizje si ga lahko namestite če uporabljate Chrome ali Edge. Na tablici odprite meni ter izberite dodaj na začetni zaslon."
},
"title": "Pogosta vprašanja"
},
"contribute": {
"donation": "V kolikor ti je Pattern Projector prihranil denar v fotokopirnici, olajšal delo ali zmanjšal stroške PDF prikazovalnika, bom zelo vesela če mi kupiš kavo ali podpreš z donacijo preko PayPal. Hvala!",
"title": "Podpri razvoj tega projekta",
"develop": "Pomagaj mi pri razvoju novih možnosti ter odpravi težav na GitHub.",
"translate": "Lahko tudi pomagaš prevesti to orodje v več jezikov z uporabo Weblate.",
"feedback": "Povratne informacije ter predlogi za dodatne izboljšave so dobrodošli!"
},
"resources": {
"title": "Dodatni uporabni viri",
"links": {
"onePageGuide": {
"title": "Navodila na enem listu za uporabo projektorja pri šivanju",
"link": "https://bit.ly/onepageguidetoprojectorsewing"
},
"projectorsForSewing": {
"link": "https://www.facebook.com/groups/481078582801085",
"title": "Facebook grupa Projectors for Sewing"
}
}
},
"youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr",
"welcome": {
"title": "Dobrodošli pri Pattern Projector!",
"description": "Pattern Projector je brezplačna ter odprtokodna spletna aplikacija. ki hitro kalibrira projektor za uporabo kot šablone za rezanje rezanje. Ima tudi orodja za združevanje šablon razdeljenih na več strani, spremembo debeline črt, inverzijo barv, zrcaljenje/obračanje šablon ter veliko več. V kolikor želiš prebrati vse o zadnjih posodobitvah, si oglej changelog."
},
"github": "Oglej si izvorno kodo",
"requirements": {
"mat": "Rezalno podlogo z mrežo (opcijsko)",
"title": "Kaj potrebuješ",
"projector": "Projektor: Priporočeno vsaj 720p",
"mount": "Stojalo ali stensko/polično/mizno pritrditev za projektor",
"computer": "Računalnik ali tablico za povezavo na projektor",
"pattern": "Šablono v PDF datoteki"
},
"calibrate": "Zaženi kalibracijo",
"choose-language": "Jezik",
"setup": {
"title": "Nastavitev",
"connect": "Priklopite vaš računalnik na projektor ter ga nastavite tako, da bo slika iz računalnika na njem vidna kot klon namizja ali kot razširjeno namizje.",
"place": "Postavite/obesite projektor nad rezalno podlogo, tako da bo projeciral direktno na rezalno podlogo. V primeru, da nimate rezalne podloge, lahko uporabite papir ali krep lepilni strak, da na tleh označite kvader/mrežo.",
"focus": "Nastavljajte ostrino projektorja dokler ni besedilo na sredini projekcije prikazano ostro. Če vam ne uspe nastavit ostre slike, preverite da je razdalja med projektorjem in rezalno podlaga v območju projekcije predlagane iz strani proizvajalca projektorja.",
"keystone": "V kolikor ima vaš projektor funkcijo keystone(trapezna korekcija), jo nastavite tako, da bo projekcija čim bolj podobna pravokotniku, saj se bo s tem izboljšala ostrina ob robovih."
},
"tools": "Orodja"
},
"InstallButton": {
"descriptionAndroid": "Za najboljšo izkušnjo na Android uporabite Firefox. Prenesite aplikacijo z klikom v brskalniku na gumb več ter nato , da jo dodate na začetni zaslon.",
"descriptionIOS": "Prenesite aplikacijo z klikom na deli ikonco v brskalniku ter nato na dodaj na začetni zaslon .",
"description": "Za najboljšo izkušnjo si namesti Pattern Projector z uporabo Google Chrome. V Chrome si prenesi aplikacijo z klikom na gumb v naslovni vrstici.",
"title": "Namesti aplikacijo",
"ok": "V redu"
},
"Header": {
"invertColor": "Inverzija barv vklopljena",
"width": "Š:",
"mail": "Sporočilo do Courtney",
"ok": "V redu",
"arrowForward": "Naprej 1 stran",
"rotate90": "Rotiraj za 90 stopinj v desno (R)",
"zoomOut": "Pomanjšaj (Z)",
"overlayOptionBorder": "Robovi",
"overlayOptionFlippedPattern": "Spodnja stran",
"stitchMenuShow": "Prikaži meni spajanja",
"lineWeight": "Debelina črt",
"gridOff": "Izklopi mrežo",
"overlayOptions": "Pripomočki za šablone",
"flipHorizontal": "Zrcali navpično (H)",
"overlayOptionPaper": "List paprija",
"overlayOptionFliplines": "Zrcalne črtne",
"calibrationAlertContinue": "Velikost okna se ne more spremeniti po kalibraciji. Za projiciranje v celozaslonskem načinu, kalibrirajte v celozaslonskem načinu. Če želite spremeniti velikost okna, izvedite kalibracijo ponovno.",
"magnify": "Približaj (M)",
"fourCornersOn": "Poudari vsa oglišča",
"menuShow": "Prikaži meni",
"fullscreenExit": "Izklopi celozaslonski način",
"showMovement": "Prikaži orodje za premikanje",
"menuHide": "Skrij meni",
"openPDF": "Odpri",
"height": "V:",
"project": "Projiciraj",
"calibrate": "Kalibiriraj",
"delete": "Ponastavi kalibracijo",
"gridOn": "Vklopi mrežo",
"overlayOptionDisabled": "Izklopljeno",
"overlayOptionGrid": "Mreža",
"invertColorOff": "Inverzija barv izklopljena",
"flipVertical": "Zrcali vodoravno (V)",
"flipCenterOn": "Zrcaljenje čez sredino vklopljeno",
"flipCenterOff": "Zrcaljenje čez sredino izklopljeno",
"fourCornersOff": "Poudari samo izbrano oglišče",
"arrowBack": "Nazaj 1 stran",
"info": "Stran z informacijami",
"recenter": "Centriraj ter Ponastavi (C)",
"fullscreen": "Vklopi celozaslonski način",
"calibrationAlert": "Velikost okna je drugačna, kot ob začetku projiciranja! Za odpravo napake, kliknite na gumb za celozaslonski način ali izvedite kalibracijo na novo.",
"continue": "Shrani velikost okna in nadaljuj",
"checkCalibration": "Preveri kalibracijo",
"stitchMenuHide": "Skrij meni spajanja",
"stitchMenuDisabled": "Za uporabo menija spajanja odpri PDF",
"measure": "Orodje za črte (L)",
"hideMovement": "Skrij orodje za premikanje",
"invalidCalibration": "Kalibracijska mreža ni veljavna. Kliknite \"Ponastavi kalibracijo\" in poizkusite znova.",
"calibrationAlertTitle": "Opozorilo kalibracije"
},
"Troubleshooting": {
"dimensionsSwapped": {
"description": "Če projicirana mreža zgleda kot kvadrat namesto pravokotnik, so mogoče zamenjane vrednosti za širino in višino. Preprosto zamenjajte vrednosti v teh poljih.",
"caption": "Širina ter višina sta v napačnih poljih. Mere bi morale izgledati kot 'Š: 24' ter 'V: 18'.",
"title": "Kvadrat namesto pravokotnik?"
},
"dragCorners": {
"description": "Povlecite vogale mreže na vsaj velikost kvadrata 24x16 inch na vaši rezalni podlagi. Mreža naj bo kar se da velika a ne rabi prekrivat celotne rezalne podlage.",
"title": "Kako kalibrirati",
"caption": "Pravilno kalibrirana mreža."
},
"inputMeasurement": "Uporabite ravnilo, da izmerite širino ter višino mreže na vaši rezalni podlogi. Vnesite te mere v polji za širino ter višino.",
"offByOne": {
"description": "Če so projicirane črte ravne a niso poravnane z vašo rezalno podlago, mogoče vaša vnesena širina ali višina odstopa za 1 inch/centimeter. Ponovno preverite z ravnilom. Mere na rezalni šabloni so lahko dvomljive ali netočne, zato je kontrola z ravnilom pomembna.",
"caption": "Vnesene so napačne mere (23 x 17) namesto (24 x 18). Rezalna površina prikazuje le številke do 23 ter 17, kar je hitro dvoumno. Preverite mero s uporabo ravnila!",
"title": "Črte so ravne a niso poravnane?"
},
"unevenSurface": {
"title": "Zvite črte?",
"caption": "Ta podloga ima pod njo ravnilo, kar povzroča, da črte na sredini niso ravne. Rahlo zvita črta je še v redu a večje vam bodo povzročale težave.",
"description": "Če projicirane črte izgledajo zvite je zelo verjetno vaša površina pod rezalno podlago ni ravna. Poizkusite zravnat vašo podlago s podlaganjem kartona na nižjih mestih ali uporabite drugi podlago."
},
"close": "Zapri",
"notMatching": "Črte se ne ujemajo z vašo rezalno podlago?",
"title": "Odpravljanje napak kalibracije"
},
"PdfViewer": {
"noData": "Kliknite na gumb \"Odpri\", da odprete šablono",
"error": "Šablone ni bilo možno naložiti"
},
"StitchMenu": {
"horizontal": "Vodoravno prekrivanje",
"vertical": "Navpično prekrivanje",
"columnCount": "Stolpci",
"rowCount": "Vrstice",
"pageRange": "Strani za spajanje",
"zeros": "Vstavi 0-le za prazne strani. npr: 1-3,0,0,4"
},
"SaveMenu": {
"encryptedPDF": "Ta PDF je šifriran. Vnesite geslo za izvoz v spojeni PDF.",
"save": "Izvozi v PDF",
"saveTooltip": "Izvozi v PDF s spojenimi stranmi ter izbranimi plastmi"
},
"LayerMenu": {
"hideAll": "Skrij vse",
"layersOn": "Prikaži meni plasti",
"title": "Plasti",
"showAll": "Prikaži vse",
"layersOff": "Skrij meni plasti",
"noLayers": "Ta šablona nima plasti"
},
"MeasureCanvas": {
"line": "črta",
"lines": "črte",
"flipAlong": "Zrcali čez črto",
"rotateToHorizontal": "Poravnaj v sredino",
"rotateAndCenterPrevious": "Postavi prejšnjo črto v sredino",
"rotateAndCenterNext": "Postavi naslednjo črto v sredino",
"deleteLine": "Izbriši črto",
"translate": "Premakni šablono za dolžino črte"
},
"OverlayCanvas": {
"zoomedOut": "klikni na šablono za približanje",
"magnifying": "klikni na šablono za izhod iz približanja",
"scaled": "× povečava"
},
"Mail": {
"title": "Imaš sporočilo",
"donate": "Doniraj"
},
"General": {
"close": "Zapri",
"error": "Napaka"
},
"ScaleMenu": {
"hide": "Skrij meni povečave",
"show": "Pokaži meni povečave",
"scale": "Povečava šablone"
},
"MovementPad": {
"up": "Gor",
"down": "Dol",
"left": "Levo",
"right": "Desno",
"next": "Naslednje oglišče"
}
}
================================================
FILE: messages/sv.json
================================================
{
"HomePage": {
"choose-language": "Språk",
"welcome": {
"title": "Välkommen till Pattern Projector!",
"description": "Pattern projector är en gratis webb app med öppen källkod som snabbt kalibrerar projektorer för symönster. Den har också verktyg för att sätta ihop flera sidor av mönstret, ändra linjetjocklek, invertera färger, vända/rotera mönster och mer. För att läsa om de senaste uppdateringarna, kolla in ändringsloggen."
},
"calibrate": "Starta kalibrering",
"project": {
"scale": {
"title": "Visa eller dölj skala menyn",
"description": "Ändra mönstrets storlek genom att ange en multiplikator: mellan 0-1 gör mönstret mindre och större än 1 gör mönstret större."
},
"title": "Projicera ett mönster",
"open": "Klicka (eller tryck) för att öppna ett mönster.",
"move": "Flytta PDFn genom att klicka och dra det runt på skärmen.",
"cut": "Klipp längs med den projicerade designen.",
"fullscreen": {
"title": "Helskärm",
"description": "Det är generellt lättare att använda mjukvara i helskärmsläge."
},
"tools": "I projektions läge finns flera verktyg. Några har kortkommandon, som anges inom parenteserna.",
"showMenu": {
"title": "Visa/dölj meny",
"description": "Visa eller dölj top meny."
},
"invert": {
"title": "Invertera färger",
"description": "Vid projesering, är det vanligare att se gröna eller vita linjer på svart. Klicka/tryck en gång för gröna linjer två gånger för vita linjer, och tre gånger för att komma tillbaka till svarta linjer."
},
"moveTool": {
"title": "Vis eller dölj verktyg för förflyttning",
"description": "Förflyttnings verktyget har en knapp med fyra pilar för att flytta kalibrerings rutnätets hörn/kanter. Det har också en nästa knapp i mitten för att växla till nästa hörn/kant."
},
"overlayOptions": {
"title": "Visa eller dölj överläggsalternativ",
"description": "Rutnät, gräns, pappersark, vänd linjer eller fel sida överlägg kan visas på PDF:n för att hjälpa till med kalibrering och klippning. För mer information, se överläggnings val sektionen."
},
"lineWeight": {
"title": "Linjebredd",
"description": "Ändra linjens tjocklek på PDF mönstret."
},
"flip": {
"title": "Växla Vertikal(V) eller Horisontellt (H)",
"description": "Värdefull hjälp när man speglar halva mönster."
},
"rotate": {
"title": "Rotera (R)",
"description": "Ändra mönstrets orientering med 90 grader."
},
"recenter": {
"title": "Centrera och Nollställ (C)",
"description": "Centra mönstret på skärmattan och nollställ rotationen/vändningen."
},
"magnify": {
"title": "Förstora (M)",
"description": "Klicka (eller tryck) på PDF:n för att förstora den. Klicka (eller) tryck) igen för att stoppa förstoringen."
},
"zoomOut": {
"title": "Zooma ut (Z)",
"description": "Zooma ut för att se hela PDF:n. Klicka (eller tryck) på PDF:n för att zooma tillbaka till valt område."
},
"layers": {
"title": "Visa eller dölj lager",
"description": "Visa eller dölj lager i PDF:n."
},
"stitch": {
"title": "Visa eller dölj slå ihop meny",
"description": "Visa eller dölj slå ihop meny, som tillåter dig att sätta ihop flersidiga PDF mönster."
},
"measure": {
"description": "Markera linjer i PDF:n för rotering, vändning, flyttning, eller markering. För en detaljerad beskrivning av linje verktyget, se line tool section.",
"title": "Linje verktyg (L)"
}
},
"overlayOptions": {
"flipLines": {
"description": "Visar linjer som hjälper till med att vända mönstret. Anpassa mönstrets viklinje med vändlinjen och tryck vänd horisontellt eller vertikalt för att spegla delen.",
"title": "Vänd linjer"
},
"title": "Alternativ för överlägg",
"border": {
"title": "Gräns",
"description": "Visa gränsen av kalibreringsrutnätet."
},
"grid": {
"title": "Rutnät",
"description": "Visar kalibreringsrutnätet."
},
"paper": {
"title": "Pappersark",
"description": "Visar en rektangel för storleken av Letter eller A4-papper för att verifiera kalibreringen."
},
"flippedPattern": {
"title": "Avig sidan",
"description": "Visar prickar när avigsidan av mönstret projeceras."
},
"description": "Valmöjligheten till överläggning kan användas för att bekräfta kalibrering medan projecering och hjälp med att klippa mönster. Följande möjligheter är tillgängliga:"
},
"faq": {
"title": "FAQ",
"mobileSupport": {
"question": "Har ni support för mobiltelefoner eller surfplattor?",
"answer": "Det är möjligt att besöka och använda websidan på en smart telefon, men den begränsade storleken på skärmen gör den svårt att använda."
},
"wrongSizePdf": {
"question": "Är din PDF projecering för liten eller stor?",
"answer": "Pattern Projectors kalibreringsverktyg har ingen zoom eftersom skalan på projektionen kommer från storleksinformationen i PDF-filen. Mönster från designers har vanligtvis rätt skala, så en förändring i skalan kan ha införts när mönstret öppnades i Affinity Designer eller Inkscape."
},
"saveAsApp": {
"answer": "Pattern Projector är en Progressive Web App (PWA) så den kan sparas som en app. På skrivbordet, du kan insallera det genom att användaChrome or Edge. På en surfplatta, öppna Dela menyn och tryck på Add to Home Screen."
}
},
"youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr",
"github": "Se källkoden",
"requirements": {
"title": "Vad du behöver",
"projector": "Projektor: minst 720p rekommenderas",
"mat": "En skär matta med linjer med rutnät (frivilligt)",
"mount": "Stativ eller vägg/hylla/bord för montering av projektor",
"computer": "Dator eller surfplatta för anslutning till projektorn",
"pattern": "Mönster i PDF format"
},
"setup": {
"title": "Inställningar",
"place": "Placera projektorn över skärmattan, så att den riktas direkt mot skärmattan. Om du inte har en skärmatta, kan du använda papper eller maskeringstejp för att markera ut ett rutnät/rektangel på ett bord eller golv.",
"connect": "Anslut din dator eller surfplatta till projektorn och antingen spegla eller utöka displayen så att bilden från datorn eller surfplattan visas på projektorn.",
"focus": "Justera fokus på projektorn, tills texten är klar i mitten av projektionen. Om du inte kan få en klar bild, säkerställ att avståndet mellan projektor och skärmatta är inom funktions området som rekommenderas av tillverkaren.",
"keystone": "Om din projektor har ”keystone”, justera den så att projektionen är nära till rektangeln och fokus kanterna förbättras."
},
"calibration": {
"title": "Kalibrering",
"start": "Klicka (eller tryck) ”Start Calibrating.”",
"fullscreen": "Gå till helskärmsläge genom att klicka (eller trycka) ",
"drag": "Drag hörnen i rutnätet för att linjera med din matta. Med ögonen på mattan, justera hörnen på din dator eller surfplatta. Justera placeringen av hörnen tills det projicerade rutnätet matchar din mattas rutnät.",
"size": "Du måste inte använda hela mattan för att kalibrera, välj istället den största ytan som kan passa kalibreringens rutnät och sätt in bredd och höjd att matcha bredd och höjd av det rutnätet.",
"project": "När det projicerade rutnätet är i linje med din matta, klicka (eller tryck) ”Project.”"
},
"lineTool": {
"title": "Linjeverktyg",
"delete": {
"title": "Tag bort linje",
"description": "Tag bort vald linje."
},
"description": "Linjeverktyget kan användas för att mäta avstånd, marker linjer, rotera, vända, och flytta PDF:n. Klicka (eller tryck) på linjeverktygets knapp, sedan klicka (eller tryck) på PDF:n och dra för att rita en linje. När en linje är ritad, visas en meny i botten av skärmen med följande val:",
"rotate": {
"use": "För justering av trådriktningen till en mönster del med trådriktningen i ditt tyg: rita en linje på mönsterdelens trådriktningen, klicka sedan (eller tryck) justera till mittknappen.",
"title": "Anpassa till centrum",
"description": "Rotera PDF:n så att den valdea linjen är horisontell eller centrerad på din matta."
},
"previousNext": {
"use": "För att flytta mellan mönster delar vid klippning: rita en linje på trådriktnings linjen till varje mönster del, sedan flytta till de olika mönster delarna genom att använda dessa knappar. Tag bort linjer vart efter du förflyttar dig genom mönsterdelarn för att hålla reda på vilken del som är kvar att klippa.",
"title": "Tag föregående linje/nästa linje till mitten",
"description": "Flyttar PDF:n så att föregående/nästa ritade linje är horisontell och centrerad på din matta."
},
"flip": {
"title": "Vänd längs linje",
"description": "Vänder PDF:n längs linjen.",
"use": "För att vika ut mönster delar: rita en linje på viklinjen av mönsterdelen, klipp fram till viklinjen, klicka (eller tryck) på linje knappen vänd längs linjen, och klipp resterande del av delen."
},
"move": {
"title": "Flytta PDF med linje längd",
"description": "Flyttar PDF:n med linjens längd.",
"use": "För förlänga/förkorta mönster delar: rita en linje vinkelrät mot förlänga/förkorta linjen på mönsterdelen, klipp till förlänga/förkorta linjen, klicka(tryck) sedan på flytta med linje längdens knapp, och fortsätt sedan att klippa delen. Riktningen av den ritade linjen kommer att bero på om du förlänger eller förkortar mönsterdelen."
}
},
"tools": "Verktyg",
"resources": {
"links": {
"projectorsForSewing": {
"title": "Projectors for Sewing Facebook Group",
"link": "https://www.facebook.com/groups/481078582801085"
},
"onePageGuide": {
"title": "En guide till projektorsömnad",
"link": "https://bit.ly/onepageguidetoprojectorsewing"
}
},
"title": "Ytterligare resurser"
},
"contribute": {
"title": "Bidrag till projektet",
"donation": "Om Pattern Projector har sparat pengar för dig av kopiering eller löst dig från en prenumeration på PDF-visning, vänligen överväg buying me a coffee or supporting via PayPal!",
"feedback": "Feedback och funktionsförfrågningar är välkomna!",
"translate": "Översätt detta verktyg till flera språk genom att användaWeblate.",
"develop": "Hjälp till att genomföra funktioner och åtgärda problemGitHub."
},
"contact": "Kontakt"
},
"MeasureCanvas": {
"translate": "Flytta mönstret med linjens längd",
"line": "linje",
"lines": "linjer",
"rotateToHorizontal": "Anpassa till centrum",
"rotateAndCenterPrevious": "Tag föregående linje till mitten",
"rotateAndCenterNext": "Tag nästa linje till mitten",
"deleteLine": "Tag bort linje",
"flipAlong": "Vänd längs linje"
},
"OverlayCanvas": {
"zoomedOut": "klicka på mönstret för att zooma in"
},
"InstallButton": {
"title": "Installera app",
"ok": "Ok",
"descriptionAndroid": "För den bästa upplevelsen på Android, använd Firefox. Ladda ner appen genom att trycka på mer-knappen i webbläsarmenyn och sedan Lägg till på startskärmen.",
"description": "För bästa upplevelse, installera Pattern Projector med Google Chrome. I Chrome laddar du ned appen genom att trycka på i adressfältet."
},
"General": {
"error": "Fel",
"close": "Stäng"
},
"Header": {
"delete": "Återställ kalibreringen",
"overlayOptionFliplines": "Vänd linjer",
"gridOff": "Rutnät av",
"fullscreen": "Öppna helskärm",
"flipCenterOff": "Vänd på mitten av",
"flipCenterOn": "Vänd på mitten",
"fullscreenExit": "Avsluta helskärm",
"height": "H:",
"width": "W:",
"project": "Projekt",
"calibrate": "Kalibrera",
"gridOn": "Rutnät på",
"overlayOptionDisabled": "Inaktiverad",
"overlayOptionGrid": "Rutnät",
"overlayOptionPaper": "Pappersark",
"overlayOptions": "Överlagringsalternativ",
"overlayOptionBorder": "Gräns",
"flipVertical": "Vänd vertikalt (V)",
"invertColor": "Invertera färgerna",
"flipHorizontal": "Vänd horisontellt (H)",
"invertColorOff": "Invertera färgerna av",
"fourCornersOn": "Markera alla hörn",
"fourCornersOff": "Markera bara valda hörn",
"arrowBack": "Backa 1 sida",
"rotate90": "Rotera 90 grader medsols (R)",
"arrowForward": "Framåt 1 sida",
"info": "Informations sida",
"menuHide": "Dölj meny",
"lineWeight": "Linje storlek",
"stitchMenuShow": "Visa sammansättnings menyn",
"stitchMenuHide": "Dölj sammansättningsmenyn",
"stitchMenuDisabled": "Öppna PDF för att använda sammansättnings menyn",
"measure": "Linje verktyg (L)",
"zoomOut": "Zooma ut",
"hideMovement": "Dölj flytt verktyg",
"magnify": "Förstora",
"menuShow": "Visa meny",
"mail": "E-post från Courntey",
"ok": "Ok",
"recenter": "Centrera och återställ (C)",
"openPDF": "Öppna",
"overlayOptionFlippedPattern": "Avigsida",
"showMovement": "Visa flytt verktyg"
},
"ScaleMenu": {
"show": "Visa skala menyn",
"scale": "Skala mönster",
"hide": "Dölj skala menyn"
},
"StitchMenu": {
"zeros": "Lägg till 0:or för blanka sidor, ex. 1-3,0,0,4",
"vertical": "Vertikalt",
"horizontal": "Horisontellt",
"columnCount": "Kolumner",
"pageRange": "Sammansättning av sidor"
},
"Mail": {
"title": "Du har fått e-post"
},
"Troubleshooting": {
"dragCorners": {
"caption": "Korrekt kalibrerat rutnät."
},
"dimensionsSwapped": {
"caption": "Bredden och höjden ligger i fel rutor. Måtten ska vara 'B: 24' och 'H: 18'."
}
}
}
================================================
FILE: messages/ta.json
================================================
{
"HomePage": {
"youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr",
"github": "மூலக் குறியீட்டைக் காண்க",
"calibrate": "அளவீடு செய்யத் தொடங்குங்கள்",
"choose-language": "மொழி",
"welcome": {
"title": "பேட்டர்ன் ப்ரொசெக்டருக்கு வருக!",
"description": "பேட்டர்ன் ப்ரொசெக்டர் என்பது ஒரு இலவச மற்றும் திறந்த மூல வலை பயன்பாடாகும், இது தையல் வடிவங்களுக்கான ப்ரொசெக்டர்களை விரைவாக அளவீடு செய்கிறது. பல பக்க வடிவங்களை ஒன்றாக இணைப்பதற்கான கருவிகளும் இதில் உள்ளன, வரி தடிமன் மாற்றுதல், வண்ணங்களை தலைகீழாக மாற்றுதல், புரட்டுதல்/சுழலும் வடிவங்கள் மற்றும் பல. அண்மைக் கால புதுப்பிப்புகளைப் பற்றி படிக்க, சேஞ்ச்லாக் ஐப் பாருங்கள்."
},
"project": {
"overlayOptions": {
"title": "மேலடுக்கு விருப்பங்களைக் காட்டவும் அல்லது மறைக்கவும்",
"description": "கட்டம், எல்லை, காகிதத் தாள், ஃபிளிப் கோடுகள் அல்லது தவறான பக்க மேலடுக்குகள் PDF இல் அளவுத்திருத்தம் மற்றும் வெட்டுவதற்கு உதவுகின்றன. மேலும் தகவலுக்கு, மேலதிகமாக: மேலடுக்கு விருப்பங்கள் பிரிவு ஐப் பார்க்கவும்."
},
"lineWeight": {
"title": "வரி எடை",
"description": "PDF வடிவத்தில் கோடுகளின் தடிமன் மாற்றவும்."
},
"title": "ஒரு வடிவத்தை திட்டமிடுதல்",
"open": "ஒரு வடிவத்தைத் திறக்க என்பதைக் சொடுக்கு செய்க (அல்லது தட்டவும்).",
"move": "திரையைச் சுற்றி சொடுக்கு செய்து இழுப்பதன் மூலம் PDF ஐ நகர்த்தவும்.",
"cut": "திட்டமிடப்பட்ட வடிவமைப்பில் வெட்டு.",
"tools": "திட்ட பயன்முறையில் பல கருவிகள் வழங்கப்பட்டுள்ளன. சிலவற்றில் விசைப்பலகை குறுக்குவழிகள் உள்ளன, அவை அடைப்புக்குறிக்குள் குறிக்கப்படுகின்றன.",
"fullscreen": {
"title": "முழுத் திரை",
"description": "முழு திரை பயன்முறையில் மென்பொருளைப் பயன்படுத்துவது பொதுவாக எளிதானது."
},
"showMenu": {
"title": "மெனுவைக் காட்டு/மறைக்கவும்",
"description": "மேல் மெனுவைக் காட்டவும் அல்லது மறைக்கவும்."
},
"invert": {
"title": "வண்ணங்களை தலைகீழ்",
"description": "திட்டமிடும்போது, பொதுவாக பச்சை அல்லது வெள்ளை கோடுகளை கருப்பு நிறத்தில் பார்ப்பது எளிதானது. பச்சை கோடுகளுக்கு ஒரு முறை, வெள்ளை கோடுகளுக்கு இரண்டு முறை, மற்றும் கருப்பு கோடுகளுக்குத் திரும்ப மூன்று முறை சொடுக்கு செய்யவும்/தட்டவும்."
},
"moveTool": {
"title": "நகரும் கருவியைக் காட்டவும் அல்லது மறைக்கவும்",
"description": "மூவ் கருவியில் அளவுத்திருத்த கட்டம் மூலைகள்/விளிம்புகளை நகர்த்த நான்கு அம்பு பொத்தான்கள் உள்ளன. அடுத்த மூலையில்/விளிம்பிற்கு மாறுவதற்கு இது நடுவில் அடுத்த பொத்தானைக் கொண்டுள்ளது."
},
"flip": {
"title": "செங்குத்து (வி) அல்லது கிடைமட்ட (எச்)",
"description": "வடிவங்களை பாதியாக பிரதிபலிக்கும் போது உதவியாக இருக்கும்."
},
"rotate": {
"title": "சுழலும் (ஆர்)",
"description": "முறையின் நோக்குநிலையை 90 டிகிரி மாற்றவும்."
},
"recenter": {
"title": "நடுவண் மற்றும் மீட்டமை (சி)",
"description": "கட்டிங் பாயில் வடிவத்தை மையமாகக் கொண்டு சுழற்சி/புரட்டலை மீட்டமைக்கிறது."
},
"magnify": {
"title": "பெரிதாக்கு (மீ)",
"description": "அதை பெரிதாக்க PDF இல் சொடுக்கு செய்க (அல்லது தட்டவும்). பெரிதாக்குவதை நிறுத்த மீண்டும் சொடுக்கு செய்க (அல்லது தட்டவும்)."
},
"zoomOut": {
"title": "பெரிதாக்கவும் (z)",
"description": "முழு PDF ஐக் காண பெரிதாக்கவும். தேர்ந்தெடுக்கப்பட்ட இடத்தில் மீண்டும் பெரிதாக்க PDF இல் சொடுக்கு செய்க (அல்லது தட்டவும்)."
},
"layers": {
"title": "அடுக்குகளைக் காட்டவும் அல்லது மறைக்கவும்",
"description": "PDF இல் அடுக்குகளைக் காட்டவும் அல்லது மறைக்கவும்."
},
"stitch": {
"title": "தையல் மெனுவைக் காட்டவும் அல்லது மறைக்கவும்",
"description": "தையல் மெனுவைக் காட்டவும் அல்லது மறைக்கவும், இது பல பக்க PDF வடிவங்களை ஒன்றாக இணைக்க அனுமதிக்கிறது."
},
"scale": {
"title": "அளவிலான மெனுவைக் காட்டவும் அல்லது மறைக்கவும்",
"description": "ஒரு பெருக்கியை உள்ளிடுவதன் மூலம் வடிவத்தின் அளவை மாற்றவும்: 0-1 க்கு இடையில் வடிவத்தை சிறியதாகவும், 1 ஐ விட அதிகமாகவும் மாற்றும்."
},
"measure": {
"title": "வரி (எல்)",
"description": "சுழற்றுதல், புரட்டுதல், நகர்த்துதல், அளவிடுதல் அல்லது குறிப்பதற்கு PDF இல் கோடுகளைக் குறிக்கவும். வரி கருவியின் விரிவான விளக்கத்திற்கு, வரி கருவி பிரிவு ஐப் பார்க்கவும்."
}
},
"tools": "கருவிகள்",
"requirements": {
"title": "உங்களுக்கு என்ன தேவை",
"projector": "ப்ரொசெக்டர்: குறைந்தது 720p பரிந்துரைக்கப்படுகிறது",
"mount": "ப்ரொசெக்டருக்கு முக்காலி அல்லது சுவர்/அலமாரி/அட்டவணை மவுண்ட்",
"mat": "கட்டம் கோடுகளுடன் ஒரு வெட்டு பாய் (விரும்பினால்)",
"computer": "ப்ரொசெக்டருடன் இணைக்க கணினி அல்லது டேப்லெட்",
"pattern": "ஒரு PDF தையல் முறை"
},
"setup": {
"title": "அமைவு",
"place": "கட்டிங் பாய்க்கு மேலே ப்ரொசெக்டரை வைக்கவும், வெட்டும் பாயில் நேரடியாக சுட்டிக்காட்டவும். உங்களிடம் வெட்டு பாய் இல்லையென்றால், ஒரு அட்டவணை அல்லது தரையில் ஒரு கட்டம்/செவ்வகத்தைக் குறிக்க காகிதம் அல்லது முகமூடி நாடாவைப் பயன்படுத்தலாம்.",
"connect": "உங்கள் கணினி அல்லது டேப்லெட்டை ப்ரொசெக்டருடன் இணைத்து, காட்சியை மிரர் அல்லது நீட்டிக்கவும், இதனால் கணினி அல்லது டேப்லெட்டிலிருந்து படம் ப்ரொசெக்டரில் காட்டப்படும்.",
"focus": "திட்டத்தின் மையத்தில் உரை மிருதுவாக இருக்கும் வரை, ப்ரொசெக்டரில் கவனம் செலுத்துங்கள். நீங்கள் ஒரு தெளிவான படத்தைப் பெற முடியாவிட்டால், ப்ரொசெக்டர் மற்றும் கட்டிங் பாய் இடையேயான தூரம் உற்பத்தியாளரால் பரிந்துரைக்கப்பட்ட செயல்பாட்டு வரம்பிற்குள் இருப்பதை உறுதிசெய்க.",
"keystone": "உங்கள் ப்ரொசெக்டரில் ஒரு கீச்டோன் இருந்தால், அதை சரிசெய்யவும், இதனால் திட்டம் செவ்வகத்திற்கு நெருக்கமாக இருக்கும், மேலும் விளிம்புகளுக்கு அருகில் கவனம் மேம்படுகிறது."
},
"calibration": {
"title": "அளவுத்திருத்தம்",
"start": "சொடுக்கு (அல்லது தட்டவும்) “அளவீடு செய்யத் தொடங்குங்கள்.”",
"fullscreen": "சொடுக்கு செய்வதன் மூலம் முழு திரை பயன்முறையை உள்ளிடவும் (அல்லது தட்டுகிறது) ",
"drag": "உங்கள் பாயுடன் சீரமைக்க கட்டத்தின் மூலைகளை இழுக்கவும். பாயில் உங்கள் கண்களால், உங்கள் கணினி அல்லது டேப்லெட்டில் உள்ள மூலைகளை சரிசெய்யவும். திட்டமிடப்பட்ட கட்டம் உங்கள் பாயின் கட்டத்துடன் பொருந்தும் வரை மூலைகளின் இடத்தை சரிசெய்யவும்.",
"size": "உங்கள் முழு பாயைப் பயன்படுத்தி நீங்கள் அளவீடு செய்ய வேண்டியதில்லை, அதற்கு பதிலாக நீங்கள் அளவுத்திருத்த கட்டத்தை பொருத்தக்கூடிய மிகப்பெரிய பகுதியைத் தேர்வுசெய்து, கட்டத்தின் அகலம் மற்றும் உயரத்துடன் பொருந்த அகலம் மற்றும் உயரத்தை உள்ளிடவும்.",
"project": "திட்டமிடப்பட்ட கட்டம் உங்கள் பாயுடன் சீரமைக்கப்படும்போது, “திட்டம்” என்பதைக் சொடுக்கு செய்க (அல்லது தட்டவும்)."
},
"lineTool": {
"title": "வரி கருவி",
"description": "வரி கருவி தூரங்களை அளவிடவும், கோடுகளைக் குறிக்க, சுழற்றவும், புரட்டவும், PDF ஐ நகர்த்தவும் பயன்படுத்தப்படலாம். வரி கருவி பொத்தானைக் சொடுக்கு செய்து (அல்லது தட்டவும்), பின்னர் PDF இல் சொடுக்கு செய்து ஒரு வரியை வரைய இழுக்கவும். ஒரு வரி வரையப்பட்டதும், பின்வரும் விருப்பங்களுடன் திரையின் அடிப்பகுதியில் ஒரு பட்டியல் தோன்றும்:",
"delete": {
"title": "வரியை நீக்கு",
"description": "தேர்ந்தெடுக்கப்பட்ட வரியை நீக்குகிறது."
},
"rotate": {
"title": "மையத்துடன் சீரமைக்கவும்",
"description": "PDF ஐ சுழற்றுகிறது, எனவே தேர்ந்தெடுக்கப்பட்ட வரி கிடைமட்டமாகவும் உங்கள் பாயை மையமாகக் கொண்டதாகவும் இருக்கும்.",
"use": "ஒரு மாதிரி துண்டின் தானியத்தை உங்கள் துணியின் தானியத்துடன் சீரமைக்க: வடிவத் துண்டின் தானிய வரியில் ஒரு கோட்டை வரையவும், பின்னர் மைய பொத்தானைக் சொடுக்கு செய்யவும் (அல்லது தட்டவும்)."
},
"previousNext": {
"title": "முந்தைய/அடுத்த வரியை மையத்திற்கு கொண்டு வாருங்கள்",
"description": "PDF ஐ நகர்த்துகிறது, இதனால் முந்தைய/அடுத்த வரி வரையப்பட்ட கிடைமட்டமாகவும் உங்கள் பாயை மையமாகக் கொண்டதாகவும் இருக்கும்.",
"use": "வெட்டும்போது மாதிரி துண்டுகளுக்கு இடையில் நகர்த்த: ஒவ்வொரு முறை துண்டின் தானியக் கோட்டில் ஒரு கோட்டை வரையவும், பின்னர் இந்த பொத்தான்களைப் பயன்படுத்தி மாதிரி துண்டுகள் வழியாக நகர்த்தவும். எந்த துண்டுகள் வெட்டப்பட வேண்டும் என்பதைக் கண்காணிக்க நீங்கள் மாதிரி துண்டுகள் வழியாக செல்லும்போது கோடுகளை நீக்கவும்."
},
"flip": {
"title": "வரியுடன் புரட்டவும்",
"description": "PDF ஐ வரிசையில் புரட்டுகிறது.",
"use": "விரிவடையும் முறை துண்டுகளுக்கு: ஒரு மாதிரி துண்டின் மடிப்பு வரியில் ஒரு கோட்டை வரைந்து, துண்டுகளை மடிப்பு வரிக்கு வெட்டி, வரி பொத்தானைக் சொடுக்கு செய்து (அல்லது தட்டவும்), பின்னர் மீதமுள்ள துண்டுகளை வெட்டுங்கள்."
},
"move": {
"title": "வரி நீளத்தால் PDF ஐ நகர்த்தவும்",
"description": "PDF ஐ வரியின் நீளத்தால் நகர்த்துகிறது.",
"use": "முறை துண்டுகளை நீளப்படுத்த/சுருக்கவும்: ஒரு கோட்டை செங்குத்தாக ஒரு மாதிரி துண்டின் நீள/குறுகலான வரியை வரையவும், நீள/சுருக்கக் கோட்டிற்கு வெட்டி, பின்னர் வரி நீள பொத்தானைக் சொடுக்கு செய்து (அல்லது தட்டவும்), பின்னர் துண்டுகளை வெட்டுவதைத் தொடரவும். வரையப்பட்ட கோட்டின் திசை நீங்கள் மாதிரி பகுதியை நீட்டிக்கிறீர்களா அல்லது சுருக்குகிறீர்களா என்பதைப் பொறுத்தது."
}
},
"overlayOptions": {
"title": "மேலடுக்கு விருப்பங்கள்",
"description": "மேலடுக்கு விருப்பங்கள் திட்டமிடும்போது அளவுத்திருத்தத்தை சரிபார்க்கவும், வெட்டும் வடிவங்களுக்கு உதவவும் பயன்படுத்தப்படலாம். பின்வரும் விருப்பங்கள் கிடைக்கின்றன:",
"border": {
"title": "எல்லை",
"description": "அளவுத்திருத்த கட்டத்தின் எல்லையைக் காட்டுகிறது."
},
"grid": {
"title": "வலைவாய்",
"description": "அளவுத்திருத்த கட்டத்தைக் காட்டுகிறது."
},
"paper": {
"title": "காகித தாள்",
"description": "அளவுத்திருத்தத்தை சரிபார்க்க ஒரு கடிதத்தின் அளவு அல்லது A4 துண்டு காகிதத்தை ஒரு செவ்வகத்தைக் காட்டுகிறது."
},
"flipLines": {
"title": "புரட்ட கோடுகள்",
"description": "விரிவடையும் வடிவங்களுக்கு உதவும் வரிகளைக் காட்டுகிறது. ஃபிளிப் கோட்டோடு வடிவத்தின் மடிப்பு கோட்டை வரிசைப்படுத்தி, துண்டுகளை பிரதிபலிக்க கிடைமட்டமாக அல்லது செங்குத்தாக புரட்டவும்."
},
"flippedPattern": {
"title": "தவறான பக்கம்",
"description": "வடிவத்தின் தவறான பக்கம் திட்டமிடப்படும்போது புள்ளிகளைக் காட்டுகிறது."
}
},
"faq": {
"title": "கேள்விகள்",
"wrongSizePdf": {
"question": "உங்கள் PDF திட்டமிடல் மிகச் சிறியதா அல்லது பெரியதா?",
"answer": "பேட்டர்ன் ப்ரொசெக்டர் அளவுத்திருத்த கருவிக்கு சூம் இல்லை, ஏனெனில் திட்டத்தின் அளவு PDF இல் உள்ள அளவு தகவல்களிலிருந்து வருகிறது. வடிவமைப்பாளர்களிடமிருந்து வரும் வடிவங்கள் வழக்கமாக சரியான அளவைக் கொண்டிருக்கின்றன, எனவே இணைப்பு வடிவமைப்பாளர் அல்லது இன்க்ச்கேப்பில் வடிவத்தைத் திறக்கும்போது அளவின் மாற்றத்தை அறிமுகப்படுத்தியிருக்கலாம்."
},
"saveAsApp": {
"answer": "பேட்டர்ன் ப்ரொசெக்டர் ஒரு முற்போக்கான வலை பயன்பாடு (PWA) ஆகும், எனவே இதை ஒரு பயன்பாடாக சேமிக்க முடியும். டெச்க்டாப்பில், Chrome அல்லது எட்ச் ஐப் பயன்படுத்தி இதை நிறுவலாம். ஒரு டேப்லெட்டில், பகிர்வு மெனுவைத் திறந்து முகப்புத் திரையில் சேர் என்பதைத் தட்டவும்."
},
"mobileSupport": {
"question": "உங்களிடம் உதவி மொபைல் போன்கள் மற்றும் டேப்லெட்டுகள் உள்ளதா?",
"answer": "அறிவுள்ள தொலைபேசியில் வலைப்பக்கத்தைப் பார்வையிடவும் பயன்படுத்தவும் முடிந்தாலும், வரையறுக்கப்பட்ட திரை அளவு பயன்படுத்த கடினமாக உள்ளது."
}
},
"resources": {
"title": "கூடுதல் ஆதாரங்கள்",
"links": {
"projectorsForSewing": {
"title": "பேச்புக் குழுவை தையல் செய்வதற்கான ப்ரொசெக்டர்கள்",
"link": "https://www.facebook.com/groups/481078582801085"
},
"onePageGuide": {
"title": "ப்ரொசெக்டர் தையலுக்கான ஒரு பக்க வழிகாட்டி",
"link": "https://bit.ly/onepageguidetoprojectorsewing"
}
}
},
"contribute": {
"title": "திட்டத்திற்கு பங்களிக்கவும்",
"donation": "பேட்டர்ன் ப்ரொசெக்டர் உங்கள் பணத்தை நகலெடுப்பதில் அல்லது பி.டி.எஃப் பார்வையாளர் சந்தாவிலிருந்து விடுவித்திருந்தால், தயவுசெய்து எனக்கு ஒரு காபி வாங்குவதைக் கவனியுங்கள் அல்லது பேபால் வழியாக ஆதரிக்கிறது!",
"develop": "அறிவிலிமையம் இல் அம்சங்களை செயல்படுத்தவும் சிக்கல்களை சரிசெய்யவும் உதவுங்கள்.",
"translate": " வலைபெயர்ப்பு ஐப் பயன்படுத்தி இந்த கருவியை மேலும் மொழிகளில் மொழிபெயர்க்கவும்.",
"feedback": "கருத்து மற்றும் அம்ச கோரிக்கைகள் வரவேற்கப்படுகின்றன!"
},
"contact": "தொடர்பு"
},
"Troubleshooting": {
"unevenSurface": {
"title": "வளைந்த கோடுகள்?",
"description": "திட்டமிடப்பட்ட கோடுகள் வளைந்திருத்தால், உங்கள் பாய் அல்லது பாய்க்கு கீழே உள்ள மேற்பரப்பு தட்டையாக இருக்காது. பாயைத் தட்டச்சு செய்ய முயற்சிக்கவும், ஆனால் குறைந்த புள்ளிகளின் கீழ் அட்டை பங்குகளை வைப்பது அல்லது வேறு வெட்டு மேற்பரப்புக்கு நகர்த்தவும்.",
"caption": "இந்த பாயின் கீழ் ஒரு ஆட்சியாளரைக் கொண்டுள்ளது, இதனால் கோடுகள் மையத்தில் வளைக்கின்றன. லேசான வளைவு சரி, ஆனால் ஒரு பெரிய வளைவு சிக்கல்களை ஏற்படுத்தும்."
},
"notMatching": "உங்கள் பாயுடன் பொருந்தவில்லையா?",
"title": "அளவுத்திருத்த சரிசெய்தல்",
"dragCorners": {
"title": "அளவீடு செய்வது எப்படி",
"description": "கட்டத்தின் மூலைகளை உங்கள் பாயில் குறைந்தபட்சம் 24x16 அங்குல செவ்வகத்திற்கு இழுக்கவும். கட்டம் முடிந்தவரை பெரியதாக இருக்க வேண்டும், ஆனால் முழு பாயையும் மறைக்க தேவையில்லை.",
"caption": "சரியாக அளவீடு செய்யப்பட்ட கட்டம்."
},
"inputMeasurement": "உங்கள் பாயில் கட்டத்தின் அகலம் மற்றும் உயரத்தை அளவிட ஒரு அளவீட்டு நாடாவைப் பயன்படுத்தவும். இந்த அளவீடுகளை அகலம் மற்றும் உயர புலங்களில் உள்ளிடவும்.",
"offByOne": {
"title": "கோடுகள் நேராக உள்ளன, ஆனால் சீரமைக்கவில்லையா?",
"description": "திட்டமிடப்பட்ட கோடுகள் நேராக இருந்தால், ஆனால் உங்கள் பாயுடன் ஒத்துப்போகவில்லை என்றால், உங்கள் அகலம் அல்லது உயரம் 1 அங்குல/சென்டிமீட்டர் வரை இருக்கக்கூடும். அளவிடும் நாடா மூலம் உங்கள் அளவீடுகளை இருமுறை சரிபார்க்கவும். MAT அளவீடுகள் குழப்பமானதாகவோ அல்லது துல்லியமாகவோ இருக்கலாம், எனவே அளவிடும் நாடாவுடன் சரிபார்ப்பது முதன்மை.",
"caption": "தவறான அளவீடுகள் (24 ஃச் 18) க்கு பதிலாக உள்ளீடு (23 ஃச் 17). MAT 23 மற்றும் 17 வரை எண்களைக் காட்டுகிறது, எனவே அளவீடுகளை குழப்புவது எளிது. அளவிடும் நாடா மூலம் நீங்கள் அளவிடுவதை உறுதிப்படுத்திக் கொள்ளுங்கள்!"
},
"dimensionsSwapped": {
"title": "சதுரங்களுக்கு பதிலாக செவ்வகங்கள்?",
"description": "திட்டமிடப்பட்ட கட்டம் சதுரங்களுக்குப் பதிலாக செவ்வகங்கள் போல் தோன்றினால், அகலம் மற்றும் உயரம் மாற்றப்படலாம். புலங்களில் உள்ள மதிப்புகளை மாற்றவும்.",
"caption": "அகலம் மற்றும் உயரம் தவறான பெட்டிகளில் உள்ளன. பரிமாணம் 'W: 24' மற்றும் 'H: 18' ஆகியவற்றைப் படிக்க வேண்டும்."
},
"close": "மூடு"
},
"OverlayCanvas": {
"magnifying": "click pattern பெறுநர் நிறுத்து magnifying",
"scaled": " × அளவு",
"zoomedOut": "பெரிதாக்க வடிவத்தைக் சொடுக்கு செய்க"
},
"PdfViewer": {
"noData": "Press the \"Open\" button பெறுநர் load a pattern",
"error": "Failed பெறுநர் load pattern"
},
"InstallButton": {
"title": "பயன்பாட்டை நிறுவவும்",
"description": "சிறந்த அனுபவத்திற்கு, கூகிள் குரோம் ஐப் பயன்படுத்தி மாதிரி ப்ரொசெக்டரை நிறுவவும். Chrome இல், முகவரி பட்டியில் ஐ நிறுவுவதன் மூலம் பயன்பாட்டைப் பதிவிறக்கவும்.",
"descriptionAndroid": "ஆண்ட்ராய்டு இல் சிறந்த அனுபவத்திற்கு, பயர்பாக்சைப் பயன்படுத்தவும். உலாவி பட்டியலில் மேலும் பொத்தானைத் தட்டுவதன் மூலம் பயன்பாட்டைப் பதிவிறக்கி, பின்னர் முகப்புத் திரையில் சேர்க்கவும்.",
"descriptionIOS": "உலாவி பட்டியலில் பங்கு பொத்தானை ஐத் தட்டுவதன் மூலம் பயன்பாட்டைப் பதிவிறக்கி, பின்னர் முகப்புத் திரை இல் சேர்க்கவும்.",
"ok": "சரி"
},
"Mail": {
"title": "உங்களுக்கு அஞ்சல் கிடைத்துள்ளது",
"donate": "நன்கொடை"
},
"General": {
"close": "மூடு",
"error": "பிழை"
},
"Header": {
"openPDF": "திற",
"height": "H:",
"width": "W:",
"project": "திட்டம்",
"calibrate": "அளவீடு செய்யுங்கள்",
"delete": "அளவுத்திருத்தத்தை மீட்டமைக்கவும்",
"gridOn": "கட்டம் இயக்கு",
"gridOff": "கட்டம்",
"overlayOptions": "மேலடுக்கு விருப்பங்கள்",
"overlayOptionBorder": "எல்லை",
"overlayOptionDisabled": "முடக்கப்பட்டது",
"overlayOptionGrid": "வலைவாய்",
"overlayOptionPaper": "காகித தாள்",
"overlayOptionFliplines": "புரட்ட கோடுகள்",
"overlayOptionFlippedPattern": "தவறான பக்கம்",
"invertColor": "வண்ணங்களை தலைகீழ்",
"invertColorOff": "வண்ணங்களை தலைகீழாக மாற்றவும்",
"flipVertical": "செங்குத்தாக புரட்டவும் (வி)",
"flipHorizontal": "கிடைமட்டமாக புரட்டவும் (ம)",
"flipCenterOn": "நடுவண் முழுவதும் புரட்டவும்",
"flipCenterOff": "சென்டர் முழுவதும் புரட்டவும்",
"fourCornersOn": "எல்லா மூலைகளையும் முன்னிலைப்படுத்தவும்",
"fourCornersOff": "தேர்ந்தெடுக்கப்பட்ட மூலையை மட்டுமே முன்னிலைப்படுத்தவும்",
"rotate90": "90 டிகிரி கடிகார திசையில் சுழற்றுங்கள் (ஆர்)",
"arrowBack": "பின் 1 பக்கம்",
"arrowForward": "முன்னோக்கி 1 பக்கம்",
"info": "செய்தி பக்கம்",
"recenter": "நடுவண் மற்றும் மீட்டமை (சி)",
"fullscreen": "முழுத் திரையை உள்ளிடவும்",
"fullscreenExit": "முழுத் திரையில் வெளியேறவும்",
"ok": "சரி",
"calibrationAlertTitle": "அளவுத்திருத்த எச்சரிக்கை",
"calibrationAlert": "திட்டமிடப்பட்டதிலிருந்து சாளர அளவு மாறிவிட்டது, இந்த சிக்கலை சரிசெய்ய முழு திரை பொத்தானை அழுத்தவும் அல்லது மறுபரிசீலனை செய்யவும்.",
"calibrationAlertContinue": "அளவுத்திருத்தத்திற்குப் பிறகு சாளர அளவு மாற முடியாது. முழுத் திரையில் திட்டமிட, முழுத் திரையில் அளவீடு செய்யுங்கள். நீங்கள் சாளர அளவை மாற்ற விரும்பினால், மறுபரிசீலனை செய்யுங்கள்.",
"continue": "சாளர அளவைச் சேமித்து தொடரவும்",
"checkCalibration": "அளவுத்திருத்தத்தை சரிபார்க்கவும்",
"menuShow": "மெனுவைக் காட்டு",
"menuHide": "மெனுவை மறைக்கவும்",
"lineWeight": "வரி எடை",
"stitchMenuShow": "தையல் மெனுவைக் காட்டு",
"stitchMenuHide": "தையல் மெனுவை மறைக்கவும்",
"stitchMenuDisabled": "தையல் மெனுவைப் பயன்படுத்த PDF ஐத் திறக்கவும்",
"measure": "வரி (எல்)",
"showMovement": "நகரும் கருவியைக் காட்டு",
"hideMovement": "நகரும் கருவியை மறைக்கவும்",
"magnify": "பெரிதாக்கு (மீ)",
"zoomOut": "பெரிதாக்கவும் (z)",
"mail": "கர்ட்னியில் இருந்து அஞ்சல்",
"invalidCalibration": "அளவுத்திருத்த கட்டம் செல்லுபடியாகாது. மீண்டும் முயற்சிக்க 'அளவுத்திருத்தத்தை மீட்டமை' என்பதை அழுத்தவும்."
},
"ScaleMenu": {
"scale": "அளவிலான முறை",
"hide": "அளவிலான மெனுவை மறைக்கவும்",
"show": "அளவிலான மெனுவைக் காட்டு"
},
"StitchMenu": {
"columnCount": "நெடுவரிசைகள்",
"pageRange": "தையல் பக்கங்கள்",
"horizontal": "கிடைமட்ட ஒன்றின்மேல்ஒன்று",
"vertical": "செங்குத்து ஒன்றுடன் ஒன்று",
"zeros": "வெற்று பக்கங்களுக்கு 0 களைச் சேர்க்கவும், எ.கா. 1-3,0,0,4",
"rowCount": "வரிசைகள்"
},
"SaveMenu": {
"save": "ஏற்றுமதி PDF",
"saveTooltip": "தைக்கப்பட்ட பக்கங்கள் மற்றும் தேர்ந்தெடுக்கப்பட்ட அடுக்குகளுடன் PDF ஐ ஏற்றுமதி செய்யுங்கள்",
"encryptedPDF": "இந்த PDF குறியாக்கம் செய்யப்பட்டுள்ளது. தையல் செய்யப்பட்ட PDF ஐ சேமிக்க கடவுச்சொல்லை உள்ளிடவும்."
},
"LayerMenu": {
"title": "அடுக்குகள்",
"showAll": "அனைத்தையும் காட்டு",
"hideAll": "அனைத்தையும் மறைக்கவும்",
"layersOn": "அடுக்கு மெனுவைக் காட்டு",
"layersOff": "அடுக்கு மெனுவை மறைக்கவும்",
"noLayers": "இல்லை layers in pattern"
},
"MovementPad": {
"up": "மேலே",
"down": "கீழே",
"left": "இடது",
"right": "வலது",
"next": "அடுத்த மூலையில்"
},
"MeasureCanvas": {
"rotateToHorizontal": "மையத்துடன் சீரமைக்கவும்",
"rotateAndCenterPrevious": "முந்தைய வரியை மையத்திற்கு கொண்டு வாருங்கள்",
"rotateAndCenterNext": "அடுத்த வரியை மையத்திற்கு கொண்டு வாருங்கள்",
"deleteLine": "வரியை நீக்கு",
"flipAlong": "வரியுடன் புரட்டவும்",
"translate": "வரி நீளத்தால் வடிவத்தை நகர்த்தவும்",
"line": "வரி",
"lines": "வரிகள்"
}
}
================================================
FILE: messages/tr.json
================================================
{}
================================================
FILE: middleware.ts
================================================
import createMiddleware from "next-intl/middleware";
// Language names are not translated because we want them to be the same always, regardless of the current language.
// *** IMPORTANT *** Add new languages to the localeData and matcher in the config below
export const localeData = {
cs: "Čeština",
da: "Dansk",
de: "Deutsch",
en: "English",
es: "Español",
fr: "Français",
hu: "Magyar",
it: "Italiano",
"nb-NO": "Norwegian Bokmål", // Needs to be in format nb-NO instead of nb_NO for next-intl to recognize it
nl: "Nederlands",
sl: "Slovenščina",
sv: "Svenska",
ta: "தமிழ்",
};
export const locales = Object.keys(localeData);
export default createMiddleware({
// A list of all locales that are supported
locales: locales,
// Used when no locale matches
defaultLocale: "en",
});
export const config = {
// Match only internationalized pathnames
// Match /calibrate for people who saved a link to /calibrate before internationalization was added
// *** IMPORTANT *** New language codes must be added here as well as in the localeData above
matcher: [
"/",
"/(cs|da|de|en|es|fr|hu|it|nb-NO|nl|sl|sv|ta)/:path*",
"/calibrate",
],
};
================================================
FILE: navigation.ts
================================================
import { locales } from "middleware";
import { createSharedPathnamesNavigation } from "next-intl/navigation";
export const localePrefix = "always"; // Default
export const { Link, redirect, usePathname, useRouter } =
createSharedPathnamesNavigation({ locales, localePrefix });
================================================
FILE: next.config.mjs
================================================
import withSerwistInit from "@serwist/next";
import createNextIntlPlugin from "next-intl/plugin";
const withSerwist = withSerwistInit({
swSrc: "app/sw.ts",
swDest: "public/sw.js",
cacheOnFrontEndNav: true,
reloadOnOnline: true,
disable: process.env.NODE_ENV === "development",
});
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
config.resolve.alias.canvas = false;
config.resolve.fallback = { fs: false };
return config;
},
};
export default withNextIntl(withSerwist(nextConfig));
================================================
FILE: package.json
================================================
{
"name": "pattern-projector",
"version": "0.1.0",
"private": true,
"main": "index.js",
"repository": "git@github.com:courtneypattison/pattern-projector.git",
"author": "courtneypattison ",
"license": "MIT",
"scripts": {
"dev": "next dev",
"build": "prettier . --write; next build",
"start": "next start",
"lint": "next lint",
"test:unit": "jest",
"cypress:open": "cypress open",
"postinstall": "patch-package"
},
"dependencies": {
"@cantoo/pdf-lib": "^2.1.3",
"@next/third-parties": "^14.2.5",
"@serwist/next": "^8.4.4",
"@serwist/precaching": "^8.4.4",
"@serwist/sw": "^8.4.4",
"@types/node": "20.6.2",
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
"@vercel/analytics": "^1.1.3",
"@vercel/speed-insights": "^1.0.9",
"autoprefixer": "10.4.15",
"eslint": "^8.57.0",
"eslint-config-next": "13.4.19",
"ml-matrix": "^6.10.5",
"next": "13.4.19",
"next-intl": "^3.9.0",
"postcss": "8.4.29",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-full-screen": "^1.1.1",
"react-pdf": "^7.7.0",
"tailwindcss": "3.3.3",
"typescript": "^5.4.4"
},
"devDependencies": {
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/typography": "^0.5.10",
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.5",
"cypress": "^13.6.3",
"eslint-config-prettier": "^9.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0",
"prettier": "3.2.5",
"ts-node": "^10.9.2",
"typescript-eslint": "^7.5.0",
"webpack": "^5.91.0"
},
"resolutions": {
"jackspeak": "2.1.1"
},
"prettier": {
"trailingComma": "all"
}
}
================================================
FILE: patches/react-pdf+7.7.0.patch
================================================
diff --git a/node_modules/react-pdf/dist/cjs/Page/PageCanvas.js b/node_modules/react-pdf/dist/cjs/Page/PageCanvas.js
index b5e8e11..5fb6371 100644
--- a/node_modules/react-pdf/dist/cjs/Page/PageCanvas.js
+++ b/node_modules/react-pdf/dist/cjs/Page/PageCanvas.js
@@ -82,8 +82,13 @@ function PageCanvas(props) {
}
canvas.width = renderViewport.width;
canvas.height = renderViewport.height;
- canvas.style.width = `${Math.floor(viewport.width)}px`;
- canvas.style.height = `${Math.floor(viewport.height)}px`;
+
+ const CSS = 96.0;
+ const PDF = 72.0;
+ const PDF_TO_CSS_UNITS = CSS / PDF;
+
+ canvas.style.width = `${Math.floor(viewport.width) * PDF_TO_CSS_UNITS}px`;
+ canvas.style.height = `${Math.floor(viewport.height) * PDF_TO_CSS_UNITS}px`;
canvas.style.visibility = 'hidden';
const renderContext = {
annotationMode: renderForms ? ANNOTATION_MODE.ENABLE_FORMS : ANNOTATION_MODE.ENABLE,
diff --git a/node_modules/react-pdf/dist/cjs/Page/TextLayer.css b/node_modules/react-pdf/dist/cjs/Page/TextLayer.css
index 592ec1c..fc3b617 100644
--- a/node_modules/react-pdf/dist/cjs/Page/TextLayer.css
+++ b/node_modules/react-pdf/dist/cjs/Page/TextLayer.css
@@ -101,7 +101,6 @@
position: absolute;
inset: 100% 0 0;
z-index: -1;
- cursor: default;
user-select: none;
}
diff --git a/node_modules/react-pdf/dist/esm/Page/PageCanvas.js b/node_modules/react-pdf/dist/esm/Page/PageCanvas.js
index 1b72392..0df7d1a 100644
--- a/node_modules/react-pdf/dist/esm/Page/PageCanvas.js
+++ b/node_modules/react-pdf/dist/esm/Page/PageCanvas.js
@@ -54,8 +54,13 @@ export default function PageCanvas(props) {
}
canvas.width = renderViewport.width;
canvas.height = renderViewport.height;
- canvas.style.width = `${Math.floor(viewport.width)}px`;
- canvas.style.height = `${Math.floor(viewport.height)}px`;
+
+ const CSS = 96.0;
+ const PDF = 72.0;
+ const PDF_TO_CSS_UNITS = CSS / PDF;
+
+ canvas.style.width = `${Math.floor(viewport.width) * PDF_TO_CSS_UNITS}px`;
+ canvas.style.height = `${Math.floor(viewport.height) * PDF_TO_CSS_UNITS}px`;
canvas.style.visibility = 'hidden';
const renderContext = {
annotationMode: renderForms ? ANNOTATION_MODE.ENABLE_FORMS : ANNOTATION_MODE.ENABLE,
diff --git a/node_modules/react-pdf/dist/esm/Page/TextLayer.css b/node_modules/react-pdf/dist/esm/Page/TextLayer.css
index 592ec1c..fc3b617 100644
--- a/node_modules/react-pdf/dist/esm/Page/TextLayer.css
+++ b/node_modules/react-pdf/dist/esm/Page/TextLayer.css
@@ -101,7 +101,6 @@
position: absolute;
inset: 100% 0 0;
z-index: -1;
- cursor: default;
user-select: none;
}
diff --git a/node_modules/react-pdf/src/Page/TextLayer.css b/node_modules/react-pdf/src/Page/TextLayer.css
index 592ec1c..fc3b617 100644
--- a/node_modules/react-pdf/src/Page/TextLayer.css
+++ b/node_modules/react-pdf/src/Page/TextLayer.css
@@ -101,7 +101,6 @@
position: absolute;
inset: 100% 0 0;
z-index: -1;
- cursor: default;
user-select: none;
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
================================================
FILE: tailwind.config.ts
================================================
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: "class",
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
corePlugins: {
aspectRatio: false,
},
plugins: [
require("@tailwindcss/typography"),
require("@tailwindcss/aspect-ratio"),
],
};
export default config;
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es2015",
"lib": ["dom", "dom.iterable", "esnext", "webworker"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./app/*"]
},
"types": ["@serwist/next/typings"]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"types/**/*.ts",
"global.d.ts"
],
"exclude": ["node_modules"]
}